vite-legacy-interop - v1.0.3
    Preparing search index...

    vite-legacy-interop - v1.0.3

    vite-legacy-interop ⚑

    npm version npm downloads license CI

    A Vite plugin that wraps legacy CJS subpath imports in ESM-compatible virtual modules, preventing CommonJS interop errors at runtime with Rolldown.


    When building a library with Vite 8 that consumes a legacy CJS package via subpath imports (legacy-lib/lib/Button), Rolldown can produce invalid output that breaks at runtime:

    SyntaxError: The requested module 'legacy-lib/lib/Button' does not provide an export named 'default'
    

    or worse:

    ReferenceError: require is not defined
    

    This happens because the legacy package exposes CommonJS modules under a lib/ folder (e.g. legacy-lib/lib/Button.js) and Rolldown does not perform the CJS→ESM interop needed to consume them cleanly.

    vite-legacy-interop intercepts those subpath imports and replaces them with virtual ESM modules that handle the interop layer transparently:

    flowchart TD
        subgraph without["❌ Without the plugin"]
            A["import Button from 'legacy-lib/lib/Button'"]
            A --> B["Rolldown bundles CJS module as-is"]
            B --> C["πŸ’₯ Runtime error: no default export / require is not defined"]
        end
    
        subgraph with["βœ… With vite-legacy-interop"]
            D["import Button from 'legacy-lib/lib/Button'"]
            D --> E["resolveId β†’ virtual module '\0legacy-interop:legacy-lib/lib/Button'"]
            E --> F["load β†’ ESM wrapper: import _mod from '....js'; export default _mod.default ?? _mod"]
            F --> G["πŸš€ Clean ESM output, works at runtime"]
        end
    

    npm install -D vite-legacy-interop
    

    // vite.config.ts
    import { defineConfig } from 'vite'
    import { legacyInterop } from 'vite-legacy-interop'

    export default defineConfig({
    plugins: [
    legacyInterop({
    libs: ['legacy-lib'],
    }),
    ],
    })

    If your legacy package exposes modules under a folder other than lib/:

    legacyInterop({
    libs: [{ name: 'legacy-lib', libDir: 'dist' }],
    })

    Mix string shorthand and full config objects freely:

    legacyInterop({
    libs: [
    'leg-libl',
    { name: 'another-legacy-lib', libDir: 'dist' },
    ],
    })

    Works out of the box β€” the plugin scans recursively:

    import Column from 'legacy-lib/lib/Grid/Column'
    import Row from 'legacy-lib/lib/Grid/Row'
    legacyInterop({
    libs: ['legacy-lib'],
    showLog: true,
    })

    Output:

    [vite-legacy-interop] Resolving: legacy-lib/lib/Button
    [vite-legacy-interop] Resolving: legacy-lib/lib/Grid/Column

    Use apply to restrict the plugin to a specific Vite phase:

    legacyInterop({
    libs: ['legacy-lib'],
    apply: 'build', // or 'serve'
    })

    When omitted, the plugin runs during both build and serve (Vite default).


    Option Type Required Default Description
    libs (string | LibConfig)[] Yes β€” Libraries to intercept. Empty strings are ignored. At least one valid entry is required.
    showLog boolean No false Logs each resolved import path to the console.
    apply 'build' | 'serve' No both Restricts the plugin to the build or serve phase only.
    Property Type Required Default Description
    name string Yes β€” Package name as it appears in import statements.
    libDir string No 'lib' Subfolder inside the package to scan for modules.

    At startup, the plugin scans the libDir folder of each configured package and builds a Set of available modules (recursively, supporting nested paths). At build time it hooks into two Vite phases:

    1. resolveId (enforce: 'pre') β€” intercepts any import matching <lib>/<libDir>/<path>. If the module exists in the Set, returns a virtual module ID. If not, emits a warning and lets Vite handle it normally.

    2. load β€” receives the virtual ID and returns an ESM wrapper:

    import * as _modNs from '/absolute/path/to/legacy-lib/lib/Button.js';
    const _mod = 'default' in _modNs ? _modNs.default : _modNs;
    const _default = _mod && _mod.__esModule && 'default' in _mod ? _mod.default : _mod;
    export default _default;

    Using a namespace import (import * as) avoids the "does not provide an export named 'default'" error that Rolldown throws when a CJS module has no explicit default export. The wrapper then normalises the value regardless of whether the original module uses module.exports, exports.default, or __esModule interop.


    Scenario Plugin
    Legacy lib will be available at runtime β€” you don't need to bundle it vite-legacy-pass-through
    Legacy lib must be included in the bundle but causes CJS/ESM interop errors vite-legacy-interop

    • Vite: ^8.0.0
    • Node.js: >=18

    MIT