Repository navigation
feat(testing): loud-fail the alias trap + bare-import Prerender/createLoader/Static (#542) - #543
Merged
ivogt merged 4 commits intoJun 9, 2026
Conversation
…eLoader/Static (#542) Follow-up to #533, capability + coverage polish. - renderHandler: detect the server-only-API stub throw (a handler reading getRequestContext()/cookies() when the rsc-project alias is missing) and rethrow an actionable RenderHandlerSetupError pointing at rangoTestAliases, instead of swallowing it into an opaque tree:undefined. Dual-substring match so a normal handler throw is NOT reclassified. - Bare-test $$id fallback for the component-handler kinds — Prerender() (prerender.ts) and Static() (static-handler.ts) — joining createHandle / createLoader. Each assigns a process-stable runtime fallback $$id when the Vite plugin injected none, gating the dev-throw on NODE_ENV, so a router using them CONSTRUCTS in bare vitest (for dispatch / assertGeneratedRoutesMatch). Provably inert in production (never fires under the plugin; prerender lookup keys on routeName+paramHash; the Static manifest keys on the plugin id; Static's $$id is read only during real RSC serving) — both red-teamed (Prerender 0/3 refuted; Static go-with-fix, the only fix being doc sync). Importing a real whole-router FILE can still fail on app-specific page-module imports / plugin virtual: modules — app/plugin-specific, not a handler-id limitation. - docs/skills/dogfood: corrected the import contract everywhere to the accurate statement (all three construct; the whole-router-file caveat is app/plugin module resolution). Steer to renderServerTree over raw renderToFlightString wire matchers; assertCacheStatus default, telemetry sink advanced. Considered and dropped: a renderRoute mount-prefix guard — production simply PREFIXES include-relative patterns (/shop + /shop -> /shop/shop), so a path starting with the mount is a legitimate shape, not an error. The mount trap stays documented, not enforced. (Also deferred, tracked in #542: idle-nav loud-fail; use-cache-without-store warn.) Tests: render-handler +2, render-route +1 (prefixing parity), prerender +3, static +4 (incl. a whole-router construct+dispatch integration test); updated the pre-existing static-handler-types "missing $$id" test to the gated contract. Gate: typecheck 12/12, test:unit:all EXIT=0 (router 3495 + 3 todo, RSC 27), lint 0/0, format. Relates to #533.
…tes [skip ci] The Setup-section import-contract blockquote was reflowed badly (lost the > quote prefix on continuation lines and ate spaces around inline code: $$idis, realrouter.tsx, pluginvirtual:). Convert it to two normal paragraphs so it renders correctly. Also clear stale Prerender-free / blocked-by-Prerender stragglers in cloudflare-basic + vite-rsc-demo FINDINGS to the new contract (the whole-router FILE import is blocked by app page-module deps / plugin virtual: modules, not by handler $$id, which now falls back).
… production throw)
Review finding (Medium): gating the missing-$$id throw on NODE_ENV==="development"
meant the runtime fallback fired in ANY non-dev runtime, including a production
`vite build`. For an UNSUPPORTED handler shape (e.g. `export let X = Static(...)`)
the Vite plugin leaves the id un-injected and untracked with no warning, so a
production build that previously threw loud ("missing $$id") would silently assign
__rango_runtime_static_0 and degrade to a runtime static/prerender miss. The
"fallback never fires under the plugin" assumption only held for SUPPORTED shapes.
Fix: gate the fallback on `process.env.VITEST` — the only signal true in both
vitest projects (the RSC project forces NODE_ENV=production) yet absent in a real
build. The fallback now fires ONLY under a test runner; a missing id in dev OR a
production build throws, restoring the pre-PR safety net. Applied to all five
create-functions (handle.ts, loader.ts, loader.rsc.ts, prerender.ts,
static-handler.ts) — they shared the identical gate and the same latent bug.
Tests: the id-guard / fallback tests now toggle VITEST (not NODE_ENV) to exercise
the throw ("throws outside a test runner" cases). Docs (testing/vitest.ts JSDoc +
docs/testing.md) note the throw-on-unsupported-shape in a real build. Gate:
typecheck 12/12, test:unit:all EXIT=0 (router 3495 + 3 todo, cloudflare-basic 39,
RSC 27), lint 0/0, format.
Relates to #533, #542.
…gnostic dev/test-only
Review finding (Medium): the previous `!process.env.VITEST` guard shipped to
production with two problems. (1) It is a BARE `process.env` access — a runtime
without `process` (browser, certain edge/worker RSC envs) can ReferenceError before
reaching the intended Rango error. (2) Unlike `process.env.NODE_ENV` (folded by the
app build `define`), `VITEST` is not folded, so switching to it un-DCE'd the rich
stack-parsing `missingInjectedIdError` diagnostic — it shipped in every prod chunk.
Fix:
- New runtime-safe helper `isUnderTestRunner()` = `!!globalThis.process?.env?.VITEST`
(optional-chained — never a bare `process.env`). Used by all five create-fns.
- Restructure handle.ts / loader.ts / loader.rsc.ts so the rich diagnostic stays
behind `process.env.NODE_ENV !== "production"` (build-folded -> DCE'd, tree-shakes
missing-id-error.ts out) with a SMALL production throw in its place. Static() /
Prerender() already throw small messages — only the runtime-safe check changed.
Verified via `pnpm --filter rangojs-e2e-mini build` + chunk inspection: shipped .js
uses `globalThis.process?.env?.VITEST` / `{}.VITEST` (no bare access); the rich-only
markers ("Unsupported create", "created at", stack parsing, node_modules filtering)
are DCE'd (0 across all chunks); only the small throw ships.
Tests toggle VITEST (not NODE_ENV); added a prod-small-throw test (NODE_ENV=production
+ no VITEST -> small, non-stack-parsing message). Gate: typecheck 12/12, test:unit:all
EXIT=0 (router 3496 + 3 todo, RSC 27), lint 0/0, format.
Relates to #533, #542.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #533, addressing the capability + coverage items tracked in #542. Stacked on
feat/consumer-testing-primitives(base will retarget tomainafter #533 merges).Problem (consumer-observable)
getRequestContext()/cookies()in an rsc test without therangoTestAliasesresolve.alias resolved to the throwing out-of-react-server stub, andrenderHandlerswallowed it — the consumer saw an opaquetree: undefined+ a bare stub throw with no hint about the missing alias.Prerender()/createLoader()/Static()threw "missing$$id" at construction in a bare test, sodispatch/assertGeneratedRoutesMatchagainst it were impossible without the Vite plugin or e2e.What changed
is only available from+react-server,instanceof Error) and rethrows an actionableRenderHandlerSetupErrorpointing atrangoTestAliases. A normal handler throw is not reclassified (control test included).$$idfallback forPrerender()+Static()(joiningcreateHandle/createLoader). Each assigns a process-stable__rango_runtime_*_Nid when the plugin injected none, gating the dev-throw onNODE_ENV. A router using them now constructs in bare vitest.renderServerTreeover rawrenderToFlightStringwire matchers,assertCacheStatusover the telemetry sink.renderRoutemount-prefix guard (it was unsound — production simply prefixes include-relative patterns, so/shop+/shop→/shop/shopis legitimate, not an error).Before / after (whole-router construct)
Semantics — handler kind × bare-test behavior
pnpm dev(non-exported)Prerender()/Static()/createLoader()/createHandle()$$id$$id)$$id; fallback never firesRemaining whole-router-file caveat (unchanged, app/plugin-specific, not a handler-id limit): importing your real
router.tsxcan still fail on page-module deps or pluginvirtual:modules — build whole-router checks from a focused include or run them at e2e.Production safety
Both prod-source changes (
prerender.ts,static-handler.ts) were adversarially red-teamed: Prerender 0/3 refuted; Static go-with-fix (fix = doc sync only). The fallback never fires under the plugin; prerender lookup keys onrouteName+paramHash, the Static manifest keys on the plugin id, andStatic's$$idis read only during real RSC serving (never indispatch/assertGeneratedRoutesMatch— verifiedstaticHandlerIdlives on internalEntryData, not the public routeMap, so no false drift).Test plan
static-handler-types"missing$$id" test to the gated contract.typecheck12/12,test:unit:allEXIT=0 (router 3495 + 3 todo, RSC 27),lint0/0,formatclean.Deferred (still tracked in #542): idle-nav loud-fail and the use-cache-without-store warn — neither can be made loud without false positives / touching production; both stay documented in the trap table.
Relates to #533. Closes the buildable items of #542.