cheat sheet

rollup

Package-level reference for rollup on npm — library bundling, multi-format output, plugin ecosystem, tree shaking, and the Rolldown migration.

rollup

What it is

rollup is a JavaScript module bundler optimised for libraries — it takes an ESM source tree and emits clean, tree-shaken bundles in ES modules, CommonJS, UMD, IIFE, or AMD formats. It was the first mainstream tool to do tree shaking properly (drop unused exports across module boundaries) and remains the default choice for publishing reusable code to npm.

Rollup powers Vite's production builds, most TypeScript library tooling (tsup wraps esbuild, but many projects still pick Rollup direct), Astro's component bundling, and the npm pipeline for nearly every popular framework (React's own build is Rollup-based, as are Vue, Svelte, Preact, and most of the ecosystem). For apps you usually want Vite (which uses Rollup under the hood); for libraries you publish, Rollup direct gives you the most control.

Install

bash
# Project-local
npm install -D rollup
pnpm add -D rollup
yarn add -D rollup
bun add -d rollup

Output: rollup CLI under node_modules/.bin/rollup.

bash
# Common companion plugins
npm install -D @rollup/plugin-node-resolve @rollup/plugin-commonjs
npm install -D @rollup/plugin-typescript tslib
npm install -D @rollup/plugin-terser            # minification
npm install -D rollup-plugin-dts                # bundle .d.ts files

Output: plugins under node_modules/; import in rollup.config.js.

Versioning & Node support

  • Current line is 4.x (released mid-2023). The 4.0 switch was significant — internal SWC-based parser, faster builds, breaking plugin-API tweaks.
  • Requires Node 18 or newer for the 4.x line.
  • rollup.config.js (CJS) and rollup.config.mjs (ESM) both supported; Rollup transpiles configs on the fly.
  • SemVer respected for the core; plugins from @rollup/* and community have their own versioning.
  • The 5.x line is on the roadmap but not yet released; Rolldown (Rust port) is being developed in parallel and will eventually subsume Rollup.

Package metadata

  • Maintainers: Rich Harris (creator) + Rollup core team (now under VoidZero/vitejs umbrella)
  • Project home: github.com/rollup/rollup
  • Docs: rollupjs.org
  • npm: npmjs.com/package/rollup
  • License: MIT
  • First released: 2015
  • Downloads: ~30M weekly — high because Vite depends on it transitively.

Peer dependencies & extras

rollup has zero peers. The plugin model is its core extension point — most non-trivial setups use 3–6 plugins:

PluginRole
@rollup/plugin-node-resolveResolve node_modules imports (Rollup is browser-first by default).
@rollup/plugin-commonjsConvert CJS deps to ESM so they bundle cleanly.
@rollup/plugin-typescriptTS compilation via the TypeScript compiler.
@rollup/plugin-swc / @rollup/plugin-esbuildFaster TS compilation via SWC or esbuild.
@rollup/plugin-terserJS minification.
@rollup/plugin-replaceStatic string substitution (process.env.NODE_ENV"production").
@rollup/plugin-jsonImport JSON files.
rollup-plugin-dtsBundle .d.ts files (TypeScript declarations) into one file.
rollup-plugin-visualizerBundle treemap for size analysis.

Alternatives

ToolTrade-off
tsupWrapper around esbuild aimed at TS libraries. Faster than Rollup; less control over output.
esbuildUsed directly for fast builds; smaller plugin ecosystem; less ergonomic library output.
RolldownRust rewrite of Rollup. Aiming for Rollup-API compatibility; pre-1.0 — production use with care.
webpackApp-focused; library output is verbose and ugly without manual config.
Vite library modeWraps Rollup under the hood; ergonomic for simple libs. Use Vite for library mode if you don't need exotic plugin chains.
bunup / unbuildHigher-level lib bundlers (Bun's, unjs's) that wrap rollup/esbuild.

Real-world recipes

Library bundling — single ESM + CJS output

javascript
// rollup.config.js
import typescript from "@rollup/plugin-typescript";
import nodeResolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";

export default {
  input: "src/index.ts",
  output: [
    { file: "dist/index.mjs", format: "es" },
    { file: "dist/index.cjs", format: "cjs", exports: "named" },
  ],
  external: ["react"],
  plugins: [
    nodeResolve(),
    commonjs(),
    typescript({ tsconfig: "./tsconfig.build.json" }),
  ],
};
bash
npx rollup -c

Output:

text
src/index.ts → dist/index.mjs, dist/index.cjs...
created dist/index.mjs, dist/index.cjs in 412ms

Match package.json:

json
{
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

Plugin chain — TypeScript + JSX + replace + minify

javascript
import typescript from "@rollup/plugin-typescript";
import nodeResolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import replace from "@rollup/plugin-replace";
import terser from "@rollup/plugin-terser";

export default {
  input: "src/index.tsx",
  output: { file: "dist/bundle.js", format: "es", sourcemap: true },
  external: ["react", "react-dom"],
  plugins: [
    nodeResolve({ extensions: [".js", ".ts", ".tsx"] }),
    commonjs(),
    replace({
      "process.env.NODE_ENV": JSON.stringify("production"),
      preventAssignment: true,
    }),
    typescript({ jsx: "react-jsx" }),
    terser(),
  ],
};

Plugins run top-to-bottom in the plugins array. replace before typescript substitutes in source; after would substitute in compiled JS — usually you want source-level so type-checking sees the right values.

Multi-format output (ESM + CJS + UMD)

javascript
import { defineConfig } from "rollup";

export default defineConfig({
  input: "src/index.ts",
  output: [
    { file: "dist/my-lib.mjs", format: "es" },
    { file: "dist/my-lib.cjs", format: "cjs", exports: "named" },
    {
      file: "dist/my-lib.umd.js",
      format: "umd",
      name: "MyLib",
      globals: { react: "React", "react-dom": "ReactDOM" },
    },
    {
      file: "dist/my-lib.umd.min.js",
      format: "umd",
      name: "MyLib",
      globals: { react: "React", "react-dom": "ReactDOM" },
      plugins: [terser()],
    },
  ],
  external: ["react", "react-dom"],
});

UMD output supports <script src> consumption in browsers; the globals map says where to read external deps at runtime. For modern libraries, ESM + CJS is enough — UMD is increasingly legacy.

javascript
import pkg from "./package.json" with { type: "json" };

export default {
  // ...
  output: {
    file: "dist/index.mjs",
    format: "es",
    banner: `/*! ${pkg.name} v${pkg.version} | (c) ${new Date().getFullYear()} ${pkg.author} | ${pkg.license} */`,
    footer: `/* end of ${pkg.name} */`,
  },
};

Output: the bundle starts with /*! my-lib v1.0.0 | ... */. Important for tools that auto-scan headers for licence info.

Bundling .d.ts declarations

@rollup/plugin-typescript emits one .d.ts per source file. To ship one consolidated .d.ts, add a second Rollup config:

javascript
import dts from "rollup-plugin-dts";

export default {
  input: "dist/types/index.d.ts",
  output: { file: "dist/index.d.ts", format: "es" },
  plugins: [dts()],
};

Or use the tsc --emitDeclarationOnly + rollup-plugin-dts two-step approach. Most published TS libraries do this.

Watch mode

bash
npx rollup -c -w

Output:

text
rollup v4.21.0
bundles src/index.ts → dist/index.mjs...
created dist/index.mjs in 412ms

[1500] waiting for changes...

For library work the watch loop is "edit → rollup rebuilds → consumer in another window re-runs". For dev applications use Vite; Rollup watch is library-development-flow.

Production deployment

Rollup output is the production deployment for libraries — npm publish dist/ (or with files in package.json). Key knobs:

  • sourcemap: true for debugging support; consumers can opt-in.
  • treeshake: { moduleSideEffects: false } if your library has no side-effecting modules — lets consumers tree-shake more aggressively.
  • preserveModules: true when you want to ship individual files rather than one bundle — better tree-shaking for consumers, but more files in the tarball.
javascript
output: {
  dir: "dist",
  format: "es",
  preserveModules: true,
  preserveModulesRoot: "src",
}

Output: mirrors src/ structure under dist/. Best for libraries where consumers do their own tree-shaking.

Performance tuning

Rollup is fast by default — most slowness comes from plugin choice or huge inputs.

Swap plugin-typescript for plugin-swc or plugin-esbuild

javascript
import esbuild from "rollup-plugin-esbuild";

plugins: [esbuild({ target: "es2022" })];

rollup-plugin-typescript calls tsc (slow). rollup-plugin-esbuild and @rollup/plugin-swc use native-speed transpilers. For libraries, pair with tsc --emitDeclarationOnly for .d.ts generation.

Disable sourcemaps when not needed

javascript
output: { sourcemap: false }

Sourcemap generation is a significant cost on large bundles. Disable for prod builds where you upload to an error tracker separately.

Cache plugin output

Plugins can opt into Rollup's cache via this.cache. Most off-the-shelf plugins already use it; custom plugins should too.

Parallel multi-config

bash
npx rollup -c rollup.config.lib.js -c rollup.config.umd.js

Output:

text
src/index.js → dist/index.esm.js, dist/index.cjs.js
created dist in 482ms
src/index.js → dist/index.umd.js
created dist in 391ms

Multiple -c flags run in parallel. Saves wallclock when emitting many formats.

Version migration guide

From → ToHighlights
2 → 3ESM-only Node API. output.intro / output.outro renamed; plugin hooks transform and renderChunk got cache support.
3 → 4SWC-based parser (faster, stricter). Some experimental plugin hooks made stable. Node 14 dropped.
4.x ongoingSteady; minor releases tweak the SWC parser and output.experimentalMinChunkSize heuristics.

The 3→4 jump is mostly transparent for users — only plugin authors using deprecated hooks were affected. The bigger upcoming change is the Rolldown migration: Vite is steering toward Rolldown as the default backend, and Rollup itself will continue as a smaller, stable library.

Security considerations

  1. Plugin supply chain. Same as webpack — plugins run with full Node privileges. Pin versions; review additions.
  2. commonjs() plugin's dynamic require. @rollup/plugin-commonjs parses CJS modules and rewrites require() calls. Malformed CJS (or one using eval-style require) can break the bundle silently. Use the plugin's strictRequires: true mode in libraries.
  3. output.intro content from env. Banners that include process.env.SECRET literally embed the secret in published bundles. Same trap as webpack's DefinePlugin.
  4. external mismatch. Marking a dep external means it's expected to be present at runtime. Forgetting to mark a peerDep external bundles it in — bloating the library and risking double-load issues in consumers.
  5. Source-map leaks in published packages. Don't publish .map files unless you intend consumers to debug into your sources. Add *.map to files exclusion in package.json.

Testing & CI integration

yaml
# .github/workflows/ci.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx tsc --noEmit                  # type-check
      - run: npx rollup -c                     # build
      - run: npx vitest run                    # tests against built output
      - run: npx publint                        # validate package.json exports
      - run: npx attw --pack .                  # arethetypeswrong

The publint + arethetypeswrong (attw) duo is essential for library publishers — they catch ESM/CJS interop issues that don't surface until consumers complain.

Ecosystem integrations

  • Vite uses Rollup for prod builds and shares its plugin API (Vite plugins are a superset of Rollup plugins). Migration between them is mostly mechanical.
  • Storybook ships a Rollup mode alongside webpack.
  • tsup wraps esbuild — Rollup's main library-bundler competitor.
  • unbuild by unjs — wraps Rollup with sane defaults; preferred in the Vue/Nuxt ecosystem for publishing.
  • Astro uses Rollup transitively (via Vite).
  • React publishes via Rollup; the React core repo's rollup.config.js is a canonical large-library example.

Troubleshooting common errors

Could not resolve "X" from "src/Y.js"

The import path doesn't resolve. Either:

  • Missing @rollup/plugin-node-resolve.
  • The module is meant to be external — add to external: ["X"].
  • Path typo or wrong extension.

'default' is not exported by "node_modules/foo"

A CJS dep doesn't have a real default export. Add @rollup/plugin-commonjs and configure requireReturnsDefault: "auto".

Mixing named and default exports warning

text
The 'output.exports' option was set to "named", but it would be better to remove this setting...

Set output.exports: "named" explicitly to silence, or refactor source to use only named OR only default. Mixed exports work but interop awkwardly in CJS consumers.

Tree shaking dropped my side-effecting import

import "./register-polyfills" is removed if Rollup can't see side effects. Add "sideEffects": ["./src/register-polyfills.ts"] to package.json.

Bundle has process.env.NODE_ENV un-substituted

Add @rollup/plugin-replace with "process.env.NODE_ENV": JSON.stringify("production") and preventAssignment: true. Without preventAssignment, the plugin replaces process.env.NODE_ENV = "production" assignments too, often unintentionally.

Watch mode is slow on first run

The plugin chain re-runs from scratch on the first build. Subsequent builds use Rollup's in-memory cache; subsequent processes (CI restarts) don't. Enable persistent cache via rollup-plugin-cache or migrate to Vite for the dev loop.

When NOT to use this

  • Building applications. Use Vite — it wraps Rollup for the prod build but gives you a fast dev server and HMR.
  • Performance-critical builds. Rolldown (Rust) is the future; for very large monorepo libraries the speed delta matters.
  • Tiny single-file scripts. esbuild as a one-liner is enough; Rollup's config overhead isn't worth it.
  • CJS-only projects. Rollup is ESM-first; output works for CJS but the surface area for CJS-specific tricks is smaller than webpack's.
  • No plugin chain. If your build is "transpile TS and emit JS", tsc or esbuild alone is faster and simpler.

See also