vite-plugin-barrel-subpath-imports
v0.1.1
Published
Vite plugin that rewrites barrel-file imports into per-subpath imports at transform time, avoiding the full-barrel inline-sourcemap cost that hangs DevTools on every dev-server page load.
Maintainers
Readme
vite-plugin-barrel-subpath-imports
Rewrites import { X, Y } from '<barrel>' (and the export { X } from '...' re-export form) into
one import per subpath, at transform time — so you can keep writing normal barrel-style imports in
your own code while Vite's dev server never has to serve the whole barrel file on every request.
Why
A barrel file (one module re-exporting everything a package or directory contains) is convenient,
but under Vite's dev pipeline, any file that isn't pre-bundled — a symlinked workspace package
excluded from optimizeDeps, or your own project source — gets served through Vite's live
per-request transform. That transform unconditionally embeds a full-content inline sourcemap. For a
barrel that re-exports dozens or hundreds of modules, the sourcemap scales with the whole
barrel's content, not just the few exports any given file actually imports — which is what can
hang the browser DevTools' own Sources/Network panels for several seconds on every page load,
even though only a handful of icons or components were ever used.
This plugin rewrites the import statement instead, so each consumer only ever pulls in the specific subpath module it needs — normal barrel-style code stays fast, without anyone needing to remember a per-subpath import convention by hand.
Install
npm install --save-dev vite-plugin-barrel-subpath-importsUsage
// vite.config.ts
import { barrelSubpathImports, buildBarrelIndexExportMap } from 'vite-plugin-barrel-subpath-imports';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
barrelSubpathImports('my-ui-subpath-imports', '@my-org/ui', () =>
buildBarrelIndexExportMap('../packages/ui/src/index.ts'),
),
],
});Any import { Button } from '@my-org/ui' in your source is rewritten (at transform time only —
your source files are never touched) into import { Button } from '@my-org/ui/components/Button';
(or whatever subpath buildBarrelIndexExportMap resolved from the barrel's own export { Button }
from './components/Button' line).
barrelSubpathImports(pluginName, moduleSpecifier, loadExportMap)
The plugin factory. moduleSpecifier is the barrel's import specifier as it appears in your source
('@my-org/ui', 'lucide-react', a relative alias like 'shared/components', …).
loadExportMap is called once (lazily, on first matching file) and must return a
Record<string, BarrelExportInfo> mapping each exported name to where it actually lives:
interface BarrelExportInfo {
from: string; // subpath appended to moduleSpecifier, e.g. 'components/Button'
isType?: boolean; // re-emit as `import type` / `export type`
isDefault?: boolean; // the subpath module's export is `default`, not a named export matching the key
}You're free to build this map however fits your barrel's shape — three ready-made builders cover the common cases:
buildBarrelIndexExportMap(barrelIndexFile)— parses a single-file barrel ofexport { X, Y } from './sub'/export type {...} from './sub're-export statements (one level deep — each re-exported module is a leaf, not itself another barrel to recurse into).buildDirectoryBarrelExportMap(rootDir)— for a directory of subfolders, each its own small barrel (index.ts/.tsxre-exporting that subfolder's components/types), referenced from one rootindex.tsviaexport * from './subfolder'lines. Recurses throughexport * from './relative're-exports within each subfolder's own files too.buildDefaultReexportBarrelExportMap(options)— for a barrel where each export is adefault as Xre-export from its own leaf module (an icon library is the classic case: one file per icon, each a default export). TakesbarrelPathplus optionalstatementPattern/toSubpathoverrides for a barrel whose generated syntax or on-disk layout doesn't match the permissive default. A ready-madelucide-reactpreset built on top of this is deliberately not shipped here — see the example below.
// A lucide-react preset, built from the generic pieces above — worth keeping in your own repo
// (once, shared across every vite.config.ts that needs it) rather than hand-rolling per project:
import { createRequire } from 'node:module';
import { buildDefaultReexportBarrelExportMap } from 'vite-plugin-barrel-subpath-imports';
export function buildLucideReactExportMap() {
const require = createRequire(import.meta.url);
return buildDefaultReexportBarrelExportMap({
barrelPath: require.resolve('lucide-react/dist/esm/lucide-react.mjs'),
// lucide-react's generated barrel also re-exports a few non-icon helpers from this same file —
// restrict to just the icon re-exports.
statementPattern: /export\s*\{([^}]+)\}\s*from\s*'(\.\/icons\/[^']+)';/g,
// The barrel lives inside dist/esm/ itself, so the icon paths it re-exports (relative to
// itself) need that prefix added back to be valid `lucide-react/...` subpaths; the `.mjs`
// extension is stripped to match how this plugin writes its rewritten imports.
toSubpath: (p) => `dist/esm/${p.replace('./', '').replace('.mjs', '')}`,
});
}If none of these fit, write your own loadExportMap function — it just needs to return the same
Record<string, BarrelExportInfo> shape.
Caveats
- Statement detection uses
es-module-lexer(the same parser Vite uses internally), so it only ever rewrites confirmedimport/export ... fromstatements — never touches an unrelated statement. Its one gap is TypeScript'sexport type { X } from '...'re-export syntax, whiches-module-lexerdoesn't recognize as either an import or an export; this plugin covers that one shape with a narrowly-anchored regex fallback. - Runs at Vite's default transform stage (no
enforce: 'pre'), so JSX/TSX syntax has already been stripped by@vitejs/plugin-react's ownpretransform by the time this plugin sees the code —es-module-lexerthrows on raw JSX. - If any name in an import/export statement isn't present in the export map, that whole statement is left untouched rather than partially rewritten.
License
MIT
