@rangojs/router 0.5.2 → 0.7.0

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 (178) hide show
  1. package/dist/bin/rango.js +343 -125
  2. package/dist/types/browser/react/use-router.d.ts +10 -3
  3. package/dist/types/browser/react/use-search-params.d.ts +57 -10
  4. package/dist/types/browser/types.d.ts +22 -0
  5. package/dist/types/build/merge-full-manifests.d.ts +3 -0
  6. package/dist/types/build/route-trie.d.ts +4 -73
  7. package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
  8. package/dist/types/build/route-types/router-processing.d.ts +2 -3
  9. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  10. package/dist/types/cache/taint.d.ts +12 -6
  11. package/dist/types/client-urls/client-root.d.ts +38 -0
  12. package/dist/types/client-urls/client-urls.d.ts +5 -0
  13. package/dist/types/client-urls/navigation.d.ts +38 -0
  14. package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
  15. package/dist/types/client-urls/server-projection.d.ts +71 -0
  16. package/dist/types/client-urls/types.d.ts +147 -0
  17. package/dist/types/client.d.ts +12 -4
  18. package/dist/types/client.rsc.d.ts +4 -1
  19. package/dist/types/decode-loader-results.d.ts +37 -0
  20. package/dist/types/errors.d.ts +1 -0
  21. package/dist/types/index.d.ts +1 -1
  22. package/dist/types/loader-redirect.d.ts +27 -0
  23. package/dist/types/outlet-context.d.ts +12 -0
  24. package/dist/types/outlet-provider.d.ts +3 -1
  25. package/dist/types/redirect-origin.d.ts +4 -0
  26. package/dist/types/route-content-wrapper.d.ts +42 -1
  27. package/dist/types/route-definition/helpers-types.d.ts +13 -2
  28. package/dist/types/router/error-handling.d.ts +35 -1
  29. package/dist/types/router/intercept-resolution.d.ts +12 -0
  30. package/dist/types/router/loader-resolution.d.ts +24 -2
  31. package/dist/types/router/revalidation.d.ts +7 -0
  32. package/dist/types/router/route-trie-builder.d.ts +77 -0
  33. package/dist/types/router/router-interfaces.d.ts +20 -0
  34. package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
  35. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -9
  36. package/dist/types/router/trie-matching.d.ts +1 -1
  37. package/dist/types/rsc/manifest-init.d.ts +5 -5
  38. package/dist/types/rsc/shell-capture.d.ts +9 -0
  39. package/dist/types/rsc/shell-serve.d.ts +11 -0
  40. package/dist/types/rsc/types.d.ts +30 -0
  41. package/dist/types/segment-system.d.ts +2 -0
  42. package/dist/types/server/context.d.ts +10 -0
  43. package/dist/types/server/handle-store.d.ts +34 -3
  44. package/dist/types/server/request-context.d.ts +11 -1
  45. package/dist/types/server.d.ts +1 -0
  46. package/dist/types/ssr/index.d.ts +22 -0
  47. package/dist/types/ssr/ssr-root.d.ts +10 -0
  48. package/dist/types/testing/dom.entry.d.ts +1 -1
  49. package/dist/types/testing/render-route.d.ts +16 -6
  50. package/dist/types/testing/run-loader.d.ts +9 -0
  51. package/dist/types/types/boundaries.d.ts +22 -0
  52. package/dist/types/types/index.d.ts +1 -1
  53. package/dist/types/types/loader-types.d.ts +57 -5
  54. package/dist/types/types/segments.d.ts +7 -0
  55. package/dist/types/urls/path-helper-types.d.ts +13 -4
  56. package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
  57. package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
  58. package/dist/types/vite/discovery/state.d.ts +8 -1
  59. package/dist/types/vite/plugins/client-ref-dedup.d.ts +0 -11
  60. package/dist/vite/index.js +6159 -2959
  61. package/package.json +6 -5
  62. package/skills/breadcrumbs/SKILL.md +39 -9
  63. package/skills/catalog.json +7 -1
  64. package/skills/client-urls/SKILL.md +338 -0
  65. package/skills/comparison/references/framework-comparison.md +23 -9
  66. package/skills/hooks/SKILL.md +2 -2
  67. package/skills/hooks/data.md +11 -2
  68. package/skills/hooks/handle-and-actions.md +7 -0
  69. package/skills/hooks/outlets.md +26 -5
  70. package/skills/hooks/urls.md +40 -3
  71. package/skills/loader/SKILL.md +132 -20
  72. package/skills/migrate-nextjs/SKILL.md +70 -10
  73. package/skills/migrate-react-router/SKILL.md +49 -13
  74. package/skills/migrate-react-router/component-migration.md +18 -13
  75. package/skills/migrate-react-router/data-and-actions.md +14 -3
  76. package/skills/migrate-react-router/route-mapping.md +15 -2
  77. package/skills/parallel/SKILL.md +32 -1
  78. package/skills/ppr/SKILL.md +16 -6
  79. package/skills/prerender/SKILL.md +8 -4
  80. package/skills/rango/SKILL.md +21 -17
  81. package/skills/react-compiler/SKILL.md +3 -3
  82. package/skills/route/SKILL.md +5 -2
  83. package/skills/router-setup/SKILL.md +16 -2
  84. package/skills/scripts/SKILL.md +16 -6
  85. package/skills/shell-manifest/SKILL.md +16 -7
  86. package/skills/testing/SKILL.md +2 -2
  87. package/skills/testing/client-components.md +6 -0
  88. package/skills/testing/handles.md +30 -8
  89. package/skills/testing/loader.md +51 -49
  90. package/skills/testing/middleware.md +1 -1
  91. package/skills/theme/SKILL.md +8 -5
  92. package/src/bin/rango.ts +7 -3
  93. package/src/browser/navigation-bridge.ts +6 -0
  94. package/src/browser/navigation-client.ts +5 -0
  95. package/src/browser/partial-update.ts +65 -13
  96. package/src/browser/react/use-router.ts +40 -11
  97. package/src/browser/react/use-search-params.ts +140 -17
  98. package/src/browser/rsc-router.tsx +59 -0
  99. package/src/browser/server-action-bridge.ts +26 -0
  100. package/src/browser/types.ts +22 -0
  101. package/src/build/merge-full-manifests.ts +161 -0
  102. package/src/build/route-trie.ts +9 -332
  103. package/src/build/route-types/include-resolution.ts +66 -11
  104. package/src/build/route-types/per-module-writer.ts +11 -6
  105. package/src/build/route-types/router-processing.ts +184 -153
  106. package/src/build/runtime-discovery.ts +23 -12
  107. package/src/cache/cache-exec-scope.ts +47 -0
  108. package/src/cache/cache-runtime.ts +24 -25
  109. package/src/cache/taint.ts +28 -9
  110. package/src/client-urls/client-root.tsx +168 -0
  111. package/src/client-urls/client-urls.ts +776 -0
  112. package/src/client-urls/navigation.ts +237 -0
  113. package/src/client-urls/revalidation-protocol.ts +56 -0
  114. package/src/client-urls/server-projection.ts +670 -0
  115. package/src/client-urls/types.ts +201 -0
  116. package/src/client.rsc.tsx +12 -0
  117. package/src/client.tsx +49 -6
  118. package/src/decode-loader-results.ts +113 -0
  119. package/src/errors.ts +14 -0
  120. package/src/handles/deferred-resolution.ts +14 -7
  121. package/src/index.ts +1 -0
  122. package/src/loader-redirect.tsx +64 -0
  123. package/src/outlet-context.ts +12 -0
  124. package/src/outlet-provider.tsx +15 -1
  125. package/src/redirect-origin.ts +29 -0
  126. package/src/route-content-wrapper.tsx +96 -3
  127. package/src/route-definition/dsl-helpers.ts +28 -3
  128. package/src/route-definition/helpers-types.ts +13 -0
  129. package/src/route-definition/redirect.ts +17 -18
  130. package/src/router/error-handling.ts +65 -11
  131. package/src/router/intercept-resolution.ts +29 -0
  132. package/src/router/loader-resolution.ts +261 -28
  133. package/src/router/match-result.ts +7 -0
  134. package/src/router/revalidation.ts +24 -11
  135. package/src/router/route-trie-builder.ts +334 -0
  136. package/src/router/router-interfaces.ts +38 -0
  137. package/src/router/segment-resolution/fresh.ts +55 -2
  138. package/src/router/segment-resolution/helpers.ts +9 -11
  139. package/src/router/segment-resolution/loader-cache.ts +34 -23
  140. package/src/router/segment-resolution/loader-mask.ts +9 -9
  141. package/src/router/segment-resolution/revalidation.ts +23 -2
  142. package/src/router/trie-matching.ts +3 -3
  143. package/src/router.ts +46 -1
  144. package/src/rsc/full-payload.ts +6 -0
  145. package/src/rsc/handler.ts +10 -7
  146. package/src/rsc/loader-fetch.ts +2 -2
  147. package/src/rsc/manifest-init.ts +28 -9
  148. package/src/rsc/rsc-rendering.ts +15 -1
  149. package/src/rsc/shell-capture.ts +12 -0
  150. package/src/rsc/shell-serve.ts +15 -2
  151. package/src/rsc/ssr-setup.ts +10 -1
  152. package/src/rsc/types.ts +31 -2
  153. package/src/segment-system.tsx +83 -26
  154. package/src/server/context.ts +10 -0
  155. package/src/server/cookie-store.ts +19 -19
  156. package/src/server/handle-store.ts +185 -48
  157. package/src/server/request-context.ts +30 -6
  158. package/src/server.ts +7 -0
  159. package/src/ssr/index.tsx +37 -2
  160. package/src/ssr/ssr-root.tsx +29 -2
  161. package/src/testing/dom.entry.ts +1 -1
  162. package/src/testing/render-route.tsx +22 -8
  163. package/src/testing/run-loader.ts +51 -13
  164. package/src/types/boundaries.ts +19 -0
  165. package/src/types/index.ts +1 -0
  166. package/src/types/loader-types.ts +60 -5
  167. package/src/types/segments.ts +7 -0
  168. package/src/urls/include-helper.ts +22 -4
  169. package/src/urls/path-helper-types.ts +17 -1
  170. package/src/use-loader.tsx +67 -6
  171. package/src/vite/discovery/client-urls-projection.ts +322 -0
  172. package/src/vite/discovery/discover-routers.ts +43 -17
  173. package/src/vite/discovery/state.ts +11 -1
  174. package/src/vite/discovery/virtual-module-codegen.ts +20 -0
  175. package/src/vite/plugins/client-ref-dedup.ts +281 -19
  176. package/src/vite/plugins/expose-action-id.ts +45 -23
  177. package/src/vite/plugins/virtual-entries.ts +12 -3
  178. package/src/vite/router-discovery.ts +163 -12
@@ -0,0 +1,776 @@
1
+ import type { ComponentType, ReactNode } from "react";
2
+ import { validateUserRouteName } from "../route-name.js";
3
+ import { buildRouteTrie } from "../router/route-trie-builder.js";
4
+ import { tryTrieMatch } from "../router/trie-matching.js";
5
+ import type { LoaderDefinition } from "../types.js";
6
+ import type {
7
+ ClientPathOptions,
8
+ ClientRevalidateFn,
9
+ ClientTransitionConfig,
10
+ ClientUrlBuilder,
11
+ ClientUrlHelpers,
12
+ ClientUrlInterceptRecord,
13
+ ClientUrlItem,
14
+ ClientUrlLoaderRecord,
15
+ ClientUrlPatterns,
16
+ ClientUrlRouteRecord,
17
+ ClientUrlUse,
18
+ } from "./types.js";
19
+ import type { ExtractRoutes } from "../urls/type-extraction.js";
20
+ import type { ClientUrlItems } from "./types.js";
21
+
22
+ const ITEM_KIND = Symbol("client-url-item");
23
+
24
+ const TRANSITION_CONFIG_KEYS = new Set<PropertyKey>([
25
+ "enter",
26
+ "exit",
27
+ "update",
28
+ "share",
29
+ "default",
30
+ "name",
31
+ "viewTransition",
32
+ ]);
33
+
34
+ const PPR_NUMBER_KEYS = new Set<PropertyKey>([
35
+ "ttl",
36
+ "swr",
37
+ "captureTimeout",
38
+ "maxSnapshotBytes",
39
+ ]);
40
+
41
+ /**
42
+ * DSL-time shape check for the projected `ppr` path option. Mirrors
43
+ * PartialPrerenderProps but validates by hand: the value must survive the
44
+ * JSON projection to the server, so only `true` or a plain object of finite
45
+ * numbers plus a string[] `tags` is legal. The projection serializer
46
+ * (server-projection.ts) re-validates independently — this copy exists so a
47
+ * bad value fails in the authoring module with a DSL-shaped message.
48
+ */
49
+ function validateClientPpr(value: unknown): void {
50
+ if (value === true) return;
51
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
52
+ throw new Error(
53
+ "clientUrls() path() ppr must be true or a PartialPrerenderProps object",
54
+ );
55
+ }
56
+ for (const key of Reflect.ownKeys(value)) {
57
+ if (PPR_NUMBER_KEYS.has(key)) {
58
+ const n = (value as Record<PropertyKey, unknown>)[key];
59
+ if (typeof n !== "number" || !Number.isFinite(n)) {
60
+ throw new Error(
61
+ `clientUrls() path() ppr.${String(key)} must be a finite number`,
62
+ );
63
+ }
64
+ continue;
65
+ }
66
+ if (key === "tags") {
67
+ const tags = (value as { tags: unknown }).tags;
68
+ if (
69
+ !Array.isArray(tags) ||
70
+ tags.some((tag) => typeof tag !== "string" || tag === "")
71
+ ) {
72
+ throw new Error(
73
+ "clientUrls() path() ppr.tags must be an array of non-empty strings",
74
+ );
75
+ }
76
+ continue;
77
+ }
78
+ throw new Error(
79
+ `clientUrls() path() ppr received unsupported key ${JSON.stringify(String(key))}`,
80
+ );
81
+ }
82
+ }
83
+
84
+ const UNSUPPORTED_HELPERS = new Set([
85
+ "include",
86
+ "parallel",
87
+ "cache",
88
+ "error",
89
+ "notFound",
90
+ "errorBoundary",
91
+ "notFoundBoundary",
92
+ "middleware",
93
+ ]);
94
+
95
+ type InternalItem =
96
+ | PathItem
97
+ | LayoutItem
98
+ | LoaderItem
99
+ | LoadingItem
100
+ | InterceptItem
101
+ | TransitionItem
102
+ | RevalidateItem;
103
+
104
+ interface ItemBase {
105
+ readonly [ITEM_KIND]: true;
106
+ }
107
+
108
+ interface PathItem extends ItemBase {
109
+ readonly type: "path";
110
+ readonly pattern: string;
111
+ readonly component: ComponentType;
112
+ readonly options: ClientPathOptions | undefined;
113
+ readonly use: readonly InternalItem[];
114
+ }
115
+
116
+ interface LayoutItem extends ItemBase {
117
+ readonly type: "layout";
118
+ readonly component: ComponentType;
119
+ readonly children: readonly InternalItem[];
120
+ }
121
+
122
+ interface InterceptItem extends ItemBase {
123
+ readonly type: "intercept";
124
+ readonly slotName: `@${string}`;
125
+ /** Bare target name — the leading dot of the dot-local argument is stripped. */
126
+ readonly targetName: string;
127
+ readonly component: ComponentType;
128
+ readonly use: readonly InternalItem[];
129
+ }
130
+
131
+ interface TransitionItem extends ItemBase {
132
+ readonly type: "transition";
133
+ readonly config: ClientTransitionConfig;
134
+ }
135
+
136
+ interface LoaderItem extends ItemBase {
137
+ readonly type: "loader";
138
+ readonly definition: LoaderDefinition<any, any>;
139
+ readonly revalidate: readonly ClientRevalidateFn[];
140
+ readonly stream?: "navigation";
141
+ }
142
+
143
+ interface RevalidateItem extends ItemBase {
144
+ readonly type: "revalidate";
145
+ readonly fn: ClientRevalidateFn;
146
+ }
147
+
148
+ interface LoadingItem extends ItemBase {
149
+ readonly type: "loading";
150
+ readonly component: ReactNode;
151
+ }
152
+
153
+ interface FlattenContext {
154
+ readonly layouts: readonly ComponentType[];
155
+ readonly loaders: readonly ClientUrlLoaderRecord[];
156
+ readonly loading: ReactNode | undefined;
157
+ }
158
+
159
+ const EMPTY_CONTEXT: FlattenContext = {
160
+ layouts: [],
161
+ loaders: [],
162
+ loading: undefined,
163
+ };
164
+
165
+ function toPublicItem(item: InternalItem): ClientUrlItem {
166
+ return item as unknown as ClientUrlItem;
167
+ }
168
+
169
+ function isInternalItem(value: unknown): value is InternalItem {
170
+ return (
171
+ typeof value === "object" &&
172
+ value !== null &&
173
+ ITEM_KIND in value &&
174
+ (value as ItemBase)[ITEM_KIND] === true
175
+ );
176
+ }
177
+
178
+ function flattenItems(value: unknown, label: string): InternalItem[] {
179
+ if (!Array.isArray(value)) {
180
+ throw new Error(`clientUrls() ${label} callback must return an array`);
181
+ }
182
+
183
+ const flattened: InternalItem[] = [];
184
+ const visit = (input: unknown): void => {
185
+ if (Array.isArray(input)) {
186
+ for (const child of input) visit(child);
187
+ return;
188
+ }
189
+ if (!isInternalItem(input)) {
190
+ throw new Error(
191
+ `clientUrls() ${label} callback must return only client URL helper items`,
192
+ );
193
+ }
194
+ flattened.push(input);
195
+ };
196
+
197
+ visit(value);
198
+ return flattened;
199
+ }
200
+
201
+ function runUse(use: ClientUrlUse, label: string): InternalItem[] {
202
+ if (typeof use !== "function") {
203
+ throw new Error(`clientUrls() ${label} expects a callback`);
204
+ }
205
+ return flattenItems(use(), label);
206
+ }
207
+
208
+ function rejectItems(
209
+ items: readonly InternalItem[],
210
+ allowed: ReadonlySet<InternalItem["type"]>,
211
+ label: string,
212
+ ): void {
213
+ for (const item of items) {
214
+ if (!allowed.has(item.type)) {
215
+ throw new Error(
216
+ `clientUrls() does not support ${item.type}() inside ${label}()`,
217
+ );
218
+ }
219
+ }
220
+ }
221
+
222
+ function assertNamedComponent(
223
+ component: unknown,
224
+ helper: "path" | "layout" | "intercept",
225
+ ): asserts component is ComponentType {
226
+ // Build-time handler wrappers get a TARGETED rejection ahead of the generic
227
+ // shape check: their handler code cannot live in a "use client" bundle and
228
+ // their render runs at build, so accepting one could only fail later and
229
+ // further from the mistake. Brand strings checked directly — importing
230
+ // static-handler.ts/prerender.ts here would pull server modules into the
231
+ // client graph for an error message.
232
+ const brand =
233
+ component !== null &&
234
+ (typeof component === "object" || typeof component === "function")
235
+ ? (component as { __brand?: unknown }).__brand
236
+ : undefined;
237
+ if (
238
+ brand === "staticHandler" ||
239
+ brand === "prerenderHandler" ||
240
+ brand === "prerenderPassthrough"
241
+ ) {
242
+ const wrapper = brand === "staticHandler" ? "Static()" : "Prerender()";
243
+ throw new Error(
244
+ `clientUrls() ${helper}() received a ${wrapper} handler. Build-time ` +
245
+ "handlers are server-DSL surface and cannot mount inside a client " +
246
+ "group — routes in a group render client components. For shell " +
247
+ "caching of a group route use the `ppr` path option; build-time " +
248
+ "prerendering belongs to the server tree around the include.",
249
+ );
250
+ }
251
+ if (typeof component !== "function" || component.name.trim() === "") {
252
+ throw new Error(
253
+ `clientUrls() ${helper}() expects a named client component value`,
254
+ );
255
+ }
256
+ }
257
+
258
+ function assertLoaderDefinition(
259
+ definition: unknown,
260
+ ): asserts definition is LoaderDefinition<any, any> {
261
+ const candidate = definition as Partial<LoaderDefinition<any, any>> | null;
262
+ if (
263
+ typeof candidate !== "object" ||
264
+ candidate === null ||
265
+ candidate.__brand !== "loader" ||
266
+ typeof candidate.$$id !== "string"
267
+ ) {
268
+ throw new Error(
269
+ "clientUrls() loader() expects a LoaderDefinition created by createLoader()",
270
+ );
271
+ }
272
+ }
273
+
274
+ function createHelpers(): ClientUrlHelpers {
275
+ const path = ((
276
+ pattern: string,
277
+ component: ComponentType,
278
+ optionsOrUse?: ClientPathOptions | ClientUrlUse,
279
+ maybeUse?: ClientUrlUse,
280
+ ): ClientUrlItem => {
281
+ if (typeof pattern !== "string" || !pattern.startsWith("/")) {
282
+ throw new Error(
283
+ 'clientUrls() path() pattern must be a string beginning with "/"',
284
+ );
285
+ }
286
+ assertNamedComponent(component, "path");
287
+
288
+ let options: ClientPathOptions | undefined;
289
+ let use: ClientUrlUse | undefined;
290
+ if (typeof optionsOrUse === "function") {
291
+ if (maybeUse !== undefined) {
292
+ throw new Error(
293
+ "clientUrls() path() received more than one use callback",
294
+ );
295
+ }
296
+ use = optionsOrUse;
297
+ } else if (optionsOrUse !== undefined) {
298
+ if (typeof optionsOrUse !== "object" || optionsOrUse === null) {
299
+ throw new Error(
300
+ "clientUrls() path() third argument must be PathOptions or a use callback",
301
+ );
302
+ }
303
+ options = { ...optionsOrUse };
304
+ use = maybeUse;
305
+ } else if (maybeUse !== undefined) {
306
+ throw new Error(
307
+ "clientUrls() path() cannot receive a fourth argument without PathOptions",
308
+ );
309
+ }
310
+
311
+ if (options?.name !== undefined) {
312
+ if (typeof options.name !== "string" || options.name === "") {
313
+ throw new Error("clientUrls() path() name must be a non-empty string");
314
+ }
315
+ validateUserRouteName(options.name);
316
+ }
317
+ if (
318
+ options?.trailingSlash !== undefined &&
319
+ options.trailingSlash !== "always" &&
320
+ options.trailingSlash !== "never" &&
321
+ options.trailingSlash !== "ignore"
322
+ ) {
323
+ throw new Error(
324
+ 'clientUrls() path() trailingSlash must be "always", "never", or "ignore"',
325
+ );
326
+ }
327
+ if (options?.ppr !== undefined) {
328
+ validateClientPpr(options.ppr);
329
+ }
330
+
331
+ const items = use ? runUse(use, "path use") : [];
332
+ rejectItems(items, new Set(["loader", "loading", "transition"]), "path");
333
+ if (items.filter((item) => item.type === "transition").length > 1) {
334
+ throw new Error(
335
+ "clientUrls() path() received more than one transition()",
336
+ );
337
+ }
338
+ return toPublicItem({
339
+ [ITEM_KIND]: true,
340
+ type: "path",
341
+ pattern,
342
+ component,
343
+ options,
344
+ use: items,
345
+ });
346
+ }) as ClientUrlHelpers["path"];
347
+
348
+ const layout = ((
349
+ component: ComponentType,
350
+ children: ClientUrlUse,
351
+ ): ClientUrlItem => {
352
+ assertNamedComponent(component, "layout");
353
+ const items = runUse(children, "layout children");
354
+ rejectItems(
355
+ items,
356
+ new Set(["path", "layout", "loader", "loading", "intercept"]),
357
+ "layout",
358
+ );
359
+ return toPublicItem({
360
+ [ITEM_KIND]: true,
361
+ type: "layout",
362
+ component,
363
+ children: items,
364
+ });
365
+ }) as ClientUrlHelpers["layout"];
366
+
367
+ const loader: ClientUrlHelpers["loader"] = (
368
+ definition,
369
+ optionsOrUse,
370
+ maybeUse,
371
+ ) => {
372
+ assertLoaderDefinition(definition);
373
+ // Same options-or-use disambiguation as the server DSL loader(): the
374
+ // 2-arg loader(Def, use) form is untouched.
375
+ if (typeof optionsOrUse === "function" && maybeUse !== undefined) {
376
+ throw new Error(
377
+ "clientUrls() loader() received two use() callbacks. Pass loader(Def, options, use) or loader(Def, use).",
378
+ );
379
+ }
380
+ const options =
381
+ typeof optionsOrUse === "function" ? undefined : optionsOrUse;
382
+ const use = typeof optionsOrUse === "function" ? optionsOrUse : maybeUse;
383
+ if (options?.stream !== undefined && options.stream !== "navigation") {
384
+ throw new Error(
385
+ `clientUrls() loader() stream must be "navigation" (got ${JSON.stringify(options.stream)}). Omit it to stream on every render.`,
386
+ );
387
+ }
388
+ const items = use ? runUse(use, "loader use") : [];
389
+ rejectItems(items, new Set(["revalidate"]), "loader");
390
+ return toPublicItem({
391
+ [ITEM_KIND]: true,
392
+ type: "loader",
393
+ definition,
394
+ revalidate: items
395
+ .filter(
396
+ (item): item is Extract<InternalItem, { type: "revalidate" }> =>
397
+ item.type === "revalidate",
398
+ )
399
+ .map((item) => item.fn),
400
+ ...(options?.stream === "navigation"
401
+ ? { stream: "navigation" as const }
402
+ : {}),
403
+ });
404
+ };
405
+
406
+ const revalidate: ClientUrlHelpers["revalidate"] = (fn) => {
407
+ if (typeof fn !== "function") {
408
+ throw new Error("clientUrls() revalidate() expects a predicate function");
409
+ }
410
+ return toPublicItem({
411
+ [ITEM_KIND]: true,
412
+ type: "revalidate",
413
+ fn,
414
+ });
415
+ };
416
+
417
+ const loading: ClientUrlHelpers["loading"] = (component) =>
418
+ toPublicItem({ [ITEM_KIND]: true, type: "loading", component });
419
+
420
+ const transition: ClientUrlHelpers["transition"] = (config) => {
421
+ if (
422
+ typeof config !== "object" ||
423
+ config === null ||
424
+ Array.isArray(config)
425
+ ) {
426
+ throw new Error(
427
+ "clientUrls() transition() expects a configuration object",
428
+ );
429
+ }
430
+ for (const key of Reflect.ownKeys(config)) {
431
+ if (key === "when") {
432
+ throw new Error(
433
+ "clientUrls() transition() does not support `when` — the gate is a " +
434
+ "server-executed predicate; declare it with a server-tree transition()",
435
+ );
436
+ }
437
+ if (!TRANSITION_CONFIG_KEYS.has(key)) {
438
+ throw new Error(
439
+ `clientUrls() transition() option ${JSON.stringify(String(key))} is not supported`,
440
+ );
441
+ }
442
+ }
443
+ if (config.name !== undefined && typeof config.name !== "string") {
444
+ throw new Error("clientUrls() transition() name must be a string");
445
+ }
446
+ if (
447
+ config.viewTransition !== undefined &&
448
+ config.viewTransition !== "auto" &&
449
+ config.viewTransition !== false
450
+ ) {
451
+ throw new Error(
452
+ 'clientUrls() transition() viewTransition must be "auto" or false',
453
+ );
454
+ }
455
+ for (const key of [
456
+ "enter",
457
+ "exit",
458
+ "update",
459
+ "share",
460
+ "default",
461
+ ] as const) {
462
+ const value = config[key];
463
+ if (value === undefined || typeof value === "string") continue;
464
+ const isStringMap =
465
+ typeof value === "object" &&
466
+ value !== null &&
467
+ !Array.isArray(value) &&
468
+ Object.values(value).every((entry) => typeof entry === "string");
469
+ if (!isStringMap) {
470
+ throw new Error(
471
+ `clientUrls() transition() ${key} must be a string or a string map of transition types`,
472
+ );
473
+ }
474
+ }
475
+ return toPublicItem({
476
+ [ITEM_KIND]: true,
477
+ type: "transition",
478
+ config: { ...config },
479
+ });
480
+ };
481
+
482
+ const intercept: ClientUrlHelpers["intercept"] = (
483
+ slotName,
484
+ targetName,
485
+ component,
486
+ use,
487
+ ) => {
488
+ if (typeof slotName !== "string" || !/^@.+/.test(slotName)) {
489
+ throw new Error(
490
+ 'clientUrls() intercept() slot name must be a string beginning with "@"',
491
+ );
492
+ }
493
+ if (
494
+ typeof targetName !== "string" ||
495
+ !targetName.startsWith(".") ||
496
+ targetName.length < 2
497
+ ) {
498
+ throw new Error(
499
+ 'clientUrls() intercept() target must be a dot-local route name like ".detail"',
500
+ );
501
+ }
502
+ assertNamedComponent(component, "intercept");
503
+ const items = use ? runUse(use, "intercept use") : [];
504
+ rejectItems(items, new Set(["loader", "loading"]), "intercept");
505
+ for (const item of items) {
506
+ // Intercept loaders run on soft navigations only — a document-render
507
+ // await can never apply, so accepting the flag would be silently inert.
508
+ if (item.type === "loader" && item.stream !== undefined) {
509
+ throw new Error(
510
+ 'clientUrls() intercept() loaders cannot use stream: "navigation" — intercepts render on client navigations only',
511
+ );
512
+ }
513
+ }
514
+ return toPublicItem({
515
+ [ITEM_KIND]: true,
516
+ type: "intercept",
517
+ slotName,
518
+ targetName: targetName.slice(1),
519
+ component,
520
+ use: items,
521
+ });
522
+ };
523
+
524
+ const supported: ClientUrlHelpers = {
525
+ path,
526
+ layout,
527
+ loader,
528
+ loading,
529
+ intercept,
530
+ transition,
531
+ revalidate,
532
+ };
533
+
534
+ return new Proxy(supported, {
535
+ get(target, property, receiver): unknown {
536
+ if (typeof property === "string" && UNSUPPORTED_HELPERS.has(property)) {
537
+ throw new Error(`clientUrls() does not support ${property}()`);
538
+ }
539
+ if (typeof property === "string" && !(property in target)) {
540
+ throw new Error(`clientUrls() has no ${property}() helper`);
541
+ }
542
+ return Reflect.get(target, property, receiver);
543
+ },
544
+ });
545
+ }
546
+
547
+ function applyConfig(
548
+ context: FlattenContext,
549
+ items: readonly InternalItem[],
550
+ ): FlattenContext {
551
+ const loaders = [...context.loaders];
552
+ let loading = context.loading;
553
+
554
+ for (const item of items) {
555
+ if (item.type === "loader") {
556
+ loaders.push(
557
+ Object.freeze({
558
+ loader: item.definition,
559
+ revalidate: Object.freeze([...item.revalidate]),
560
+ ...(item.stream ? { stream: item.stream } : {}),
561
+ }),
562
+ );
563
+ } else if (item.type === "loading") {
564
+ loading = item.component;
565
+ }
566
+ }
567
+
568
+ return {
569
+ layouts: context.layouts,
570
+ loaders,
571
+ loading,
572
+ };
573
+ }
574
+
575
+ function addPathRecord(
576
+ item: PathItem,
577
+ context: FlattenContext,
578
+ routes: ClientUrlRouteRecord[],
579
+ ): void {
580
+ const routeContext = applyConfig(context, item.use);
581
+ const options = item.options ? Object.freeze({ ...item.options }) : undefined;
582
+ // Route-scoped only: transition() is rejected outside path() use callbacks,
583
+ // so there is no layout-level inheritance to fold in.
584
+ const transitionItem = item.use.find(
585
+ (useItem): useItem is Extract<InternalItem, { type: "transition" }> =>
586
+ useItem.type === "transition",
587
+ );
588
+ routes.push(
589
+ Object.freeze({
590
+ id: `client-route-${routes.length}`,
591
+ pattern: item.pattern,
592
+ name: options?.name,
593
+ options,
594
+ component: item.component,
595
+ layouts: Object.freeze([...routeContext.layouts]),
596
+ loaders: Object.freeze([...routeContext.loaders]),
597
+ loading: routeContext.loading,
598
+ transition: transitionItem
599
+ ? Object.freeze({ ...transitionItem.config })
600
+ : undefined,
601
+ }),
602
+ );
603
+ }
604
+
605
+ function flattenLayout(
606
+ item: LayoutItem,
607
+ context: FlattenContext,
608
+ routes: ClientUrlRouteRecord[],
609
+ ): void {
610
+ const routeCount = routes.length;
611
+ const layoutContext = applyConfig(
612
+ {
613
+ ...context,
614
+ layouts: [...context.layouts, item.component],
615
+ },
616
+ item.children,
617
+ );
618
+
619
+ flattenRoutes(item.children, layoutContext, routes);
620
+ if (routes.length === routeCount) {
621
+ throw new Error(
622
+ "clientUrls() layout() children must contain at least one path()",
623
+ );
624
+ }
625
+ }
626
+
627
+ function flattenRoutes(
628
+ items: readonly InternalItem[],
629
+ context: FlattenContext,
630
+ routes: ClientUrlRouteRecord[],
631
+ ): void {
632
+ for (const item of items) {
633
+ if (item.type === "path") {
634
+ addPathRecord(item, context, routes);
635
+ } else if (item.type === "layout") {
636
+ flattenLayout(item, context, routes);
637
+ }
638
+ }
639
+ }
640
+
641
+ function validateRoutes(routes: readonly ClientUrlRouteRecord[]): void {
642
+ const patterns = new Set<string>();
643
+ const names = new Set<string>();
644
+ for (const route of routes) {
645
+ if (patterns.has(route.pattern)) {
646
+ throw new Error(
647
+ `clientUrls() cannot define duplicate path pattern "${route.pattern}"`,
648
+ );
649
+ }
650
+ patterns.add(route.pattern);
651
+
652
+ if (route.name !== undefined) {
653
+ if (names.has(route.name)) {
654
+ throw new Error(
655
+ `clientUrls() cannot define duplicate route name "${route.name}"`,
656
+ );
657
+ }
658
+ names.add(route.name);
659
+ }
660
+ }
661
+ }
662
+
663
+ function collectIntercepts(
664
+ items: readonly InternalItem[],
665
+ intercepts: ClientUrlInterceptRecord[],
666
+ ): void {
667
+ for (const item of items) {
668
+ if (item.type === "intercept") {
669
+ const loaders: ClientUrlLoaderRecord[] = [];
670
+ let loading: ReactNode | undefined;
671
+ for (const useItem of item.use) {
672
+ if (useItem.type === "loader") {
673
+ loaders.push(
674
+ Object.freeze({
675
+ loader: useItem.definition,
676
+ revalidate: Object.freeze([...useItem.revalidate]),
677
+ }),
678
+ );
679
+ } else if (useItem.type === "loading") {
680
+ loading = useItem.component;
681
+ }
682
+ }
683
+ intercepts.push(
684
+ Object.freeze({
685
+ slotName: item.slotName,
686
+ targetName: item.targetName,
687
+ component: item.component,
688
+ loaders: Object.freeze(loaders),
689
+ loading,
690
+ }),
691
+ );
692
+ } else if (item.type === "layout") {
693
+ collectIntercepts(item.children, intercepts);
694
+ }
695
+ }
696
+ }
697
+
698
+ function validateIntercepts(
699
+ intercepts: readonly ClientUrlInterceptRecord[],
700
+ routes: readonly ClientUrlRouteRecord[],
701
+ ): void {
702
+ const names = new Set<string>();
703
+ for (const route of routes) {
704
+ if (route.name !== undefined) names.add(route.name);
705
+ }
706
+ for (const intercept of intercepts) {
707
+ if (!names.has(intercept.targetName)) {
708
+ throw new Error(
709
+ `clientUrls() intercept() target ".${intercept.targetName}" does not match a named path() in this definition`,
710
+ );
711
+ }
712
+ }
713
+ }
714
+
715
+ export function clientUrls<const TItems extends ClientUrlItems>(
716
+ builder: ClientUrlBuilder<TItems>,
717
+ ): ClientUrlPatterns<ExtractRoutes<TItems>> {
718
+ if (typeof builder !== "function") {
719
+ throw new Error("clientUrls() expects a builder function");
720
+ }
721
+
722
+ const items = flattenItems(builder(createHelpers()), "builder");
723
+ rejectItems(items, new Set(["path", "layout", "intercept"]), "clientUrls");
724
+
725
+ const routes: ClientUrlRouteRecord[] = [];
726
+ const rootContext = applyConfig(EMPTY_CONTEXT, items);
727
+ flattenRoutes(items, rootContext, routes);
728
+ if (routes.length === 0) {
729
+ throw new Error("clientUrls() builder must define at least one path()");
730
+ }
731
+ validateRoutes(routes);
732
+
733
+ const intercepts: ClientUrlInterceptRecord[] = [];
734
+ collectIntercepts(items, intercepts);
735
+ validateIntercepts(intercepts, routes);
736
+
737
+ const routeManifest: Record<string, string> = {};
738
+ const staticPrefixes: Record<string, string> = {};
739
+ const trailingSlash: Record<string, string> = {};
740
+ for (const route of routes) {
741
+ routeManifest[route.id] = route.pattern;
742
+ staticPrefixes[route.id] = "";
743
+ if (route.options?.trailingSlash) {
744
+ trailingSlash[route.id] = route.options.trailingSlash;
745
+ }
746
+ }
747
+ const trie = buildRouteTrie(routeManifest, staticPrefixes, trailingSlash);
748
+
749
+ return Object.freeze({
750
+ __brand: "client-urls" as const,
751
+ routes: Object.freeze(routes),
752
+ intercepts: Object.freeze(intercepts),
753
+ match(pathname: string) {
754
+ return tryTrieMatch(trie, pathname);
755
+ },
756
+ }) as ClientUrlPatterns<ExtractRoutes<TItems>>;
757
+ }
758
+
759
+ export type {
760
+ ClientRevalidateArgs,
761
+ ClientRevalidateFn,
762
+ ClientTransitionConfig,
763
+ ClientUrlBuilder,
764
+ ClientUrlHelpers,
765
+ ClientUrlInterceptRecord,
766
+ ClientUrlItem,
767
+ ClientUrlItemInput,
768
+ ClientUrlItems,
769
+ ClientLayoutFn,
770
+ ClientPathOptions,
771
+ ClientUrlLoaderRecord,
772
+ ClientPathFn,
773
+ ClientUrlPatterns,
774
+ ClientUrlRouteRecord,
775
+ ClientUrlUse,
776
+ } from "./types.js";