Bundlers unbundled
I just released my first npm package!
It's a simple component that generates animated text, as SVG paths, given a desired font. It's quite simple, the entire source code is almost 200 lines, but my main goal was to experiment with creating a component library and learn bundling.
What is a Bundler
Nowadays applications have grown in complexity and it's useful to divide code in modules, that's basically the aim of React, the most widely used js library out there, but when it comes to fetching all this single files individually the loading time can take ages, so it's useful to bundle all of them in a unique file.
That's what bundlers do, there's many of them, but I decided to use Rollup since is one of the most used and quite simple to setup.
I'm writing my module using typescript and style it using css modules so I'll need some tools, since browsers only understand plain js and css files.
I'll also need a tool, for the sake of the component, which converts text to svg paths, I'll use opentype.js.
So I'll need:
- Babel: A compiler that converts TypeScript code into plain JavaScript and ensures compatibility with most browsers.
- PostCSS: Another compiler that converts CSS modules to plain CSS files and allows importing them in JavaScript files.
- tsc (TypeScript Compiler): A tool to generate type definitions (
.d.tsfiles), which helps users of the component (especially those using TypeScript) understand the expected data types for each parameter.
You may wonder why using babel if tsc is already a compiler and can convert ts to js, it seems like Babel is just better at doing that and from official typescript docs they recommend to do so.
Setup
Our folder will have the following structure:
| src
| components // Folder containing component with style
index.ts // File to export the component
rollup.config.js // Config files
tsconfig.json
package.json
First of all we need some setup in package.json
{
"name": "animated-text-component", // Name of the package
"version": "1.0.0", // Version
"description": "A svg animated text generator", // Brief description
"author": "Matteo Tacconi",
"license": "MIT",
"keywords": ["animated", "title", "text", "animated-text"],
"repository": {
"type": "git",
"url": "git+https://github.com/teotexe/animated-text-component.git"
},
"bugs": {
"url": "https://github.com/teotexe/animated-text-component/issues"
},
"homepage": "https://github.com/teotexe/animated-text-component#readme",
"type": "module", // To use newer ESM modules syntax instead of CommonJS
// For example to use @import statement instead of require
// Output files
"module": "dist/index.js",
"types": "dist/index.d.ts",
// Files to include in the build folder
"files": [
"dist",
"README.md",
"LICENSE"
],
// Scripts to build
"scripts": {
"build:js": "rollup -c",
"build:types": "tsc",
"build": "rm -rf dist && npm run build:js && npm run build:types"
},
// Other libraries for the component
"peerDependencies": {
"opentype.js": "^1.3.4",
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
},
"devDependencies": {
"typescript": "^5.2.0",
"postcss": "^8.4.30",
"postcss-import": "^15.1.0",
"postcss-preset-env": "^8.0.1",
"rollup": "^3.7.0",
"rollup-plugin-postcss": "^4.0.2",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.0.1",
"@babel/preset-env": "^7.28.3",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"@types/opentype.js": "^1.3.8",
"@types/react": "^18.2.12",
},
// The browsers to support (to avoid transpiling for too old ones which is too slow)
"browserslist": [
"defaults"
],
// Babel settings (a compiler to generate JS compatible with most browsers)
"babel": {
"presets": [
"@babel/preset-env",
[
"@babel/preset-react",
{
"runtime": "automatic"
}
],
"@babel/preset-typescript"
]
}
}
Then we need to setup rollup with the required plugins:
import resolve from "@rollup/plugin-node-resolve";
import postcss from "rollup-plugin-postcss";
import postcssPresetEnv from "postcss-preset-env";
import babel from "@rollup/plugin-babel";
// Which files to convert
const extensions = [".ts", ".tsx", ".js", ".jsx"];
export default {
input: "src/index.ts", // Entrypoint
output: {
dir: "dist", // Output folder
format: "esm", // Generate ESM
preserveModules: true, // Keep files separate
preserveModulesRoot: "src", // Keep folder structure starting from src
},
external: [
// Peer dependencies (do not bundle them, the user will need to provide)
"react",
"react-dom",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"opentype.js",
],
plugins: [
resolve({ extensions }), // Lets Rollup understand import something from "package" by finding it in node_modules
babel({
// Compiles and transpiles code to JS
extensions,
babelHelpers: "bundled",
include: ["src/**/*"],
exclude: "node_modules/**",
}),
postcss({
// This plugin basically generates for each css module a js file with inline css style like so
// Style.module.css
// .container {
// display: flex;
// flex-direction: column;
// justify-content: center;
// align-items: center;
// text-align: center;
// }
// Becomes
// styles.module.css.js:
// var styles = {"container":"AnimatedText_container-TsaLa"};
// export { styles as default };
// So that it can be imported with
// import styles from './styles.module.css.js';
plugins: [postcssPresetEnv()], // Include newest features
modules: {
// Generate hash for css modules to avoid collisions
generateScopedName: "[folder]_[local]-[hash:base64:5]",
},
extract: true, // Write css in a dedicated file instead of inline in js
}),
],
};
Finally we need to generate file types with tsc:
{
"compilerOptions": {
"declaration": true, // Generate declaration files
"emitDeclarationOnly": true, // Generate them only
"lib": ["DOM", "ESNext"], // Adds type definitions for:
// DOM → document, window, etc.
// ESNext → latest JS features
"outDir": "dist", // Output folder
"strict": true, // Strict type checking (safer)
"jsx": "react-jsx", // Use JSX runtime (You don’t need to import React from "react" in every file)
"allowImportingTsExtensions": true, // Allows .ts and .tsx files to import each other
"esModuleInterop": true // Handles older JS that has no default export
// import * as moment from "moment"
// acts the same as const moment = require("moment")
// import moment from "moment"
// acts the same as const moment = require("moment").default
// So if a library is in older commonJS, like opentype.js, throws this error:
// error TS1192: Module '"/animated-text-component/node_modules/@types/opentype.js/index"' has no default export.
// 2 import opentype from "opentype.js";
// But with esModuleInterop ts under the hood does something like:
// const moment = require("moment").default || require("moment");
},
"include": ["src"] // Process the files in /src
}
Now we can write out our component in /src/components
|-- src
|-- index.ts
|-- components
|-- AnimatedText // New component
|-- component.tsx
|-- styles.module.css
And export everything in "index.ts":
export { AnimatedText } from './components/AnimatedText/component.tsx';
Before building we need to declare the types for css modules since typescript doesn't know how to handle an import like import styles from "./styles.module.css";, since styles is not a function,
|-- src
|-- global.d.ts // New file
|-- index.ts
|-- components
|-- AnimatedText
|-- component.tsx
|-- styles.module.css
We can declare a file "global.d.ts" inside src and declare it inside:
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
As we finish to write our component we can build and bundle (and create types) using npm run build which will run our scripts defined in package.json, a new "dist" directory will be created with all the bundled files inside, hopefully these will run on most browsers and will be used by other developers in their projects by just importing them like:
import { AnimatedText } from "animated-text-component";
I've uploaded this template on github, feel free to use it and build your own personal component libraries!