vite-legacy-pass-through
v1.2.0
Published
Vite plugin to pass through legacy assets without transformation
Downloads
640
Maintainers
Readme
vite-legacy-pass-through ⚡
A Vite plugin that marks legacy libraries as external, preventing Rolldown from bundling them and causing CommonJS interop errors at runtime.
🧩 The story behind this plugin
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:
- 🏛️ lib-legacy — an older component library with
prop-typesas a dependency. Used directly inside a Vite-powered web app, everything worked perfectly fine. - ✨ lib-awesome — a newer library built to override and extend UI and functionality from
lib-legacy. Its components imported fromlib-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:1After 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.
📦 Installation
npm install -D vite-legacy-pass-through🚀 Usage
// 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'],
})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/formatRunning 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'],
})⚙️ Options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| libs | string[] | Yes | — | List of library names to mark as external. Empty strings are ignored. Must have at least one valid entry. |
| 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. |
| showLog | boolean | No | false | Logs each resolved import to the console. |
Default excluded extensions
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 .htmlTo disable exclusions entirely, pass excludeExtensions: [].
🔍 How it works
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.
🤔 When to use this
- You are building a library that imports and re-exports from a legacy package.
- That legacy package uses CommonJS internally (e.g.
prop-types, older UI kits). - Vite 8 / Rolldown is wrapping those CJS modules into the bundle and generating
require()calls that break in ESM environments. - The legacy package will be available at runtime in the consumer's environment (i.e. it is a peer or runtime dependency, not something you need to ship inside your bundle).
📋 Requirements
- Vite:
^8.0.0 - Node.js:
>=18
📄 License
MIT
