Repository navigation
fix(router): name-less children-fn path() collapsed the whole route map (#642) - #643
Merged
Merged
Conversation
…ap (#642) The name-less 3-arg children-fn form path("/x", C, () => [...]) inferred its route-name type param TName as the bare `string` constraint instead of the UnnamedRoute sentinel: a function value structurally satisfies the all-optional PathOptions<TName> union member of the 3rd parameter, so TS resolved TName to its constraint. ExtractRoutesFromItem then mapped over a bare `string` key producing `{ [K in string]: TPattern }`, an index signature that collapsed the entire sibling map (Rango.Path -> never), breaking every href()/Link.to. Fix: a centralized widened-name guard `string extends TName ? {}` in both ExtractRoutesFromItem and ExtractResponsesFromItem (covers all path.* variants, which funnel through these two extractors), plus a `[R] extends [never] ? {}` blast-radius guard in the *FromItems combinators. The guard is precise: template-literal names and literal unions are preserved. Tests: 13 expectTypeOf assertions through the public urls() API (full call-form matrix, response variants incl. path.stream, nested cache()/layout(), empty urls()); a name-less children-fn route in cloudflare-basic making its typecheck an end-to-end guard; and a dev+prod e2e proving the route renders and hydrates.
commit: |
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.
Closes #642
Problem
A single name-less route written in the 3-arg children-fn form collapsed the
entire app's generated route map — not just the offending route:
Downstream,
Rango.Pathresolved toneverand everyhref(...)/Link.to/named-route call in the app failed to typecheck. In
tests/cloudflare-basic(which registers
RegisteredRoutes extends typeof router.routeMap) this isobservable as ~28 cascading errors like:
The 2-arg form (
path(pattern, component)) and the named 3-arg form bothworked. Reproduced identically in two independent apps, so it was the overload,
not app-specific augmentation.
Root cause
For
path(pattern, component, () => [...]), the third argument's parameter typeis
PathOptions<TName, TSearch> | (() => UseItems<RouteUseItem>). A functionvalue structurally satisfies the all-optional
PathOptions<TName>member, soTypeScript resolves the route-name type parameter
TNameto its constraintstringinstead of theUnnamedRoute = "$unnamed"default.ExtractRoutesFromItemthen mapped over a bare
stringkey:That index signature, intersected across siblings via
UnionToIntersection,poisons the whole map. (Confirmed via repro: the 2-arg form infers TName
"$unnamed"; the children-fn form infersstring.)Fix
packages/rangojs-router/src/urls/type-extraction.ts— a centralizedwidened-name guard. This is the right location because every path variant
(
path,path.json/.text/.html/.xml/.md/.image/.stream/.any) funnels throughthese two extractors, so one guard covers them all with no change to the five
delicate
PathFnoverload return types:Plus a defense-in-depth blast-radius guard in
ExtractRoutesFromItems/ExtractResponsesFromItemsso aneverintersection can never propagate as thewhole map (also makes empty
urls(() => [])yield{}instead ofnever):The guard is precise:
string extends TNameis true only for the barestringconstraint. Template-literal names (item${number}fromArray.fromloops) and literal unions (
"a" | "b") arefalseunder it and remain preserved.Before vs. after
{ [x: string]: "/section/stream" }(index sig){}{ [x: string]: ... } & { about }-> poisoned{ about: "/about" }Rango.Pathnever"/about"(+ others)href("about")Semantics
path(p, C)(2-arg, unnamed)"$unnamed"{}path(p, C, () => [...])(name-less children-fn)string→ guarded{}(was index sig)path(p, C, { name: "x" })"x"{ x: p }path(p, C, { name: "x" }, () => [...])"x"{ x: p }path.json(p, h, () => [...])(name-less)string→ guarded{}(route+response)stringvariable (pre-existing, unchanged)string→ guarded{}(now excluded, previously poisoned)Tests
src/__tests__/unnamed-children-fn-route-types.test.ts— 9
expectTypeOfassertions through the publicurls()API across the fullcall-form matrix (name-less children-fn, empty children-fn, whole realistic map,
multiple name-less, 2-arg, named children-fn, named options) plus the response
map for name-less
path.json/path.text. Pins the exact{ about: "/about" }and
{ health: {...} }results the bug corrupted./unnamed-children-fntotests/cloudflare-basic/src/urls.tsx. Because that appwires
RegisteredRoutes extends typeof router.routeMap, itspnpm typechecknowcollapses if this regresses (proved red-before-green: 28 errors without the fix,
0 with it).
e2e/unnamed-children-fn.test.tsasserts the route resolves, renders,and hydrates in dev AND production (the runtime contract the issue asserted
but never proved).
app),
test:unit:all(router 4685+66, cloudflare-basic 46+10, vite-rsc-demo 25+1,mini 21+3), oxlint (0/0), oxfmt, e2e-bucketing + docs-api guards. No
*.gen.tschanged (unnamed route correctly stays out of the named map).
Notes
compile-time inference fix. The e2e is a runtime no-regression proof.
path-helper-types.ts) is left as-is: the onlyconsumers of the item's
TNamephantom are these two extractors, so guardingthem is the complete, minimal fix and avoids touching the five path-variant
biconditional return types.