@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1

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 (214) hide show
  1. package/README.md +9 -9
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +914 -485
  5. package/package.json +55 -11
  6. package/skills/bundle-analysis/SKILL.md +159 -0
  7. package/skills/cache-guide/SKILL.md +220 -30
  8. package/skills/caching/SKILL.md +116 -8
  9. package/skills/composability/SKILL.md +27 -2
  10. package/skills/document-cache/SKILL.md +78 -55
  11. package/skills/handler-use/SKILL.md +3 -1
  12. package/skills/hooks/SKILL.md +214 -18
  13. package/skills/host-router/SKILL.md +45 -20
  14. package/skills/intercept/SKILL.md +26 -4
  15. package/skills/layout/SKILL.md +6 -7
  16. package/skills/links/SKILL.md +173 -17
  17. package/skills/loader/SKILL.md +149 -6
  18. package/skills/middleware/SKILL.md +13 -9
  19. package/skills/migrate-nextjs/SKILL.md +1 -1
  20. package/skills/mime-routes/SKILL.md +27 -0
  21. package/skills/observability/SKILL.md +137 -0
  22. package/skills/parallel/SKILL.md +5 -6
  23. package/skills/prerender/SKILL.md +14 -33
  24. package/skills/rango/SKILL.md +242 -26
  25. package/skills/react-compiler/SKILL.md +168 -0
  26. package/skills/response-routes/SKILL.md +58 -9
  27. package/skills/route/SKILL.md +13 -4
  28. package/skills/router-setup/SKILL.md +3 -3
  29. package/skills/server-actions/SKILL.md +53 -41
  30. package/skills/testing/SKILL.md +599 -0
  31. package/skills/typesafety/SKILL.md +310 -26
  32. package/skills/use-cache/SKILL.md +34 -5
  33. package/skills/view-transitions/SKILL.md +294 -0
  34. package/src/__augment-tests__/augment.ts +81 -0
  35. package/src/__augment-tests__/augmented.check.ts +117 -0
  36. package/src/browser/action-coordinator.ts +53 -36
  37. package/src/browser/event-controller.ts +42 -66
  38. package/src/browser/history-state.ts +21 -0
  39. package/src/browser/index.ts +3 -3
  40. package/src/browser/navigation-bridge.ts +6 -6
  41. package/src/browser/navigation-client.ts +12 -15
  42. package/src/browser/navigation-store.ts +7 -8
  43. package/src/browser/navigation-transaction.ts +10 -28
  44. package/src/browser/partial-update.ts +9 -19
  45. package/src/browser/react/NavigationProvider.tsx +29 -40
  46. package/src/browser/react/index.ts +3 -0
  47. package/src/browser/react/location-state-shared.ts +175 -4
  48. package/src/browser/react/location-state.ts +39 -13
  49. package/src/browser/react/use-handle.ts +17 -9
  50. package/src/browser/react/use-params.ts +3 -4
  51. package/src/browser/react/use-reverse.ts +106 -0
  52. package/src/browser/react/use-router.ts +14 -1
  53. package/src/browser/response-adapter.ts +25 -0
  54. package/src/browser/rsc-router.tsx +30 -16
  55. package/src/browser/scroll-restoration.ts +22 -14
  56. package/src/browser/segment-structure-assert.ts +2 -2
  57. package/src/browser/server-action-bridge.ts +23 -30
  58. package/src/browser/types.ts +2 -0
  59. package/src/build/collect-fallback-refs.ts +107 -0
  60. package/src/build/generate-manifest.ts +60 -35
  61. package/src/build/generate-route-types.ts +2 -0
  62. package/src/build/index.ts +2 -0
  63. package/src/build/route-types/codegen.ts +4 -4
  64. package/src/build/route-types/include-resolution.ts +1 -1
  65. package/src/build/route-types/per-module-writer.ts +7 -4
  66. package/src/build/route-types/router-processing.ts +55 -14
  67. package/src/build/route-types/scan-filter.ts +1 -1
  68. package/src/build/route-types/source-scan.ts +118 -0
  69. package/src/build/runtime-discovery.ts +9 -20
  70. package/src/cache/cache-scope.ts +28 -42
  71. package/src/cache/cf/cf-cache-store.ts +49 -6
  72. package/src/client.rsc.tsx +3 -0
  73. package/src/client.tsx +10 -8
  74. package/src/context-var.ts +5 -5
  75. package/src/decode-loader-results.ts +36 -0
  76. package/src/errors.ts +30 -1
  77. package/src/handle.ts +26 -13
  78. package/src/host/index.ts +2 -2
  79. package/src/host/router.ts +129 -57
  80. package/src/host/types.ts +31 -2
  81. package/src/host/utils.ts +1 -1
  82. package/src/href-client.ts +140 -20
  83. package/src/index.rsc.ts +6 -4
  84. package/src/index.ts +13 -6
  85. package/src/loader-store.ts +500 -0
  86. package/src/loader.rsc.ts +2 -5
  87. package/src/loader.ts +3 -10
  88. package/src/missing-id-error.ts +68 -0
  89. package/src/prerender.ts +4 -4
  90. package/src/response-utils.ts +9 -0
  91. package/src/reverse.ts +65 -41
  92. package/src/route-content-wrapper.tsx +6 -28
  93. package/src/route-definition/dsl-helpers.ts +238 -263
  94. package/src/route-definition/helper-factories.ts +29 -139
  95. package/src/route-definition/helpers-types.ts +37 -14
  96. package/src/route-definition/use-item-types.ts +32 -0
  97. package/src/route-types.ts +19 -41
  98. package/src/router/basename.ts +14 -0
  99. package/src/router/content-negotiation.ts +15 -2
  100. package/src/router/error-handling.ts +1 -1
  101. package/src/router/handler-context.ts +4 -42
  102. package/src/router/intercept-resolution.ts +4 -18
  103. package/src/router/lazy-includes.ts +2 -2
  104. package/src/router/loader-resolution.ts +16 -2
  105. package/src/router/match-handlers.ts +62 -20
  106. package/src/router/match-middleware/cache-lookup.ts +44 -91
  107. package/src/router/match-middleware/cache-store.ts +3 -2
  108. package/src/router/match-result.ts +32 -30
  109. package/src/router/metrics.ts +1 -1
  110. package/src/router/middleware-types.ts +1 -1
  111. package/src/router/middleware.ts +46 -78
  112. package/src/router/prerender-match.ts +1 -1
  113. package/src/router/preview-match.ts +3 -1
  114. package/src/router/request-classification.ts +4 -28
  115. package/src/router/revalidation.ts +43 -1
  116. package/src/router/router-interfaces.ts +45 -28
  117. package/src/router/router-options.ts +40 -1
  118. package/src/router/router-registry.ts +2 -5
  119. package/src/router/segment-resolution/fresh.ts +19 -6
  120. package/src/router/segment-resolution/revalidation.ts +19 -6
  121. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  122. package/src/router/substitute-pattern-params.ts +56 -0
  123. package/src/router/telemetry.ts +99 -0
  124. package/src/router/types.ts +8 -0
  125. package/src/router.ts +37 -21
  126. package/src/rsc/handler-context.ts +2 -2
  127. package/src/rsc/handler.ts +20 -65
  128. package/src/rsc/helpers.ts +22 -2
  129. package/src/rsc/index.ts +1 -1
  130. package/src/rsc/origin-guard.ts +28 -10
  131. package/src/rsc/response-route-handler.ts +32 -52
  132. package/src/rsc/rsc-rendering.ts +27 -53
  133. package/src/rsc/runtime-warnings.ts +9 -10
  134. package/src/rsc/server-action.ts +13 -37
  135. package/src/rsc/ssr-setup.ts +16 -0
  136. package/src/rsc/types.ts +2 -2
  137. package/src/search-params.ts +4 -4
  138. package/src/segment-system.tsx +121 -65
  139. package/src/serialize.ts +243 -0
  140. package/src/server/context.ts +118 -51
  141. package/src/server/cookie-store.ts +28 -4
  142. package/src/server/request-context.ts +10 -0
  143. package/src/static-handler.ts +1 -1
  144. package/src/testing/cache-status.ts +166 -0
  145. package/src/testing/collect-handle.ts +63 -0
  146. package/src/testing/dispatch.ts +440 -0
  147. package/src/testing/dom.entry.ts +22 -0
  148. package/src/testing/e2e/fixture.ts +154 -0
  149. package/src/testing/e2e/index.ts +149 -0
  150. package/src/testing/e2e/matchers.ts +51 -0
  151. package/src/testing/e2e/page-helpers.ts +272 -0
  152. package/src/testing/e2e/parity.ts +306 -0
  153. package/src/testing/e2e/server.ts +183 -0
  154. package/src/testing/flight-matchers.ts +104 -0
  155. package/src/testing/flight-runtime.d.ts +21 -0
  156. package/src/testing/flight.entry.ts +22 -0
  157. package/src/testing/flight.ts +182 -0
  158. package/src/testing/generated-routes.ts +223 -0
  159. package/src/testing/index.ts +105 -0
  160. package/src/testing/internal/context.ts +193 -0
  161. package/src/testing/render-route.tsx +536 -0
  162. package/src/testing/run-loader.ts +296 -0
  163. package/src/testing/run-middleware.ts +170 -0
  164. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  165. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  166. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  167. package/src/testing/vitest-stubs/version.ts +5 -0
  168. package/src/testing/vitest.ts +183 -0
  169. package/src/types/global-namespace.ts +39 -26
  170. package/src/types/handler-context.ts +56 -11
  171. package/src/types/index.ts +1 -0
  172. package/src/types/segments.ts +18 -1
  173. package/src/urls/include-helper.ts +10 -53
  174. package/src/urls/index.ts +0 -3
  175. package/src/urls/path-helper-types.ts +11 -3
  176. package/src/urls/path-helper.ts +17 -52
  177. package/src/urls/pattern-types.ts +36 -19
  178. package/src/urls/response-types.ts +20 -19
  179. package/src/urls/type-extraction.ts +26 -116
  180. package/src/urls/urls-function.ts +1 -5
  181. package/src/use-loader.tsx +413 -42
  182. package/src/vite/debug.ts +1 -0
  183. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  184. package/src/vite/discovery/discover-routers.ts +70 -48
  185. package/src/vite/discovery/discovery-errors.ts +194 -0
  186. package/src/vite/discovery/prerender-collection.ts +19 -25
  187. package/src/vite/discovery/route-types-writer.ts +40 -84
  188. package/src/vite/discovery/state.ts +33 -0
  189. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  190. package/src/vite/index.ts +2 -0
  191. package/src/vite/plugin-types.ts +67 -0
  192. package/src/vite/plugins/cjs-to-esm.ts +3 -7
  193. package/src/vite/plugins/client-ref-hashing.ts +12 -1
  194. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
  195. package/src/vite/plugins/expose-action-id.ts +2 -2
  196. package/src/vite/plugins/expose-id-utils.ts +12 -8
  197. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  198. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  199. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  200. package/src/vite/plugins/expose-internal-ids.ts +47 -67
  201. package/src/vite/plugins/performance-tracks.ts +12 -16
  202. package/src/vite/plugins/use-cache-transform.ts +13 -11
  203. package/src/vite/plugins/version-injector.ts +2 -12
  204. package/src/vite/plugins/version-plugin.ts +59 -2
  205. package/src/vite/plugins/virtual-entries.ts +2 -2
  206. package/src/vite/rango.ts +67 -15
  207. package/src/vite/router-discovery.ts +208 -63
  208. package/src/vite/utils/ast-handler-extract.ts +15 -15
  209. package/src/vite/utils/bundle-analysis.ts +4 -2
  210. package/src/vite/utils/client-chunks.ts +190 -0
  211. package/src/vite/utils/forward-user-plugins.ts +193 -0
  212. package/src/vite/utils/manifest-utils.ts +21 -5
  213. package/src/vite/utils/shared-utils.ts +107 -26
  214. package/src/browser/action-response-classifier.ts +0 -99
@@ -11,12 +11,16 @@ import {
11
11
  getContext,
12
12
  getNamePrefix,
13
13
  getUrlPrefix,
14
+ requireDslContext,
14
15
  type EntryData,
16
+ type EntryPropDatas,
17
+ type EntryPropSegments,
18
+ type HelperContext,
15
19
  type InterceptEntry,
16
20
  } from "../server/context";
17
21
  import { invariant } from "../errors";
18
22
  import { isCachedFunction } from "../cache/taint.js";
19
- import { RSCRouterContext } from "../server/context";
23
+ import { RangoContext } from "../server/context";
20
24
  import { isStaticHandler } from "../static-handler.js";
21
25
  import RootLayout from "../server/root-layout";
22
26
  import type {
@@ -38,6 +42,7 @@ import type {
38
42
  } from "../route-types.js";
39
43
  import type { RouteHelpers } from "./helpers-types.js";
40
44
  import { resolveHandlerUse, mergeHandlerUse } from "./resolve-handler-use.js";
45
+ import { ALL_USE_ITEM_TYPES } from "./use-item-types.js";
41
46
 
42
47
  /**
43
48
  * Check if an item contains routes (directly or inside nested structures like cache).
@@ -61,16 +66,105 @@ const hasRoutesInItem = (item: AllUseItems): boolean => {
61
66
  return false;
62
67
  };
63
68
 
69
+ /**
70
+ * Fresh empty collections shared by every from-scratch segment entry. Returns
71
+ * new arrays/objects per call so no two entries share mutable references.
72
+ * mountPath is intentionally NOT included here — each call site adds it from
73
+ * getUrlPrefix() where applicable: the route() and transition() helpers add
74
+ * none, while path() (which also builds a `type: "route"` entry) and the
75
+ * structural helpers (layout/cache/middleware/parallel) do.
76
+ */
77
+ const emptySegmentBase = (): EntryPropDatas &
78
+ EntryPropSegments & { loading: undefined } => ({
79
+ loading: undefined,
80
+ middleware: [],
81
+ revalidate: [],
82
+ errorBoundary: [],
83
+ notFoundBoundary: [],
84
+ layout: [],
85
+ parallel: {},
86
+ intercept: [],
87
+ loader: [],
88
+ });
89
+
90
+ /**
91
+ * Run a children/use callback as a nested scope, flatten the result, and assert
92
+ * every item is a valid use item. `kind` preserves the existing error wording
93
+ * ("use()" vs "children" callback).
94
+ */
95
+ function runAndValidateUseItems(
96
+ store: ReturnType<typeof getContext>,
97
+ namespace: string,
98
+ entry: EntryData,
99
+ cb: () => any,
100
+ label: string,
101
+ kind: "use" | "children",
102
+ ): AllUseItems[] {
103
+ const result = store.run(namespace, entry, cb)?.flat(3);
104
+ return validateUseItems(result, namespace, label, kind);
105
+ }
106
+
107
+ /** Assert an already-invoked, flattened callback result is a use-item array. */
108
+ function validateUseItems(
109
+ result: any,
110
+ namespace: string,
111
+ label: string,
112
+ kind: "use" | "children",
113
+ ): AllUseItems[] {
114
+ invariant(
115
+ Array.isArray(result) && result.every((item) => isValidUseItem(item)),
116
+ `${label}() ${kind === "use" ? "use()" : "children"} callback must return an array of use items [${namespace}]`,
117
+ );
118
+ return result as AllUseItems[];
119
+ }
120
+
121
+ /** True when a children/use result contains no routes (directly or nested). */
122
+ const isOrphan = (result: AllUseItems[]): boolean =>
123
+ !result.some((item) => item != null && hasRoutesInItem(item));
124
+
125
+ /**
126
+ * Register a routeless structural entry as an orphan sibling: clear its parent
127
+ * pointer so it leaves the middleware/parent-pointer chain (LOAD-BEARING — see
128
+ * docs/tree-structure.md) and push it onto the parent's layout[] so it renders
129
+ * as a wrapper. Used by cache()/middleware()/transition(); layout() runs extra
130
+ * validation and registers inline.
131
+ */
132
+ const attachOrphanSibling = (
133
+ parent: EntryData | null,
134
+ entry: EntryData,
135
+ ): void => {
136
+ entry.parent = null;
137
+ if (parent && "layout" in parent) parent.layout.push(entry);
138
+ };
139
+
140
+ /**
141
+ * Run `fn` with `ctx.parent` temporarily redirected to `temp` — a satellite
142
+ * entry that captures the attachments declared by a use() callback — restoring
143
+ * the original parent afterward, including on throw. loader()/intercept() each
144
+ * build their own tempParent shape (intercept keeps a loading get/set accessor
145
+ * and a captured-layouts array); this only centralizes the save/restore.
146
+ */
147
+ function withParent<T>(ctx: HelperContext, temp: EntryData, fn: () => T): T {
148
+ const original = ctx.parent;
149
+ ctx.parent = temp;
150
+ try {
151
+ return fn();
152
+ } finally {
153
+ ctx.parent = original;
154
+ }
155
+ }
156
+
64
157
  const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
65
- const ctx = getContext().getStore();
66
- if (!ctx) throw new Error("revalidate() must be called inside map()");
158
+ const { store, ctx } = requireDslContext(
159
+ "revalidate() must be called inside urls()",
160
+ );
67
161
 
68
162
  // Attach to last entry in stack
69
163
  const parent = ctx.parent;
70
164
  if (!parent || !("revalidate" in parent)) {
71
165
  invariant(false, "No parent entry available for revalidate()");
72
166
  }
73
- const name = `$${getContext().getNextIndex("revalidate")}`;
167
+ const name = `$${store.getNextIndex("revalidate")}`;
74
168
  parent.revalidate.push(fn);
75
169
  return { name, type: "revalidate" } as RevalidateItem;
76
170
  };
@@ -108,15 +202,16 @@ const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
108
202
  * ```
109
203
  */
110
204
  const errorBoundary: RouteHelpers<any, any>["errorBoundary"] = (fallback) => {
111
- const ctx = getContext().getStore();
112
- if (!ctx) throw new Error("errorBoundary() must be called inside map()");
205
+ const { store, ctx } = requireDslContext(
206
+ "errorBoundary() must be called inside urls()",
207
+ );
113
208
 
114
209
  // Attach to parent entry in stack
115
210
  const parent = ctx.parent;
116
211
  if (!parent || !("errorBoundary" in parent)) {
117
212
  invariant(false, "No parent entry available for errorBoundary()");
118
213
  }
119
- const name = `$${getContext().getNextIndex("errorBoundary")}`;
214
+ const name = `$${store.getNextIndex("errorBoundary")}`;
120
215
  parent.errorBoundary.push(fallback);
121
216
  return { name, type: "errorBoundary" } as ErrorBoundaryItem;
122
217
  };
@@ -155,15 +250,16 @@ const errorBoundary: RouteHelpers<any, any>["errorBoundary"] = (fallback) => {
155
250
  const notFoundBoundary: RouteHelpers<any, any>["notFoundBoundary"] = (
156
251
  fallback,
157
252
  ) => {
158
- const ctx = getContext().getStore();
159
- if (!ctx) throw new Error("notFoundBoundary() must be called inside map()");
253
+ const { store, ctx } = requireDslContext(
254
+ "notFoundBoundary() must be called inside urls()",
255
+ );
160
256
 
161
257
  // Attach to parent entry in stack
162
258
  const parent = ctx.parent;
163
259
  if (!parent || !("notFoundBoundary" in parent)) {
164
260
  invariant(false, "No parent entry available for notFoundBoundary()");
165
261
  }
166
- const name = `$${getContext().getNextIndex("notFoundBoundary")}`;
262
+ const name = `$${store.getNextIndex("notFoundBoundary")}`;
167
263
  parent.notFoundBoundary.push(fallback);
168
264
  return { name, type: "notFoundBoundary" } as NotFoundBoundaryItem;
169
265
  };
@@ -177,8 +273,9 @@ const notFoundBoundary: RouteHelpers<any, any>["notFoundBoundary"] = (
177
273
  * for the intercept to activate.
178
274
  */
179
275
  const when: RouteHelpers<any, any>["when"] = (fn) => {
180
- const ctx = getContext().getStore();
181
- if (!ctx) throw new Error("when() must be called inside intercept()");
276
+ const { store, ctx } = requireDslContext(
277
+ "when() must be called inside intercept()",
278
+ );
182
279
 
183
280
  // The when() function needs to be captured by the intercept's tempParent
184
281
  // which should have a `when` array. If not present, we're not inside intercept()
@@ -190,7 +287,7 @@ const when: RouteHelpers<any, any>["when"] = (fn) => {
190
287
  );
191
288
  }
192
289
 
193
- const name = `$${getContext().getNextIndex("when")}`;
290
+ const name = `$${store.getNextIndex("when")}`;
194
291
  parent.when.push(fn);
195
292
  return { name, type: "when" } as WhenItem;
196
293
  };
@@ -217,9 +314,9 @@ const cache: RouteHelpers<any, any>["cache"] = (
217
314
  | (() => UseItems<AllUseItems>),
218
315
  maybeChildren?: () => UseItems<AllUseItems>,
219
316
  ) => {
220
- const store = getContext();
221
- const ctx = store.getStore();
222
- if (!ctx) throw new Error("cache() must be called inside map()");
317
+ const { store, ctx } = requireDslContext(
318
+ "cache() must be called inside urls()",
319
+ );
223
320
 
224
321
  // Handle overloaded signature
225
322
  let options: PartialCacheOptions | false;
@@ -232,7 +329,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
232
329
  } else if (typeof optionsOrChildren === "string") {
233
330
  // cache('profileName') or cache('profileName', () => [...])
234
331
  // Resolve from context-scoped profiles (set per-router via HelperContext).
235
- const ctxStore = RSCRouterContext.getStore();
332
+ const ctxStore = RangoContext.getStore();
236
333
  const profile = ctxStore?.cacheProfiles?.[optionsOrChildren];
237
334
  invariant(
238
335
  profile,
@@ -271,26 +368,18 @@ const cache: RouteHelpers<any, any>["cache"] = (
271
368
  // Create orphan cache entry (like orphan layout)
272
369
  // Subsequent siblings in the same array will attach to this entry
273
370
  const namespace = `${ctx.namespace}.${cacheIndex}`;
274
- const cacheUrlPrefix = getUrlPrefix();
371
+ const urlPrefix = getUrlPrefix();
275
372
 
276
373
  const entry = {
374
+ ...emptySegmentBase(),
277
375
  id: namespace,
278
376
  shortCode: store.getShortCode("cache"),
279
377
  type: "cache",
280
378
  parent: parent, // link to current parent for hierarchy
281
379
  cache: cacheConfig,
282
380
  handler: RootLayout,
283
- loading: undefined, // Allow loading() to attach loading state
284
- middleware: [],
285
- revalidate: [],
286
- errorBoundary: [],
287
- notFoundBoundary: [],
288
- layout: [],
289
- parallel: {},
290
- intercept: [],
291
- loader: [],
292
- ...(cacheUrlPrefix ? { mountPath: cacheUrlPrefix } : {}),
293
- } as EntryData;
381
+ ...(urlPrefix ? { mountPath: urlPrefix } : {}),
382
+ } satisfies EntryData;
294
383
 
295
384
  // Attach to parent's layout array (cache entries are structural like layouts)
296
385
  if (parent && "layout" in parent) {
@@ -317,9 +406,10 @@ const cache: RouteHelpers<any, any>["cache"] = (
317
406
  const namespace = `${ctx.namespace}.${cacheIndex}`;
318
407
  const cacheShortCode = store.getShortCode("cache");
319
408
 
320
- const cacheUrlPrefix2 = getUrlPrefix();
409
+ const urlPrefix = getUrlPrefix();
321
410
 
322
411
  const entry = {
412
+ ...emptySegmentBase(),
323
413
  id: namespace,
324
414
  shortCode: cacheShortCode,
325
415
  type: "cache",
@@ -327,40 +417,22 @@ const cache: RouteHelpers<any, any>["cache"] = (
327
417
  cache: cacheConfig,
328
418
  // Cache entries render like layouts (with Outlet as default handler)
329
419
  handler: RootLayout, // RootLayout just renders <Outlet />
330
- loading: undefined, // Allow loading() to attach loading state
331
- middleware: [],
332
- revalidate: [],
333
- errorBoundary: [],
334
- notFoundBoundary: [],
335
- layout: [],
336
- parallel: {},
337
- intercept: [],
338
- loader: [],
339
- ...(cacheUrlPrefix2 ? { mountPath: cacheUrlPrefix2 } : {}),
340
- } as EntryData;
420
+ ...(urlPrefix ? { mountPath: urlPrefix } : {}),
421
+ } satisfies EntryData;
341
422
 
342
423
  // Run children with cache entry as parent
343
- const result = store.run(namespace, entry, children)?.flat(3);
344
-
345
- invariant(
346
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
347
- `cache() children callback must return an array of use items [${namespace}]`,
424
+ const result = runAndValidateUseItems(
425
+ store,
426
+ namespace,
427
+ entry,
428
+ children,
429
+ "cache",
430
+ "children",
348
431
  );
349
432
 
350
- // Check if this cache has routes (including nested caches/layouts)
351
- const hasRoutes =
352
- result &&
353
- Array.isArray(result) &&
354
- result.some((item) => hasRoutesInItem(item));
355
-
356
- if (!hasRoutes) {
357
- const parent = ctx.parent;
358
- if (parent && "layout" in parent) {
359
- // Attach to parent's layout array (cache entries are structural like layouts)
360
- entry.parent = null;
361
- parent.layout.push(entry);
362
- }
363
- }
433
+ // Cache entries are structural like layouts: with no routes inside, register
434
+ // as an orphan sibling.
435
+ if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
364
436
 
365
437
  return { name: namespace, type: "cache", uses: result } as CacheItem;
366
438
  };
@@ -406,9 +478,9 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
406
478
  }
407
479
  }
408
480
 
409
- const store = getContext();
410
- const ctx = store.getStore();
411
- if (!ctx) throw new Error("middleware() must be called inside map()");
481
+ const { store, ctx } = requireDslContext(
482
+ "middleware() must be called inside urls()",
483
+ );
412
484
 
413
485
  if (!children) {
414
486
  // Sibling mode: attach to parent entry
@@ -427,22 +499,15 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
427
499
 
428
500
  const urlPrefix = getUrlPrefix();
429
501
  const entry = {
502
+ ...emptySegmentBase(),
430
503
  id: namespace,
431
504
  shortCode: store.getShortCode("layout"),
432
505
  type: "layout",
433
506
  parent: ctx.parent,
434
507
  handler: RootLayout,
435
- loading: undefined,
436
508
  middleware: [...fns],
437
- revalidate: [],
438
- errorBoundary: [],
439
- notFoundBoundary: [],
440
- layout: [],
441
- parallel: {},
442
- intercept: [],
443
- loader: [],
444
509
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
445
- } as EntryData;
510
+ } satisfies EntryData;
446
511
 
447
512
  // Run children callback. If the second arg was actually a middleware fn
448
513
  // (old variadic form: middleware(mw1, mw2)), this will return a non-array
@@ -455,25 +520,14 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
455
520
  "To pass multiple middleware, use middleware([fn1, fn2]).",
456
521
  );
457
522
 
458
- const result = rawResult.flat(3);
459
-
460
- invariant(
461
- result.every((item: any) => isValidUseItem(item)),
462
- `middleware() children callback must return an array of use items [${namespace}]`,
523
+ const result = validateUseItems(
524
+ rawResult.flat(3),
525
+ namespace,
526
+ "middleware",
527
+ "children",
463
528
  );
464
529
 
465
- const hasRoutes =
466
- result &&
467
- Array.isArray(result) &&
468
- result.some((item) => item != null && hasRoutesInItem(item));
469
-
470
- if (!hasRoutes) {
471
- const parent = ctx.parent;
472
- if (parent && "layout" in parent) {
473
- entry.parent = null;
474
- parent.layout.push(entry);
475
- }
476
- }
530
+ if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
477
531
 
478
532
  return {
479
533
  name: namespace,
@@ -483,9 +537,9 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
483
537
  };
484
538
 
485
539
  const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
486
- const store = getContext();
487
- const ctx = store.getStore();
488
- if (!ctx) throw new Error("parallel() must be called inside map()");
540
+ const { store, ctx } = requireDslContext(
541
+ "parallel() must be called inside urls()",
542
+ );
489
543
 
490
544
  if (!ctx.parent || !ctx.parent?.parallel) {
491
545
  invariant(false, "No parent entry available for parallel()");
@@ -537,20 +591,12 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
537
591
  // Create full EntryData for parallel with its own loaders/revalidate/loading
538
592
  const parallelUrlPrefix = getUrlPrefix();
539
593
  const entry = {
594
+ ...emptySegmentBase(),
540
595
  id: namespace,
541
596
  shortCode: store.getShortCode("parallel"),
542
597
  type: "parallel",
543
598
  parent: null, // Parallels don't participate in parent chain traversal
544
599
  handler: unwrappedSlots,
545
- loading: undefined, // Allow loading() to attach loading state
546
- middleware: [],
547
- revalidate: [],
548
- errorBoundary: [],
549
- notFoundBoundary: [],
550
- layout: [],
551
- parallel: {},
552
- intercept: [],
553
- loader: [],
554
600
  ...(parallelUrlPrefix ? { mountPath: parallelUrlPrefix } : {}),
555
601
  ...(hasStaticSlot
556
602
  ? {
@@ -605,10 +651,13 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
605
651
  "parallel",
606
652
  );
607
653
  if (slotMergedUse) {
608
- const result = store.run(namespace, slotEntry, slotMergedUse)?.flat(3);
609
- invariant(
610
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
611
- `parallel() use() callback must return an array of use items [${namespace}]`,
654
+ runAndValidateUseItems(
655
+ store,
656
+ namespace,
657
+ slotEntry,
658
+ slotMergedUse,
659
+ "parallel",
660
+ "use",
612
661
  );
613
662
  }
614
663
 
@@ -648,9 +697,9 @@ const intercept = (
648
697
  handler: any,
649
698
  use?: () => any[],
650
699
  ) => {
651
- const store = getContext();
652
- const ctx = store.getStore();
653
- if (!ctx) throw new Error("intercept() must be called inside map()");
700
+ const { store, ctx } = requireDslContext(
701
+ "intercept() must be called inside urls()",
702
+ );
654
703
 
655
704
  if (!ctx.parent || !ctx.parent?.intercept) {
656
705
  invariant(false, "No parent entry available for intercept()");
@@ -689,15 +738,13 @@ const intercept = (
689
738
 
690
739
  // Run merged use callback to collect loaders, revalidate, middleware, etc.
691
740
  if (mergedUse) {
692
- // Create a temporary parent context for the use() callback
693
- // so that middleware, loader, revalidate attach to the intercept entry
694
- const originalParent = ctx.parent;
695
-
696
- // Capture layouts in a temporary array
741
+ // Capture layout() calls into a temporary array
697
742
  const capturedLayouts: EntryData[] = [];
698
743
 
744
+ // Temporary parent so middleware/loader/revalidate/when attach to the
745
+ // intercept entry; the loading get/set accessor mirrors writes onto `entry`.
699
746
  const tempParent = {
700
- ...originalParent,
747
+ ...ctx.parent,
701
748
  middleware: entry.middleware,
702
749
  revalidate: entry.revalidate,
703
750
  errorBoundary: entry.errorBoundary,
@@ -705,7 +752,6 @@ const intercept = (
705
752
  loader: entry.loader,
706
753
  layout: capturedLayouts, // Capture layout() calls
707
754
  when: entry.when, // Capture when() conditions
708
- // Use getter/setter to capture loading on the entry
709
755
  get loading() {
710
756
  return entry.loading;
711
757
  },
@@ -713,12 +759,10 @@ const intercept = (
713
759
  entry.loading = value;
714
760
  },
715
761
  };
716
- ctx.parent = tempParent as EntryData;
717
-
718
- const result = mergedUse()?.flat(3);
719
762
 
720
- // Restore original parent
721
- ctx.parent = originalParent;
763
+ const result = withParent(ctx, tempParent as EntryData, () =>
764
+ mergedUse()?.flat(3),
765
+ );
722
766
 
723
767
  // Extract layout from captured layouts (use first one if multiple)
724
768
  // Layout inside intercept should always be ReactNode or Handler, not Record slots
@@ -728,10 +772,7 @@ const intercept = (
728
772
  | Handler<any, any, any>;
729
773
  }
730
774
 
731
- invariant(
732
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
733
- `intercept() use() callback must return an array of use items [${namespace}]`,
734
- );
775
+ validateUseItems(result, namespace, "intercept", "use");
735
776
  }
736
777
 
737
778
  ctx.parent.intercept.push(entry);
@@ -741,10 +782,10 @@ const intercept = (
741
782
  /**
742
783
  * Loader helper - attaches a loader to the current entry
743
784
  */
744
- const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
745
- const store = getContext();
746
- const ctx = store.getStore();
747
- if (!ctx) throw new Error("loader() must be called inside map()");
785
+ const loader: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
786
+ const { store, ctx } = requireDslContext(
787
+ "loader() must be called inside urls()",
788
+ );
748
789
 
749
790
  // Attach to last entry in stack
750
791
  if (!ctx.parent || !ctx.parent?.loader) {
@@ -765,23 +806,22 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
765
806
 
766
807
  // If any use callback is in effect, run it to collect revalidation rules and cache config
767
808
  if (mergedUse) {
768
- // Temporarily set context for revalidate()/cache() calls to target this loader
769
- const originalParent = ctx.parent;
770
809
  // Create a temporary "parent" with type "loader" so cache() can detect it.
771
810
  // Save existing .cache to distinguish inherited config from newly set config.
772
- const parentCache = (originalParent as any).cache;
811
+ const parentCache = (ctx.parent as any).cache;
773
812
  const tempParent = {
774
- ...originalParent,
813
+ ...ctx.parent,
775
814
  type: "loader",
776
815
  revalidate: loaderEntry.revalidate,
777
816
  };
778
- ctx.parent = tempParent as EntryData;
779
817
 
780
- const result = mergedUse()?.flat(3);
818
+ const result = withParent(ctx, tempParent as EntryData, () =>
819
+ mergedUse()?.flat(3),
820
+ );
781
821
 
782
822
  // Copy cache config only if cache() was called during the use() callback.
783
- // The spread from originalParent may carry an inherited .cache from
784
- // a parent cache() boundary — only copy if it was newly set.
823
+ // The spread may carry an inherited .cache from a parent cache() boundary —
824
+ // only copy if it was newly set.
785
825
  if (
786
826
  (tempParent as any).cache &&
787
827
  (tempParent as any).cache !== parentCache
@@ -789,13 +829,7 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
789
829
  (loaderEntry as any).cache = (tempParent as any).cache;
790
830
  }
791
831
 
792
- // Restore original parent
793
- ctx.parent = originalParent;
794
-
795
- invariant(
796
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
797
- `loader() use() callback must return an array of use items [${name}]`,
798
- );
832
+ validateUseItems(result, name, "loader", "use");
799
833
  }
800
834
 
801
835
  ctx.parent.loader.push(loaderEntry);
@@ -806,10 +840,10 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
806
840
  * Loading helper - attaches a loading component to the current entry
807
841
  * Loading components are static (no context) and shown during navigation
808
842
  */
809
- const loadingFn: RouteHelpers<any, any>["loading"] = (component, options) => {
810
- const store = getContext();
811
- const ctx = store.getStore();
812
- if (!ctx) throw new Error("loading() must be called inside map()");
843
+ const loading: RouteHelpers<any, any>["loading"] = (component, options) => {
844
+ const { store, ctx } = requireDslContext(
845
+ "loading() must be called inside urls()",
846
+ );
813
847
 
814
848
  const parent = ctx.parent;
815
849
  if (!parent || !("loading" in parent)) {
@@ -832,10 +866,13 @@ const loadingFn: RouteHelpers<any, any>["loading"] = (component, options) => {
832
866
  };
833
867
 
834
868
  /**
835
- * Transition helper - attaches a ViewTransition config to the current entry
836
- * or wraps a group of routes in a transparent layout with ViewTransition
869
+ * Transition helper - opts the entry (or a wrapped group of routes) into
870
+ * transition-driven navigation by attaching a TransitionConfig. This drives the
871
+ * commit through startTransition (content hold on all React versions) and, on
872
+ * experimental React, places a `<ViewTransition>` boundary unless
873
+ * `viewTransition: false`. See skills/view-transitions for the matrix.
837
874
  */
838
- const transitionFn = (
875
+ const transition = (
839
876
  configOrChildren?: TransitionConfig | (() => UseItems<AllUseItems>),
840
877
  maybeChildren?: () => UseItems<AllUseItems>,
841
878
  ): TransitionItem => {
@@ -849,9 +886,9 @@ const transitionFn = (
849
886
  const children: (() => UseItems<AllUseItems>) | undefined =
850
887
  typeof configOrChildren === "function" ? configOrChildren : maybeChildren;
851
888
 
852
- const store = getContext();
853
- const ctx = store.getStore();
854
- if (!ctx) throw new Error("transition() must be called inside map()");
889
+ const { store, ctx } = requireDslContext(
890
+ "transition() must be called inside urls()",
891
+ );
855
892
 
856
893
  const name = `$${store.getNextIndex("transition")}`;
857
894
 
@@ -868,68 +905,43 @@ const transitionFn = (
868
905
  // Position 2: wrapper — create a transparent layout with transition config
869
906
  const namespace = `${ctx.namespace}.${store.getNextIndex("transition")}`;
870
907
  const entry = {
908
+ ...emptySegmentBase(),
871
909
  id: namespace,
872
910
  shortCode: store.getShortCode("layout"),
873
911
  type: "layout",
874
912
  parent: ctx.parent,
875
913
  handler: RootLayout,
876
- loading: undefined,
877
914
  transition: config,
878
- middleware: [],
879
- revalidate: [],
880
- errorBoundary: [],
881
- notFoundBoundary: [],
882
- layout: [],
883
- parallel: {},
884
- intercept: [],
885
- loader: [],
886
- } as EntryData;
887
-
888
- const result = store.run(namespace, entry, children)?.flat(3);
915
+ } satisfies EntryData;
889
916
 
890
- invariant(
891
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
892
- `transition() children callback must return an array of use items [${namespace}]`,
917
+ const result = runAndValidateUseItems(
918
+ store,
919
+ namespace,
920
+ entry,
921
+ children,
922
+ "transition",
923
+ "children",
893
924
  );
894
925
 
895
- const hasRoutes =
896
- result &&
897
- Array.isArray(result) &&
898
- result.some((item) => hasRoutesInItem(item));
899
-
900
- if (!hasRoutes) {
901
- const parent = ctx.parent;
902
- if (parent && "layout" in parent) {
903
- entry.parent = null;
904
- parent.layout.push(entry);
905
- }
906
- }
926
+ if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
907
927
 
908
928
  return { name: namespace, type: "transition" } as TransitionItem;
909
929
  };
910
930
 
911
- const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
912
- const store = getContext();
913
- const ctx = store.getStore();
914
- if (!ctx) throw new Error("route() must be called inside map()");
931
+ const route: RouteHelpers<any, any>["route"] = (name, handler, use) => {
932
+ const { store, ctx } = requireDslContext(
933
+ "route() must be called inside urls()",
934
+ );
915
935
 
916
936
  const namespace = `${ctx.namespace}.${store.getNextIndex("route")}.${name}`;
917
937
 
918
938
  const entry = {
939
+ ...emptySegmentBase(),
919
940
  id: namespace,
920
941
  shortCode: store.getShortCode("route"),
921
942
  type: "route",
922
943
  parent: ctx.parent,
923
944
  handler: handler as unknown as Handler<any, any, any>,
924
- loading: undefined, // Allow loading() to attach loading state
925
- middleware: [],
926
- revalidate: [],
927
- errorBoundary: [],
928
- notFoundBoundary: [],
929
- layout: [],
930
- parallel: {},
931
- intercept: [],
932
- loader: [],
933
945
  } satisfies EntryData;
934
946
 
935
947
  /* We will throw if user is registring same route name twice */
@@ -944,10 +956,13 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
944
956
  const mergedUse = mergeHandlerUse(handlerUseFn, use, "route");
945
957
  /* Run use and attach handlers */
946
958
  if (mergedUse) {
947
- const result = store.run(namespace, entry, mergedUse)?.flat(3);
948
- invariant(
949
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
950
- `route() use() callback must return an array of use items [${namespace}]`,
959
+ const result = runAndValidateUseItems(
960
+ store,
961
+ namespace,
962
+ entry,
963
+ mergedUse,
964
+ "route",
965
+ "use",
951
966
  );
952
967
  return { name: namespace, type: "route", uses: result } as RouteItem;
953
968
  }
@@ -957,9 +972,9 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
957
972
  };
958
973
 
959
974
  const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
960
- const store = getContext();
961
- const ctx = store.getStore();
962
- if (!ctx) throw new Error("layout() must be called inside map()");
975
+ const { store, ctx } = requireDslContext(
976
+ "layout() must be called inside urls()",
977
+ );
963
978
 
964
979
  invariant(
965
980
  !ctx.parent || ctx.parent.type !== "parallel",
@@ -977,20 +992,12 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
977
992
 
978
993
  const urlPrefix = getUrlPrefix();
979
994
  const entry = {
995
+ ...emptySegmentBase(),
980
996
  id: namespace,
981
997
  shortCode,
982
998
  type: "layout",
983
999
  parent: ctx.parent,
984
1000
  handler: unwrappedHandler,
985
- loading: undefined, // Allow loading() to attach loading state
986
- middleware: [],
987
- revalidate: [],
988
- errorBoundary: [],
989
- notFoundBoundary: [],
990
- parallel: {},
991
- intercept: [],
992
- layout: [],
993
- loader: [],
994
1001
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
995
1002
  ...(isStatic
996
1003
  ? {
@@ -1012,11 +1019,13 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
1012
1019
  // Run merged use callback if present
1013
1020
  let result: AllUseItems[] | undefined;
1014
1021
  if (mergedUse) {
1015
- result = store.run(namespace, entry, mergedUse)?.flat(3);
1016
-
1017
- invariant(
1018
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
1019
- `layout() use() callback must return an array of use items [${namespace}]`,
1022
+ result = runAndValidateUseItems(
1023
+ store,
1024
+ namespace,
1025
+ entry,
1026
+ mergedUse,
1027
+ "layout",
1028
+ "use",
1020
1029
  );
1021
1030
  }
1022
1031
 
@@ -1058,9 +1067,7 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
1058
1067
  `Orphan layouts can only be defined inside route or layout > check [${namespace}]`,
1059
1068
  );
1060
1069
 
1061
- // Clear parent pointer for orphan layouts to prevent duplicate processing
1062
- entry.parent = null;
1063
- parent.layout.push(entry);
1070
+ attachOrphanSibling(parent, entry);
1064
1071
  }
1065
1072
  }
1066
1073
 
@@ -1073,33 +1080,15 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
1073
1080
  } as LayoutItem;
1074
1081
  };
1075
1082
 
1076
- const isValidUseItem = (item: any): item is AllUseItems | undefined | null => {
1077
- return (
1078
- typeof item === "undefined" ||
1079
- item === null ||
1080
- (item &&
1081
- typeof item === "object" &&
1082
- "type" in item &&
1083
- [
1084
- "layout",
1085
- "route",
1086
- "middleware",
1087
- "revalidate",
1088
- "parallel",
1089
- "intercept",
1090
- "loader",
1091
- "loading",
1092
- "errorBoundary",
1093
- "notFoundBoundary",
1094
- "when",
1095
- "cache",
1096
- "transition",
1097
- "include", // For urls() include() helper
1098
- ].includes(item.type))
1099
- );
1100
- };
1083
+ const isValidUseItem = (item: any): item is AllUseItems | undefined | null =>
1084
+ item == null ||
1085
+ (typeof item === "object" &&
1086
+ "type" in item &&
1087
+ ALL_USE_ITEM_TYPES.has(item.type));
1101
1088
 
1102
- // Global helper exports for direct import from @rangojs/router
1089
+ // DSL helpers exported for direct import from @rangojs/router and for
1090
+ // assembly into the RouteHelpers object in helper-factories.ts. The route-item
1091
+ // types are discriminated by their `type` literal, so the helpers carry no brand.
1103
1092
  export {
1104
1093
  layout,
1105
1094
  cache,
@@ -1110,25 +1099,11 @@ export {
1110
1099
  when,
1111
1100
  errorBoundary,
1112
1101
  notFoundBoundary,
1113
- loaderFn as loader,
1114
- loadingFn as loading,
1115
- transitionFn as transition,
1116
- };
1117
-
1118
- const isOrphanLayout = (item: AllUseItems): boolean => {
1119
- return (
1120
- item.type === "layout" &&
1121
- !item.uses?.some((child) => hasRoutesInItem(child))
1122
- );
1123
- };
1124
-
1125
- // Internal exports used by helper-factories.ts
1126
- export {
1127
- routeFn,
1128
- loaderFn,
1129
- loadingFn,
1130
- transitionFn,
1131
- hasRoutesInItem,
1102
+ route,
1103
+ loader,
1104
+ loading,
1105
+ transition,
1132
1106
  isValidUseItem,
1133
- isOrphanLayout,
1107
+ emptySegmentBase,
1108
+ runAndValidateUseItems,
1134
1109
  };