@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/AGENTS.md +6 -10
  2. package/README.md +289 -938
  3. package/dist/bin/rango.js +271 -46
  4. package/dist/vite/index.js +673 -193
  5. package/package.json +10 -8
  6. package/skills/api-client/SKILL.md +1 -1
  7. package/skills/breadcrumbs/SKILL.md +31 -14
  8. package/skills/cache-guide/SKILL.md +5 -2
  9. package/skills/caching/SKILL.md +59 -4
  10. package/skills/catalog.json +271 -0
  11. package/skills/comparison/SKILL.md +50 -0
  12. package/skills/comparison/agents/openai.yaml +4 -0
  13. package/skills/comparison/references/framework-comparison.md +837 -0
  14. package/skills/composability/SKILL.md +83 -2
  15. package/skills/debug-manifest/SKILL.md +1 -1
  16. package/skills/defer-hydration/SKILL.md +235 -0
  17. package/skills/document-cache/SKILL.md +9 -1
  18. package/skills/fonts/SKILL.md +1 -1
  19. package/skills/handler-use/SKILL.md +8 -8
  20. package/skills/hooks/SKILL.md +54 -892
  21. package/skills/hooks/data.md +273 -0
  22. package/skills/hooks/handle-and-actions.md +103 -0
  23. package/skills/hooks/navigation.md +110 -0
  24. package/skills/hooks/outlets.md +41 -0
  25. package/skills/hooks/state.md +228 -0
  26. package/skills/hooks/urls.md +135 -0
  27. package/skills/host-router/SKILL.md +4 -4
  28. package/skills/i18n/SKILL.md +1 -1
  29. package/skills/intercept/SKILL.md +46 -14
  30. package/skills/layout/SKILL.md +27 -10
  31. package/skills/links/SKILL.md +1 -1
  32. package/skills/loader/SKILL.md +23 -1
  33. package/skills/middleware/SKILL.md +7 -3
  34. package/skills/migrate-nextjs/SKILL.md +167 -6
  35. package/skills/migrate-react-router/SKILL.md +59 -677
  36. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  37. package/skills/migrate-react-router/component-migration.md +196 -0
  38. package/skills/migrate-react-router/data-and-actions.md +225 -0
  39. package/skills/migrate-react-router/route-mapping.md +271 -0
  40. package/skills/mime-routes/SKILL.md +1 -1
  41. package/skills/observability/SKILL.md +9 -1
  42. package/skills/parallel/SKILL.md +23 -4
  43. package/skills/ppr/SKILL.md +622 -0
  44. package/skills/prerender/SKILL.md +28 -18
  45. package/skills/rango/SKILL.md +84 -25
  46. package/skills/response-routes/SKILL.md +15 -1
  47. package/skills/route/SKILL.md +71 -4
  48. package/skills/router-setup/SKILL.md +14 -3
  49. package/skills/scripts/SKILL.md +1 -1
  50. package/skills/server-actions/SKILL.md +3 -2
  51. package/skills/shell-manifest/SKILL.md +185 -0
  52. package/skills/streams-and-websockets/SKILL.md +1 -1
  53. package/skills/tailwind/SKILL.md +1 -1
  54. package/skills/testing/SKILL.md +2 -1
  55. package/skills/testing/handles.md +4 -2
  56. package/skills/testing/render-handler.md +15 -14
  57. package/skills/testing/reverse-and-types.md +8 -7
  58. package/skills/theme/SKILL.md +1 -1
  59. package/skills/typesafety/SKILL.md +45 -919
  60. package/skills/typesafety/env-and-bindings.md +254 -0
  61. package/skills/typesafety/generated-files-and-cli.md +335 -0
  62. package/skills/typesafety/params-and-search.md +153 -0
  63. package/skills/typesafety/route-types.md +209 -0
  64. package/skills/use-cache/SKILL.md +30 -3
  65. package/skills/vercel/SKILL.md +1 -1
  66. package/skills/view-transitions/SKILL.md +44 -1
  67. package/src/browser/event-controller.ts +62 -10
  68. package/src/browser/logging.ts +28 -0
  69. package/src/browser/merge-segment-loaders.ts +6 -4
  70. package/src/browser/navigation-bridge.ts +65 -16
  71. package/src/browser/navigation-client.ts +32 -2
  72. package/src/browser/navigation-store.ts +128 -14
  73. package/src/browser/network-error-handler.ts +34 -7
  74. package/src/browser/partial-update.ts +76 -17
  75. package/src/browser/prefetch/cache.ts +51 -11
  76. package/src/browser/prefetch/fetch.ts +59 -21
  77. package/src/browser/prefetch/queue.ts +19 -4
  78. package/src/browser/react/Link.tsx +13 -3
  79. package/src/browser/react/NavigationProvider.tsx +108 -4
  80. package/src/browser/response-adapter.ts +38 -9
  81. package/src/browser/rsc-router.tsx +54 -4
  82. package/src/browser/scroll-restoration.ts +7 -5
  83. package/src/browser/segment-reconciler.ts +31 -21
  84. package/src/browser/server-action-bridge.ts +22 -10
  85. package/src/browser/types.ts +54 -1
  86. package/src/build/generate-manifest.ts +155 -131
  87. package/src/build/index.ts +3 -1
  88. package/src/build/route-trie.ts +35 -7
  89. package/src/build/route-types/include-resolution.ts +347 -47
  90. package/src/build/runtime-discovery.ts +4 -1
  91. package/src/cache/cache-key-utils.ts +29 -0
  92. package/src/cache/cache-runtime.ts +262 -71
  93. package/src/cache/cache-scope.ts +2 -17
  94. package/src/cache/cache-tag.ts +60 -14
  95. package/src/cache/cf/cf-cache-store.ts +243 -20
  96. package/src/cache/document-cache.ts +54 -21
  97. package/src/cache/index.ts +1 -0
  98. package/src/cache/memory-segment-store.ts +110 -3
  99. package/src/cache/profile-registry.ts +15 -0
  100. package/src/cache/read-through-swr.ts +15 -1
  101. package/src/cache/segment-codec.ts +4 -4
  102. package/src/cache/shell-snapshot.ts +417 -0
  103. package/src/cache/types.ts +158 -0
  104. package/src/cache/vercel/vercel-cache-store.ts +401 -124
  105. package/src/client.rsc.tsx +0 -3
  106. package/src/client.tsx +0 -3
  107. package/src/cloudflare/tracing.ts +7 -8
  108. package/src/defer.ts +11 -22
  109. package/src/handle.ts +37 -15
  110. package/src/handles/MetaTags.tsx +16 -82
  111. package/src/handles/breadcrumbs.ts +12 -14
  112. package/src/handles/deferred-resolution.ts +127 -0
  113. package/src/handles/is-thenable.ts +7 -8
  114. package/src/handles/meta.ts +7 -44
  115. package/src/host/errors.ts +15 -0
  116. package/src/host/index.ts +1 -0
  117. package/src/index.rsc.ts +8 -2
  118. package/src/index.ts +19 -13
  119. package/src/internal-debug.ts +11 -8
  120. package/src/prerender.ts +17 -4
  121. package/src/redirect-origin.ts +14 -0
  122. package/src/render-error-thrower.tsx +20 -0
  123. package/src/route-content-wrapper.tsx +12 -5
  124. package/src/route-definition/dsl-helpers.ts +21 -32
  125. package/src/route-definition/helper-factories.ts +0 -2
  126. package/src/route-definition/helpers-types.ts +43 -43
  127. package/src/route-definition/index.ts +1 -2
  128. package/src/route-definition/resolve-handler-use.ts +0 -1
  129. package/src/route-definition/use-item-types.ts +3 -6
  130. package/src/route-map-builder.ts +41 -4
  131. package/src/route-types.ts +0 -5
  132. package/src/router/find-match.ts +86 -8
  133. package/src/router/instrument.ts +9 -4
  134. package/src/router/lazy-includes.ts +72 -12
  135. package/src/router/loader-resolution.ts +14 -2
  136. package/src/router/manifest.ts +56 -11
  137. package/src/router/match-api.ts +76 -32
  138. package/src/router/match-handlers.ts +181 -135
  139. package/src/router/match-middleware/background-revalidation.ts +40 -23
  140. package/src/router/match-middleware/cache-store.ts +39 -24
  141. package/src/router/match-result.ts +35 -15
  142. package/src/router/middleware.ts +64 -38
  143. package/src/router/navigation-snapshot.ts +7 -5
  144. package/src/router/parse-pattern.ts +115 -0
  145. package/src/router/pattern-matching.ts +53 -64
  146. package/src/router/prefetch-limits.ts +37 -0
  147. package/src/router/prerender-match.ts +11 -5
  148. package/src/router/preview-match.ts +3 -1
  149. package/src/router/request-classification.ts +23 -8
  150. package/src/router/route-snapshot.ts +14 -2
  151. package/src/router/router-context.ts +3 -1
  152. package/src/router/router-interfaces.ts +32 -1
  153. package/src/router/router-options.ts +30 -0
  154. package/src/router/segment-resolution/fresh.ts +39 -3
  155. package/src/router/segment-resolution/loader-cache.ts +93 -2
  156. package/src/router/segment-resolution/loader-mask.ts +60 -0
  157. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  158. package/src/router/segment-resolution/mask-nested.ts +83 -0
  159. package/src/router/segment-resolution/revalidation.ts +3 -0
  160. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  161. package/src/router/substitute-pattern-params.ts +54 -35
  162. package/src/router/telemetry-otel.ts +6 -8
  163. package/src/router/telemetry.ts +9 -1
  164. package/src/router/tracing.ts +14 -5
  165. package/src/router/trie-matching.ts +19 -11
  166. package/src/router/url-params.ts +13 -0
  167. package/src/router.ts +47 -16
  168. package/src/rsc/full-payload.ts +70 -0
  169. package/src/rsc/handler.ts +60 -33
  170. package/src/rsc/manifest-init.ts +1 -1
  171. package/src/rsc/nonce.ts +10 -1
  172. package/src/rsc/progressive-enhancement.ts +61 -4
  173. package/src/rsc/redirect-guard.ts +2 -1
  174. package/src/rsc/rsc-rendering.ts +429 -37
  175. package/src/rsc/server-action.ts +25 -2
  176. package/src/rsc/shell-capture.ts +1190 -0
  177. package/src/rsc/shell-serve.ts +181 -0
  178. package/src/rsc/transition-gate.ts +89 -0
  179. package/src/rsc/types.ts +30 -0
  180. package/src/segment-loader-promise.ts +18 -0
  181. package/src/segment-system.tsx +149 -14
  182. package/src/server/context.ts +67 -9
  183. package/src/server/cookie-store.ts +73 -1
  184. package/src/server/loader-registry.ts +13 -1
  185. package/src/server/request-context.ts +169 -10
  186. package/src/ssr/index.tsx +462 -178
  187. package/src/ssr/inject-rsc-eager.ts +167 -0
  188. package/src/ssr/ssr-root.tsx +228 -0
  189. package/src/testing/collect-handle.ts +14 -8
  190. package/src/testing/dispatch.ts +152 -40
  191. package/src/testing/generated-routes.ts +27 -11
  192. package/src/testing/index.ts +6 -0
  193. package/src/testing/render-handler.ts +14 -0
  194. package/src/testing/render-route.tsx +13 -10
  195. package/src/testing/run-transition-when.ts +164 -0
  196. package/src/theme/ThemeProvider.tsx +36 -26
  197. package/src/types/handler-context.ts +1 -1
  198. package/src/types/index.ts +2 -0
  199. package/src/types/route-config.ts +19 -7
  200. package/src/types/segments.ts +100 -0
  201. package/src/urls/include-helper.ts +10 -8
  202. package/src/urls/include-provider.ts +71 -0
  203. package/src/urls/index.ts +1 -0
  204. package/src/urls/path-helper-types.ts +44 -12
  205. package/src/urls/path-helper.ts +5 -0
  206. package/src/urls/pattern-types.ts +36 -0
  207. package/src/urls/type-extraction.ts +43 -18
  208. package/src/urls/urls-function.ts +0 -1
  209. package/src/vercel/tracing.ts +7 -7
  210. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  211. package/src/vite/discovery/discover-routers.ts +1 -1
  212. package/src/vite/discovery/discovery-errors.ts +61 -0
  213. package/src/vite/index.ts +7 -0
  214. package/src/vite/inject-client-debug.ts +88 -0
  215. package/src/vite/plugins/vercel-output.ts +114 -25
  216. package/src/vite/plugins/version-injector.ts +22 -7
  217. package/src/vite/plugins/virtual-entries.ts +80 -22
  218. package/src/vite/rango.ts +29 -19
  219. package/src/vite/router-discovery.ts +171 -43
  220. package/src/vite/utils/prerender-utils.ts +17 -4
  221. package/src/vite/utils/shared-utils.ts +47 -0
  222. package/src/network-error-thrower.tsx +0 -18
@@ -17,6 +17,11 @@ import type { TrailingSlashMode } from "../types.js";
17
17
  import { createRouteHelpers } from "../route-definition.js";
18
18
  import MapRootLayout from "../server/root-layout.js";
19
19
  import { collectFallbackClientRefs } from "./collect-fallback-refs.js";
20
+ import {
21
+ isIncludeProvider,
22
+ resolveIncludeModule,
23
+ type IncludeProvider,
24
+ } from "../urls/include-provider.js";
20
25
 
21
26
  /**
22
27
  * Node in the prefix tree
@@ -61,13 +66,32 @@ export interface GeneratedManifest {
61
66
  // Merge tracked nested includes into `target`. Multiple includes can share a
62
67
  // fullPrefix (e.g. include("/", a), include("/", b)) — concat their routes and
63
68
  // Object.assign children rather than overwrite.
64
- function mergeIncludeNodes(
69
+ async function mergeIncludeNodes(
65
70
  target: Record<string, PrefixTreeNode>,
66
71
  includes: TrackedInclude[],
67
- buildChild: (include: TrackedInclude) => PrefixTreeNode,
68
- ): void {
72
+ buildChild: (include: TrackedInclude) => Promise<PrefixTreeNode>,
73
+ ): Promise<void> {
69
74
  for (const include of includes) {
70
- const node = buildChild(include);
75
+ let node: PrefixTreeNode;
76
+ try {
77
+ node = await buildChild(include);
78
+ } catch (err) {
79
+ // Discovery (build-time, and the dev trie-rebuild) populates the
80
+ // manifest / trie / generated types for the WHOLE app. A failing async
81
+ // include provider here — a broken import, a module that throws at eval —
82
+ // must HARD-FAIL, not be swallowed: swallowing produces a green build with
83
+ // the entire route group silently absent from the manifest/trie/types, so
84
+ // CI passes, the deploy ships, and every one of that group's URLs then
85
+ // 404s/500s in production. On main an eager include that threw failed the
86
+ // build loudly; the async form must keep that contract. Rethrow with the
87
+ // offending prefix so the failure is actionable. (Sibling isolation
88
+ // belongs at PER-REQUEST runtime — see find-match.ts — not at discovery.)
89
+ throw new Error(
90
+ `[@rangojs/router] Failed to resolve include at prefix "${include.fullPrefix}" ` +
91
+ `during route discovery: ${(err as Error)?.message ?? String(err)}`,
92
+ { cause: err },
93
+ );
94
+ }
71
95
  const existing = target[include.fullPrefix];
72
96
  if (existing) {
73
97
  existing.routes.push(...node.routes);
@@ -78,10 +102,10 @@ function mergeIncludeNodes(
78
102
  }
79
103
  }
80
104
 
81
- function buildPrefixTreeNode(
105
+ async function buildPrefixTreeNode(
82
106
  urlPrefix: string,
83
107
  namePrefix: string | undefined,
84
- patterns: UrlPatterns<any>,
108
+ patternsOrProvider: UrlPatterns<any> | IncludeProvider<any>,
85
109
  routeManifest: Record<string, string>,
86
110
  routeAncestry: Record<string, string[]>, // internal: feeds trie building, not exported
87
111
  mountIndex: number,
@@ -92,7 +116,14 @@ function buildPrefixTreeNode(
92
116
  passthroughRoutes?: string[],
93
117
  responseTypeRoutes?: Record<string, string>,
94
118
  routeSearchSchemas?: Record<string, Record<string, string>>,
95
- ): PrefixTreeNode {
119
+ ): Promise<PrefixTreeNode> {
120
+ // Resolve an async include provider (`() => import("./routes")`) so its routes
121
+ // are walked into the build-time manifest/types/href. Runtime matching still
122
+ // defers the import via lazy-includes; this only runs during build/dev
123
+ // discovery, which is async.
124
+ const patterns: UrlPatterns<any> = isIncludeProvider(patternsOrProvider)
125
+ ? resolveIncludeModule(await patternsOrProvider(), urlPrefix)
126
+ : patternsOrProvider;
96
127
  if (visited.has(patterns)) {
97
128
  console.warn(
98
129
  `[@rangojs/router] Circular include detected at prefix "${urlPrefix}". Skipping.`,
@@ -106,118 +137,117 @@ function buildPrefixTreeNode(
106
137
  };
107
138
  }
108
139
  visited.add(patterns);
109
- // Create context for running patterns with include tracking
110
- const manifest = new Map<string, EntryData>();
111
- const patternsMap = new Map<string, string>();
112
- const patternsByPrefix = new Map<string, Map<string, string>>();
113
- const trailingSlashMap = new Map<string, TrailingSlashMode>();
114
- const searchSchemasMap = new Map<string, Record<string, string>>();
115
- const trackedIncludes: TrackedInclude[] = [];
140
+ try {
141
+ // Create context for running patterns with include tracking
142
+ const manifest = new Map<string, EntryData>();
143
+ const patternsMap = new Map<string, string>();
144
+ const patternsByPrefix = new Map<string, Map<string, string>>();
145
+ const trailingSlashMap = new Map<string, TrailingSlashMode>();
146
+ const searchSchemasMap = new Map<string, Record<string, string>>();
147
+ const trackedIncludes: TrackedInclude[] = [];
116
148
 
117
- RangoContext.run(
118
- {
119
- manifest,
120
- patterns: patternsMap,
121
- patternsByPrefix,
122
- trailingSlash: trailingSlashMap,
123
- searchSchemas: searchSchemasMap,
124
- namespace: "build",
125
- parent: null,
126
- counters: {},
127
- mountIndex,
128
- trackedIncludes, // Enable nested include tracking
129
- },
130
- () => {
131
- const helpers = createRouteHelpers();
132
- // Wrap in root layout for correct parent hierarchy (matches runtime)
133
- helpers.layout(MapRootLayout, () => {
134
- if (urlPrefix || namePrefix) {
135
- return runWithPrefixes(urlPrefix, namePrefix, () => {
136
- return patterns.handler() as AllUseItems[];
137
- });
138
- }
139
- return patterns.handler() as AllUseItems[];
140
- });
141
- },
142
- );
149
+ RangoContext.run(
150
+ {
151
+ manifest,
152
+ patterns: patternsMap,
153
+ patternsByPrefix,
154
+ trailingSlash: trailingSlashMap,
155
+ searchSchemas: searchSchemasMap,
156
+ namespace: "build",
157
+ parent: null,
158
+ counters: {},
159
+ mountIndex,
160
+ trackedIncludes, // Enable nested include tracking
161
+ },
162
+ () => {
163
+ const helpers = createRouteHelpers();
164
+ // Wrap in root layout for correct parent hierarchy (matches runtime)
165
+ helpers.layout(MapRootLayout, () => {
166
+ if (urlPrefix || namePrefix) {
167
+ return runWithPrefixes(urlPrefix, namePrefix, () => {
168
+ return patterns.handler() as AllUseItems[];
169
+ });
170
+ }
171
+ return patterns.handler() as AllUseItems[];
172
+ });
173
+ },
174
+ );
143
175
 
144
- // Collect route names defined in this include (routes have prefixes applied)
145
- const routes: string[] = [];
146
- for (const [name, pattern] of patternsMap.entries()) {
147
- routes.push(name);
148
- routeManifest[name] = pattern;
149
- }
176
+ // Collect route names defined in this include (routes have prefixes applied)
177
+ const routes = [...patternsMap.keys()];
178
+ Object.assign(routeManifest, Object.fromEntries(patternsMap));
150
179
 
151
- // Collect trailing slash config
152
- if (routeTrailingSlash) {
153
- for (const [name, mode] of trailingSlashMap.entries()) {
154
- routeTrailingSlash[name] = mode;
180
+ // Collect trailing slash config
181
+ if (routeTrailingSlash) {
182
+ for (const [name, mode] of trailingSlashMap.entries()) {
183
+ routeTrailingSlash[name] = mode;
184
+ }
155
185
  }
156
- }
157
- if (routeSearchSchemas) {
158
- for (const [name, schema] of searchSchemasMap.entries()) {
159
- routeSearchSchemas[name] = schema;
186
+ if (routeSearchSchemas) {
187
+ for (const [name, schema] of searchSchemasMap.entries()) {
188
+ routeSearchSchemas[name] = schema;
189
+ }
160
190
  }
161
- }
162
191
 
163
- // Capture ancestry from manifest entries' parent chains
164
- captureAncestry(manifest, routeAncestry);
192
+ // Capture ancestry from manifest entries' parent chains
193
+ captureAncestry(manifest, routeAncestry);
165
194
 
166
- // Collect prerender route names and handler definitions from manifest entries
167
- if (prerenderRoutes) {
168
- for (const [name, entry] of manifest) {
169
- if (entry.type === "route" && entry.isPrerender) {
170
- prerenderRoutes.push(name);
171
- if (prerenderDefs && entry.prerenderDef) {
172
- prerenderDefs[name] = entry.prerenderDef;
173
- }
174
- if (passthroughRoutes && entry.isPassthrough === true) {
175
- passthroughRoutes.push(name);
195
+ // Collect prerender route names and handler definitions from manifest entries
196
+ if (prerenderRoutes) {
197
+ for (const [name, entry] of manifest) {
198
+ if (entry.type === "route" && entry.isPrerender) {
199
+ prerenderRoutes.push(name);
200
+ if (prerenderDefs && entry.prerenderDef) {
201
+ prerenderDefs[name] = entry.prerenderDef;
202
+ }
203
+ if (passthroughRoutes && entry.isPassthrough === true) {
204
+ passthroughRoutes.push(name);
205
+ }
176
206
  }
177
207
  }
178
208
  }
179
- }
180
209
 
181
- // Collect response type routes from manifest entries
182
- if (responseTypeRoutes) {
183
- for (const [name, entry] of manifest) {
184
- if (entry.type === "route" && entry.responseType) {
185
- responseTypeRoutes[name] = entry.responseType;
210
+ // Collect response type routes from manifest entries
211
+ if (responseTypeRoutes) {
212
+ for (const [name, entry] of manifest) {
213
+ if (entry.type === "route" && entry.responseType) {
214
+ responseTypeRoutes[name] = entry.responseType;
215
+ }
186
216
  }
187
217
  }
188
- }
189
-
190
- const children: Record<string, PrefixTreeNode> = {};
191
- mergeIncludeNodes(children, trackedIncludes, (include) =>
192
- buildPrefixTreeNode(
193
- include.fullPrefix,
194
- include.namePrefix,
195
- include.patterns as UrlPatterns<any>,
196
- routeManifest,
197
- routeAncestry,
198
- mountIndex,
199
- visited,
200
- routeTrailingSlash,
201
- prerenderRoutes,
202
- prerenderDefs,
203
- passthroughRoutes,
204
- responseTypeRoutes,
205
- routeSearchSchemas,
206
- ),
207
- );
208
218
 
209
- // Remove from visited so sibling branches can reuse the same patterns
210
- // without false circular-include detection. Only ancestors in the current
211
- // recursion path should trigger the cycle guard.
212
- visited.delete(patterns);
219
+ const children: Record<string, PrefixTreeNode> = {};
220
+ await mergeIncludeNodes(children, trackedIncludes, (include) =>
221
+ buildPrefixTreeNode(
222
+ include.fullPrefix,
223
+ include.namePrefix,
224
+ include.patterns as UrlPatterns<any> | IncludeProvider<any>,
225
+ routeManifest,
226
+ routeAncestry,
227
+ mountIndex,
228
+ visited,
229
+ routeTrailingSlash,
230
+ prerenderRoutes,
231
+ prerenderDefs,
232
+ passthroughRoutes,
233
+ responseTypeRoutes,
234
+ routeSearchSchemas,
235
+ ),
236
+ );
213
237
 
214
- return {
215
- staticPrefix: extractStaticPrefix(urlPrefix),
216
- fullPrefix: urlPrefix,
217
- namePrefix: namePrefix || undefined,
218
- children,
219
- routes,
220
- };
238
+ return {
239
+ staticPrefix: extractStaticPrefix(urlPrefix),
240
+ fullPrefix: urlPrefix,
241
+ namePrefix: namePrefix || undefined,
242
+ children,
243
+ routes,
244
+ };
245
+ } finally {
246
+ // Remove from visited so sibling branches can reuse the same patterns without
247
+ // false circular-include detection — and so a throwing handler (caught by the
248
+ // parent mergeIncludeNodes) does not leak this entry into the shared set.
249
+ visited.delete(patterns);
250
+ }
221
251
  }
222
252
 
223
253
  /**
@@ -261,7 +291,8 @@ export interface FullManifest extends GeneratedManifest {
261
291
  * import { generateManifest } from "@rangojs/router/build";
262
292
  * import { urlpatterns } from "./urls";
263
293
  *
264
- * const manifest = generateManifest(urlpatterns);
294
+ * // Async: awaits async include() providers (`() => import("./routes")`).
295
+ * const manifest = await generateManifest(urlpatterns);
265
296
  * // Write to file for runtime use
266
297
  * fs.writeFileSync(
267
298
  * "src/generated/route-manifest.json",
@@ -269,15 +300,12 @@ export interface FullManifest extends GeneratedManifest {
269
300
  * );
270
301
  * ```
271
302
  */
272
- export function generateManifest<TEnv>(
303
+ export async function generateManifest<TEnv>(
273
304
  urlpatterns: UrlPatterns<TEnv, any>,
274
305
  mountIndex: number = 0,
275
- ): GeneratedManifest {
276
- const {
277
- _routeAncestry: _,
278
- _prerenderDefs: __,
279
- ...publicManifest
280
- } = generateManifestFull(urlpatterns, mountIndex);
306
+ ): Promise<GeneratedManifest> {
307
+ const { _routeAncestry, _prerenderDefs, ...publicManifest } =
308
+ await generateManifestFull(urlpatterns, mountIndex);
281
309
  return publicManifest;
282
310
  }
283
311
 
@@ -288,7 +316,7 @@ export function generateManifest<TEnv>(
288
316
  * @rangojs/router/build), manifest-init (direct import), and trie
289
317
  * building. Not intended for external use.
290
318
  */
291
- export function generateManifestFull<TEnv>(
319
+ export async function generateManifestFull<TEnv>(
292
320
  urlpatterns: UrlPatterns<TEnv, any>,
293
321
  mountIndex: number = 0,
294
322
  options?: {
@@ -302,7 +330,7 @@ export function generateManifestFull<TEnv>(
302
330
  */
303
331
  collectClientFallbackRef?: (refKey: string) => void;
304
332
  },
305
- ): FullManifest {
333
+ ): Promise<FullManifest> {
306
334
  const routeManifest: Record<string, string> = {};
307
335
  const routeAncestry: Record<string, string[]> = {};
308
336
  const prefixTree: Record<string, PrefixTreeNode> = {};
@@ -356,17 +384,13 @@ export function generateManifestFull<TEnv>(
356
384
  }
357
385
 
358
386
  // Collect root-level routes and trailing slash config
359
- const routeTrailingSlash: Record<string, string> = {};
360
- for (const [name, pattern] of patternsMap.entries()) {
361
- routeManifest[name] = pattern;
362
- }
363
- for (const [name, mode] of trailingSlashMap.entries()) {
364
- routeTrailingSlash[name] = mode;
365
- }
366
- const routeSearchSchemas: Record<string, Record<string, string>> = {};
367
- for (const [name, schema] of searchSchemasMap.entries()) {
368
- routeSearchSchemas[name] = schema;
369
- }
387
+ Object.assign(routeManifest, Object.fromEntries(patternsMap));
388
+ const routeTrailingSlash: Record<string, string> =
389
+ Object.fromEntries(trailingSlashMap);
390
+ const routeSearchSchemas: Record<
391
+ string,
392
+ Record<string, string>
393
+ > = Object.fromEntries(searchSchemasMap);
370
394
 
371
395
  // Capture ancestry from manifest entries' parent chains
372
396
  captureAncestry(manifest, routeAncestry);
@@ -393,11 +417,11 @@ export function generateManifestFull<TEnv>(
393
417
 
394
418
  // Shared visited set for cycle detection across all root-level includes.
395
419
  const visited = new Set<unknown>();
396
- mergeIncludeNodes(prefixTree, trackedIncludes, (include) =>
420
+ await mergeIncludeNodes(prefixTree, trackedIncludes, (include) =>
397
421
  buildPrefixTreeNode(
398
422
  include.fullPrefix,
399
423
  include.namePrefix,
400
- include.patterns as UrlPatterns<any>,
424
+ include.patterns as UrlPatterns<any> | IncludeProvider<any>,
401
425
  routeManifest,
402
426
  routeAncestry,
403
427
  mountIndex,
@@ -441,14 +465,14 @@ export function generateManifestFull<TEnv>(
441
465
  *
442
466
  * @example
443
467
  * ```typescript
444
- * const code = generateManifestCode(urlpatterns);
468
+ * const code = await generateManifestCode(urlpatterns);
445
469
  * fs.writeFileSync("src/generated/route-manifest.ts", code);
446
470
  * ```
447
471
  */
448
- export function generateManifestCode<TEnv>(
472
+ export async function generateManifestCode<TEnv>(
449
473
  urlpatterns: UrlPatterns<TEnv, any>,
450
- ): string {
451
- const manifest = generateManifest(urlpatterns);
474
+ ): Promise<string> {
475
+ const manifest = await generateManifest(urlpatterns);
452
476
 
453
477
  return `/**
454
478
  * Auto-generated route manifest
@@ -10,7 +10,9 @@
10
10
  * import { generateManifest } from "@rangojs/router/build";
11
11
  * import { urlpatterns } from "./urls";
12
12
  *
13
- * const manifest = generateManifest(urlpatterns);
13
+ * // Async: generateManifest awaits any async include() providers
14
+ * // (`() => import("./routes")`) so the split groups appear in the manifest.
15
+ * const manifest = await generateManifest(urlpatterns);
14
16
  * console.log(manifest.routeManifest); // { home: "/", about: "/about", ... }
15
17
  * ```
16
18
  */
@@ -62,8 +62,13 @@ export interface TrieNode {
62
62
  p?: { n: string; c: TrieNode };
63
63
  /** Suffix-param children keyed by suffix (e.g., ".html" → { n: "productId", c: ... }) */
64
64
  xp?: Record<string, { n: string; c: TrieNode }>;
65
- /** Wildcard terminal: leaf + paramName */
66
- w?: TrieLeaf & { pn: string };
65
+ /**
66
+ * Wildcard terminal: leaf + paramName (`pn`). `pn` is "*" for the bare `/*`
67
+ * form and the param name for a named catch-all (`:name+`/`:name*`). `w1`
68
+ * marks a one-or-more catch-all (`:name+`): the runtime walker then rejects
69
+ * the zero-segment/empty-remainder case. Absent `w1` is zero-or-more.
70
+ */
71
+ w?: TrieLeaf & { pn: string; w1?: true };
67
72
  }
68
73
 
69
74
  /**
@@ -383,12 +388,35 @@ function insertSegments(
383
388
  } else if (segment.type === "wildcard") {
384
389
  // Wildcard consumes all remaining segments. Carry any params bound before
385
390
  // the wildcard in pa so they zip correctly against paramValues at match.
386
- const wildLeaf: TrieLeaf & { pn: string } = {
391
+ // `pn` is "*" for the bare `/*` and the param name for a named catch-all;
392
+ // `w1` marks the one-or-more variant (`:name+`) so the walker rejects the
393
+ // empty-remainder case.
394
+ const wildLeaf: TrieLeaf & { pn: string; w1?: true } = {
387
395
  ...buildLeaf(leafBase, paramNames),
388
- pn: "*",
396
+ pn: segment.value,
397
+ ...(segment.oneOrMore ? { w1: true as const } : {}),
389
398
  };
390
- const existing = node.w ? ({ ...node.w } as TrieLeaf) : undefined;
391
- const merged = mergeLeaves(existing, wildLeaf);
392
- node.w = merged as TrieLeaf & { pn: string };
399
+ const existing = node.w;
400
+ // Merge when there's no existing wildcard, when this is a response-type
401
+ // content-negotiation variant of the same catch-all (one side carries `rt`),
402
+ // or when it's the SAME catch-all identity (same param name + arity).
403
+ // Otherwise two DISTINCT catch-all forms (`/x/*` vs `/x/:p+`) would collide on
404
+ // the single wildcard slot with no non-lossy merge — so keep the first-declared
405
+ // (matching the regex matcher's declaration-order tiebreak) rather than let
406
+ // mergeLeaves' last-wins overwrite silently drop its `pn`/`w1` identity (which
407
+ // stranded the first route and fell through to a corrupt regex-fallback redirect).
408
+ const canMerge =
409
+ existing === undefined ||
410
+ Boolean(existing.rt) ||
411
+ Boolean(wildLeaf.rt) ||
412
+ (existing.pn === wildLeaf.pn &&
413
+ Boolean(existing.w1) === Boolean(wildLeaf.w1));
414
+ if (canMerge) {
415
+ const merged = mergeLeaves(
416
+ existing ? ({ ...existing } as TrieLeaf) : undefined,
417
+ wildLeaf,
418
+ );
419
+ node.w = merged as TrieLeaf & { pn: string; w1?: true };
420
+ }
393
421
  }
394
422
  }