@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,670 @@
1
+ import * as React from "react";
2
+ import type { SearchSchemaValue } from "../search-params.js";
3
+ import { getLoaderLazy } from "../server/loader-registry.js";
4
+ import {
5
+ getNamePrefix,
6
+ getUrlPrefix,
7
+ type InterceptSelectorContext,
8
+ } from "../server/context.js";
9
+ import type { LoaderDefinition, TrailingSlashMode } from "../types.js";
10
+ import type {
11
+ PartialPrerenderProps,
12
+ PathOptions,
13
+ UrlPatterns,
14
+ } from "../urls/pattern-types.js";
15
+ import type { PathHelpers } from "../urls/path-helper-types.js";
16
+ import type { AllUseItems } from "../route-types.js";
17
+ import { urls } from "../urls/urls-function.js";
18
+ import {
19
+ CLIENT_REVALIDATION_HEADER,
20
+ decodeClientRevalidationDecisions,
21
+ type ClientRevalidationDecisions,
22
+ } from "./revalidation-protocol.js";
23
+ import {
24
+ ClientUrlsGroupLayout,
25
+ ClientUrlsInterceptLoading,
26
+ ClientUrlsInterceptSlot,
27
+ ClientUrlsLoading,
28
+ ClientUrlsRoot,
29
+ } from "./client-root.js";
30
+ import type {
31
+ ClientTransitionConfig,
32
+ ClientUrlInterceptRecord,
33
+ ClientUrlPatterns,
34
+ ClientUrlRouteRecord,
35
+ } from "./types.js";
36
+
37
+ const CLIENT_REFERENCE = Symbol.for("react.client.reference");
38
+
39
+ const SEARCH_SCHEMA_VALUES: ReadonlySet<string> = new Set([
40
+ "string",
41
+ "number",
42
+ "boolean",
43
+ "string?",
44
+ "number?",
45
+ "boolean?",
46
+ ]);
47
+
48
+ const PROJECTION_OPTIONS = new Set<PropertyKey>([
49
+ "name",
50
+ "search",
51
+ "trailingSlash",
52
+ "ppr",
53
+ ]);
54
+
55
+ const PPR_NUMBER_KEYS = new Set<PropertyKey>([
56
+ "ttl",
57
+ "swr",
58
+ "captureTimeout",
59
+ "maxSnapshotBytes",
60
+ ]);
61
+
62
+ const clientUrlProjections = new Map<string, ClientUrlProjection>();
63
+
64
+ export interface ClientUrlReference {
65
+ readonly $$typeof: symbol;
66
+ readonly $$id: string;
67
+ }
68
+
69
+ export type ClientUrlDefinitionSource = ClientUrlReference | ClientUrlPatterns;
70
+
71
+ export interface ClientUrlProjectionOptions {
72
+ readonly search?: Readonly<Record<string, SearchSchemaValue>>;
73
+ readonly trailingSlash?: TrailingSlashMode;
74
+ /**
75
+ * The `ppr` path option, JSON-projected. Materialization passes it onto
76
+ * the group route's server `path()` verbatim, so the manifest entry
77
+ * carries it and the runtime shell capture/serve lanes engage exactly as
78
+ * for a hand-written server route (search is part of shell identity —
79
+ * the capture render seeds the key's own search, so group static parts
80
+ * may read useSearchParams).
81
+ */
82
+ readonly ppr?: true | Readonly<PartialPrerenderProps>;
83
+ }
84
+
85
+ export interface ClientUrlProjectionRoute {
86
+ readonly id: string;
87
+ readonly pattern: string;
88
+ readonly name: string | null;
89
+ readonly options: ClientUrlProjectionOptions;
90
+ readonly loaderIds: readonly string[];
91
+ readonly hasLoading: boolean;
92
+ /** Indices into loaderIds of loaders declared loader(Def, { stream:
93
+ * "navigation" }); materialization passes the option through to the server
94
+ * loader() so document renders await them before first flush. Absent (=
95
+ * none) in projections serialized before stream support. */
96
+ readonly awaitedLoaderIndices?: readonly number[];
97
+ /** Data-only transition config (no `when` — server-tree only); absent in
98
+ * projections serialized before transition support. */
99
+ readonly transition?: Readonly<ClientTransitionConfig>;
100
+ }
101
+
102
+ export interface ClientUrlProjectionIntercept {
103
+ readonly slotName: `@${string}`;
104
+ /** Bare local target name; materialization re-dots it so the server
105
+ * intercept() helper composes it against the include's name prefix. */
106
+ readonly targetName: string;
107
+ readonly loaderIds: readonly string[];
108
+ readonly hasLoading: boolean;
109
+ }
110
+
111
+ export interface ClientUrlProjection {
112
+ readonly version: 1;
113
+ readonly routes: readonly ClientUrlProjectionRoute[];
114
+ /** Absent in projections serialized before intercept support; consumers
115
+ * must treat missing as empty. */
116
+ readonly intercepts?: readonly ClientUrlProjectionIntercept[];
117
+ }
118
+
119
+ function routeDescription(route: ClientUrlRouteRecord): string {
120
+ return `route "${route.pattern}" (${route.id})`;
121
+ }
122
+
123
+ function projectionError(route: ClientUrlRouteRecord, message: string): Error {
124
+ return new Error(
125
+ `clientUrls() cannot create a server projection for ${routeDescription(route)}: ${message}`,
126
+ );
127
+ }
128
+
129
+ function serializeSearchSchema(
130
+ route: ClientUrlRouteRecord,
131
+ value: unknown,
132
+ ): Readonly<Record<string, SearchSchemaValue>> {
133
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
134
+ throw projectionError(route, 'path option "search" must be an object');
135
+ }
136
+
137
+ const entries: Array<[string, SearchSchemaValue]> = [];
138
+ for (const key of Object.keys(value).sort()) {
139
+ const schemaValue = (value as Record<string, unknown>)[key];
140
+ if (
141
+ typeof schemaValue !== "string" ||
142
+ !SEARCH_SCHEMA_VALUES.has(schemaValue)
143
+ ) {
144
+ throw projectionError(
145
+ route,
146
+ `path option "search.${key}" has unsupported value ${JSON.stringify(schemaValue)}`,
147
+ );
148
+ }
149
+ entries.push([key, schemaValue as SearchSchemaValue]);
150
+ }
151
+
152
+ return Object.freeze(Object.fromEntries(entries));
153
+ }
154
+
155
+ function serializeOptions(
156
+ route: ClientUrlRouteRecord,
157
+ ): ClientUrlProjectionOptions {
158
+ const source = route.options;
159
+ if (!source) return Object.freeze({});
160
+
161
+ for (const key of Reflect.ownKeys(source)) {
162
+ if (!PROJECTION_OPTIONS.has(key)) {
163
+ throw projectionError(
164
+ route,
165
+ `path option ${JSON.stringify(String(key))} is not supported`,
166
+ );
167
+ }
168
+ }
169
+
170
+ const options: {
171
+ search?: Readonly<Record<string, SearchSchemaValue>>;
172
+ trailingSlash?: TrailingSlashMode;
173
+ ppr?: true | Readonly<PartialPrerenderProps>;
174
+ } = {};
175
+ if (source.search !== undefined) {
176
+ options.search = serializeSearchSchema(route, source.search);
177
+ }
178
+ if (source.trailingSlash !== undefined) {
179
+ options.trailingSlash = source.trailingSlash;
180
+ }
181
+ if (source.ppr !== undefined) {
182
+ options.ppr = serializePpr(route, source.ppr);
183
+ }
184
+ return Object.freeze(options);
185
+ }
186
+
187
+ /**
188
+ * Serialize the `ppr` path option for the projection. The DSL already
189
+ * validated shape in the authoring module (validateClientPpr in
190
+ * client-urls.ts); this re-validates independently and re-copies
191
+ * defensively — the projection must carry only JSON, and the copy keeps a
192
+ * later mutation of the consumer's options object from leaking into the
193
+ * frozen projection.
194
+ */
195
+ function serializePpr(
196
+ route: ClientUrlRouteRecord,
197
+ source: unknown,
198
+ ): true | Readonly<PartialPrerenderProps> {
199
+ if (source === true) return true;
200
+ if (typeof source !== "object" || source === null || Array.isArray(source)) {
201
+ throw projectionError(
202
+ route,
203
+ 'path option "ppr" must be true or a PartialPrerenderProps object',
204
+ );
205
+ }
206
+ const out: {
207
+ ttl?: number;
208
+ swr?: number;
209
+ captureTimeout?: number;
210
+ maxSnapshotBytes?: number;
211
+ tags?: readonly string[];
212
+ } = {};
213
+ for (const key of Reflect.ownKeys(source)) {
214
+ if (PPR_NUMBER_KEYS.has(key)) {
215
+ const value = (source as Record<PropertyKey, unknown>)[key];
216
+ if (typeof value !== "number" || !Number.isFinite(value)) {
217
+ throw projectionError(
218
+ route,
219
+ `path option "ppr.${String(key)}" must be a finite number`,
220
+ );
221
+ }
222
+ out[key as "ttl" | "swr" | "captureTimeout" | "maxSnapshotBytes"] = value;
223
+ continue;
224
+ }
225
+ if (key === "tags") {
226
+ const tags = (source as { tags: unknown }).tags;
227
+ if (
228
+ !Array.isArray(tags) ||
229
+ tags.some((tag) => typeof tag !== "string" || tag === "")
230
+ ) {
231
+ throw projectionError(
232
+ route,
233
+ 'path option "ppr.tags" must be an array of non-empty strings',
234
+ );
235
+ }
236
+ out.tags = Object.freeze([...tags]);
237
+ continue;
238
+ }
239
+ throw projectionError(
240
+ route,
241
+ `path option "ppr.${String(key)}" is not supported`,
242
+ );
243
+ }
244
+ return Object.freeze(out) as Readonly<PartialPrerenderProps>;
245
+ }
246
+
247
+ function serializeTransition(
248
+ route: ClientUrlRouteRecord,
249
+ ): Readonly<ClientTransitionConfig> | undefined {
250
+ const source = route.transition;
251
+ if (!source) return undefined;
252
+ // The DSL already validated shape and rejected `when`; re-copy defensively
253
+ // so the projection is a plain JSON value (class maps included).
254
+ const config: Record<string, unknown> = {};
255
+ for (const [key, value] of Object.entries(source)) {
256
+ if (value === undefined) continue;
257
+ config[key] =
258
+ typeof value === "object" && value !== null
259
+ ? Object.freeze({ ...(value as Record<string, string>) })
260
+ : value;
261
+ }
262
+ return Object.freeze(config) as Readonly<ClientTransitionConfig>;
263
+ }
264
+
265
+ function serializeRoute(route: ClientUrlRouteRecord): ClientUrlProjectionRoute {
266
+ const loaderIds = route.loaders.map(({ loader }, index) => {
267
+ if (typeof loader.$$id !== "string" || loader.$$id.length === 0) {
268
+ throw projectionError(
269
+ route,
270
+ `loader at index ${index} is missing a non-empty $$id`,
271
+ );
272
+ }
273
+ return loader.$$id;
274
+ });
275
+
276
+ const awaitedLoaderIndices = route.loaders
277
+ .map(({ stream }, index) => (stream === "navigation" ? index : -1))
278
+ .filter((index) => index >= 0);
279
+
280
+ const transition = serializeTransition(route);
281
+ return Object.freeze({
282
+ id: route.id,
283
+ pattern: route.pattern,
284
+ name: route.name ?? null,
285
+ options: serializeOptions(route),
286
+ loaderIds: Object.freeze(loaderIds),
287
+ hasLoading: route.loading !== undefined,
288
+ ...(awaitedLoaderIndices.length > 0
289
+ ? { awaitedLoaderIndices: Object.freeze(awaitedLoaderIndices) }
290
+ : {}),
291
+ ...(transition ? { transition } : {}),
292
+ });
293
+ }
294
+
295
+ function serializeIntercept(
296
+ record: ClientUrlInterceptRecord,
297
+ index: number,
298
+ ): ClientUrlProjectionIntercept {
299
+ const loaderIds = record.loaders.map(({ loader }, loaderIndex) => {
300
+ if (typeof loader.$$id !== "string" || loader.$$id.length === 0) {
301
+ throw new Error(
302
+ `clientUrls() cannot create a server projection for intercept ".${record.targetName}" ` +
303
+ `(index ${index}): loader at index ${loaderIndex} is missing a non-empty $$id`,
304
+ );
305
+ }
306
+ return loader.$$id;
307
+ });
308
+
309
+ return Object.freeze({
310
+ slotName: record.slotName,
311
+ targetName: record.targetName,
312
+ loaderIds: Object.freeze(loaderIds),
313
+ hasLoading: record.loading !== undefined,
314
+ });
315
+ }
316
+
317
+ export function serializeClientUrlPatterns(
318
+ patterns: ClientUrlPatterns,
319
+ ): ClientUrlProjection {
320
+ return Object.freeze({
321
+ version: 1 as const,
322
+ routes: Object.freeze(patterns.routes.map(serializeRoute)),
323
+ intercepts: Object.freeze(
324
+ (patterns.intercepts ?? []).map(serializeIntercept),
325
+ ),
326
+ });
327
+ }
328
+
329
+ export function isClientUrlPatterns(
330
+ value: unknown,
331
+ ): value is ClientUrlPatterns {
332
+ return (
333
+ typeof value === "object" &&
334
+ value !== null &&
335
+ (value as { __brand?: unknown }).__brand === "client-urls" &&
336
+ Array.isArray((value as { routes?: unknown }).routes)
337
+ );
338
+ }
339
+
340
+ export function isClientUrlReference(
341
+ value: unknown,
342
+ ): value is ClientUrlReference {
343
+ if (
344
+ (typeof value !== "object" || value === null) &&
345
+ typeof value !== "function"
346
+ ) {
347
+ return false;
348
+ }
349
+
350
+ try {
351
+ const reference = value as Partial<ClientUrlReference>;
352
+ return (
353
+ reference.$$typeof === CLIENT_REFERENCE &&
354
+ typeof reference.$$id === "string" &&
355
+ reference.$$id.length > 0
356
+ );
357
+ } catch {
358
+ return false;
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Strip vite's HMR timestamp query from a module id. After an HMR update of
364
+ * a clientUrls module, the RSC graph re-imports it under
365
+ * `<path>?t=<timestamp>#default`, so the include's client reference carries a
366
+ * t-stamped $$id while the projection was registered under the bare id — the
367
+ * lookup missed and every request 500'd until a server restart. Only the
368
+ * `t` param is removed (other queries are part of module identity).
369
+ */
370
+ function stripHmrTimestamp(id: string): string {
371
+ return id.replace(/([?&])t=\d+(&)?/, (_match, lead: string, trail?: string) =>
372
+ trail ? lead : "",
373
+ );
374
+ }
375
+
376
+ function projectionKey(reference: string | ClientUrlReference): string {
377
+ if (typeof reference === "string" && reference.length > 0) {
378
+ return stripHmrTimestamp(reference);
379
+ }
380
+ if (isClientUrlReference(reference)) {
381
+ return stripHmrTimestamp(reference.$$id);
382
+ }
383
+ throw new Error(
384
+ "Client URL projections require a React client reference with a non-empty $$id",
385
+ );
386
+ }
387
+
388
+ export function setClientUrlProjection(
389
+ reference: string | ClientUrlReference,
390
+ projection: ClientUrlProjection,
391
+ ): void {
392
+ clientUrlProjections.set(projectionKey(reference), projection);
393
+ }
394
+
395
+ export function getClientUrlProjection(
396
+ reference: string | ClientUrlReference,
397
+ ): ClientUrlProjection | undefined {
398
+ return clientUrlProjections.get(projectionKey(reference));
399
+ }
400
+
401
+ export function clearClientUrlProjections(): void {
402
+ clientUrlProjections.clear();
403
+ }
404
+
405
+ /**
406
+ * Per-request memo for decoded client revalidation decisions: the synthesized
407
+ * predicate below runs once per loader stub per revalidation pass, and every
408
+ * stub reads the same header.
409
+ */
410
+ const decisionsByRequest = new WeakMap<
411
+ Request,
412
+ ClientRevalidationDecisions | null
413
+ >();
414
+
415
+ function clientDecisionsFor(
416
+ request: Request | undefined,
417
+ ): ClientRevalidationDecisions | null {
418
+ if (!request) return null;
419
+ if (decisionsByRequest.has(request)) {
420
+ return decisionsByRequest.get(request) ?? null;
421
+ }
422
+ const decoded = decodeClientRevalidationDecisions(
423
+ request.headers.get(CLIENT_REVALIDATION_HEADER),
424
+ );
425
+ decisionsByRequest.set(request, decoded);
426
+ return decoded;
427
+ }
428
+
429
+ /**
430
+ * The server end of client-run per-loader revalidation: clientUrls()
431
+ * revalidate() predicates EXECUTE in the browser (they are client-module
432
+ * code), and their decisions arrive on the request header. This synthesized
433
+ * predicate — attached to every materialized loader stub — honors a decision
434
+ * addressed to its loader id and otherwise returns the locked default, which
435
+ * is also the behavior for requests that carry no decisions (no-JS, PE,
436
+ * prefetch, document loads). Decisions can only address client-urls stubs;
437
+ * server-tree loaders never see this predicate.
438
+ */
439
+ function makeClientDecisionRevalidate(
440
+ loaderId: string,
441
+ ): (args: {
442
+ defaultShouldRevalidate: boolean;
443
+ context: { request: Request };
444
+ }) => boolean {
445
+ return ({ defaultShouldRevalidate, context }) => {
446
+ const decisions = clientDecisionsFor(context?.request);
447
+ if (decisions) {
448
+ if (decisions.skip.includes(loaderId)) return false;
449
+ if (decisions.force.includes(loaderId)) return true;
450
+ }
451
+ return defaultShouldRevalidate;
452
+ };
453
+ }
454
+
455
+ function createLoaderStub(id: string): LoaderDefinition<unknown> {
456
+ return {
457
+ __brand: "loader",
458
+ $$id: id,
459
+ fn: async (ctx) => {
460
+ const registeredLoader = await getLoaderLazy(id);
461
+ if (!registeredLoader) {
462
+ throw new Error(
463
+ `Projected loader "${id}" was not found in the server loader registry`,
464
+ );
465
+ }
466
+ return registeredLoader.fn(ctx);
467
+ },
468
+ };
469
+ }
470
+
471
+ function materializedPathOptions(route: ClientUrlProjectionRoute): PathOptions {
472
+ return {
473
+ ...(route.name === null ? {} : { name: route.name }),
474
+ ...(route.options.search ? { search: { ...route.options.search } } : {}),
475
+ ...(route.options.trailingSlash
476
+ ? { trailingSlash: route.options.trailingSlash }
477
+ : {}),
478
+ // ppr rides onto the materialized server path() verbatim: path-helper
479
+ // stamps it on the manifest entry, so resolvePprConfig classifies the
480
+ // group route exactly like a hand-written ppr page. Shallow-copy the
481
+ // object form — path-helper receives a mutable PathOptions.
482
+ ...(route.options.ppr !== undefined
483
+ ? {
484
+ ppr: route.options.ppr === true ? true : { ...route.options.ppr },
485
+ }
486
+ : {}),
487
+ };
488
+ }
489
+
490
+ function materializeRouteItems(
491
+ reference: ClientUrlDefinitionSource,
492
+ projection: ClientUrlProjection,
493
+ {
494
+ path,
495
+ layout,
496
+ loader,
497
+ loading,
498
+ intercept,
499
+ transition,
500
+ revalidate,
501
+ }: PathHelpers<any>,
502
+ ): AllUseItems[] {
503
+ // The urls() builder runs under the include's prefixes, so the composed
504
+ // route-name prefix is available here. ClientUrlsRoot needs it to compose
505
+ // canonical names for intercept-target coordination in the browser.
506
+ const namePrefix = getNamePrefix();
507
+
508
+ // Helper calls attach to the CURRENT ctx.parent as they execute, so these
509
+ // builders must run where the items belong: at the module top level for the
510
+ // flat (no-intercept) shape, or inside the group layout's children callback
511
+ // when intercepts wrap the group.
512
+ const buildRouteItems = (): AllUseItems[] =>
513
+ projection.routes.map(
514
+ (route) =>
515
+ path(
516
+ route.pattern,
517
+ () =>
518
+ React.createElement(ClientUrlsRoot, {
519
+ definition: reference as ClientUrlPatterns,
520
+ routeId: route.id,
521
+ namePrefix,
522
+ }),
523
+ materializedPathOptions(route),
524
+ () => [
525
+ ...route.loaderIds.map((id, loaderIndex) =>
526
+ loader(
527
+ createLoaderStub(id),
528
+ route.awaitedLoaderIndices?.includes(loaderIndex)
529
+ ? { stream: "navigation" }
530
+ : undefined,
531
+ () => [revalidate(makeClientDecisionRevalidate(id))],
532
+ ),
533
+ ),
534
+ ...(route.hasLoading
535
+ ? [
536
+ loading(
537
+ React.createElement(ClientUrlsLoading, {
538
+ definition: reference as ClientUrlPatterns,
539
+ routeId: route.id,
540
+ }),
541
+ ),
542
+ ]
543
+ : []),
544
+ // Data-only per-route transition config: same child position as a
545
+ // hand-written server transition(config) — no `when`.
546
+ ...(route.transition ? [transition({ ...route.transition })] : []),
547
+ ],
548
+ ) as AllUseItems,
549
+ );
550
+
551
+ const intercepts = projection.intercepts ?? [];
552
+ if (intercepts.length === 0) return buildRouteItems();
553
+
554
+ // Two server-side mechanics make client-declared intercepts work:
555
+ //
556
+ // 1. GROUP LAYOUT ATTACHMENT. findInterceptForRoute walks the TARGET
557
+ // route's parent chain, so the intercept must live on an entry in that
558
+ // chain that survives evaluation. Emitting intercepts at the module top
559
+ // level attaches them to the lazy expansion's ISOLATED parent clone
560
+ // (getIsolatedLazyParent in server/context.ts), which is discarded.
561
+ // Wrapping the group in one materialized layout gives them a persistent
562
+ // home in the target chain — and the layout renders each declared slot,
563
+ // so the modal presents inside the group's own subtree (the slot segment
564
+ // attaches to the declaring entry; an outer layout's same-named outlet
565
+ // would never receive it).
566
+ //
567
+ // 2. SYNTHESIZED ORIGIN `when`. The target-chain walk is origin-blind: a
568
+ // bare intercept would claim navigations from ANY origin — and for an
569
+ // out-of-group origin the slot's host layout is not in the rendered
570
+ // tree, so the modal would silently vanish. The declaration contract is
571
+ // module-local, so materialization synthesizes the origin check the DSL
572
+ // deliberately does not expose: activate only when the origin pathname
573
+ // is inside this include's mount. This is server-generated code — no
574
+ // function crosses the "use client" projection boundary.
575
+ const mount = getUrlPrefix() || "/";
576
+ const isInGroupOrigin = ({ from }: InterceptSelectorContext): boolean =>
577
+ mount === "/"
578
+ ? true
579
+ : from.pathname === mount || from.pathname.startsWith(`${mount}/`);
580
+ const slotNames = [...new Set(intercepts.map((record) => record.slotName))];
581
+ const buildInterceptItems = (): AllUseItems[] =>
582
+ intercepts.map(
583
+ (record, interceptIndex) =>
584
+ intercept(
585
+ record.slotName,
586
+ `.${record.targetName}`,
587
+ () =>
588
+ React.createElement(ClientUrlsInterceptSlot, {
589
+ definition: reference as ClientUrlPatterns,
590
+ interceptIndex,
591
+ }),
592
+ { when: isInGroupOrigin },
593
+ () => [
594
+ ...record.loaderIds.map((id) => loader(createLoaderStub(id))),
595
+ ...(record.hasLoading
596
+ ? [
597
+ loading(
598
+ React.createElement(ClientUrlsInterceptLoading, {
599
+ definition: reference as ClientUrlPatterns,
600
+ interceptIndex,
601
+ }),
602
+ ),
603
+ ]
604
+ : []),
605
+ ],
606
+ ) as AllUseItems,
607
+ );
608
+
609
+ function ClientUrlsGroup(): React.ReactNode {
610
+ return React.createElement(ClientUrlsGroupLayout, { slotNames });
611
+ }
612
+
613
+ return [
614
+ layout(ClientUrlsGroup, () => [
615
+ ...buildRouteItems(),
616
+ ...buildInterceptItems(),
617
+ ]) as AllUseItems,
618
+ ];
619
+ }
620
+
621
+ function assertClientUrlSource(
622
+ value: unknown,
623
+ caller: string,
624
+ ): asserts value is ClientUrlDefinitionSource {
625
+ if (!isClientUrlReference(value) && !isClientUrlPatterns(value)) {
626
+ throw new Error(
627
+ `${caller} expects clientUrls() patterns or a React client reference with a non-empty $$id`,
628
+ );
629
+ }
630
+ }
631
+
632
+ export function materializeClientUrlPatterns(
633
+ reference: ClientUrlDefinitionSource,
634
+ projection: ClientUrlProjection,
635
+ ): UrlPatterns {
636
+ assertClientUrlSource(reference, "materializeClientUrlPatterns()");
637
+ return urls((helpers) =>
638
+ materializeRouteItems(reference, projection, helpers),
639
+ );
640
+ }
641
+
642
+ /**
643
+ * Adapt a clientUrls() source for `include()` mounting inside the canonical
644
+ * urls() tree. Materialization is DEFERRED into the returned handler: it
645
+ * resolves the discovery-installed projection at evaluation time (lazy include
646
+ * expansion, build manifest generation, dev per-request trie rebuild) — by
647
+ * then the routes-manifest bootstrap has installed projections, so registration
648
+ * order never matters. The include machinery applies the URL and route-name
649
+ * prefixes exactly as it does for any urls() module; surrounding RSC layouts,
650
+ * middleware scoping, and boundaries derive from the canonical server tree.
651
+ */
652
+ export function clientUrlIncludePatterns(
653
+ source: ClientUrlDefinitionSource,
654
+ ): UrlPatterns {
655
+ assertClientUrlSource(source, "include() with a clientUrls() definition");
656
+ return urls((helpers) => {
657
+ const projection = isClientUrlPatterns(source)
658
+ ? serializeClientUrlPatterns(source)
659
+ : getClientUrlProjection(source);
660
+ if (!projection) {
661
+ const id = isClientUrlReference(source) ? source.$$id : "(inline)";
662
+ throw new Error(
663
+ `include() could not resolve the server projection for clientUrls() module "${id}". ` +
664
+ 'The definition must be the default export of a "use client" module so the ' +
665
+ "Vite discovery pass can project it; in unit tests pass the clientUrls() object directly.",
666
+ );
667
+ }
668
+ return materializeRouteItems(source, projection, helpers);
669
+ });
670
+ }