Runtime Import Path Rewriting in Vite (for Plugins, Multi-Tenant Apps, or Dynamic Modules)
Most bundlers resolve imports at build time. But what if you need to rewrite import paths at runtime — say, to support custom plugins, multi-tenant module overrides, or dynamic remote loading? In this guide, you’ll learn how to intercept and rewrite ES module imports dynamically inside a Vite-based React app, without breaking tree-shaking, dev server, or production builds. Use Case: Dynamic Plugin Loader Imagine you're building a React app that supports 3rd-party plugins. You want to allow paths like: import Widget from "@plugins/widget-abc"; But you won’t know widget-abc at build time — it could change per user or tenant. Let’s fix that. Step 1: Alias a Fake Module Prefix In your vite.config.ts, create an alias that points to a virtual module handler: import { defineConfig } from "vite"; export default defineConfig({ resolve: { alias: { "@plugins/": "/@virtual/plugin/", }, }, }); This tells Vite: any @plugins/foo should redirect to /@virtual/plugin/foo. Step 2: Create a Vite Plugin for Virtual Module Resolution Now, use a Vite plugin to handle those fake imports: export default function PluginImportResolver() { return { name: "dynamic-plugin-resolver", resolveId(source) { if (source.startsWith("/@virtual/plugin/")) { return source; } }, async load(id) { if (!id.startsWith("/@virtual/plugin/")) return; const pluginName = id.split("/").pop(); // Simulate remote resolution or tenant-specific override const resolvedPath = `/src/plugins/${pluginName}.tsx`; return \`export { default } from "${resolvedPath}";\`; }, }; } Add it to vite.config.ts: plugins: [PluginImportResolver()] Step 3: Add a Fallback Plugin or Remote Loader (Optional) If the plugin isn’t local, you can even fetch remote modules dynamically: if (!(await fileExists(resolvedPath))) { const url = \`https://cdn.example.com/plugins/\${pluginName}.js\`; return \`export * from "\${url}";\`; } Or fallback to a default: return \`export { default } from "/src/plugins/fallback.tsx";\`; Step 4: Use It in Your App Now your app can import plugins dynamically — even if the plugin is resolved per-user or per-tenant: import Widget from "@plugins/widget-abc"; This works in dev (thanks to Vite's virtual modules), and builds correctly in production too. ✅ Pros:
Most bundlers resolve imports at build time. But what if you need to rewrite import paths at runtime — say, to support custom plugins, multi-tenant module overrides, or dynamic remote loading?
In this guide, you’ll learn how to intercept and rewrite ES module imports dynamically inside a Vite-based React app, without breaking tree-shaking, dev server, or production builds.
Use Case: Dynamic Plugin Loader
Imagine you're building a React app that supports 3rd-party plugins. You want to allow paths like:
import Widget from "@plugins/widget-abc";
But you won’t know widget-abc
at build time — it could change per user or tenant.
Let’s fix that.
Step 1: Alias a Fake Module Prefix
In your vite.config.ts
, create an alias that points to a virtual module handler:
import { defineConfig } from "vite";
export default defineConfig({
resolve: {
alias: {
"@plugins/": "/@virtual/plugin/",
},
},
});
This tells Vite: any @plugins/foo
should redirect to /@virtual/plugin/foo
.
Step 2: Create a Vite Plugin for Virtual Module Resolution
Now, use a Vite plugin to handle those fake imports:
export default function PluginImportResolver() {
return {
name: "dynamic-plugin-resolver",
resolveId(source) {
if (source.startsWith("/@virtual/plugin/")) {
return source;
}
},
async load(id) {
if (!id.startsWith("/@virtual/plugin/")) return;
const pluginName = id.split("/").pop();
// Simulate remote resolution or tenant-specific override
const resolvedPath = `/src/plugins/${pluginName}.tsx`;
return \`export { default } from "${resolvedPath}";\`;
},
};
}
Add it to vite.config.ts
:
plugins: [PluginImportResolver()]
Step 3: Add a Fallback Plugin or Remote Loader (Optional)
If the plugin isn’t local, you can even fetch remote modules dynamically:
if (!(await fileExists(resolvedPath))) {
const url = \`https://cdn.example.com/plugins/\${pluginName}.js\`;
return \`export * from "\${url}";\`;
}
Or fallback to a default:
return \`export { default } from "/src/plugins/fallback.tsx";\`;
Step 4: Use It in Your App
Now your app can import plugins dynamically — even if the plugin is resolved per-user or per-tenant:
import Widget from "@plugins/widget-abc";
This works in dev (thanks to Vite's virtual modules), and builds correctly in production too.
✅ Pros: