Skip to content

Virtual module imports break esbuild dependency optimization when vinext is installed from npm #1

@threepointone

Description

@threepointone

Description

When vinext is installed as an npm package (rather than a symlinked local dependency), vite dev fails with:

✘ [ERROR] Could not resolve "virtual:vinext-rsc-entry"

    node_modules/.pnpm/vinext@0.0.1_.../node_modules/vinext/dist/server/app-router-entry.js:15:23:
      15 │ import rscHandler from "virtual:vinext-rsc-entry";
         ╵                        ~~~~~~~~~~~~~~~~~~~~~~~~~~

This happens during esbuild's dependency optimization pass. The optimizer scans vinext/dist/server/app-router-entry.js, encounters the virtual:vinext-rsc-entry import, and fails because virtual modules only exist at Vite plugin resolution time — not during esbuild's pre-bundling scan.

Why this wasn't an issue before

When vinext is a symlinked package ("vinext": "link:../vinext/packages/vinext"), Vite automatically excludes linked packages from dependency optimization. Once installed from npm ("vinext": "^0.0.1"), esbuild starts scanning it and hits the unresolvable virtual imports.

The error occurs in multiple Vite environments (client, rsc, ssr) since app-router-entry.js can be pulled into the dep optimization graph from any of them.

Proposed fix

The vinext plugin should add an esbuild plugin in its config() hook to externalize its own virtual modules during dependency optimization:

// In the vinext plugin config() hook
config() {
  return {
    optimizeDeps: {
      esbuildOptions: {
        plugins: [{
          name: "vinext-externalize-virtual",
          setup(build) {
            build.onResolve({ filter: /^virtual:vinext-/ }, (args) => ({
              path: args.path,
              external: true,
            }));
          },
        }],
      },
    },
  };
}

This is targeted — it only externalizes vinext's own virtual:vinext-* modules during the esbuild scan, without affecting pre-bundling of anything else.

An alternative (less targeted) approach would be to add "vinext" to optimizeDeps.exclude for all environments, but this prevents pre-bundling of all vinext exports even when unnecessary.

Current workaround

Users can add this to their vite.config.ts:

export default defineConfig({
  optimizeDeps: {
    exclude: ["vinext"],
    esbuildOptions: {
      plugins: [{
        name: "externalize-virtual-modules",
        setup(build) {
          build.onResolve({ filter: /^virtual:/ }, (args) => ({
            path: args.path,
            external: true,
          }));
        },
      }],
    },
  },
  environments: {
    rsc: {
      optimizeDeps: {
        exclude: ["vinext"],
      },
    },
    ssr: {
      optimizeDeps: {
        exclude: ["vinext"],
      },
    },
  },
});

This needs to cover all three environments (client via top-level, rsc, ssr) to prevent the error in each.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions