You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Vite started from another working directory: plugin-rsc externalizes the app's React-peer dependencies, so build-time and dev PPR shell captures are skipped #1044
Status: open, waiting on upstream. The cause is in @vitejs/plugin-rsc (0.5.35): it looks for the dependencies the ssr environment must bundle starting from process.cwd(), not from the Vite root. It affects dev and build whenever Vite is started from a directory other than the app's. Until it is fixed there, run vite from the app directory. No workaround is planned in rango for now; the evaluation and the options are in the comment below.
Building an app with vite build <dir> from another working directory skips every build-time PPR shell. The build still exits 0 and each route falls back to runtime capture, so nothing fails; the prebuilt shells are just missing.
Found while verifying the fix for #1037 (PR #1045). Before that fix this build failed earlier (the build-time prerender of /build-env), which hid it.
Reproduction
With the #1037 fix (PR #1045), from the repository root:
[rango] Shell-prerendering 5 URL(s)...
[DocumentCache] middleware: TypeError: Cannot read properties of null (reading 'useState')
at exports.useState (node_modules/.pnpm/[email protected]/node_modules/react/cjs/react.production.js:549:33)
at useIsSSR (node_modules/.pnpm/[email protected][email protected]/node_modules/spin-delay/src/index.ts:31:29)
at useSpinDelay (node_modules/.pnpm/[email protected][email protected]/node_modules/spin-delay/src/index.ts:46:17)
at NavigationProgress (tests/cloudflare-basic/src/components/NavigationProgress.tsx:8:24)
at renderWithHooks (tests/cloudflare-basic/node_modules/.vite/deps_ssr/react-dom-server.edge.development-Dv2CV4uZ.js:2744:18)
[rango] SHELL SKIP /ppr-shell/prerendered/alpha (81ms) - Cannot read properties of null (reading 'useState'); route keeps runtime capture
... (all 5 shells)
The same build run inside tests/cloudflare-basic: 5 of 5 SHELL OK, 0 skipped.
Cause
plugin-rsc's rsc plugin runs crawlFrameworkPkgs({ root: process.cwd(), ... }) in its config() hook (dist/plugin-CLvIS10V.js:759) to list the packages with a react peer dependency, and puts them in the ssr environment's resolve.noExternal. From another working directory the crawl reads the wrong package.json, so the app's dependencies (here spin-delay) are externalized, Node loads them from node_modules with their own react, and the renderer in deps_ssr is a different copy: a hook called there has no dispatcher.
The dev server started from the repository root shows the same error ([rango] Dev shell capture error ... (route keeps runtime capture)), and still answers 200.
Expected
The same result from any working directory: the shells are captured, in dev and in the build. That needs plugin-rsc to crawl from the config's root (one line upstream), or rango to run the same crawl from the Vite root and add what is missing (about 15 to 25 lines, duplicating plugin-rsc).
Separately, a build that skips every shell it was asked to prerender should say so more loudly than a per-route line: today the exit code is 0.
Test
tests/cloudflare-basic built from the repository root should report SHELL OK for its five prerendered shells. #1034 added a second-working-directory build test for the node fixture (cache-versions-build.test.ts); it has no ppr build shells, which is why this was not seen there.
Evaluated. The cause is upstream, in @vitejs/plugin-rsc (0.5.35), and it affects dev as well as the build.
Cause
plugin-rsc's rsc plugin decides which dependencies the ssr environment must not externalize by crawling for packages with a react peer dependency, and it starts that crawl at the working directory, not at the Vite root (dist/plugin-CLvIS10V.js:759):
From the repository root that reads the monorepo's package.json, so the app's own dependencies are not found. The resolved config of the capture's temporary server differs between the two runs in exactly one value, the ssr environment's resolve.noExternal:
from the app directory: react, react-dom, server-only, client-only, @vitejs/plugin-rsc, @rangojs/router, @testing-library/react, rango-e2e-deep-context-lib, spin-delay
from the repository root: react, react-dom, server-only, client-only, @vitejs/plugin-rsc
root, cacheDir, dedupe, conditions, optimizeDeps and ssr.external are identical. So from another directory spin-delay is externalized, Node loads it from node_modules with its own react, and the renderer in deps_ssr is a different copy: no dispatcher, useState on null.
Dev too
vite tests/cloudflare-basic started from the repository root answers 200, and logs the same Invalid hook call / Cannot read properties of null (reading 'useState') with [rango] Dev shell capture error for /ppr-shell/prerendered/alpha (route keeps runtime capture).
Confirmed by experiment
Forcing the temporary server's ssr environment to resolve.noExternal: true (one line in createTempRscServer, src/vite/router-discovery.ts, build only) gives five SHELL OK from the repository root and five from the app directory. Not committed: it is blunt (every dependency goes through the optimizer) and was not tried on the node and vercel presets.
Forwarding the main config's noExternal to the temporary server does not help: that list comes from the same crawl.
Options
Upstream: plugin-rsc crawls from the config's root. One line there; the real fix.
In rango, until then: run the same crawl from the Vite root in rango's own config() hook and add what is missing to the ssr environment's noExternal, for the main config and the temporary server, dev and build. About 15 to 25 lines; it duplicates what plugin-rsc does.
Document that vite has to be run from the app directory.
Also read in the same code, not tested
src/vite/utils/package-resolution.ts:20: isInstalledFromNpm() checks node_modules/@rangojs/router under process.cwd().
src/vite/plugins/vercel-output.ts:411: let root = process.cwd() (may be overwritten in configResolved).
plugin-rsc makes module ids relative to process.cwd() at dist/plugin-CLvIS10V.js:1335 and :1589.
changed the title [-]Build-time PPR shells are all skipped when `vite build <dir>` runs from another working directory[/-][+]Vite started from another working directory: plugin-rsc externalizes the app's React-peer dependencies, so build-time and dev PPR shell captures are skipped[/+]on Oct 4, 2026
Activity
ivogt commentedon Oct 4, 2026
Evaluated. The cause is upstream, in
@vitejs/plugin-rsc(0.5.35), and it affects dev as well as the build.Cause
plugin-rsc's
rscplugin decides which dependencies the ssr environment must not externalize by crawling for packages with areactpeer dependency, and it starts that crawl at the working directory, not at the Vite root (dist/plugin-CLvIS10V.js:759):From the repository root that reads the monorepo's
package.json, so the app's own dependencies are not found. The resolved config of the capture's temporary server differs between the two runs in exactly one value, the ssr environment'sresolve.noExternal:react, react-dom, server-only, client-only, @vitejs/plugin-rsc, @rangojs/router, @testing-library/react, rango-e2e-deep-context-lib, spin-delayreact, react-dom, server-only, client-only, @vitejs/plugin-rscroot,cacheDir,dedupe,conditions,optimizeDepsandssr.externalare identical. So from another directoryspin-delayis externalized, Node loads it fromnode_moduleswith its ownreact, and the renderer indeps_ssris a different copy: no dispatcher,useStateonnull.Dev too
vite tests/cloudflare-basicstarted from the repository root answers 200, and logs the sameInvalid hook call/Cannot read properties of null (reading 'useState')with[rango] Dev shell capture error for /ppr-shell/prerendered/alpha (route keeps runtime capture).Confirmed by experiment
Forcing the temporary server's ssr environment to
resolve.noExternal: true(one line increateTempRscServer,src/vite/router-discovery.ts, build only) gives fiveSHELL OKfrom the repository root and five from the app directory. Not committed: it is blunt (every dependency goes through the optimizer) and was not tried on the node and vercel presets.Forwarding the main config's
noExternalto the temporary server does not help: that list comes from the same crawl.Options
root. One line there; the real fix.config()hook and add what is missing to the ssr environment'snoExternal, for the main config and the temporary server, dev and build. About 15 to 25 lines; it duplicates what plugin-rsc does.vitehas to be run from the app directory.Also read in the same code, not tested
src/vite/utils/package-resolution.ts:20:isInstalledFromNpm()checksnode_modules/@rangojs/routerunderprocess.cwd().src/vite/plugins/vercel-output.ts:411:let root = process.cwd()(may be overwritten inconfigResolved).process.cwd()atdist/plugin-CLvIS10V.js:1335and:1589.[-]Build-time PPR shells are all skipped when `vite build <dir>` runs from another working directory[/-][+]Vite started from another working directory: plugin-rsc externalizes the app's React-peer dependencies, so build-time and dev PPR shell captures are skipped[/+]