Repository navigation
fix(router): stabilize multi-agent review findings (15 mediums + cleanups) - #571
Merged
Merged
Conversation
Resolve 15 of 16 medium-severity findings from a 54-reviewer audit (9 subsystems x 6 lenses, adversarially verified), each with red->green tests, plus 90+ low/info cleanups (dead code, simplify, stale docs). Correctness: render intercept slot-name D mis-parse dropping loader data; SSR renderSegments double-render memoized; trie backtracks on constraint failure to a lower-priority sibling (was forcing the regex fallback + false R3 warning); loading:false wraps loaders before await (per-loader boundary, no unhandled rejections); notFoundBoundary orphan-sibling parity; dev warning for a Response swallowed under loading(); prerender detects ctx.passthrough() through a loading()-deferred component; loader-cache failures routed through reportCacheError; cache() threads TEnv into callbacks; redirect basename double-prefix on query/hash. API hygiene: removed the build-breaking cache(profileName) overload (use cache: <profile> remains); added transition() no-arg/children overloads to match the runtime; removed dead types/exports (SegmentCacheProvider, CacheConfig, CachedEntryResult, and others). Perf/internal: gated the build-time loader pre-scan to the RSC environment (was running once per build pass); de-duplicated paramsEqual and observeStreamedHandler into shared modules. Docs: corrected manifests.md (bundle-regression footgun), use-cache JSON-fallback claim, host apex heuristic, feature-file-map paths, and the execution-model loading()+redirect limitation. M11 (orphan-revalidation extraction) deferred as a documented follow-up. Gate: typecheck, 3651 unit tests, lint, format all green.
Add missing load-bearing re-exports to the canonical type entry (index.rsc.ts): PrerenderPassthroughResult (the ctx.passthrough() return type) and the OriginCheckConfig/OriginCheckContext/OriginCheckPhase types referenced by the RangoOptions.originCheck JSDoc but previously unexported. Remove the dead public OutletProvider export from ./client (an internal component re-exported for backwards-compat with no real consumer): drop it from client.tsx/client.rsc.tsx, point segment-system.tsx at the real ./outlet-provider source, fix the segment-system test mock, and remove the unused demo import. generateManifestCode left in place (documented intended ./build API). Gate: typecheck, 3651 unit tests, lint, format all green.
initNavigationStore/getNavigationStore/resetNavigationStore had no production caller (grep-verified: only the unit test + two explanatory comments). The boot path calls createNavigationStore() directly via the module-level handle; the singleton only ever threw in a running app. Remove the trio, its self-referential test block, and refresh the two comments that referenced it. Gate: typecheck, navigation-store unit suite (42), lint green.
commit: |
…files, 65 lines removed)
Strip ~3.3k lines of redundant comments (decorative section banners, restatements of the next line, obvious step narration) across the shipped source, keeping the load-bearing ones: WHY/rationale, non-obvious invariants and scar-tissue, public-API JSDoc, and contract notes (handler-first ordering, cache scope, dev/prod parity, bundle hygiene). Directive comments (@ts-expect-error, eslint/oxlint-disable, @__PURE__) and the use client/server/cache string directives are untouched. Applied by a fan-out of conservative per-file agents; gated after: typecheck, 3841 unit tests, lint, format all green.
… M9 completion)
A targeted review of surfaces the per-subsystem pass missed (the new @rangojs/router/testing module, a security pass, cross-subsystem seams, and an adversarial self-review of the stabilization diff) surfaced one HIGH and several refinements.
HIGH (cross-seam): the loader-level cache() default key omitted the host, while the route-level cache (cache-scope: `${host}${pathname}`) and "use cache" (cache-runtime pushes ctx.url.host) both include it. In a multi-tenant host router serving the same pathname for different hosts, one host's cached loader data could be served to another. Fixed by threading the host into getDefaultLoaderCacheKey; red->green cross-host test added.
M9 was incomplete: warnOnStreamedResponse only covered fresh.ts. Extended it to the revalidation route + parallel-slot paths and the intercept loading() path, and broadened it to detect a Response on both resolve and reject (the raw parallel-slot handler is not run through handleHandlerResult). Public export: MiddlewareFn (so the documented 'extract and unit-test the middleware fn' pattern has a nameable type).
Self-review verified all prior stabilization fixes are correct (trie backtracking, loader wrap, passthrough detect, loud cache errors, dead-export removals). Gate: typecheck, 3843 unit tests, lint, format all green.
ivogt
added a commit
that referenced
this pull request
Jun 13, 2026
Four one-off review/orchestration files (comment-groups.json, gen-comment-groups.cjs, wf-comments.js, wf-gap-review.js) slipped into #571's squash merge — their .gitignore entries were added after a git add -A had already staged them, and .gitignore cannot untrack. Remove them and drop the now-vestigial review-artifacts .gitignore block.
Merged
6 tasks done
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.
Problem
A multi-agent review (9 subsystems × 6 lenses, every finding adversarially verified) surfaced 191 findings against
@rangojs/routerat HEAD. Nothing at HEAD corrupts data or crashes the router on the common path, but a cluster of latent corners degrade silently or surprise a consumer who follows the documented contract: a typed-and-documentedcache("profileName")form that throws at build, atransition()type that rejects two of its own documented call shapes,cache()callbacks wherectx.envisunknown, a trie that quietly falls back to the regex matcher (and fires an alarming "please report this" warning) on perfectly valid config, an asyncredirect()underloading()that vanishes into the RSC stream, a build that bakes a corrupt prerender artifact when aPassthroughroute also declaresloading(), loader-cache failures that never reachonError, and — found in a follow-up gap pass — a HIGH cross-host loader-cache leak where one tenant's cached loader data could be served to another.This PR stabilizes 15 of 16 verified medium defects plus the HIGH cross-host leak, each with a red→green test, and lands 90+ low/info cleanups (dead exports, dedup, stale docs) and a comment-noise reduction. The full per-finding disposition lives in the tracked follow-up issues linked at the bottom.
Userland improvements
Every consumer-facing change below is shown from the seat of someone writing route definitions, handlers, loaders, or a custom store.
1.
cache("profileName")removed — was typed, documented, and build-breaking (M12)The
(profileName: string)overload was advertised in theRouteHelperstype, the helper JSDoc, anddocs/design/caching.md, but the top-level non-lazy build-time manifest generator constructs itsRangoContextstore with nocacheProfileskey, so anycache("name")in the top-level route tree hit theinvariant("unknown cache profile")at build. Per the pre-1.0 API-hygiene rule (no advertised-but-broken surface in main), the overload and its runtime string branch are removed.The
"use cache: <profile>"directive and thecreateRouter({ cacheProfiles })option are untouched — only the route-treecache("name")call form is gone.2.
transition()now compiles in all four runtime forms (M7)The runtime (
dsl-helpers.ts) has always handled four shapes, and the type's own JSDoc documents the no-arg form as a meaningful choice — but theRouteHelpers.transitiontype declared only two overloads, so two documented forms were a hard compile error.Two overloads added to
RouteHelpers.transition:(): TransitionItemand(children) => TransitionItem, mirroring the already-correctcache()overload shape. A type-pin (dsl-helper-types.check.ts) freezes the four-form contract; the pre-fix red (TS2554/TS2559) was captured.3.
cache()callbacks now receive a typedctx.env(TEnv) (M14)PathHelpers<TEnv>/RouteHelpers<T, TEnv>are parameterized over the app env, but thecachemember declared barePartialCacheOptions(which defaultsTEnvtounknown), socondition/key/tagscallbacks sawctx.env: unknown— exactly on the pattern the JSDoc holds up as the motivating example.Threaded
PartialCacheOptions<TEnv>through all fourcacheoverloads on bothPathHelpersandRouteHelpers. A compile probe confirms the fix clears all three TS18046 errors.4. Trie backtracks on a constraint miss — correct route + no false "trie gap" warning (M5)
walkTriecommitted to the first structurally-matching branch and validated per-leaf constraints (leaf.cv) only after the walk, invalidateAndBuild, which returnednullon a miss. Because the constraint check lived outside the recursive frames, a constraint failure could not unwind to a lower-priority sibling that would match. The fix threadsleafConstraintsPass()into every candidate terminal (terminal + wildcard-empty + wildcard-rest) so a miss backtracks during the walk;constraintsSatisfied()remains a backstop on final params.Consumer impact: the false R3 warning (which fired once per distinct failing pathname — unbounded for a catch-all locale) stops manufacturing bug reports, and production no longer silently leans on the regex fallback's definition-order precedence (where an early catch-all can shadow a specific route). Red→green tests for the param+wildcard and suffix-param+param sibling cases; 168 pattern/host regression tests pass.
5.
notFoundBoundaryon an orphan-layout sibling now resolves — parity witherrorBoundary(M8)findNearestErrorBoundarywalked the parent chain and inspected each level's orphan-layout siblings;findNearestNotFoundBoundaryonly walkedcurrent.parent. Since an orphan layout getsparent = nullafterattachOrphanSibling, an orphan-hosted boundary was reachable only via the orphan scan — present for errors, missing for notFound.Added the same orphan-sibling scan to
findNearestNotFoundBoundary(error-handling.ts). Red→green unit test with anerrorBoundarybaseline proving the two finders no longer diverge.6.
loading:falseroute — a rejecting loader no longer takes down its siblings (M10)On the
loading:falsebranch, each loader's raw promise was started and thenawait Promise.all(...)ran before any promise was wrapped. A single rejecting loader (1) propagated to the segment-level boundary, collapsing the whole entry and discarding successful sibling loader data, and (2) left the other in-flight raw promises with no.catch, producing unhandled rejections (log noise / isolate churn on workerd).fresh.tsnow wraps each loader withdeps.wrapLoaderPromisebefore awaiting (the wrapped promise resolves to aLoaderDataResultand never rejects), mirroring the already-correct streaming and intercept-resolution paths. Red→green unit test (loading-false-loader-error.test.ts).7. Dev warning when an async handler returns
Response/redirect()underloading()(M9)On a route that declares
loading(), an async handler that returns aResponse(e.g.async () => redirect("/x")) has its redirect swallowed: the handler result is streamed (not awaited at the resolution boundary), so theResponsesurfaces only during RSC serialization and is rendered into the stream instead of becoming a 302/308. A synchronousResponsereturn on the same route still redirects correctly. This is a documented limitation; the fix adds a loud dev-only warning at every streamed path so the silent failure stops being silent.warnOnStreamedResponse()(helpers.ts) is wired into theloading()branch offresh.tsand (completed in the gap pass) the revalidation route + parallel-slot paths and the interceptloading()path. It detects aResponseon both resolve and reject, because the raw parallel-slot handler is not run throughhandleHandlerResult. Documented inexecution-model.mdandskills/route/SKILL.md.8. Prerender
Passthrough+loading()+ctx.passthrough()no longer bakes a corrupt artifact (M16)matchForPrerenderdetected the passthrough sentinel synchronously viaisPrerenderPassthrough(seg.component), which only works when the build handler's return value lands directly onseg.component. But when the route declaresloading(), the handler result is deferred, so for an async build handler (every documentedPassthroughbuild handler is async)seg.componentis a Promise resolving to the sentinel — the sync guard inspected__brandon the Promise, returnedfalse, and the build serialized a Flight payload of "a Promise resolving to the frozen sentinel" as a real manifest entry. At runtime that param set served the garbage artifact instead of falling through to the live handler.Extracted
detectPrerenderPassthrough()inprerender.tsthatawaits any thenableseg.componentbefore the sentinel check (rejections are swallowed and resurface during serialization, preserving prior error behavior), and wired it intoprerender-match.ts. Red→green unit test (prerender-passthrough-detect.test.ts) pins the Promise-wrapped-sentinel case.9. Loader-cache read/write/revalidate failures now reach
onError(M13)cache-error.tsdocuments a package-wide contract: cache failures must be LOUD (console + routed throughonErrorwith aCacheErrorCategory). Every other axis honored it;read-through-swr.ts— the engine behind opt-in loader caching — swallowed three failures in barecatch {}blocks, giving a consumer relying ononErrorfor cache-health telemetry a silent blind spot precisely on loader caching.WaitUntilHostnow intersectsCacheErrorReporter;loader-cache.tsalready passes the request context (which provides bothwaitUntiland the reporter), soonErrorfires from the detachedwaitUntilwith no new plumbing. Test coverage extended inread-through-swr.test.ts(previously hit/stale/miss only).10. HIGH — cross-host loader-cache leak in a multi-tenant host router (gap pass)
The route-level cache keys by
`${host}${pathname}`(cache-scope) and"use cache"pushesctx.url.host— but the loader-levelcache()default key omitted the host entirely (loader:{loaderId}:{pathname}:{params}). In a multi-tenant host router serving the same pathname for different hosts, one host's cached loader data could be served to another tenant.getDefaultLoaderCacheKeynow threadsgetRequestContext()?.url?.host ?? "localhost"into the default key, matching the route-level cache and"use cache"identity. A customoptions.keyor a store-levelkeyGeneratorstill takes precedence (3-tier resolution unchanged). Red→green cross-host test (loader-cache.test.ts) assertskeyA !== keyBfor two hosts at the same path. (No app in the repo currently runs a multi-tenant loader cache, so this never shipped a live exploit — but it is a correctness HIGH on a supported configuration.)11. Bonus consumer fix:
redirect()basename no longer double-prefixes a query/hash-only URLThe auto-prefix guard now treats the basename as already-present when followed by
/,?,#, or end-of-string. Red→green tests inredirect.test.ts.12. Public surface cleanup (pre-1.0 API hygiene)
Removed dead exports (no real consumer; cheapest to remove before first stable adoption):
OutletProvider./client@internal ResolvedSegment; only "importer" was an unused demo importSegmentCacheProvider./cacheCachedEntryResult./cacheCacheConfig/CacheConfigOrFactory./cacheCacheStatus/MAX_REVALIDATION_INTERVAL./cache/cf@internal, re-exported with no consumerAdded missing load-bearing re-exports that public JSDoc already references but were unimportable:
MiddlewareFnin particular gives the documented "extract the middleware function and unit-test it withrunMiddleware" pattern a nameable type.Internal improvements
createSSRHandlerdid not memoizereconstructedRootlike its siblingpayload/handlespromises, soReact.use()on a freshrenderSegmentspromise suspended and replayedSsrRoot, re-running the entire segment-tree build on every initial document render. NowrootPromise ??= Promise.resolve(renderSegments(...))so it runs once.expose-internal-ids.tsnow skips whenthis.environmentis known and notrsc(an unavailable environment still falls through, so no empty registry)."D"(segment.id.split("D")[0]). For an intercept slot loader${parent}.${slotName}D${i}.${loaderId}, a slot name containing an uppercaseD(e.g.@Detail) cut inside the slot name, so the loader matched no parent and its data was silently dropped. NewloaderParentId()respects the id grammar. Red→green test insegment-system.test.tsx.navigation-storesingleton removed.initNavigationStore/getNavigationStore/resetNavigationStorehad no production caller (the boot path callscreateNavigationStore()directly via the module handle); the singleton only ever threw in a running app. Removed the trio plus its self-referential test block.paramsEqual→router/params-util.ts;observeStreamedHandler→segment-resolution/streamed-handler-telemetry.ts(was an inline copy infresh.ts).RouterDependencies,InterceptResolutionDeps,LoaderRevalidationResult,RouteContentWrapperCallback, strayexport/re-export keywords).manifests.md(the eagersetRouteTrie/setPrecomputedEntriesexample that primed the exact CLAUDE.md rule Router idea 2 revisit #1 bundle regression),use-cache-api-design.md(phantom JSON fallback),caching.md,feature-file-map.md(phantom paths),host/pattern-matcher.ts(apex heuristic comment),internal/execution-model.md(theloading()+redirect limitation), SSR JSDoc package names (@rangojs/router/ssr, notrsc-router/ssr).@ts-expect-error,eslint/oxlint-disable,@__PURE__) and theuse client/use server/use cachestring directives are untouched.Behavior-change semantics table
cache("profileName")in the route tree"use cache: <profile>"orcache({ ttl })transition()/transition(children)cache()condition/key/tagscallbacksctx.env: unknown(TS18046 on the JSDoc pattern, or cast)ctx.env: TEnv/:locale(en|gb)/blog+/*vs/de/blog)notFoundBoundaryon an orphan-layout siblingerrorBoundaryworked)loading:false, one of N loaders rejectsResponse/redirect()underloading()Response/redirect()underloading()Passthrough+loading()+ctx.passthrough()onError(LOUD) with the rightCacheErrorCategoryloader:{id}:{host}{path}:{params}); isolatedredirect("/admin?tab=x")with basename/admin"/admin/admin?tab=x"(double-prefix)"/admin?tab=x"import { OutletProvider } from ".../client"import type { MiddlewareFn / PrerenderPassthroughResult / OriginCheck* }Code-change summary
Correctness
src/segment-system.tsx—loaderParentId()grammar-aware slot-Dparse;isRenderableLoadingthree-term gate extracted.src/ssr/index.tsx— memoizerootPromise(single initial render).src/router/trie-matching.ts—leafConstraintsPass()threaded into walk frames;constraintsSatisfied()shared backstop.src/router/error-handling.ts— orphan-sibling scan infindNearestNotFoundBoundary.src/router/segment-resolution/fresh.ts— wrap-before-await on theloading:falsebranch;warnOnStreamedResponseon theloading()branch.src/router/segment-resolution/helpers.ts—warnOnStreamedResponse()(resolve and reject).src/router/segment-resolution/revalidation.ts,src/router/intercept-resolution.ts—warnOnStreamedResponsewired into revalidation route/parallel-slot and interceptloading()paths.src/prerender.ts+src/router/prerender-match.ts—detectPrerenderPassthrough()awaits thenable components; intercept flatten simplified.src/cache/read-through-swr.ts— threereportCacheErrorpaths (cache-corrupt/stale-revalidation/cache-write);WaitUntilHost & CacheErrorReporter.src/router/segment-resolution/loader-cache.ts— host threaded intogetDefaultLoaderCacheKey(cross-host leak).src/route-definition/redirect.ts— basename guard for?/#/EOS.Types / public API
src/route-definition/helpers-types.ts—transition()overloads;cache→PartialCacheOptions<TEnv>.src/urls/path-helper-types.ts—cache→PartialCacheOptions<TEnv>.src/route-definition/dsl-helpers.ts— drop thestringprofile branch +| stringparam + JSDoc.src/index.rsc.ts—+ OriginCheck* / PrerenderPassthroughResult / MiddlewareFn.src/client.tsx/src/client.rsc.tsx— drop publicOutletProvider;segment-system.tsxpoints at./outlet-provider.src/cache/index.ts,src/cache/cf/index.ts,src/cache/types.ts— dead cache exports/types removed.Perf / internal
src/vite/plugins/expose-internal-ids.ts— gate the build-time pre-scan to the RSC env (was ~5×).src/router/params-util.ts,src/router/segment-resolution/streamed-handler-telemetry.ts(dedup).src/browser/navigation-store.ts/navigation-store-handle.ts/rsc-router.tsx— dead singleton removed.Test plan
Dparse (segment-system.test.tsx), trie backtracking (trie-matching.test.ts), orphannotFoundBoundary(boundary-orphan-resolution.test.ts),loading:falseloader isolation (loading-false-loader-error.test.ts), loud cache errors (read-through-swr.test.ts), prerender-passthrough detection (prerender-passthrough-detect.test.ts), dev warning (warn-streamed-response.test.ts), cross-host loader key (loader-cache.test.ts), redirect double-prefix (redirect.test.ts), and type-pins fortransition()/cache()TEnv (dsl-helper-types.check.ts, with the TS2554/TS2559/TS18046 red captured).0.0.0-experimental.124:typecheck, 3843 unit tests,lint(oxlint),format(oxfmt).Passthrough+loading()+ctx.passthrough()(M16) and async-redirect()-under-loading()(M9). Both are unit-pinned here; the e2e fixtures are written and embedded in the issue. (Per the repo's dev+prod e2e rule, this gap is flagged, not silently closed.)Notes / deferred
Tracked as GitHub issues, each with userland examples:
Passthrough+loading()(M16), async-redirect-under-loading()(M9), intercept_currentSegmentId, server-action-bridge over-invalidation, document-cacheprivate,s-maxage; plus a layer-by-layer unit-vs-e2e assessment using the new@rangojs/router/testingprimitives.yieldFromStorededup, loader-cachectx.userebind, trie longest-suffix.router.usepattern-param overload,buildEnvper-preset type restructure, plugin renamevite-plugin-poke→@rangojs/router:poke,StaticHandlerOptionsdoc-sync.~13 low/info findings deliberately dropped (intentional patterns, negligible perf, consistency warts with no present defect) — each recorded in the tracker rather than actioned.