A Vite plugin that marks legacy libraries as external, preventing Rolldown from bundling them and causing CommonJS interop errors at runtime.
This plugin was born out of a real-world headache while juggling a legacy component library and a newer one built on top of it.
The setup looked like this:
prop-types as a dependency. Used directly inside a Vite-powered web app, everything worked perfectly fine.lib-legacy. Its components imported from lib-legacy, added behaviour, and re-exported them.The problem surfaced the moment lib-awesome was built with Vite 8. Because it imported components from lib-legacy and re-exported them, Rolldown pulled prop-types deep into the bundle. The output contained a file named something like prop-types-a1b2c3d4.js with a bare require(...) call ā which blew up at runtime in ESM environments:
ReferenceError: require is not defined
at prop-types-a1b2c3d4.js:1:1
After a lot of reading about how Vite 8 and Rolldown handle module bundling and CJS/ESM interop, the cleanest escape hatch turned out to be telling Rolldown: "don't touch lib-legacy ā let it pass through as-is."
That's exactly what this plugin does.
flowchart TD
subgraph without["ā Without the plugin"]
A[lib-awesome] -->|imports & re-exports| B[lib-legacy]
B -->|has dependency| C[prop-types CJS]
A -->|build| D[Rolldown bundles everything]
D --> E["prop-types-a1b2c3d4.js\nā ļø require() call inside"]
E --> F["š„ ReferenceError: require is not defined"]
end
subgraph with["ā
With vite-legacy-pass-through"]
G[lib-awesome] -->|imports & re-exports| H[lib-legacy]
H -->|has dependency| I[prop-types CJS]
G -->|build| J[Rolldown sees lib-legacy as external]
J --> K["lib-legacy stays as import statement\nā
no bundling, no require()"]
K --> L["š Works at runtime"]
end
ā ļø Important: Rolldown does not recommend marking packages as external this way in library builds. Doing so shifts the module resolution responsibility entirely to the consumer ā they must have the library available in their environment. Use this plugin only when you understand that trade-off and the legacy library is guaranteed to be present at runtime.
npm install -D vite-legacy-pass-through
// vite.config.ts
import { defineConfig } from 'vite'
import { legacyPassThrough } from 'vite-legacy-pass-through'
export default defineConfig({
plugins: [
legacyPassThrough({
libs: ['lib-legacy'],
}),
],
})
Multiple libraries:
legacyPassThrough({
libs: ['lib-legacy', 'another-legacy-lib'],
})
To externalize a bare library import as well as its subpaths:
legacyPassThrough({
libs: ['lib-legacy'],
matchBareImports: true,
})
With logging enabled (useful during development to confirm which imports are being bypassed):
legacyPassThrough({
libs: ['lib-legacy'],
showLog: true,
})
Output when showLog: true:
[vite-legacy-pass-through] Resolving: lib-legacy/components/Button
[vite-legacy-pass-through] Resolving: lib-legacy/utils/format
Running in both build and dev (e.g. if you need it in Storybook too):
legacyPassThrough({
libs: ['lib-legacy'],
apply: 'serve', // or omit for the default 'build'
})
Overriding the excluded extensions (replaces the default list entirely):
import { legacyPassThrough, DEFAULT_EXCLUDE_EXTENSIONS } from 'vite-legacy-pass-through'
legacyPassThrough({
libs: ['lib-legacy'],
// extend the default list
excludeExtensions: [...DEFAULT_EXCLUDE_EXTENSIONS, '.yaml'],
})
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
libs |
string[] |
Yes | ā | List of library names to mark as external. Names are trimmed; empty entries are ignored. At least one valid entry is required. |
apply |
'build' | 'serve' |
No | 'build' |
When to apply the plugin. Defaults to 'build' to avoid interfering with dev tools like Storybook. |
excludeExtensions |
string[] |
No | See below | File extensions to skip ā imports ending with these are left for Vite to handle normally. Replaces the default list when provided. |
matchBareImports |
boolean |
No | false |
Also externalize an exact bare import such as import 'lib-legacy'. |
showLog |
boolean |
No | false |
Logs each resolved import to the console. |
Imports from a matched lib that end with any of these extensions are not marked as external:
.css .scss .sass .less .styl
.png .jpg .jpeg .gif .svg .webp
.woff .woff2 .ttf .eot
.json .html
To disable exclusions entirely, pass excludeExtensions: [].
/. This supports scoped packages such as @scope/library/components/Button and prevents matching library-extra when library is configured.import 'library' are intentionally left untouched unless matchBareImports: true is set.library/styles.css?inline.The plugin hooks into Vite's resolveId phase with enforce: 'pre' ā meaning it runs before any other plugin ā and marks any import whose path starts with <lib>/ as external. Rolldown then skips bundling it entirely and leaves the import statement untouched in the output.
import Button from 'lib-legacy/components/Button'
ā resolveId hook intercepts
{ id: 'lib-legacy/components/Button', external: true }
ā Rolldown skips it, output keeps the import
import Button from 'lib-legacy/components/Button'
Note: bare imports (
import 'lib-legacy'without a subpath) are not affected ā only subpath imports (lib-legacy/...) are matched. This is intentional to avoid over-matching.
prop-types, older UI kits).require() calls that break in ESM environments.^8.0.0>=20.19.0Install dependencies with npm ci, then use the following commands:
| Command | Purpose |
|---|---|
npm run check |
Run ESLint, Biome, TypeScript, and the test suite. |
npm run build |
Build ESM, CommonJS, and declaration outputs with tsup. |
npm run test:coverage |
Run the test suite with V8 coverage. |
npm run pack:check |
Preview the files that would be published to npm. |
npm audit --audit-level=high |
Fail on high- or critical-severity dependency advisories. |
npm run docs |
Generate the public API reference in docs/api. |
The release workflow uses a clean, lockfile-based install, runs these quality and security checks, and validates the package contents before publishing. Dependency and GitHub Action updates are proposed weekly by Dependabot.
See SECURITY.md for supported versions and private vulnerability reporting instructions.
See CONTRIBUTING.md for the development workflow, test expectations, and release requirements.
MIT