[GH-ISSUE #1001] Internal 'use client' shims use relative imports, breaking @vitejs/plugin-rsc's client-in-server-package-proxy against the exports field #219

Closed
opened 2026-05-06 12:38:16 +02:00 by BreizhHardware · 7 comments

Originally created by @eashish93 on GitHub (May 1, 2026).
Original GitHub issue: https://github.com/cloudflare/vinext/issues/1001

Summary

vinext's internal server modules import their own 'use client' shims via relative paths like:

// dist/server/app-page-route-wiring.js
import { LayoutSegmentProvider } from "../shims/layout-segment-context.js";

@vitejs/plugin-rsc's packageSource tracking only fires for bare specifier imports (vinext/shims/X), not relative ones. When plugin-rsc later sees the resolved file is 'use client' and inside node_modules/, it falls back to the client-in-server-package-proxy branch and generates a virtual module that imports via the absolute filesystem path:

// generated virtual module
export * from "/workspace/node_modules/vinext/dist/shims/layout-segment-context.js";
import * as __all__ from "/workspace/node_modules/vinext/dist/shims/layout-segment-context.js";
export default __all__.default;

Vite's resolver respects the package's exports field. vinext's exports only exposes ./shims/*./dist/shims/*.js. The absolute path …/vinext/dist/shims/X.js doesn't match any exports key, so resolution fails and the dev server returns:

[plugin:vite:import-analysis] Failed to resolve import "/workspace/node_modules/vinext/dist/shims/layout-segment-context.js" from "virtual:vite-rsc/client-in-server-package-proxy/%2Fworkspace%2Fnode_modules%2Fvinext%2Fdist%2Fshims%2Flayout-segment-context.js". Does the file exist?

The file IS on disk. The fail is the exports field gate, not fs absence.

Reproduce

Minimal vinext app (just app/page.tsx + app/layout.tsx) on:

  • vinext@0.0.45
  • @vitejs/plugin-rsc@0.5.25
  • vite@8.0.10

vinext dev boots successfully. Loading any page that triggers the app router (i.e. anything reaching dist/server/app-page-route-wiring.js or dist/server/app-page-boundary-render.js) surfaces the error overlay above. Affects every 'use client' shim (layout-segment-context, link, navigation, error-boundary, form, script, slot).

Root cause (in plugin-rsc terms)

In @vitejs/plugin-rsc/dist/plugin-BhzHKRFo.js:

// line 1383 — packageSources only set for bare specifiers
{
  name: "rsc:virtual-client-package",
  resolveId: {
    order: "pre",
    async handler(source, importer, options) {
      if (this.environment.name === serverEnvironmentName && bareImportRE.test(source) && ...) {
        const resolved = await this.resolve(source, importer, options);
        if (resolved && resolved.id.includes("/node_modules/")) {
          packageSources.set(resolved.id, source);  // ← never fires for relative imports
        }
      }
    }
  }
}

// line 1238 — fallback when packageSource is missing
const packageSource = packageSources.get(id);
if (!packageSource && this.environment.mode === "dev" && id.includes("/node_modules/")) {
  importId = `/@id/__x00__virtual:vite-rsc/client-in-server-package-proxy/${encodeURIComponent(id)}`;
  // ↑ generates proxy using absolute fs path; vite resolver then rejects against exports field
}

vinext's relative ../shims/X.js imports never set packageSources, so the broken branch always wins for these files.

Proposed fix

vinext should import its own 'use client' shim files via the bare specifier vinext/shims/<name> instead of relative paths in dist/server/*. The exports field already maps these:

"./shims/*": {
  "types": "./dist/shims/*.d.ts",
  "import": "./dist/shims/*.js"
}

So changing:

// dist/server/app-page-route-wiring.js, dist/server/app-page-boundary-render.js, dist/index.js
import { LayoutSegmentProvider } from "../shims/layout-segment-context.js";

to:

import { LayoutSegmentProvider } from "vinext/shims/layout-segment-context";

makes plugin-rsc's resolveId hook (which runs for bare specifiers) populate packageSources, taking the client-package-proxy branch instead and generating a virtual module that imports the bare specifier — which Vite's resolver handles correctly via the exports map.

Files that need this rewrite (grep for from "\.\./shims/.*\.js" in dist/server/):

  • dist/server/app-page-route-wiring.jsLayoutSegmentProvider
  • dist/server/app-page-boundary-render.jsLayoutSegmentProvider
  • dist/index.js — references the same shims via path.join(shimsDir, …)
  • Possibly others touching error-boundary.js, link.js, navigation.js, script.js, slot.js, form.js, client-hook-error.js

(Source-level: probably the import statements in src/server/*.ts need to change from ../shims/X to the package's own bare specifier.)

Workaround

Patch vinext/package.json post-install to also expose ./dist/shims/*.js in the exports map:

"exports": {
  "./shims/*": { ... },
  "./dist/shims/*.js": "./dist/shims/*.js"  // ← added
}

This makes the absolute path resolvable, but it leaks internal paths. Real fix is in vinext's import statements.

Environment

vinext: 0.0.45
@vitejs/plugin-rsc: 0.5.25
vite: 8.0.10
@cloudflare/vite-plugin: 1.35.0
node: 24
bun: 1.3.13
host: macOS arm64 (running container amd64 via Rosetta)
Originally created by @eashish93 on GitHub (May 1, 2026). Original GitHub issue: https://github.com/cloudflare/vinext/issues/1001 ## Summary vinext's internal server modules import their own `'use client'` shims via **relative paths** like: ```js // dist/server/app-page-route-wiring.js import { LayoutSegmentProvider } from "../shims/layout-segment-context.js"; ``` `@vitejs/plugin-rsc`'s `packageSource` tracking only fires for **bare specifier** imports (`vinext/shims/X`), not relative ones. When plugin-rsc later sees the resolved file is `'use client'` and inside `node_modules/`, it falls back to the `client-in-server-package-proxy` branch and generates a virtual module that imports via the **absolute filesystem path**: ```js // generated virtual module export * from "/workspace/node_modules/vinext/dist/shims/layout-segment-context.js"; import * as __all__ from "/workspace/node_modules/vinext/dist/shims/layout-segment-context.js"; export default __all__.default; ``` Vite's resolver respects the package's `exports` field. vinext's `exports` only exposes `./shims/*` → `./dist/shims/*.js`. The absolute path `…/vinext/dist/shims/X.js` doesn't match any exports key, so resolution fails and the dev server returns: ``` [plugin:vite:import-analysis] Failed to resolve import "/workspace/node_modules/vinext/dist/shims/layout-segment-context.js" from "virtual:vite-rsc/client-in-server-package-proxy/%2Fworkspace%2Fnode_modules%2Fvinext%2Fdist%2Fshims%2Flayout-segment-context.js". Does the file exist? ``` The file IS on disk. The fail is the `exports` field gate, not fs absence. ## Reproduce Minimal vinext app (just `app/page.tsx` + `app/layout.tsx`) on: - `vinext@0.0.45` - `@vitejs/plugin-rsc@0.5.25` - `vite@8.0.10` `vinext dev` boots successfully. Loading any page that triggers the app router (i.e. anything reaching `dist/server/app-page-route-wiring.js` or `dist/server/app-page-boundary-render.js`) surfaces the error overlay above. Affects every `'use client'` shim (`layout-segment-context`, `link`, `navigation`, `error-boundary`, `form`, `script`, `slot`). ## Root cause (in plugin-rsc terms) In `@vitejs/plugin-rsc/dist/plugin-BhzHKRFo.js`: ```js // line 1383 — packageSources only set for bare specifiers { name: "rsc:virtual-client-package", resolveId: { order: "pre", async handler(source, importer, options) { if (this.environment.name === serverEnvironmentName && bareImportRE.test(source) && ...) { const resolved = await this.resolve(source, importer, options); if (resolved && resolved.id.includes("/node_modules/")) { packageSources.set(resolved.id, source); // ← never fires for relative imports } } } } } // line 1238 — fallback when packageSource is missing const packageSource = packageSources.get(id); if (!packageSource && this.environment.mode === "dev" && id.includes("/node_modules/")) { importId = `/@id/__x00__virtual:vite-rsc/client-in-server-package-proxy/${encodeURIComponent(id)}`; // ↑ generates proxy using absolute fs path; vite resolver then rejects against exports field } ``` vinext's relative `../shims/X.js` imports never set `packageSources`, so the broken branch always wins for these files. ## Proposed fix vinext should import its own `'use client'` shim files via the bare specifier `vinext/shims/<name>` instead of relative paths in `dist/server/*`. The exports field already maps these: ```json "./shims/*": { "types": "./dist/shims/*.d.ts", "import": "./dist/shims/*.js" } ``` So changing: ```js // dist/server/app-page-route-wiring.js, dist/server/app-page-boundary-render.js, dist/index.js import { LayoutSegmentProvider } from "../shims/layout-segment-context.js"; ``` to: ```js import { LayoutSegmentProvider } from "vinext/shims/layout-segment-context"; ``` makes plugin-rsc's `resolveId` hook (which runs for bare specifiers) populate `packageSources`, taking the `client-package-proxy` branch instead and generating a virtual module that imports the bare specifier — which Vite's resolver handles correctly via the exports map. Files that need this rewrite (grep for `from "\.\./shims/.*\.js"` in `dist/server/`): - `dist/server/app-page-route-wiring.js` — `LayoutSegmentProvider` - `dist/server/app-page-boundary-render.js` — `LayoutSegmentProvider` - `dist/index.js` — references the same shims via `path.join(shimsDir, …)` - Possibly others touching `error-boundary.js`, `link.js`, `navigation.js`, `script.js`, `slot.js`, `form.js`, `client-hook-error.js` (Source-level: probably the `import` statements in `src/server/*.ts` need to change from `../shims/X` to the package's own bare specifier.) ## Workaround Patch `vinext/package.json` post-install to also expose `./dist/shims/*.js` in the exports map: ```jsonc "exports": { "./shims/*": { ... }, "./dist/shims/*.js": "./dist/shims/*.js" // ← added } ``` This makes the absolute path resolvable, but it leaks internal paths. Real fix is in vinext's import statements. ## Environment ``` vinext: 0.0.45 @vitejs/plugin-rsc: 0.5.25 vite: 8.0.10 @cloudflare/vite-plugin: 1.35.0 node: 24 bun: 1.3.13 host: macOS arm64 (running container amd64 via Rosetta) ```
Author
Owner

@Divkix commented on GitHub (May 1, 2026):

Alright, I dug into this. The issue is real — here's what I found.

The relative imports are everywhere. 56+ instances of from "../shims/..." across src/server/, src/build/, and src/client/. The built dist/ output has the same relative imports — dist/server/app-page-route-wiring.js imports LayoutSegmentProvider, ErrorBoundary, MetadataHead, Slot, and Children all via ../shims/. Exactly what you described.

The @vitejs/plugin-rsc code path checks out. In plugin-DMfc_Eqq.js (that's 0.5.23, but the logic is the same), packageSources.set() at line 1356 only fires when bareImportRE.test(source) passes. Relative imports never match. So when the fallback at line 1217 runs, it generates a proxy module with the absolute filesystem path. That proxy contains:

export * from "/abs/path/node_modules/vinext/dist/shims/layout-segment-context.js"

And that's where it breaks. Node.js 24 confirms:

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath
'./dist/server/app-page-route-wiring.js' is not defined by "exports"

Weirdly, it doesn't break locally. I ran vp dev on the app-basic fixture and it serves fine — 200 OK, full HTML. vp build passes too. We're on vite@8.0.8 via voidzero's build — I'm guessing their fork handles absolute-path resolution differently than standard Vite 8.0.10. For anyone installing vinext from npm with standard Vite, the exports field enforcement kicks in and you get the Failed to resolve import error overlay.

The fix is straightforward. Change from "../shims/X.js" to from "vinext/shims/X" everywhere. The exports field already maps ./shims/*./dist/shims/*.js. One file (deploy.ts:433) already does this correctly:

import { setCacheHandler } from "vinext/shims/cache";

About 56 lines to update across ~30 files. The packageSources map would then be populated for these imports, and the RSC plugin would take the working client-package-proxy branch instead of the broken one.

Files that need changing (just the highlights, not exhaustive):

  • src/server/app-page-route-wiring.tsxErrorBoundary, LayoutSegmentProvider, MetadataHead, Slot
  • src/server/app-page-boundary-render.tsErrorBoundary, LayoutSegmentProvider, MetadataHead, client-hook-error
  • src/server/dev-server.ts — 4 shim imports
  • src/server/prod-server.tsrequest-context
  • src/server/app-ssr-entry.tsnavigation, script-nonce-context, slot
  • src/server/app-browser-entry.tsnavigation, slot, url-safety
  • src/server/app-route-handler-dispatch.tsfetch-cache, headers, navigation, request-context, unified-request-context
  • src/server/app-middleware.tsheaders, navigation
  • src/server/app-route-handler-execution.tsheaders, request-context, cache, server
  • src/build/prerender.ts
  • src/cloudflare/kv-cache-handler.ts
  • src/client/vinext-next-data.ts
  • src/config/next-config.ts
  • src/server/isr-cache.ts, src/server/instrumentation.ts, src/server/file-based-metadata.ts, and about 15 more server files

The dev server works locally because of the voidzero Vite build, but anyone on standard Vite 8 is hitting this. Worth fixing before more users run into it.

<!-- gh-comment-id:4360803471 --> @Divkix commented on GitHub (May 1, 2026): Alright, I dug into this. The issue is real — here's what I found. **The relative imports are everywhere.** 56+ instances of `from "../shims/..."` across `src/server/`, `src/build/`, and `src/client/`. The built `dist/` output has the same relative imports — `dist/server/app-page-route-wiring.js` imports `LayoutSegmentProvider`, `ErrorBoundary`, `MetadataHead`, `Slot`, and `Children` all via `../shims/`. Exactly what you described. **The @vitejs/plugin-rsc code path checks out.** In `plugin-DMfc_Eqq.js` (that's 0.5.23, but the logic is the same), `packageSources.set()` at line 1356 only fires when `bareImportRE.test(source)` passes. Relative imports never match. So when the fallback at line 1217 runs, it generates a proxy module with the absolute filesystem path. That proxy contains: ```js export * from "/abs/path/node_modules/vinext/dist/shims/layout-segment-context.js" ``` And that's where it breaks. Node.js 24 confirms: ``` Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './dist/server/app-page-route-wiring.js' is not defined by "exports" ``` **Weirdly, it doesn't break locally.** I ran `vp dev` on the `app-basic` fixture and it serves fine — 200 OK, full HTML. `vp build` passes too. We're on `vite@8.0.8` via voidzero's build — I'm guessing their fork handles absolute-path resolution differently than standard Vite 8.0.10. For anyone installing vinext from npm with standard Vite, the exports field enforcement kicks in and you get the `Failed to resolve import` error overlay. **The fix is straightforward.** Change `from "../shims/X.js"` to `from "vinext/shims/X"` everywhere. The `exports` field already maps `./shims/*` → `./dist/shims/*.js`. One file (`deploy.ts:433`) already does this correctly: ```ts import { setCacheHandler } from "vinext/shims/cache"; ``` About 56 lines to update across ~30 files. The `packageSources` map would then be populated for these imports, and the RSC plugin would take the working `client-package-proxy` branch instead of the broken one. **Files that need changing** (just the highlights, not exhaustive): - `src/server/app-page-route-wiring.tsx` — `ErrorBoundary`, `LayoutSegmentProvider`, `MetadataHead`, `Slot` - `src/server/app-page-boundary-render.ts` — `ErrorBoundary`, `LayoutSegmentProvider`, `MetadataHead`, `client-hook-error` - `src/server/dev-server.ts` — 4 shim imports - `src/server/prod-server.ts` — `request-context` - `src/server/app-ssr-entry.ts` — `navigation`, `script-nonce-context`, `slot` - `src/server/app-browser-entry.ts` — `navigation`, `slot`, `url-safety` - `src/server/app-route-handler-dispatch.ts` — `fetch-cache`, `headers`, `navigation`, `request-context`, `unified-request-context` - `src/server/app-middleware.ts` — `headers`, `navigation` - `src/server/app-route-handler-execution.ts` — `headers`, `request-context`, `cache`, `server` - `src/build/prerender.ts` - `src/cloudflare/kv-cache-handler.ts` - `src/client/vinext-next-data.ts` - `src/config/next-config.ts` - `src/server/isr-cache.ts`, `src/server/instrumentation.ts`, `src/server/file-based-metadata.ts`, and about 15 more server files The dev server works locally because of the voidzero Vite build, but anyone on standard Vite 8 is hitting this. Worth fixing before more users run into it.
Author
Owner

@james-elicx commented on GitHub (May 2, 2026):

Same pattern issue was hit by Waku during their plugin-rsc migration: waku.gg/blog/migration-to-vite-plugin-rsc — they resolved it by routing client-boundary modules through the framework's own bare specifier instead of absolute paths. Same fix shape.

I can't see a reference to this problem in that blog post. Please can you share what you're referring to?

<!-- gh-comment-id:4363579209 --> @james-elicx commented on GitHub (May 2, 2026): > Same pattern issue was hit by Waku during their plugin-rsc migration: [waku.gg/blog/migration-to-vite-plugin-rsc](https://waku.gg/blog/migration-to-vite-plugin-rsc) — they resolved it by routing client-boundary modules through the framework's own bare specifier instead of absolute paths. Same fix shape. I can't see a reference to this problem in that blog post. Please can you share what you're referring to?
Author
Owner

@eashish93 commented on GitHub (May 2, 2026):

I've been testing vinext on a new project (running on Cloudflare Sandbox containers) and encountered many bugs simultaneously which I already filled, so maybe that's why the hallucination by opus 4.7 on citation.

This part: https://waku.gg/blog/migration-to-vite-plugin-rsc#transforming-server-packages is actually related to issue #1008 somehow.

I'll edit the issue and remove the related upstream part.

<!-- gh-comment-id:4363627473 --> @eashish93 commented on GitHub (May 2, 2026): I've been testing vinext on a new project (running on Cloudflare Sandbox containers) and encountered many bugs simultaneously which I already filled, so maybe that's why the hallucination by opus 4.7 on citation. This part: https://waku.gg/blog/migration-to-vite-plugin-rsc#transforming-server-packages is actually related to issue #1008 somehow. I'll edit the issue and remove the related upstream part.
Author
Owner

@james-elicx commented on GitHub (May 2, 2026):

I see, no worries. I want to try and reproduce this issue this afternoon before merging the pr - concerned this wasn't caught by existing e2es. If you can share a minimal repro that would be super handy as well.

<!-- gh-comment-id:4363683440 --> @james-elicx commented on GitHub (May 2, 2026): I see, no worries. I want to try and reproduce this issue this afternoon before merging the pr - concerned this wasn't caught by existing e2es. If you can share a minimal repro that would be super handy as well.
Author
Owner

@eashish93 commented on GitHub (May 2, 2026):

Hard to reproduce it normally, but it happen inside sandbox container. I tried it with codesandbox, but I guess that may not be sufficient: https://codesandbox.io/p/devbox/rsc-vinext-repro-kcddvm

If I able to create minimal repro from my codebase locally, I'll paste it here soon.

<!-- gh-comment-id:4363943621 --> @eashish93 commented on GitHub (May 2, 2026): Hard to reproduce it normally, but it happen inside sandbox container. I tried it with codesandbox, but I guess that may not be sufficient: https://codesandbox.io/p/devbox/rsc-vinext-repro-kcddvm If I able to create minimal repro from my codebase locally, I'll paste it here soon.
Author
Owner

@james-elicx commented on GitHub (May 2, 2026):

I've tried reproducing locally in individual projects, inside Docker containers, inside the monorepo, etc. but had no luck unfortunately...

Did (somewhat quickly) try a couple patches in the rsc plugin the codesandbox but continued having issues, so I'll merge that PR.

<!-- gh-comment-id:4364218126 --> @james-elicx commented on GitHub (May 2, 2026): I've tried reproducing locally in individual projects, inside Docker containers, inside the monorepo, etc. but had no luck unfortunately... Did (somewhat quickly) try a couple patches in the rsc plugin the codesandbox but continued having issues, so I'll merge that PR.
Author
Owner

@james-elicx commented on GitHub (May 2, 2026):

I would note that without a reproduction outside of codesandbox that can be encountered via direct navigation rather than a curl for internal vite logic, there's no guarantee this won't regress.

<!-- gh-comment-id:4364275398 --> @james-elicx commented on GitHub (May 2, 2026): I would note that without a reproduction outside of codesandbox that can be encountered via direct navigation rather than a curl for internal vite logic, there's no guarantee this won't regress.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#219
No description provided.