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:

Apr 29, 2025 - 08:39
 0
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: