Repository navigation
fix(router): nested unnamed include() routes match; dispatch matches through the route trie - #1128
Merged
Merged
Conversation
commit: |
ivogt
force-pushed
the
fix/include-names-dispatch-trie
branch
from
October 13, 2026 02:38
7838448 to
f4ee465
Compare
…mes come from tree position (#1104) An unnamed include() took its hidden scope name ($prefix_N) from a per-context counter. Build discovery runs each include's handler with fresh counters and runtime expansion with the parent's snapshot, so an unnamed include nested in another was $prefix_0.$prefix_0.login to the trie and $prefix_0.$prefix_1.login at runtime: the route 404'd whenever the trie was live, with no warning. The same counter gave an unnamed include beside a { name: "" } include the name of an unnamed include behind it, so one URL rendered the other's handler. Every include() now takes its index among its handler run's includes, and the DSL context carries the position path (scopePath / hiddenScopePath) into the nested run through TrackedInclude (discovery) and LazyIncludeContext (runtime expansion, render-time loadManifest). An unnamed include's hidden segment is $prefix_ plus its positions since the nearest include that added a name segment, e.g. $prefix_1_0 behind a transparent include. Hidden names stay out of every generated file and public route name; they can shift on upgrade, so the changelog asks apps that pin createRouter({ version }) and use on-demand prerender under unnamed includes to bump it. In dev, a trie key that no entry at its static prefix registered now fails the request with an error naming the key, the prefix and the includes checked (it can also show briefly in the HMR window; the message says a reload fixes that); production keeps the fallback. Tests: include-scope-names.test.ts (build keys equal runtime keys; dispatch serves every route through the dev and the production trie), find-match dev check, e2e include-scope (dev + production) in the router test-app and cloudflare-basic.
…duction do (#1107) dispatch matched a router with no trie by URL pattern alone (the regex fallback), while serveShellRequest and the router's handler build the trie first. A router therefore answered by what ran before it: a trailing-slash URL for a route with no trailingSlash option got a 308 on a fresh router and the route after a serveShellRequest call. Trie-only bugs such as #1104 passed in dispatch and 404'd in the app. dispatch now loads a registered build manifest and otherwise matches through a dev trie built from the router itself, the way rsc/handler.ts builds it (one in-flight build per router). Router ids can repeat across instances, so rsc/manifest-init.ts records which router object installed each id's trie (isRouterTrieBuiltFrom); dispatch rebuilds when the id's trie came from another router, and reuses a trie serveShellRequest built for the same router. A route with no trailingSlash serves both forms with no redirect, as production does; "never"/"always" still 308. Building the trie runs route discovery, so include handlers run once more and async include providers are called. loadManifest now shares one in-flight provider resolution per lazy entry, so concurrent first renders of a leaf async include (resolved at render time behind the precomputed shortcut) import it once, as match-time expansion does. Tests that relied on the fallback: - async-include-dispatch.test.ts: provider-call and handler-run counts start after a first dispatch (discovery calls each provider); the leaf include's handler runs once (precomputed routes at match time); concurrent dedup is pinned for a non-leaf include (match time) and a leaf include (render time). - cloudflare-basic prefetch-false-client-urls.rsc-test.tsx: the #1107 workaround router is gone; a fresh and a served router both serve the trailing-slash URL as the route. New: dispatch.test.ts routers sharing an id each match their own routes; userland cloudflare-basic include-scope-dispatch.test.ts (#1104 and the transparent-scope collision through dispatch) and dispatch-after-shell.rsc-test.tsx (#1107).
ivogt
force-pushed
the
fix/include-names-dispatch-trie
branch
from
October 13, 2026 03:05
f4ee465 to
7f1ad11
Compare
8 of 9 tasks
ivogt
added a commit
that referenced
this pull request
Oct 13, 2026
… in 0.24.3 A clientUrls() layout() loader is held across the group's routes since #1137, so the e-commerce recipe no longer steers the cart loader to a server layout for that reason, and large-app loses its known-gap section. The nested unnamed include note now says 0.24.x: #1128 ships after 0.24.3.
ivogt
added a commit
that referenced
this pull request
Oct 13, 2026
…-app, content and e-commerce apps (#1135) * docs(skills): app-structure — concrete urls() layouts for SaaS, multi-app, content and e-commerce apps A new skill, packages/rangojs-router/skills/app-structure/, puts the primitives together into whole apps. SKILL.md holds the rules every recipe shares (route tree in its own module, a named sync include() per section, identity in router.use() and gates in route middleware, shared chrome in a server layout, generated files), sync vs async include(), include naming (name nested includes through 0.24.2, #1104), where shared chrome lives, a growing-an-app path, and the minimal one-module app. Four companion recipes: - large-app.md: a server shell over auth, onboarding, app, settings and admin clientUrls() groups; session in global middleware, sign-in, onboarding and role guards around the includes; prefetch: false with its measured trade-off; actions + revalidate(), refetch()/refreshGroup and cache tags for updates; fetchable loaders guard themselves; the group layout() loader gap through 0.24.2 and its workaround. - multi-app.md: createHostRouter() with apex, app., admin. and docs. apps, what is shared and how, one TypeScript program per app, the host-override cookie in dev, cross-app links as document navigations. - content-site.md: docs with Prerender + ppr and a Static nav, a blog with Prerender onDemand + Passthrough, the publish webhook beside the entry, machine routes. - e-commerce.md: ppr category and product pages with tagged "use cache" reads and live loader holes, the cart as a layout loader, a guarded server-rendered checkout, the clientUrls() shop variant. Wiring: the rango index and README tables list the skill, catalog.json is regenerated, check-docs-api guards the five files, and check-skills allowlists the /app and /settings route examples. * docs(skills): app-structure: drop the group layout() loader gap fixed in 0.24.3 A clientUrls() layout() loader is held across the group's routes since #1137, so the e-commerce recipe no longer steers the cart loader to a server layout for that reason, and large-app loses its known-gap section. The nested unnamed include note now says 0.24.x: #1128 ships after 0.24.3. * docs(skills): app-structure: links by name (ctx.reverse, useReverse with the generated map) and throw rewrite()
This was referenced Oct 23, 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.
devcorner: dc-t7 · dc-t26
Closes #1104
Closes #1107
Problem
#1104. With the trie live (every request in
vite devand production), the routes of an unnamedinclude()inside another unnamedinclude()answered 404, with no warning from the build,rango generateor the dev server. The same cause made a route behind a{ name: "" }include render another module's handler, or 404 in production, when two included modules named a route alike (index).#1107.
dispatchfrom@rangojs/router/testingmatched a router with no trie by URL pattern, whileserveShellRequestand the real handler build the trie first. A router answered differently depending on what ran before:/api/items/got a 308 on a fresh router and a 200 afterserveShellRequest. A route only the trie gets wrong (the #1104 shape) passed indispatchand 404'd in the app.Cause of #1104: the hidden include name
$prefix_Ncame from a counter whose value differs by realm. Build discovery produced$prefix_0.$prefix_0.login; lazy expansion at runtime produced$prefix_0.$prefix_1.login, because the counter snapshot is carried into lazy includes.find-match.tsthen fell back silently.Before and after
End-to-end consumer example
An app mounts a public area and a sign-in area, both unnamed, and tests them in userland:
No source change is needed. The
include("/", signInPatterns, { name: "" })workaround keeps working.Semantics
{ name: "" }include holding another unnamed include, same route name in bothvite devdispatch, route with notrailingSlashand the other slash formserveShellRequestdispatch,trailingSlash: "never"or"always"dispatch, wildcard vs specific routectx.routeName,transition({ when })dispatch, include handlers and async providersdispatchper router, when the trie is builtCode changes
src/urls/include-helper.ts: hidden names come from tree position, not a counter.nextIncludePositioncounts per handler run (keyed byscopePath, which no ancestor snapshot holds), so every run counts from 0 in every realm. A nested unnamed include is$prefix_0.$prefix_0; one behind a transparent include is$prefix_1_0.Why here: it is the one place that allocates the name; deriving it from position makes build and runtime agree by construction.
IncludeScopePositionrides on the DSL context and throughTrackedIncludeandLazyIncludeContext(server/context.ts,types/route-entry.ts,build/generate-manifest.ts).src/router/lazy-includes.ts: oneresolveEntryPatternsfor match-time expansion andloadManifest; replaces_lazyInflightand theproviderResolutionsWeakMap. Concurrent first hits share the import.src/router/find-match.ts: the dev tripwire (describeUnownedTrieRoute).src/testing/dispatch.ts,src/rsc/manifest-init.ts:dispatchcallsensureRouterTrie(router), which builds the trie unless the id has one from this router object. ANO_TRIEmarker stops a zero-route router rebuilding on every dispatch.handler.tsis untouched: dev on Node gets tries pushed by router-discovery that no router object built, so the dispatch predicate would rebuild there.Docs:
docs/internal/matching-and-lazy-discovery.md,async-includes.md,feature-map.md,docs/testing.md,skills/testing/response-routes.md.Not applied: deriving
includeScopefromscopePath, becauseincludeScopefeeds shortCodes, which are segment ids.Test plan
src/router/__tests__/include-scope-names.test.ts(24 tests): build discovery and runtime expansion produce the same keys; dispatch serves every route through the dev and the production trie. 20 red without the fix.src/router/__tests__/find-match.test.ts: the dev throw names the route; red without the fix.src/testing/__tests__/dispatch.test.ts,async-include-dispatch.test.ts: trie built once per router; theNO_TRIEtest failed with "expected 4 to be 2" before the marker.tests/cloudflare-basic/test/include-scope-dispatch.test.tsanddispatch-after-shell.rsc-test.tsx: both red without the fix; the second pins the 200-before-and-after answer.packages/rangojs-router/e2e/include-scope.test.tsandtests/cloudflare-basic/e2e/include-scope.test.ts, each with a dev and a(production)describe. Router 115/115 (include-scope, route-resolution, semantic-matrix, include-middleware, optional-include), cloudflare-basic include-scope 4/4, dev and production, on the pre-rebase tree. Red was shown for cloudflare-basic dev only; the production counterparts were not run red.test:unit:allrouter 8,733 passed (5 skipped, 6 todo), rsc 830 passed (2 expected fail), consumer suites 1, 3, 20, 25, 27, 27, 87 passed; lint 0 warnings; format clean;check:e2e-bucketingOK;check-bundle-guardsgreen, router chunk 49,527 B against 49,536 B.Notes
createRouter({ version })and prerenders on demand under unnamed includes should bumpversionon upgrade. This is in the CHANGELOG entry.tools/generate-route-types.mjsdoes not exist on this base. When the gen-files PR (dc-t21) lands first, this branch adds fixture modules (urls/include-scope.tsx,pages/include-scope.tsx) and needsnode tools/generate-route-types.mjsrun and committed after the rebase.git status --porcelain | grep '\.gen\.ts'is empty now.dispatchper router runs every includedurls()handler once more; tests counting handler or provider calls count from after it.