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:
- 🧩 Enables dynamic plugin architectures with zero runtime import hacks
- 🏗️ Works seamlessly with Vite and React, even in production
- 🧠 Avoids eval hacks or dynamic
import()
messes - 🌍 Can support tenant-aware or remote module resolution
⚠️ Cons:
- 📦 No support in vanilla webpack (you’d need custom loader equivalents)
- 🔎 Must manage your plugin directory structure tightly
- 🐞 Poor DX if a plugin fails to resolve — consider fallback/error boundaries
Summary
If you're building a plugin system, tenant-aware override engine, or any kind of app that needs to swap modules dynamically, this Vite-based approach is gold. With custom path aliasing + virtual module handling, you get the flexibility of runtime module resolution without compromising on dev experience or bundle safety. Perfect for platforms, marketplaces, and tools with user-contributed extensions.
If this was helpful, you can support me here: Buy Me a Coffee ☕