Rollup (v4) is the bundler of choice for libraries. It produces smaller, cleaner code than Webpack and pioneered Tree Shaking.
When to Use
- Modern JavaScript/TypeScript Library Packaging: Generating optimized ESM and CommonJS bundles with tree-shaking.
- Zero-Overhead Code Splitting: Emitting dynamic import chunks and shared modules with minimal wrapper boilerplate.
- Design System & Component Library Distribution: Bundling React, Vue, or Web Components with externalized peer dependencies.
- Custom Toolchain Compilation: Powering modern bundlers (such as Vite) under the hood for production build generation.
Quick Start
1. Minimal rollup.config.mjs
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import terser from "@rollup/plugin-terser";
export default {
input: "src/index.ts",
output: [
{ file: "dist/index.cjs", format: "cjs", sourcemap: true },
{ file: "dist/index.mjs", format: "esm", sourcemap: true },
],
plugins: [
resolve(),
commonjs(),
typescript({ tsconfig: "./tsconfig.json" }),
terser(),
],
external: ["react", "react-dom"],
};
2. Build via CLI
npx rollup -c --bundleConfigAsCjs
Core Concepts
Production ESM / CJS Library Configuration (rollup.config.mjs)
Building a TypeScript library with external dependencies and source maps:
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import terser from "@rollup/plugin-terser";
import peerDepsExternal from "rollup-plugin-peer-deps-external";
export default {
input: "src/index.ts",
output: [
{
file: "dist/index.mjs",
format: "esm",
sourcemap: true,
},
{
file: "dist/index.cjs",
format: "cjs",
sourcemap: true,
exports: "named",
},
],
plugins: [
peerDepsExternal(),
resolve({ extensions: [".ts", ".js"] }),
commonjs(),
typescript({
tsconfig: "./tsconfig.json",
declaration: true,
declarationDir: "dist/types",
}),
terser({
format: { comments: false },
compress: { drop_console: true },
}),
],
external: ["react", "react-dom"],
};
Tree-Shaking Verification & Pure Annotations
Optimizing tree-shaking by marking side-effect-free packages and pure function calls:
// package.json
{
"name": "my-math-lib",
"sideEffects": false
}
// src/utils.ts
/* @__PURE__ */
export const complexCalculation = (a, b) => {
return a * 1000 + b;
};
Multi-Entrypoint Build Automation
Bundling multiple package subpath exports (my-lib/core, my-lib/react):
export default [
{
input: "src/core/index.ts",
output: { dir: "dist/core", format: "esm" },
plugins: [typescript()],
},
{
input: "src/react/index.ts",
output: { dir: "dist/react", format: "esm" },
plugins: [typescript()],
external: ["react"],
},
];
Common Patterns
Library Bundling with External Dependencies
Problem: Third-party peer dependencies (e.g. lodash, react) get bundled into output library instead of being treated as externals.
Solution: Declare externals using regular expression or peerDependencies keys.
import pkg from "./package.json" assert { type: "json" };
export default {
input: "src/index.ts",
external: [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
/^node:/, // Mark node built-ins external
],
// ...
};
Preserving Directory Structure (Multi-entry Code Splitting)
Problem: Need to build a library where users can import submodules (e.g. import { helper } from 'my-lib/utils').
Solution: Set output.preserveModules = true.
export default {
input: ["src/index.ts", "src/utils.ts"],
output: {
dir: "dist",
format: "esm",
preserveModules: true,
preserveModulesRoot: "src",
},
};
Best Practices
Do:
- Externalize runtime peer dependencies (
react,lodash) to avoid bundling duplicate copies into consumer packages. - Set
"sideEffects": falseinpackage.jsonto enable consumer bundlers to aggressively tree-shake unused code. - Emit TypeScript declaration files (
.d.ts) alongside bundles using@rollup/plugin-typescript. - Use
.mjsextension for Rollup configuration files to ensure native ESM loading in modern Node.js.
Don't:
- Use Rollup for complex SPAs when modern bundlers like Vite or Turbopack provide superior dev server workflows.
- Disable source maps for production library builds; consumers need them for stack trace debugging.
- Bundle large Node.js built-ins (
fs,path) into browser library targets without polyfill wrappers.
Troubleshooting
| Error | Cause | Solution |
|---|---|---|
Unresolved dependencies: '...' treated as external |
Package import not found by Rollup resolver | Add @rollup/plugin-node-resolve to the plugins array. |
Cannot find module ... or its corresponding type declarations |
TypeScript plugin tsconfig path mismatch | Configure typescript({ tsconfig: "./tsconfig.json", declaration: true, outDir: "dist" }). |
Circular dependency: a.js -> b.js -> a.js |
Circular imports causing runtime initialization issues | Refactor circular references or suppress specific warning via onwarn(warning, warn) hook. |