Rollup Bundler Configuration for Web Project
Rollup occupies a specific niche among bundlers: it's optimal when the result is a reusable library, not an application. NPM packages, design systems, SDKs, utilities — this is where Rollup beats Webpack and Vite due to tree-shaking quality and support for multiple output formats from one config.
When to choose Rollup
Rollup is not a universal tool. For SPAs with hot reload and dev server, Vite is more convenient (which itself uses Rollup internally for production builds). Choose Rollup when you need to:
- build a library in ESM + CJS + UMD formats simultaneously
- get maximally clean output without extra wrappers
- control which dependencies are included in bundle, which remain
external - generate TypeScript declarations alongside compiled files
Installation and basic config
npm install --save-dev rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-dts
rollup.config.ts for typical TypeScript library:
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import dts from 'rollup-plugin-dts';
import { defineConfig } from 'rollup';
import pkg from './package.json' assert { type: 'json' };
export default defineConfig([
// Main build: ESM + CJS
{
input: 'src/index.ts',
external: Object.keys(pkg.peerDependencies ?? {}),
plugins: [
resolve({ extensions: ['.ts', '.tsx'] }),
commonjs(),
typescript({ tsconfig: './tsconfig.build.json' }),
],
output: [
{
file: pkg.module, // dist/index.esm.js
format: 'esm',
sourcemap: true,
},
{
file: pkg.main, // dist/index.cjs.js
format: 'cjs',
sourcemap: true,
exports: 'named',
},
],
},
// TypeScript declarations
{
input: 'dist/types/index.d.ts',
output: { file: 'dist/index.d.ts', format: 'esm' },
plugins: [dts()],
},
]);
package.json: exports map
Modern package.json for library with proper exports:
{
"name": "my-lib",
"version": "1.0.0",
"type": "module",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js",
"types": "./dist/index.d.ts"
}
},
"files": ["dist"]
}
External dependencies and peer deps
Common mistake — including React or lodash in library bundle. external should list all peer dependencies:
// Automatically from package.json
const external = [
...Object.keys(pkg.peerDependencies ?? {}),
...Object.keys(pkg.dependencies ?? {}),
];
If partial externalization is needed (e.g., include only lodash/merge, not all of lodash):
external: (id) => id.startsWith('react') || /^lodash/.test(id),
Working with CSS and assets
For libraries with CSS modules:
npm install --save-dev rollup-plugin-postcss
import postcss from 'rollup-plugin-postcss';
plugins: [
postcss({
modules: true,
extract: 'dist/styles.css', // separate file instead of inject
minimize: true,
}),
]
Assets (SVG, images):
import url from '@rollup/plugin-url';
plugins: [
url({
include: ['**/*.svg'],
limit: 8192, // inline base64 up to 8KB, otherwise copy file
destDir: 'dist/assets',
}),
]
Multi-entry build
For design system where each component is imported separately (import Button from 'ui/Button'):
import { glob } from 'glob';
const entries = Object.fromEntries(
(await glob('src/components/**/*.tsx')).map((file) => [
file.replace('src/', '').replace(/\.tsx$/, ''),
file,
])
);
export default defineConfig({
input: entries,
output: {
dir: 'dist',
format: 'esm',
preserveModules: true,
preserveModulesRoot: 'src',
},
// ...
});
preserveModules: true doesn't combine everything into one file but preserves directory structure — this allows tree-shaking on consumer side to work at file level.
Bundle size analysis
npm install --save-dev rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer';
plugins: [
visualizer({
filename: 'dist/stats.html',
gzipSize: true,
brotliSize: true,
}),
]
After build, open dist/stats.html — interactive dependency tree with actual sizes after gzip.
Watch mode and development
rollup -c --watch
Or via config for finer control:
watch: {
include: 'src/**',
exclude: 'node_modules/**',
clearScreen: false,
},
For library development in parallel with consumer application — use npm link or file: dependency in consumer's package.json. In monorepo — workspace links.
Timeline
Basic Rollup setup for TypeScript library (single entry, ESM+CJS, declarations): 2–4 hours. Complex setup with CSS modules, multi-entry build, CI setup for automatic npm release: 1–2 days.







