Skip to content

fix(router): resolve the plugin-rsc static.edge vendor entry through @rangojs/router under pnpm - #1051

Merged
ivogt merged 1 commit into
mainfrom
fix/1050-pnpm-optimize-deps
Oct 4, 2026
Merged

ivogt merged 1 commit into
mainfrom
fix/1050-pnpm-optimize-deps

Conversation

@ivogt

@ivogt ivogt commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator

Closes #1050

Problem

A project that installs only @rangojs/router, react, react-dom and vite (the README's promise) printed this on every pnpm dev with pnpm's strict node_modules, in all three templates (cloudflare, basic, vercel):

Failed to resolve dependency: @vitejs/plugin-rsc/vendor/react-server-dom/static.edge, present in rsc 'optimizeDeps.include'

The app still served (GET / 200). @vitejs/plugin-rsc adds five @vitejs/plugin-rsc/vendor/react-server-dom/* entries to the environments' optimizeDeps.include; Vite resolves them from the project root, where pnpm does not link plugin-rsc. rango() already redirected four of them through resolve.alias (absolute paths resolved from this package), but missed static.edge, which only the rsc environment includes.

Before vs. after

Consumer package.json (unchanged in both):

"dependencies": { "@rangojs/router": "^0.20.0", "react": "^19.3.0", "react-dom": "^19.3.0" }
pnpm dev output rsc pre-bundle
before Failed to resolve dependency: @vitejs/plugin-rsc/vendor/react-server-dom/static.edge ... static.edge not pre-bundled
after no warning static.edge pre-bundled in deps_rsc/_metadata

End-to-end consumer example

Scaffold with pnpm create rango@latest my-app --template cloudflare --package-manager pnpm, pnpm install, pnpm dev. Before: warning above. After (router tarball built from this branch installed in place of 0.20.0, app still not listing @vitejs/plugin-rsc): no warning, / answers 200, pnpm build passes.

Semantics

Entries in each environment's optimizeDeps.include that start with @vitejs/plugin-rsc/ (scaffolded cloudflare app, 0.20.0), and whether each resolves from the project root under pnpm:

env entry in root node_modules covered before covered after
client .../vendor/.../client.browser no alias alias
ssr .../vendor/.../client.edge no alias (+ nested duplicate) alias
rsc .../vendor/.../server.edge no alias (+ nested duplicate) alias
rsc .../vendor/.../client.edge no alias alias
rsc .../vendor/.../static.edge no none: warns, not pre-bundled alias

react, react-dom, react/jsx-*, react-dom/{client,server.edge,static.edge} resolve from the root (the app lists react/react-dom). The @rangojs/router > ... nested entries (rsc-html-stream, plugin-rsc client.edge/server.edge) resolve through the router and do not warn.

An app that lists @vitejs/plugin-rsc itself: the alias target resolves to the same file the root's copy resolves to when the versions agree (pinned by a unit test with a symlinked root install). Same behavior as the four existing entries; with a different plugin-rsc version in the root, the router's copy wins for these entries, as before.

Code change

packages/rangojs-router/src/vite/utils/package-resolution.ts: one line added to the specs list in getVendorAliases(). rango.ts merges those aliases into resolve.alias for both presets (rango.ts:151, used at :211 cloudflare and :432 node).

Why alias and not a per-root createRequire check or the A > B nested form: the alias is already the mechanism for the other four entries, it covers every environment and both presets at once, and it needs no root-dependent branch. The nested form would add a second optimizer entry for the same file (the existing nested client.edge/server.edge entries already do, visible as duplicate keys in deps_ssr/deps_rsc _metadata.json; left as is here).

Test plan

  • src/vite/__tests__/optimize-deps-vendor-resolution.test.ts (new): resolves the real rango() config (node and cloudflare presets) and asserts every @vitejs/plugin-rsc/ include entry of client/ssr/rsc has an alias to an existing file when the root has no plugin-rsc, and that the alias target equals the root's own resolution when plugin-rsc is installed there. Red on origin/main (4 failures), e.g. AssertionError: node/rsc: no alias for @vitejs/plugin-rsc/vendor/react-server-dom/static.edge: expected undefined to be truthy at optimize-deps-vendor-resolution.test.ts:90; green with the change.
  • package-resolution.test.ts: getVendorAliases() has static.edge.
  • Real install: tarball from pnpm pack, scaffolded cloudflare app under pnpm, dev: no warning, / 200; pnpm build passes.
  • npm (flat node_modules), published 0.20.0: no warning (plugin-rsc is hoisted to the root).
  • typecheck, test:unit:all, lint, format, check:e2e-bucketing, check:e2e-parity --strict, check:docs-api.
  • test:hmr-routes-local equivalent: 20 passed, 1 flaky (an invalid transition({ when }) edit prints the error and shows the Vite overlay, passed on retry).
  • Dev e2e: hook-render-stability in packages/rangojs-router (8 passed) and tests/cloudflare-basic (4 passed).
  • No e2e added: the change is dev-server config resolution with no runtime output; the pinned contract is the resolved config (unit), and the dev/production start of both e2e apps is covered by the existing suites.

Notes

  • Cost of the unresolved entry: noise for Rango apps. Nothing in the router imports plugin-rsc's vendor static.edge (router sources use react-dom/static.edge), so the only effect was that module missing from the rsc pre-bundle. I did not construct a case where an app imports it unoptimized.
  • Declared peer (facts, no change made): packages/rangojs-router/package.json lists @vitejs/plugin-rsc ^0.5.35 under dependencies and under peerDependencies, not in peerDependenciesMeta, so it is a required peer. The README (README.md:61: "rango() includes @vitejs/plugin-rsc") and the install commands (README npm install @rangojs/router@latest react react-dom / npm install -D vite; skills/cloudflare/SKILL.md:21-22; skills/migrate-nextjs/SKILL.md:118-119) do not install it; after this fix they agree with behavior. The declaration is what disagrees with the docs. Recommendation for the maintainer: drop it from peerDependencies and keep it as a dependency. As a peer it signals "install this yourself" and makes npm auto-install it at the root, which is why npm never showed the warning, while the README says the opposite. Not changed here.
  • The templates (rangojs/templates) need no change.
  • The alias points at the copy the router resolves, so for these five entries the router's @vitejs/plugin-rsc wins over an app's own copy at another version. That was already true for the other four; the helper's doc comment now says it.
  • Review: one independent four-angle pass (reuse, simplification, efficiency, altitude). Confirmed: the new test reads the include entries from the real resolved config of both presets, so a plugin-rsc release that adds a sixth vendor entry fails it; deriving the alias list from plugin-rsc's exports does not work (it exports one ./* wildcard, and its vendor directory holds files the plugin never includes). Applied: the doc comment on getVendorAliases. Skipped: a shared resolveRangoConfig test helper (the same few lines exist in two sibling tests; out of scope here).
  • Second gate run by the reviewer: green. In the first run telemetry.test.ts ("safeEmit with no-op sink performs no work", a 50 ms budget for 10k calls) took 51.5 ms while the machine had a load average of 17; alone it passes, and this change does not touch telemetry.

@pkg-pr-new

pkg-pr-new Bot commented Oct 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@rangojs/router@1051

commit: 0d661b7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pnpm: dev warns "Failed to resolve dependency: @vitejs/plugin-rsc/vendor/react-server-dom/static.edge" in a fresh Cloudflare template

1 participant