@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19

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 (177) hide show
  1. package/README.md +46 -8
  2. package/dist/bin/rango.js +105 -18
  3. package/dist/vite/index.js +227 -93
  4. package/package.json +15 -14
  5. package/skills/hooks/SKILL.md +1 -1
  6. package/skills/intercept/SKILL.md +79 -0
  7. package/skills/layout/SKILL.md +62 -2
  8. package/skills/loader/SKILL.md +94 -1
  9. package/skills/middleware/SKILL.md +81 -0
  10. package/skills/parallel/SKILL.md +57 -2
  11. package/skills/prerender/SKILL.md +187 -17
  12. package/skills/route/SKILL.md +42 -1
  13. package/skills/router-setup/SKILL.md +77 -0
  14. package/src/__internal.ts +1 -1
  15. package/src/bin/rango.ts +38 -19
  16. package/src/browser/action-coordinator.ts +97 -0
  17. package/src/browser/event-controller.ts +25 -27
  18. package/src/browser/history-state.ts +80 -0
  19. package/src/browser/intercept-utils.ts +1 -1
  20. package/src/browser/link-interceptor.ts +0 -3
  21. package/src/browser/merge-segment-loaders.ts +9 -2
  22. package/src/browser/navigation-bridge.ts +46 -13
  23. package/src/browser/navigation-client.ts +32 -61
  24. package/src/browser/navigation-store.ts +1 -31
  25. package/src/browser/navigation-transaction.ts +46 -207
  26. package/src/browser/partial-update.ts +102 -150
  27. package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
  28. package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
  29. package/src/browser/prefetch/policy.ts +42 -0
  30. package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
  31. package/src/browser/react/Link.tsx +28 -23
  32. package/src/browser/react/NavigationProvider.tsx +9 -1
  33. package/src/browser/react/index.ts +2 -6
  34. package/src/browser/react/location-state-shared.ts +1 -1
  35. package/src/browser/react/location-state.ts +2 -0
  36. package/src/browser/react/nonce-context.ts +23 -0
  37. package/src/browser/react/use-action.ts +9 -1
  38. package/src/browser/react/use-handle.ts +3 -25
  39. package/src/browser/react/use-params.ts +2 -4
  40. package/src/browser/react/use-pathname.ts +2 -3
  41. package/src/browser/react/use-router.ts +1 -1
  42. package/src/browser/react/use-search-params.ts +2 -1
  43. package/src/browser/react/use-segments.ts +7 -60
  44. package/src/browser/response-adapter.ts +73 -0
  45. package/src/browser/rsc-router.tsx +29 -23
  46. package/src/browser/scroll-restoration.ts +10 -7
  47. package/src/browser/server-action-bridge.ts +115 -96
  48. package/src/browser/types.ts +1 -31
  49. package/src/browser/validate-redirect-origin.ts +29 -0
  50. package/src/build/generate-manifest.ts +5 -0
  51. package/src/build/generate-route-types.ts +2 -0
  52. package/src/build/route-types/codegen.ts +13 -4
  53. package/src/build/route-types/include-resolution.ts +13 -0
  54. package/src/build/route-types/per-module-writer.ts +15 -3
  55. package/src/build/route-types/router-processing.ts +45 -3
  56. package/src/build/runtime-discovery.ts +13 -1
  57. package/src/cache/background-task.ts +34 -0
  58. package/src/cache/cache-key-utils.ts +44 -0
  59. package/src/cache/cache-policy.ts +125 -0
  60. package/src/cache/cache-runtime.ts +132 -96
  61. package/src/cache/cache-scope.ts +71 -73
  62. package/src/cache/cf/cf-cache-store.ts +9 -4
  63. package/src/cache/document-cache.ts +72 -47
  64. package/src/cache/handle-capture.ts +81 -0
  65. package/src/cache/memory-segment-store.ts +18 -7
  66. package/src/cache/profile-registry.ts +43 -8
  67. package/src/cache/read-through-swr.ts +134 -0
  68. package/src/cache/segment-codec.ts +101 -112
  69. package/src/cache/taint.ts +26 -0
  70. package/src/client.tsx +53 -30
  71. package/src/errors.ts +6 -1
  72. package/src/handle.ts +1 -1
  73. package/src/handles/MetaTags.tsx +5 -2
  74. package/src/host/cookie-handler.ts +8 -3
  75. package/src/host/router.ts +14 -1
  76. package/src/href-client.ts +3 -1
  77. package/src/index.rsc.ts +33 -1
  78. package/src/index.ts +27 -0
  79. package/src/loader.rsc.ts +12 -4
  80. package/src/loader.ts +8 -0
  81. package/src/prerender/store.ts +4 -3
  82. package/src/prerender.ts +76 -18
  83. package/src/reverse.ts +11 -7
  84. package/src/root-error-boundary.tsx +30 -26
  85. package/src/route-definition/dsl-helpers.ts +9 -6
  86. package/src/route-definition/redirect.ts +15 -3
  87. package/src/route-map-builder.ts +38 -2
  88. package/src/route-name.ts +53 -0
  89. package/src/route-types.ts +7 -0
  90. package/src/router/content-negotiation.ts +1 -1
  91. package/src/router/debug-manifest.ts +16 -3
  92. package/src/router/handler-context.ts +94 -15
  93. package/src/router/intercept-resolution.ts +6 -4
  94. package/src/router/lazy-includes.ts +4 -0
  95. package/src/router/loader-resolution.ts +1 -0
  96. package/src/router/logging.ts +100 -3
  97. package/src/router/manifest.ts +32 -3
  98. package/src/router/match-api.ts +61 -7
  99. package/src/router/match-context.ts +3 -0
  100. package/src/router/match-handlers.ts +185 -11
  101. package/src/router/match-middleware/background-revalidation.ts +65 -85
  102. package/src/router/match-middleware/cache-lookup.ts +69 -4
  103. package/src/router/match-middleware/cache-store.ts +2 -0
  104. package/src/router/match-pipelines.ts +8 -43
  105. package/src/router/middleware-types.ts +7 -0
  106. package/src/router/middleware.ts +93 -8
  107. package/src/router/pattern-matching.ts +41 -5
  108. package/src/router/prerender-match.ts +34 -6
  109. package/src/router/preview-match.ts +7 -1
  110. package/src/router/revalidation.ts +61 -2
  111. package/src/router/router-context.ts +15 -0
  112. package/src/router/router-interfaces.ts +34 -0
  113. package/src/router/router-options.ts +200 -0
  114. package/src/router/segment-resolution/fresh.ts +123 -30
  115. package/src/router/segment-resolution/helpers.ts +19 -0
  116. package/src/router/segment-resolution/loader-cache.ts +37 -146
  117. package/src/router/segment-resolution/revalidation.ts +358 -94
  118. package/src/router/segment-wrappers.ts +3 -0
  119. package/src/router/telemetry-otel.ts +299 -0
  120. package/src/router/telemetry.ts +300 -0
  121. package/src/router/timeout.ts +148 -0
  122. package/src/router/types.ts +7 -1
  123. package/src/router.ts +155 -11
  124. package/src/rsc/handler-context.ts +11 -0
  125. package/src/rsc/handler.ts +380 -88
  126. package/src/rsc/helpers.ts +25 -16
  127. package/src/rsc/loader-fetch.ts +84 -42
  128. package/src/rsc/origin-guard.ts +141 -0
  129. package/src/rsc/progressive-enhancement.ts +232 -19
  130. package/src/rsc/response-route-handler.ts +37 -26
  131. package/src/rsc/rsc-rendering.ts +12 -5
  132. package/src/rsc/runtime-warnings.ts +42 -0
  133. package/src/rsc/server-action.ts +134 -58
  134. package/src/rsc/types.ts +8 -0
  135. package/src/search-params.ts +22 -10
  136. package/src/server/context.ts +53 -5
  137. package/src/server/fetchable-loader-store.ts +11 -6
  138. package/src/server/handle-store.ts +66 -9
  139. package/src/server/loader-registry.ts +11 -46
  140. package/src/server/request-context.ts +90 -9
  141. package/src/ssr/index.tsx +63 -27
  142. package/src/static-handler.ts +7 -0
  143. package/src/theme/ThemeProvider.tsx +6 -1
  144. package/src/theme/index.ts +1 -6
  145. package/src/theme/theme-context.ts +1 -28
  146. package/src/theme/theme-script.ts +2 -1
  147. package/src/types/cache-types.ts +5 -0
  148. package/src/types/error-types.ts +3 -0
  149. package/src/types/global-namespace.ts +9 -0
  150. package/src/types/handler-context.ts +35 -13
  151. package/src/types/loader-types.ts +7 -0
  152. package/src/types/route-entry.ts +28 -0
  153. package/src/urls/include-helper.ts +49 -8
  154. package/src/urls/index.ts +1 -0
  155. package/src/urls/path-helper-types.ts +30 -12
  156. package/src/urls/path-helper.ts +17 -2
  157. package/src/urls/pattern-types.ts +21 -1
  158. package/src/urls/response-types.ts +27 -2
  159. package/src/urls/type-extraction.ts +23 -15
  160. package/src/use-loader.tsx +12 -4
  161. package/src/vite/discovery/bundle-postprocess.ts +12 -7
  162. package/src/vite/discovery/discover-routers.ts +30 -18
  163. package/src/vite/discovery/prerender-collection.ts +24 -27
  164. package/src/vite/discovery/route-types-writer.ts +7 -7
  165. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  166. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  167. package/src/vite/plugins/use-cache-transform.ts +91 -3
  168. package/src/vite/rango.ts +3 -3
  169. package/src/vite/router-discovery.ts +99 -36
  170. package/src/vite/utils/prerender-utils.ts +21 -0
  171. package/src/vite/utils/shared-utils.ts +3 -1
  172. package/src/browser/request-controller.ts +0 -164
  173. package/src/href-context.ts +0 -33
  174. package/src/router.gen.ts +0 -6
  175. package/src/static-handler.gen.ts +0 -5
  176. package/src/urls.gen.ts +0 -8
  177. /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
package/src/prerender.ts CHANGED
@@ -34,10 +34,36 @@ import type {
34
34
  } from "./types.js";
35
35
  import type { Handle } from "./handle.js";
36
36
  import type { ContextVar } from "./context-var.js";
37
+ import type { ReverseFunction } from "./reverse.js";
38
+ import type { DefaultReverseRouteMap } from "./types/global-namespace.js";
37
39
  import { isCachedFunction } from "./cache/taint.js";
38
40
 
39
41
  // -- Named route resolution types -------------------------------------------
40
42
 
43
+ /**
44
+ * Reverse function for build contexts (BuildContext, StaticBuildContext, GetParamsContext).
45
+ * Global names get full autocomplete and param validation from the generated route map.
46
+ * Local `.name` calls are accepted but not validated (the include() scope is unknown
47
+ * at the type level).
48
+ */
49
+ type BuildReverseFunction = [DefaultReverseRouteMap] extends [
50
+ Record<string, string>,
51
+ ]
52
+ ? // No generated route map — permissive fallback
53
+ (
54
+ name: string,
55
+ params?: Record<string, string>,
56
+ search?: Record<string, unknown>,
57
+ ) => string
58
+ : // Generated route map available — typed globals + permissive locals
59
+ ReverseFunction<DefaultReverseRouteMap> & {
60
+ (
61
+ name: `.${string}`,
62
+ params?: Record<string, string>,
63
+ search?: Record<string, unknown>,
64
+ ): string;
65
+ };
66
+
41
67
  /**
42
68
  * Default route map for Prerender named route resolution.
43
69
  * Uses GeneratedRouteMap (from gen file) to avoid circular dependencies.
@@ -143,11 +169,14 @@ export interface BuildContext<TParams> {
143
169
  search: {};
144
170
 
145
171
  /** URL generation by route name. */
146
- reverse: (
147
- name: string,
148
- params?: Record<string, string>,
149
- search?: Record<string, unknown>,
150
- ) => string;
172
+ reverse: BuildReverseFunction;
173
+
174
+ /**
175
+ * Signal that this param set should not produce a local prerender artifact.
176
+ * At runtime the handler runs live instead. Only valid on routes declared
177
+ * with `{ passthrough: true }`.
178
+ */
179
+ passthrough: () => PrerenderPassthroughResult;
151
180
  }
152
181
 
153
182
  /**
@@ -174,11 +203,7 @@ export interface StaticBuildContext {
174
203
  use: <T>(handle: Handle<T>) => (data: T) => void;
175
204
 
176
205
  /** URL generation by route name. */
177
- reverse: (
178
- name: string,
179
- params?: Record<string, string>,
180
- search?: Record<string, unknown>,
181
- ) => string;
206
+ reverse: BuildReverseFunction;
182
207
  }
183
208
 
184
209
  /**
@@ -196,11 +221,7 @@ export interface GetParamsContext {
196
221
  };
197
222
 
198
223
  /** URL generation by route name. */
199
- reverse: (
200
- name: string,
201
- params?: Record<string, string>,
202
- search?: Record<string, unknown>,
203
- ) => string;
224
+ reverse: BuildReverseFunction;
204
225
  }
205
226
 
206
227
  /**
@@ -216,7 +237,9 @@ export interface GetParamsContext {
216
237
  export type PrerenderPassthroughContext<
217
238
  TParams = {},
218
239
  TEnv = DefaultEnv,
219
- > = HandlerContext<TParams, TEnv>;
240
+ > = HandlerContext<TParams, TEnv> & {
241
+ passthrough: () => PrerenderPassthroughResult;
242
+ };
220
243
 
221
244
  export interface PrerenderHandlerDefinition<
222
245
  TParams extends Record<string, any> = any,
@@ -269,7 +292,10 @@ export function Prerender<
269
292
  ResolvePrerenderParams<T, TRouteMap>,
270
293
  TEnv
271
294
  >,
272
- ) => ReactNode | Promise<ReactNode>,
295
+ ) =>
296
+ | ReactNode
297
+ | PrerenderPassthroughResult
298
+ | Promise<ReactNode | PrerenderPassthroughResult>,
273
299
  options: PrerenderOptions & { passthrough: true },
274
300
  __injectedId?: string,
275
301
  ): PrerenderHandlerDefinition<ResolvePrerenderParams<T, TRouteMap>>;
@@ -313,7 +339,10 @@ export function Prerender<
313
339
  ResolvePrerenderParams<T, TRouteMap>,
314
340
  TEnv
315
341
  >,
316
- ) => ReactNode | Promise<ReactNode>,
342
+ ) =>
343
+ | ReactNode
344
+ | PrerenderPassthroughResult
345
+ | Promise<ReactNode | PrerenderPassthroughResult>,
317
346
  options: PrerenderOptions & { passthrough: true },
318
347
  __injectedId?: string,
319
348
  ): PrerenderHandlerDefinition<ResolvePrerenderParams<T, TRouteMap>>;
@@ -388,6 +417,35 @@ export function Prerender<TParams extends Record<string, any>>(
388
417
  };
389
418
  }
390
419
 
420
+ // -- Passthrough sentinel ---------------------------------------------------
421
+
422
+ /**
423
+ * Sentinel returned by `ctx.passthrough()` to signal that a specific param set
424
+ * should not produce a local prerender artifact. The build skips writing the
425
+ * entry; at runtime the handler runs live (requires `{ passthrough: true }`).
426
+ */
427
+ export const PRERENDER_PASSTHROUGH: Readonly<{
428
+ __brand: "prerenderPassthrough";
429
+ }> = Object.freeze({
430
+ __brand: "prerenderPassthrough" as const,
431
+ });
432
+
433
+ export type PrerenderPassthroughResult = typeof PRERENDER_PASSTHROUGH;
434
+
435
+ /**
436
+ * Type guard to check if a value is the passthrough sentinel.
437
+ */
438
+ export function isPrerenderPassthrough(
439
+ value: unknown,
440
+ ): value is PrerenderPassthroughResult {
441
+ return (
442
+ typeof value === "object" &&
443
+ value !== null &&
444
+ "__brand" in value &&
445
+ (value as { __brand: unknown }).__brand === "prerenderPassthrough"
446
+ );
447
+ }
448
+
391
449
  // -- Type guard -------------------------------------------------------------
392
450
 
393
451
  /**
package/src/reverse.ts CHANGED
@@ -304,13 +304,17 @@ export function createReverse<TRoutes extends Record<string, string>>(
304
304
  let result = pattern;
305
305
  if (params) {
306
306
  // Replace :param placeholders with actual values
307
- result = result.replace(/:([^/]+)/g, (_: string, key: string) => {
308
- const value = params[key];
309
- if (value === undefined) {
310
- throw new Error(`Missing param "${key}" for route "${name}"`);
311
- }
312
- return encodeURIComponent(value);
313
- });
307
+ // Strip constraint syntax: :param(a|b) -> use "param" as key
308
+ result = result.replace(
309
+ /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\??/g,
310
+ (_, key) => {
311
+ const value = params[key];
312
+ if (value === undefined) {
313
+ throw new Error(`Missing param "${key}" for route "${name}"`);
314
+ }
315
+ return encodeURIComponent(value);
316
+ },
317
+ );
314
318
  }
315
319
 
316
320
  // Append search params as query string
@@ -109,6 +109,8 @@ function RootErrorFallback({
109
109
  error,
110
110
  reset,
111
111
  }: ClientErrorBoundaryFallbackProps): ReactNode {
112
+ const isDev = process.env.NODE_ENV !== "production";
113
+
112
114
  return (
113
115
  <div
114
116
  style={{
@@ -135,38 +137,40 @@ function RootErrorFallback({
135
137
  >
136
138
  An unexpected error occurred while processing your request.
137
139
  </p>
138
- <div
139
- style={{
140
- background: "#fef2f2",
141
- border: "1px solid #fecaca",
142
- borderRadius: "0.5rem",
143
- padding: "1rem",
144
- marginBottom: "1rem",
145
- }}
146
- >
147
- <p
140
+ {isDev && (
141
+ <div
148
142
  style={{
149
- fontWeight: 600,
150
- color: "#991b1b",
151
- marginBottom: "0.5rem",
143
+ background: "#fef2f2",
144
+ border: "1px solid #fecaca",
145
+ borderRadius: "0.5rem",
146
+ padding: "1rem",
147
+ marginBottom: "1rem",
152
148
  }}
153
149
  >
154
- {error.name}: {error.message}
155
- </p>
156
- {error.stack && (
157
- <pre
150
+ <p
158
151
  style={{
159
- fontSize: "0.75rem",
160
- color: "#6b7280",
161
- overflow: "auto",
162
- whiteSpace: "pre-wrap",
163
- wordBreak: "break-word",
152
+ fontWeight: 600,
153
+ color: "#991b1b",
154
+ marginBottom: "0.5rem",
164
155
  }}
165
156
  >
166
- {error.stack}
167
- </pre>
168
- )}
169
- </div>
157
+ {error.name}: {error.message}
158
+ </p>
159
+ {error.stack && (
160
+ <pre
161
+ style={{
162
+ fontSize: "0.75rem",
163
+ color: "#6b7280",
164
+ overflow: "auto",
165
+ whiteSpace: "pre-wrap",
166
+ wordBreak: "break-word",
167
+ }}
168
+ >
169
+ {error.stack}
170
+ </pre>
171
+ )}
172
+ </div>
173
+ )}
170
174
  <div style={{ display: "flex", gap: "1rem" }}>
171
175
  <button
172
176
  type="button"
@@ -16,7 +16,7 @@ import {
16
16
  } from "../server/context";
17
17
  import { invariant } from "../errors";
18
18
  import { isCachedFunction } from "../cache/taint.js";
19
- import { getCacheProfile } from "../cache/profile-registry.js";
19
+ import { RSCRouterContext } from "../server/context";
20
20
  import { isStaticHandler } from "../static-handler.js";
21
21
  import RootLayout from "../server/root-layout";
22
22
  import type {
@@ -227,7 +227,9 @@ const cache: RouteHelpers<any, any>["cache"] = (
227
227
  children = undefined;
228
228
  } else if (typeof optionsOrChildren === "string") {
229
229
  // cache('profileName') or cache('profileName', () => [...])
230
- const profile = getCacheProfile(optionsOrChildren);
230
+ // Resolve from context-scoped profiles (set per-router via HelperContext).
231
+ const ctxStore = RSCRouterContext.getStore();
232
+ const profile = ctxStore?.cacheProfiles?.[optionsOrChildren];
231
233
  invariant(
232
234
  profile,
233
235
  `cache("${optionsOrChildren}"): unknown cache profile. ` +
@@ -245,7 +247,9 @@ const cache: RouteHelpers<any, any>["cache"] = (
245
247
  children = maybeChildren;
246
248
  }
247
249
 
248
- const name = `$${store.getNextIndex("cache")}`;
250
+ // Allocate a single index for this cache() call (used in all paths)
251
+ const cacheIndex = store.getNextIndex("cache");
252
+ const name = `$${cacheIndex}`;
249
253
  const cacheConfig = { options };
250
254
 
251
255
  // If no children, create an orphan cache entry (like orphan layouts)
@@ -262,7 +266,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
262
266
 
263
267
  // Create orphan cache entry (like orphan layout)
264
268
  // Subsequent siblings in the same array will attach to this entry
265
- const namespace = `${ctx.namespace}.${store.getNextIndex("cache")}`;
269
+ const namespace = `${ctx.namespace}.${cacheIndex}`;
266
270
  const cacheUrlPrefix = getUrlPrefix();
267
271
 
268
272
  const entry = {
@@ -297,8 +301,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
297
301
  }
298
302
 
299
303
  // With children: create a cache entry (like layout with caching semantics)
300
- const cacheNextIndex = store.getNextIndex("cache");
301
- const namespace = `${ctx.namespace}.${cacheNextIndex}`;
304
+ const namespace = `${ctx.namespace}.${cacheIndex}`;
302
305
  const cacheShortCode = store.getShortCode("cache");
303
306
 
304
307
  const cacheUrlPrefix2 = getUrlPrefix();
@@ -43,11 +43,16 @@ import {
43
43
  export function redirect(url: string, status?: number): Response;
44
44
  export function redirect(
45
45
  url: string,
46
- options: { status?: number; state?: LocationStateEntry[] },
46
+ options: {
47
+ status?: number;
48
+ state?: LocationStateEntry | LocationStateEntry[];
49
+ },
47
50
  ): Response;
48
51
  export function redirect(
49
52
  url: string,
50
- statusOrOptions?: number | { status?: number; state?: LocationStateEntry[] },
53
+ statusOrOptions?:
54
+ | number
55
+ | { status?: number; state?: LocationStateEntry | LocationStateEntry[] },
51
56
  ): Response {
52
57
  const status =
53
58
  typeof statusOrOptions === "number"
@@ -62,7 +67,14 @@ export function redirect(
62
67
 
63
68
  if (process.env.NODE_ENV !== "production") {
64
69
  const reqCtx = getRequestContext();
65
- if (reqCtx && !reqCtx.url.searchParams.has("_rsc_partial")) {
70
+ // Warn only on true full-page SSR loads. SPA partial requests and server
71
+ // actions both deliver state through Flight payloads, so suppress for those.
72
+ if (
73
+ reqCtx &&
74
+ !reqCtx.url.searchParams.has("_rsc_partial") &&
75
+ !reqCtx.request.headers.has("rsc-action") &&
76
+ !reqCtx.url.searchParams.has("_rsc_action")
77
+ ) {
66
78
  console.warn(
67
79
  `[Router] redirect() with state during a full-page (SSR) request to "${url}". ` +
68
80
  "Location state is only delivered during SPA navigations and will be lost on this request.",
@@ -128,15 +128,23 @@ const perRouterPrecomputedEntriesMap: Map<
128
128
  > = new Map();
129
129
 
130
130
  /**
131
- * Clear all per-router cached data (manifest, trie, precomputed entries).
131
+ * Clear all cached route data (global and per-router).
132
132
  * Called during HMR when route definitions change so the handler rebuilds
133
133
  * the trie from the updated router.urlpatterns on the next request.
134
+ *
135
+ * The virtual module calls this before repopulating with fresh data,
136
+ * preventing stale entries from removed routes from accumulating.
134
137
  */
135
138
  export function clearAllRouterData(): void {
139
+ globalRouteMap = {};
140
+ cachedManifest = null;
141
+ cachedPrecomputedEntries = null;
142
+ cachedRouteTrie = null;
143
+ rootScopeRoutes.clear();
144
+ globalSearchSchemas.clear();
136
145
  perRouterManifestMap.clear();
137
146
  perRouterTrieMap.clear();
138
147
  perRouterPrecomputedEntriesMap.clear();
139
- cachedRouteTrie = null;
140
148
  }
141
149
 
142
150
  export function setRouterManifest(
@@ -217,6 +225,34 @@ export function waitForManifestReady(): Promise<void> | null {
217
225
  return manifestReadyPromise;
218
226
  }
219
227
 
228
+ // ============================================================================
229
+ // Route Scope Registry
230
+ // ============================================================================
231
+
232
+ // Tracks whether each route is at root scope (no named include boundary above).
233
+ // Used by dot-local reverse resolution to decide whether bare-name fallback
234
+ // is allowed after scoped lookups are exhausted.
235
+ const rootScopeRoutes: Map<string, boolean> = new Map();
236
+
237
+ /**
238
+ * Register whether a route is at root scope.
239
+ * Called by path() during route evaluation.
240
+ */
241
+ export function registerRouteRootScope(
242
+ routeName: string,
243
+ rootScoped: boolean,
244
+ ): void {
245
+ rootScopeRoutes.set(routeName, rootScoped);
246
+ }
247
+
248
+ /**
249
+ * Check if a route is at root scope.
250
+ * Returns undefined if the route has not been registered (e.g. in unit tests).
251
+ */
252
+ export function isRouteRootScoped(routeName: string): boolean | undefined {
253
+ return rootScopeRoutes.get(routeName);
254
+ }
255
+
220
256
  // ============================================================================
221
257
  // Search Schema Registry
222
258
  // ============================================================================
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Route name utilities for filtering internal route names.
3
+ *
4
+ * Internal names stay active in the runtime manifest for matching and local
5
+ * reverse() resolution, but they must not leak into public APIs or generated
6
+ * route maps.
7
+ */
8
+
9
+ export const AUTO_GENERATED_ROUTE_PREFIX = "$path_";
10
+ export const INTERNAL_INCLUDE_SCOPE_PREFIX = "$prefix_";
11
+
12
+ const RESERVED_PREFIXES = [
13
+ AUTO_GENERATED_ROUTE_PREFIX,
14
+ INTERNAL_INCLUDE_SCOPE_PREFIX,
15
+ ] as const;
16
+
17
+ /**
18
+ * Check if a route name is internal.
19
+ * Internal names include:
20
+ * - unnamed path() routes like "$path__health" or "docs.$path__health"
21
+ * - hidden include scopes like "$prefix_0.index" or "blog.$prefix_1.post"
22
+ *
23
+ * User-defined names containing "$" (e.g. "docs.$admin") are valid and must
24
+ * be preserved.
25
+ */
26
+ export function isAutoGeneratedRouteName(name: string): boolean {
27
+ return name.split(".").some((segment) => {
28
+ return (
29
+ segment.startsWith(AUTO_GENERATED_ROUTE_PREFIX) ||
30
+ segment.startsWith(INTERNAL_INCLUDE_SCOPE_PREFIX)
31
+ );
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Validate that a user-provided route name does not collide with
37
+ * reserved internal prefixes. Checks every dot-separated segment,
38
+ * mirroring the same rule used by isAutoGeneratedRouteName().
39
+ *
40
+ * Throws with a clear message when a reserved prefix is detected.
41
+ */
42
+ export function validateUserRouteName(name: string): void {
43
+ for (const segment of name.split(".")) {
44
+ for (const prefix of RESERVED_PREFIXES) {
45
+ if (segment.startsWith(prefix)) {
46
+ throw new Error(
47
+ `Route name "${name}" contains segment "${segment}" which uses reserved internal prefix "${prefix}". ` +
48
+ `Choose a different name to avoid collision with auto-generated route names.`,
49
+ );
50
+ }
51
+ }
52
+ }
53
+ }
@@ -169,6 +169,13 @@ export type IncludeItem = {
169
169
  parent: unknown; // EntryData - avoid circular import
170
170
  /** Counter snapshot from pattern extraction for consistent shortCode indices */
171
171
  counters?: Record<string, number>;
172
+ /** Cache profiles for DSL-time cache("profileName") resolution */
173
+ cacheProfiles?: Record<
174
+ string,
175
+ import("./cache/profile-registry.js").CacheProfile
176
+ >;
177
+ /** Root scope flag for dot-local reverse resolution */
178
+ rootScoped?: boolean;
172
179
  };
173
180
  [IncludeBrand]: void;
174
181
  };
@@ -52,7 +52,7 @@ export function parseAcceptTypes(accept: string): AcceptEntry[] {
52
52
  for (let i = 0; i < parts.length; i++) {
53
53
  const part = parts[i]!;
54
54
  const segments = part.split(";");
55
- const mime = segments[0]!.trim();
55
+ const mime = segments[0]!.trim().toLowerCase();
56
56
  if (!mime) continue;
57
57
  let q = 1.0;
58
58
  for (let j = 1; j < segments.length; j++) {
@@ -45,10 +45,23 @@ export async function buildDebugManifest<TEnv = any>(
45
45
  if (promiseResult !== null) {
46
46
  const load = await (promiseResult as Promise<any>);
47
47
  if (load && typeof load === "object" && "default" in load) {
48
- const useItems = load.default;
49
- if (typeof useItems === "function") {
50
- useItems(helpers);
48
+ // Promise<{ default: fn }> — e.g. dynamic import
49
+ if (typeof load.default !== "function") {
50
+ throw new Error(
51
+ `[@rangojs/router] Unsupported async handler: { default } must be a function, ` +
52
+ `got ${typeof load.default}. Use () => import('./urls') for lazy loading.`,
53
+ );
51
54
  }
55
+ load.default(helpers);
56
+ } else if (typeof load === "function") {
57
+ // Promise<fn>
58
+ load(helpers);
59
+ } else {
60
+ // Reject unsupported async handler results (same policy as manifest.ts)
61
+ throw new Error(
62
+ `[@rangojs/router] Unsupported async handler result (${typeof load}). ` +
63
+ `Lazy route handlers must resolve to a function or { default: fn }.`,
64
+ );
52
65
  }
53
66
  }
54
67
  },