Repository navigation
fix(router): review follow-ups — #567 dual-mode e2e, #569 pre-1.0 API decisions, #570 cleanup - #573
Merged
Merged
Conversation
… and redirect under loading() (M9) Adds the dev + production e2e fixtures the M16/M9 unit-tested fixes still lacked (CLAUDE.md mandates dual-mode coverage). Deferred from the stabilization PR; addresses the two main gaps in #567. - prerender-loading: Prerender + loading() + ctx.passthrough(). The loading() boundary defers the build handler so the passthrough sentinel arrives Promise-wrapped; detectPrerenderPassthrough awaits it. getParams lists [baked, skip]: baked is prerendered, skip passes through to the live handler. Production asserts the manifest holds baked but NOT skip (M16 regression guard: count === 1; without the fix skip would bake a corrupt artifact). - loading-redirect: a synchronous redirect() return under loading() short-circuits to a real HTTP redirect. Verified for hard + soft navigation in both modes. Regenerated router.named-routes.gen.ts for the two new includes (urls.gen.ts is legacy/unreferenced and no longer auto-generated, so left untouched). Lower-priority #567 gaps (build-time intercept _currentSegmentId handle drop, server-action-bridge pre-dispatch over-invalidation, document-cache private+s-maxage) need fixes first and are deferred; #567 stays open for them.
Internal-only, behavior-preserving cleanups; grep-verified safe and gate-green (typecheck, full unit suite, lint, format). - Remove write-only MiddlewareEntry.mountPrefix (read nowhere; scoping folds into fullPattern before the entry is built). Dropped 3 production assignments plus the test fixtures. - Dedupe cookie helpers: delete the test-only middleware-cookies.ts (byte-for-byte dup of request-context's parseCookiesFromHeader/serializeCookieValue), export the canonical copies from request-context for the unit tests, drop the middleware.ts re-export. - Delete the zero-importer src/handles/index.ts barrel (Meta/MetaTags/Breadcrumbs are exported directly from the client/index entries); repoint collect-handle.test. - Drop the MiddlewareCollectableEntry re-export from middleware.ts; its one importer now imports from middleware-types. - Extract hasContextVars() into context-var.ts and use it in the two prerender entry points that duplicated the keys-or-symbols predicate. - renderStaticSegment: throw a TypeError when a Static handler returns a Response (was silently serialized as a corrupt build artifact); the fresh/revalidation paths already throw via handleHandlerResult. Adds a unit test. - ctx.reverse() middleware fallback: widen the signature to (name, params?, search?) to match ScopedReverseFunction and make the error message accurate. - docs: tree-structure.md inTransitionScope also covers error/notFound segments. Deferred, still open in #570: the optionalParams all-or-nothing removal and the loaderPromises param removal both sit on matrix-critical matching/segment-resolution paths and warrant a semantic-matrix run rather than a cleanup sweep. The document-cache "timing-gap caveat" cited at document-cache.ts:45-53 does not exist there (those lines are Cache-Control parsing), so that doc item is a no-op.
…part 1) Type/naming/export-surface decisions (no behavior change). See #569. - #1 router.use(pattern, mw): add a pattern-generic overload so `.use("/users/:id", (ctx) => ...)` types `ctx.params.id` via ExtractParams. Bare `.use(mw)` stays generic. Added on both Rango and RangoInternal. Pins it with a type-level test (router-use-types.test.ts). - #2 useAction: export a stable public `ActionState` (and `ActionLifecycleState`) from ./client so consumers can name the hook's return type; TrackedActionState stays the @internal impl name (ActionState aliases it). - #4 buildEnv: move off the shared base onto per-preset interfaces. Cloudflare keeps `"auto"` (wrangler getPlatformProxy); Node now rejects `"auto"` at the type level (Exclude<BuildEnvOption,"auto">) instead of only at build time. - #5 poke plugin: rename name "vite-plugin-poke" -> "@rangojs/router:poke" so it matches the @rangojs/router:* family for consumers ordering/filtering by name. - #6 ./build: move realm-crossing pipeline internals (buildRouteTrie, buildPerRouterTrie, TrieNode, TrieLeaf, collectFallbackClientRefs) to a new ./build/internal subpath; ./build keeps the documented public API (generateManifest*, route-type generation, createScanFilter, hashParams). - #8 mergeHandlerUse: document the intentional validation asymmetry (explicit use() items bypass the mount-site allow-list); chosen over enforcing it. Deferred to part 2 (behavior changes): #3 prefetch options, #7 scanFilter wiring, #9 profile-registry trim, #10 redirect-in-action idiom + examples.
…omics (#569 part 2a) - #3 router.prefetch(url, { key: ":source" }): add an options arg so imperative prefetch can source-scope its cache entry, at parity with <Link prefetchKey=":source">. Wired through to prefetchDirect. - #10 redirect() in actions: drop the `as any` on non-form actions by typing them Promise<Response> (saveAndRedirect, actionSimpleRedirect). The form-bound peReturnRedirect keeps a documented intrinsic cast (React's form-action type is () => void | Promise<void>, which can't express a Response return); consumers should prefer `throw redirect(...)`. Strengthened the server-actions skill. Deferred to part 2b: #7 (complete scanFilter — needs a new public plugin option) and #9 (trim profile-registry — runtime cache-resolution refactor).
…istry (#569 part 2b) - #7 complete the scanFilter feature. Adds a public rango() option `discovery: { include, exclude }` (glob, root-relative; mirrors the CLI's --include/--exclude). The consume side was already wired (findRouterFiles(state.projectRoot, state.scanFilter) at 3 sites); this threads the option through PluginOptions and compiles it via createScanFilter into state.scanFilter in configResolved, once projectRoot is known. Filters which files route discovery / static type-gen scan. Tests in generate-route-types.test.ts (include restricts, exclude removes). - #9 trim the profile registry. Removed the global mutable `_profiles`, `setCacheProfiles`, and `getCacheProfile` — all dead: `getCacheProfile` had no production reader (its own JSDoc said so), and `setCacheProfiles` only populated the unread global. Kept the load-bearing `resolveCacheProfiles`, which createRouter() already uses to thread a per-request profile map (the "use cache: <profile>" path reads it request-scoped via ctx._cacheProfiles). Dropped the router.ts setCacheProfiles() call; repointed the tests to resolveCacheProfiles (and reframed the "global replace" test as per-router map independence). #569 is now fully implemented (all 10 decisions).
commit: |
…in return-form redirect actions (#569.10) CI on #573 caught a regression from #569 part 1: the Vite discovery pipeline imports buildRouteTrie / buildPerRouterTrie / collectFallbackClientRefs at build time via runner.import("@rangojs/router/build") (discover-routers.ts), and the RSC runner does not resolve a ./build/internal subpath in that realm. Moving them left collectFallbackClientRefs undefined, so the router-level default error / notFound boundary was never collected into the app-fallback chunk (mini.test.ts clientChunks: "mini-default-error must be in app-fallback"). Revert #569.6: restore buildRouteTrie/buildPerRouterTrie/collectFallbackClientRefs + TrieNode/TrieLeaf on ./build; delete ./build/internal + its package.json export. #569.6 is re-deferred — internalizing these needs runner subpath resolution or a different seam (noted in build/index.ts). #569.10 revision: per owner, the return-form redirect test actions (saveAndRedirect, actionSimpleRedirect) now `throw redirect()` instead of being typed Promise<Response>. The form-bound peReturnRedirect keeps its documented intrinsic cast. Verified: mini clientChunks 6/6, location-state e2e 47/47 (dev+prod), typecheck, 3866 unit, lint, format.
This was referenced Jun 13, 2026
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.
Summary
Follow-up work deferred from the multi-agent review stabilization PR (#571, now merged), tracked across #567, #569, and #570. Rebased onto the post-#571 main.
This bundles three things a consumer can feel — new public API surface (#569), mandated dual-mode e2e for two correctness fixes (#567), and internal cleanup (#570). It deliberately excludes the matrix-critical items (see Deferred) so they can land in a focused, semantic-matrix-gated PR.
#569 — pre-1.0 public-API decisions (9 of 10; #6 deferred)
Each was an owner decision. Userland before/after for the consumer-facing ones, then a semantics table for the full set.
1.
router.use(pattern, mw)now typesctx.params2.
useAction()return type is now nameable3.
router.prefetch()source-scope option (parity with<Link>)7.
discoveryplugin option (completes the half-wired scanFilter)10.
redirect()in actions — drop theas anyDecision table
router.useparam typingExtractParams<P>); bare form unchangeduseActionreturn typeActionState/ActionLifecycleState;TrackedActionStatestays@internalprefetch(url, { key: ":source" })buildEnv: "auto""auto", Node rejects it at the type levelvite-plugin-poke→@rangojs/router:poke./buildinternals./build/internalmove; reverted (CI caught it). The Vite discovery pipeline importsbuildRouteTrie/buildPerRouterTrie/collectFallbackClientRefsat build time viarunner.import("@rangojs/router/build"), and the RSC runner doesn't resolve a./build/internalsubpath in that realm. Needs runner subpath resolution or a different seam.scanFilterdiscovery: { include, exclude }option compiled intostate.scanFiltermergeHandlerUsevalidation gap_profiles/setCacheProfiles/getCacheProfile; kept request-scopedresolveCacheProfilesreturn redirect()in actionsthrow redirect()(chosen over widening the type); return-form test actions now throw; skill guidance strengthened#10 nuance: a form-bound action (
<form action={fn}>) returning a redirect keeps an intrinsic, now-documentedas any— React's form-action type is() => void | Promise<void>and can't express aResponsereturn;throwis the clean path.#567 — dual-mode e2e for two landed fixes (M16, M9)
The fixes shipped in #571 with unit coverage but lacked the mandated dev + production e2e.
loading()+ctx.passthrough(): theloading()boundary defers the build handler, so the passthrough sentinel arrives Promise-wrapped;detectPrerenderPassthroughawaits it. New fixture pre-rendersbaked, skipsskipviactx.passthrough(). Production asserts the manifest holdsbakedbut notskip(count === 1) — the regression guard.redirect()underloading(): short-circuits to a real HTTP redirect. Verified for hard + soft navigation.Both fixtures wired into the test-app,
router.named-routes.gen.tsregenerated, 11 tests across dev +(production build).#570 — safe cleanup (8 of 10 items)
Internal-only, behavior-preserving: removed write-only
MiddlewareEntry.mountPrefix; deduped cookie helpers (deleted test-onlymiddleware-cookies.ts, canonicalized inrequest-context); deleted the zero-importerhandles/index.tsbarrel; dropped a staleMiddlewareCollectableEntryre-export; extractedhasContextVars(); added a loudTypeErrorguard when aStatichandler returns aResponse(was a silent corrupt artifact); clarified thectx.reverse()middleware fallback; doc fix forinTransitionScope.Deferred (tracked, intentionally not here)
./buildinternals →./build/internal): blocked by the build-runtimerunner.import("@rangojs/router/build")consumers; needs runner subpath resolution or a different seam.Matrix-critical (need a
semantic-matrixrun), saved for a focused PR:optionalParamsall-or-nothing removal;loaderPromisesunused-param removal._currentSegmentIdhandle, server-action pre-dispatch over-invalidation, document-cacheprivate, s-maxage).The
document-cache"timing-gap caveat" cited in #570 is a no-op — it doesn't exist at the cited lines (those are Cache-Control parsing).Test plan
pnpm run typecheck— 12/12pnpm run test:unit:all— 3866 router + all consumer suitespnpm run lint/pnpm run format— cleanprerender-loading,loading-redirect)clientChunkse2e 6/6, location-state e2e 47/47 (dev+prod) — verifying the post-CI fix (feat: improve server action UI update scheduling and add Prettier #6 revert + feat: add kanban dashboard with optimistic updates #10 throw)router-use-types(Router idea 2 revisit #1),static-handler-response-guard(Safe cleanup backlog from review (write-only fields, dedup helpers, test-only dupes) #570), discovery scanFilter (feat: LRU segment cache with transaction-based state management #7), repointedcache-remediation(feat: server action concurrency and error boundary support #9)