@rangojs/router 0.0.0-experimental.115 → 0.0.0-experimental.116
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.
- package/dist/vite/index.js +148 -97
- package/package.json +17 -18
- package/skills/api-client/SKILL.md +211 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/rango/SKILL.md +1 -0
- package/skills/response-routes/SKILL.md +61 -43
- package/skills/typesafety/SKILL.md +3 -3
- package/src/__augment-tests__/augmented.check.ts +2 -3
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +28 -1
- package/src/build/index.ts +8 -1
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +43 -0
- package/src/client.tsx +4 -23
- package/src/errors.ts +0 -3
- package/src/href-client.ts +7 -8
- package/src/index.rsc.ts +1 -2
- package/src/index.ts +1 -2
- package/src/router/find-match.ts +54 -6
- package/src/router/lazy-includes.ts +33 -14
- package/src/router/manifest.ts +19 -6
- package/src/router/pattern-matching.ts +15 -2
- package/src/router/router-interfaces.ts +11 -0
- package/src/router/trie-matching.ts +22 -3
- package/src/router.ts +21 -7
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +16 -13
- package/src/urls/index.ts +1 -2
- package/src/urls/type-extraction.ts +33 -24
- package/src/vite/discovery/discover-routers.ts +46 -29
- package/src/vite/discovery/state.ts +7 -0
- package/src/vite/plugins/client-ref-hashing.ts +12 -1
- package/src/vite/rango.ts +32 -4
- package/src/vite/utils/client-chunks.ts +41 -7
- package/src/vite/utils/manifest-utils.ts +8 -75
- package/src/vite/utils/shared-utils.ts +58 -0
|
@@ -1,37 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Problem Details (RFC 9457) Builder
|
|
3
3
|
*
|
|
4
|
-
* Builds a
|
|
5
|
-
*
|
|
4
|
+
* Builds a problem+json error body from a caught error, controlling what
|
|
5
|
+
* information is exposed based on error type and environment.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { RouterError } from "../errors.js";
|
|
9
|
-
import type {
|
|
9
|
+
import type { ProblemDetails } from "../urls.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* HTTP reason phrases for the problem `title` member. Inlined because the
|
|
13
|
+
* router targets edge/worker runtimes without node's `http.STATUS_CODES`;
|
|
14
|
+
* covers the full standard 4xx/5xx range, with a generic fallback for any
|
|
15
|
+
* non-standard status a handler might set.
|
|
16
|
+
*/
|
|
17
|
+
const STATUS_PHRASES: Record<number, string> = {
|
|
18
|
+
400: "Bad Request",
|
|
19
|
+
401: "Unauthorized",
|
|
20
|
+
402: "Payment Required",
|
|
21
|
+
403: "Forbidden",
|
|
22
|
+
404: "Not Found",
|
|
23
|
+
405: "Method Not Allowed",
|
|
24
|
+
406: "Not Acceptable",
|
|
25
|
+
407: "Proxy Authentication Required",
|
|
26
|
+
408: "Request Timeout",
|
|
27
|
+
409: "Conflict",
|
|
28
|
+
410: "Gone",
|
|
29
|
+
411: "Length Required",
|
|
30
|
+
412: "Precondition Failed",
|
|
31
|
+
413: "Payload Too Large",
|
|
32
|
+
414: "URI Too Long",
|
|
33
|
+
415: "Unsupported Media Type",
|
|
34
|
+
416: "Range Not Satisfiable",
|
|
35
|
+
417: "Expectation Failed",
|
|
36
|
+
418: "I'm a Teapot",
|
|
37
|
+
421: "Misdirected Request",
|
|
38
|
+
422: "Unprocessable Entity",
|
|
39
|
+
423: "Locked",
|
|
40
|
+
424: "Failed Dependency",
|
|
41
|
+
425: "Too Early",
|
|
42
|
+
426: "Upgrade Required",
|
|
43
|
+
428: "Precondition Required",
|
|
44
|
+
429: "Too Many Requests",
|
|
45
|
+
431: "Request Header Fields Too Large",
|
|
46
|
+
451: "Unavailable For Legal Reasons",
|
|
47
|
+
500: "Internal Server Error",
|
|
48
|
+
501: "Not Implemented",
|
|
49
|
+
502: "Bad Gateway",
|
|
50
|
+
503: "Service Unavailable",
|
|
51
|
+
504: "Gateway Timeout",
|
|
52
|
+
505: "HTTP Version Not Supported",
|
|
53
|
+
506: "Variant Also Negotiates",
|
|
54
|
+
507: "Insufficient Storage",
|
|
55
|
+
508: "Loop Detected",
|
|
56
|
+
510: "Not Extended",
|
|
57
|
+
511: "Network Authentication Required",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function statusPhrase(status: number): string {
|
|
61
|
+
return STATUS_PHRASES[status] ?? "Error";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build an RFC 9457 problem+json body from a caught error.
|
|
66
|
+
* RouterError messages/codes are always exposed (developer-crafted).
|
|
14
67
|
* Standard Error messages are hidden in production.
|
|
68
|
+
*
|
|
69
|
+
* The `type` member is omitted in this phase: per RFC 9457 an absent `type` is
|
|
70
|
+
* treated as `"about:blank"` (no semantics beyond the HTTP status), so emitting
|
|
71
|
+
* it adds nothing. Per-route problem-type URIs arrive with the declared-errors
|
|
72
|
+
* map later. `code` is always present so consumers can branch on it
|
|
73
|
+
* (`"INTERNAL"` for non-RouterError failures).
|
|
15
74
|
*/
|
|
16
|
-
export function
|
|
75
|
+
export function createProblemDetails(
|
|
17
76
|
error: unknown,
|
|
77
|
+
status: number,
|
|
18
78
|
isDev: boolean,
|
|
19
|
-
):
|
|
79
|
+
): ProblemDetails {
|
|
20
80
|
if (error instanceof RouterError) {
|
|
21
81
|
return {
|
|
22
|
-
|
|
82
|
+
title: statusPhrase(status),
|
|
83
|
+
status,
|
|
84
|
+
detail: error.message,
|
|
23
85
|
code: error.code,
|
|
24
|
-
...(error.type ? { type: error.type } : {}),
|
|
25
86
|
...(isDev && error.stack ? { stack: error.stack } : {}),
|
|
26
87
|
};
|
|
27
88
|
}
|
|
28
89
|
if (error instanceof Error) {
|
|
29
90
|
return {
|
|
30
|
-
|
|
91
|
+
title: statusPhrase(status),
|
|
92
|
+
status,
|
|
93
|
+
detail: isDev ? error.message : "Internal Server Error",
|
|
94
|
+
code: "INTERNAL",
|
|
31
95
|
...(isDev && error.stack ? { stack: error.stack } : {}),
|
|
32
96
|
};
|
|
33
97
|
}
|
|
34
98
|
return {
|
|
35
|
-
|
|
99
|
+
title: statusPhrase(status),
|
|
100
|
+
status,
|
|
101
|
+
detail: isDev ? String(error) : "Internal Server Error",
|
|
102
|
+
code: "INTERNAL",
|
|
36
103
|
};
|
|
37
104
|
}
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
22
22
|
import type { EntryData } from "../server/context.js";
|
|
23
23
|
import type { HandlerContext } from "./handler-context.js";
|
|
24
|
-
import {
|
|
24
|
+
import { createProblemDetails } from "./response-error.js";
|
|
25
25
|
import {
|
|
26
26
|
createResponseWithMergedHeaders,
|
|
27
27
|
finalizeResponse,
|
|
@@ -131,13 +131,10 @@ export async function handleResponseRoute<TEnv>(
|
|
|
131
131
|
|
|
132
132
|
// Handled before the MIME lookup (json is also a RESPONSE_TYPE_MIME key).
|
|
133
133
|
if (preview.responseType === "json") {
|
|
134
|
-
return createResponseWithMergedHeaders(
|
|
135
|
-
|
|
136
|
-
{
|
|
137
|
-
|
|
138
|
-
headers: { "content-type": "application/json;charset=utf-8" },
|
|
139
|
-
},
|
|
140
|
-
);
|
|
134
|
+
return createResponseWithMergedHeaders(JSON.stringify(result), {
|
|
135
|
+
status: 200,
|
|
136
|
+
headers: { "content-type": "application/json;charset=utf-8" },
|
|
137
|
+
});
|
|
141
138
|
}
|
|
142
139
|
|
|
143
140
|
// Object.hasOwn (not truthiness) so prototype names like "toString" are not
|
|
@@ -157,16 +154,22 @@ export async function handleResponseRoute<TEnv>(
|
|
|
157
154
|
} catch (error) {
|
|
158
155
|
handlerCtx.callOnError(error, "handler", errorCtx);
|
|
159
156
|
const isDev = process.env.NODE_ENV !== "production";
|
|
160
|
-
const
|
|
157
|
+
const derivedStatus = error instanceof RouterError ? error.status : 500;
|
|
158
|
+
// Resolve the effective status the same way createResponseWithMergedHeaders
|
|
159
|
+
// will (ctx.res.status override) so the problem body's status/title match
|
|
160
|
+
// the actual HTTP status — e.g. when a handler called ctx.setStatus()
|
|
161
|
+
// before throwing.
|
|
162
|
+
const status =
|
|
163
|
+
reqCtx.res.status !== 200 ? reqCtx.res.status : derivedStatus;
|
|
161
164
|
|
|
162
165
|
if (preview.responseType === "json") {
|
|
163
166
|
return createResponseWithMergedHeaders(
|
|
164
|
-
JSON.stringify(
|
|
165
|
-
error: createResponseErrorPayload(error, isDev),
|
|
166
|
-
}),
|
|
167
|
+
JSON.stringify(createProblemDetails(error, status, isDev)),
|
|
167
168
|
{
|
|
168
169
|
status,
|
|
169
|
-
headers: {
|
|
170
|
+
headers: {
|
|
171
|
+
"content-type": "application/problem+json;charset=utf-8",
|
|
172
|
+
},
|
|
170
173
|
},
|
|
171
174
|
);
|
|
172
175
|
}
|
package/src/urls/index.ts
CHANGED
|
@@ -220,44 +220,53 @@ export type ExtractResponses<T extends readonly any[]> =
|
|
|
220
220
|
ExtractResponsesFromItems<T>;
|
|
221
221
|
|
|
222
222
|
// ============================================================================
|
|
223
|
-
// Response
|
|
223
|
+
// Response Error (RFC 9457 problem+json) Type
|
|
224
224
|
// ============================================================================
|
|
225
225
|
|
|
226
226
|
/**
|
|
227
|
-
*
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
message: string;
|
|
231
|
-
code?: string;
|
|
232
|
-
type?: string;
|
|
233
|
-
stack?: string;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Discriminated union envelope for JSON response routes.
|
|
238
|
-
* Consumers check `result.error` to discriminate between success and failure.
|
|
227
|
+
* RFC 9457 (problem+json) error body returned by JSON response routes on a
|
|
228
|
+
* non-2xx status. Sent verbatim as the response body (not wrapped) with
|
|
229
|
+
* content-type `application/problem+json`.
|
|
239
230
|
*
|
|
240
231
|
* @example
|
|
241
232
|
* ```typescript
|
|
242
|
-
* const
|
|
243
|
-
* if (
|
|
244
|
-
*
|
|
233
|
+
* const res = await fetch(url);
|
|
234
|
+
* if (!res.ok) {
|
|
235
|
+
* const problem: ProblemDetails = await res.json();
|
|
236
|
+
* console.log(problem.code, problem.detail); // "NOT_FOUND", "Product not found"
|
|
245
237
|
* return;
|
|
246
238
|
* }
|
|
247
|
-
*
|
|
239
|
+
* const product = await res.json(); // bare value, no envelope
|
|
248
240
|
* ```
|
|
249
241
|
*/
|
|
250
|
-
export
|
|
251
|
-
|
|
252
|
-
|
|
242
|
+
export interface ProblemDetails {
|
|
243
|
+
/**
|
|
244
|
+
* URI reference identifying the problem type. Omitted in this phase (per RFC
|
|
245
|
+
* 9457 an absent `type` is treated as `"about:blank"` — no semantics beyond
|
|
246
|
+
* the HTTP status); per-route problem-type URIs arrive with the
|
|
247
|
+
* declared-errors map later.
|
|
248
|
+
*/
|
|
249
|
+
type?: string;
|
|
250
|
+
/** Short, human-readable summary (the HTTP status reason phrase). */
|
|
251
|
+
title: string;
|
|
252
|
+
/** The HTTP status code. */
|
|
253
|
+
status: number;
|
|
254
|
+
/** Human-readable explanation specific to this occurrence (the error message). */
|
|
255
|
+
detail: string;
|
|
256
|
+
/** Stable machine-readable error code (`RouterError.code`, else `"INTERNAL"`). */
|
|
257
|
+
code: string;
|
|
258
|
+
/** Stack trace, included in development only. */
|
|
259
|
+
stack?: string;
|
|
260
|
+
}
|
|
253
261
|
|
|
254
262
|
// ============================================================================
|
|
255
263
|
// Response Type Consumer Utilities
|
|
256
264
|
// ============================================================================
|
|
257
265
|
|
|
258
266
|
/**
|
|
259
|
-
* Extract the response
|
|
260
|
-
*
|
|
267
|
+
* Extract the JSON response payload type for a named route from a UrlPatterns
|
|
268
|
+
* instance. JSON response routes send the handler's return value verbatim
|
|
269
|
+
* (bare), so this resolves to the wire value a consumer receives — no envelope.
|
|
261
270
|
*
|
|
262
271
|
* @example
|
|
263
272
|
* ```typescript
|
|
@@ -266,7 +275,7 @@ export type ResponseEnvelope<T> =
|
|
|
266
275
|
* ]);
|
|
267
276
|
*
|
|
268
277
|
* type HealthData = RouteResponse<typeof apiPatterns, "health">;
|
|
269
|
-
* //
|
|
278
|
+
* // { status: string; timestamp: number }
|
|
270
279
|
* ```
|
|
271
280
|
*
|
|
272
281
|
* The payload is the JSON wire shape (via `Rango.JsonSerialize`), matching
|
|
@@ -277,6 +286,6 @@ export type RouteResponse<TPatterns, TName extends string> = TPatterns extends {
|
|
|
277
286
|
readonly _responses?: infer R;
|
|
278
287
|
}
|
|
279
288
|
? TName extends keyof R
|
|
280
|
-
?
|
|
289
|
+
? JsonSerialize<Exclude<R[TName], Response>>
|
|
281
290
|
: never
|
|
282
291
|
: never;
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type CaughtDiscoveryError,
|
|
27
27
|
} from "./discovery-errors.js";
|
|
28
28
|
import { createRangoDebugger, timed, NS } from "../debug.js";
|
|
29
|
+
import { computeProductionHash } from "../plugins/client-ref-hashing.js";
|
|
29
30
|
|
|
30
31
|
const debug = createRangoDebugger(NS.discovery);
|
|
31
32
|
|
|
@@ -143,6 +144,28 @@ export async function discoverRouters(
|
|
|
143
144
|
// Collect all manifests for trie building (avoid re-running generateManifest)
|
|
144
145
|
const allManifests: Array<{ id: string; manifest: any }> = [];
|
|
145
146
|
|
|
147
|
+
// Built-in clientChunks context (present only when the built-in strategy is
|
|
148
|
+
// active). Collect the production hashes of "use client" error/notFound
|
|
149
|
+
// fallback modules so the strategy can route them into app-fallback.
|
|
150
|
+
const clientChunkCtx = state.opts?.clientChunkCtx;
|
|
151
|
+
const collectClientFallbackRef = clientChunkCtx
|
|
152
|
+
? (refKey: string) =>
|
|
153
|
+
clientChunkCtx.fallbackRefs.add(
|
|
154
|
+
computeProductionHash(state.projectRoot, refKey),
|
|
155
|
+
)
|
|
156
|
+
: undefined;
|
|
157
|
+
// Router-level boundary defaults (`createRouter({ defaultErrorBoundary, ... })`)
|
|
158
|
+
// are NOT in EntryData, so generateManifestFull's walk misses them. Collect any
|
|
159
|
+
// "use client" default boundary directly off the router instance. The value is
|
|
160
|
+
// commonly a handler function wrapping the client boundary in server providers,
|
|
161
|
+
// so collectFallbackClientRefs invokes + walks the tree. Routed through buildMod
|
|
162
|
+
// so it runs in the same RSC runner realm the boundary value came from.
|
|
163
|
+
const collectFromBoundaryNode = (node: unknown): void => {
|
|
164
|
+
if (collectClientFallbackRef && buildMod.collectFallbackClientRefs) {
|
|
165
|
+
buildMod.collectFallbackClientRefs(node, collectClientFallbackRef);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
146
169
|
const manifestGenStart = debug ? performance.now() : 0;
|
|
147
170
|
for (const [id, router] of registry) {
|
|
148
171
|
if (!router.urlpatterns || !generateManifestFull) {
|
|
@@ -152,10 +175,23 @@ export async function discoverRouters(
|
|
|
152
175
|
const manifest = generateManifestFull(
|
|
153
176
|
router.urlpatterns,
|
|
154
177
|
routerMountIndex,
|
|
155
|
-
|
|
178
|
+
{
|
|
179
|
+
...(router.__basename ? { urlPrefix: router.__basename } : {}),
|
|
180
|
+
...(collectClientFallbackRef ? { collectClientFallbackRef } : {}),
|
|
181
|
+
},
|
|
156
182
|
);
|
|
157
183
|
routerMountIndex++;
|
|
158
184
|
allManifests.push({ id, manifest });
|
|
185
|
+
|
|
186
|
+
// Router-level "use client" boundary defaults -> app-fallback (the
|
|
187
|
+
// route-tree errorBoundary()/notFoundBoundary() helpers are already
|
|
188
|
+
// collected inside generateManifestFull via collectClientFallbackRef).
|
|
189
|
+
if (collectClientFallbackRef) {
|
|
190
|
+
collectFromBoundaryNode(router.__defaultErrorBoundary);
|
|
191
|
+
collectFromBoundaryNode(router.__defaultNotFoundBoundary);
|
|
192
|
+
collectFromBoundaryNode(router.__notFound);
|
|
193
|
+
}
|
|
194
|
+
|
|
159
195
|
const routeCount = Object.keys(manifest.routeManifest).length;
|
|
160
196
|
const staticRoutes = Object.values(manifest.routeManifest).filter(
|
|
161
197
|
(p: any) => !p.includes(":") && !p.includes("*"),
|
|
@@ -304,36 +340,17 @@ export async function discoverRouters(
|
|
|
304
340
|
mergedResponseTypeRoutes,
|
|
305
341
|
);
|
|
306
342
|
|
|
307
|
-
// Build per-router tries for multi-router isolation.
|
|
343
|
+
// Build per-router tries for multi-router isolation. Uses the single
|
|
344
|
+
// shared buildPerRouterTrie so the production serialized trie is built by
|
|
345
|
+
// exactly the same code as the dev/HMR runtime rebuild (manifest-init.ts).
|
|
346
|
+
const buildPerRouterTrie = buildMod.buildPerRouterTrie;
|
|
308
347
|
for (const { id, manifest } of allManifests) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
const perRouterStaticPrefix: Record<string, string> = {};
|
|
315
|
-
for (const name of Object.keys(manifest.routeManifest)) {
|
|
316
|
-
perRouterStaticPrefix[name] = "";
|
|
348
|
+
const perRouterTrie = buildPerRouterTrie
|
|
349
|
+
? buildPerRouterTrie(manifest)
|
|
350
|
+
: null;
|
|
351
|
+
if (perRouterTrie) {
|
|
352
|
+
newPerRouterTrieMap.set(id, perRouterTrie);
|
|
317
353
|
}
|
|
318
|
-
buildRouteToStaticPrefix(manifest.prefixTree, perRouterStaticPrefix);
|
|
319
|
-
|
|
320
|
-
const perRouterPrerenderNames = manifest.prerenderRoutes
|
|
321
|
-
? new Set<string>(manifest.prerenderRoutes)
|
|
322
|
-
: undefined;
|
|
323
|
-
const perRouterPassthroughNames = manifest.passthroughRoutes
|
|
324
|
-
? new Set<string>(manifest.passthroughRoutes)
|
|
325
|
-
: undefined;
|
|
326
|
-
|
|
327
|
-
const perRouterTrie = buildRouteTrie(
|
|
328
|
-
manifest.routeManifest,
|
|
329
|
-
manifest._routeAncestry,
|
|
330
|
-
perRouterStaticPrefix,
|
|
331
|
-
manifest.routeTrailingSlash,
|
|
332
|
-
perRouterPrerenderNames,
|
|
333
|
-
perRouterPassthroughNames,
|
|
334
|
-
manifest.responseTypeRoutes,
|
|
335
|
-
);
|
|
336
|
-
newPerRouterTrieMap.set(id, perRouterTrie);
|
|
337
354
|
}
|
|
338
355
|
}
|
|
339
356
|
}
|
|
@@ -20,6 +20,13 @@ export interface PluginOptions {
|
|
|
20
20
|
buildEnv?: import("../plugin-types.js").BuildEnvOption;
|
|
21
21
|
/** Deployment preset (needed for buildEnv "auto" resolution). */
|
|
22
22
|
preset?: "node" | "cloudflare";
|
|
23
|
+
/**
|
|
24
|
+
* Shared context the built-in clientChunks strategy reads. Discovery populates
|
|
25
|
+
* it (registered fallback hashes + single-router name) before the client build
|
|
26
|
+
* invokes the strategy. Present only when the built-in strategy is active
|
|
27
|
+
* (`clientChunks: true`/default); undefined for `false` or a custom function.
|
|
28
|
+
*/
|
|
29
|
+
clientChunkCtx?: import("../utils/client-chunks.js").ClientChunkContext;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
export interface PrecomputedEntry {
|
|
@@ -22,6 +22,17 @@ const FS_PREFIX = "/@fs/";
|
|
|
22
22
|
* Returns the input unchanged if it doesn't match a known dev-mode pattern
|
|
23
23
|
* (e.g., already a production hash).
|
|
24
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* The production client-reference key hash: `sha256(relativeId).slice(0,12)`,
|
|
27
|
+
* matching @vitejs/plugin-rsc's `hashString`. Exported so the client-chunks
|
|
28
|
+
* strategy can hash a `clientChunks` callback's `meta.normalizedId` (already the
|
|
29
|
+
* project-root-relative id) and compare it against fallback hashes collected
|
|
30
|
+
* during discovery.
|
|
31
|
+
*/
|
|
32
|
+
export function hashRefKey(relativeId: string): string {
|
|
33
|
+
return createHash("sha256").update(relativeId).digest("hex").slice(0, 12);
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
export function computeProductionHash(
|
|
26
37
|
projectRoot: string,
|
|
27
38
|
refKey: string,
|
|
@@ -49,7 +60,7 @@ export function computeProductionHash(
|
|
|
49
60
|
return refKey;
|
|
50
61
|
}
|
|
51
62
|
|
|
52
|
-
return
|
|
63
|
+
return hashRefKey(toHash);
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
// Regex to match registerClientReference() calls as emitted by @vitejs/plugin-rsc.
|
package/src/vite/rango.ts
CHANGED
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
onwarn,
|
|
24
24
|
getManualChunks,
|
|
25
25
|
} from "./utils/shared-utils.js";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
resolveClientChunks,
|
|
28
|
+
type ClientChunkContext,
|
|
29
|
+
} from "./utils/client-chunks.js";
|
|
27
30
|
import type { RangoOptions } from "./plugin-types.js";
|
|
28
31
|
import { printBanner, rangoVersion } from "./utils/banner.js";
|
|
29
32
|
import { createVersionInjectorPlugin } from "./plugins/version-injector.js";
|
|
@@ -69,9 +72,17 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
69
72
|
// @vitejs/plugin-rsc in both presets. The built-in strategy only splits where it
|
|
70
73
|
// recognizes a route structure, so this default is a no-op for flat / host-split
|
|
71
74
|
// apps and never duplicates the shared runtime.
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
+
const clientChunksOption = resolvedOptions.clientChunks ?? true;
|
|
76
|
+
// Shared context the built-in strategy reads at build time: the production
|
|
77
|
+
// hashes of registered error/notFound fallback modules (-> app-fallback).
|
|
78
|
+
// Populated by the discovery plugin in buildStart, before the client build
|
|
79
|
+
// invokes the strategy. Only wired when the built-in strategy is active; a
|
|
80
|
+
// custom function owns its own grouping.
|
|
81
|
+
const useBuiltInClientChunks = clientChunksOption === true;
|
|
82
|
+
const clientChunkCtx: ClientChunkContext | undefined = useBuiltInClientChunks
|
|
83
|
+
? { fallbackRefs: new Set<string>() }
|
|
84
|
+
: undefined;
|
|
85
|
+
const clientChunks = resolveClientChunks(clientChunksOption, clientChunkCtx);
|
|
75
86
|
debugConfig?.("rango(%s) setup start", preset);
|
|
76
87
|
|
|
77
88
|
const plugins: PluginOption[] = [];
|
|
@@ -157,6 +168,14 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
157
168
|
client: {
|
|
158
169
|
build: {
|
|
159
170
|
rollupOptions: {
|
|
171
|
+
// FILE_NAME_CONFLICT (and any other client-build warning) is
|
|
172
|
+
// emitted by the CLIENT environment build, which consults THIS
|
|
173
|
+
// env's onwarn -- Vite 8's environment builds do NOT propagate
|
|
174
|
+
// the top-level build.rollupOptions.onwarn into the client env.
|
|
175
|
+
// Wire it here so the suppression runs where the conflicts
|
|
176
|
+
// originate (the top-level handler is invoked 0x for these; the
|
|
177
|
+
// client-env handler is invoked for all of them).
|
|
178
|
+
onwarn,
|
|
160
179
|
output: {
|
|
161
180
|
manualChunks: getManualChunks,
|
|
162
181
|
},
|
|
@@ -317,6 +336,14 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
317
336
|
client: {
|
|
318
337
|
build: {
|
|
319
338
|
rollupOptions: {
|
|
339
|
+
// FILE_NAME_CONFLICT (and any other client-build warning) is
|
|
340
|
+
// emitted by the CLIENT environment build, which consults THIS
|
|
341
|
+
// env's onwarn -- Vite 8's environment builds do NOT propagate
|
|
342
|
+
// the top-level build.rollupOptions.onwarn into the client env.
|
|
343
|
+
// Wire it here so the suppression runs where the conflicts
|
|
344
|
+
// originate (the top-level handler is invoked 0x for these; the
|
|
345
|
+
// client-env handler is invoked for all of them).
|
|
346
|
+
onwarn,
|
|
320
347
|
output: {
|
|
321
348
|
manualChunks: getManualChunks,
|
|
322
349
|
},
|
|
@@ -508,6 +535,7 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
508
535
|
enableBuildPrerender: prerenderEnabled,
|
|
509
536
|
buildEnv: options?.buildEnv,
|
|
510
537
|
preset,
|
|
538
|
+
clientChunkCtx,
|
|
511
539
|
}),
|
|
512
540
|
);
|
|
513
541
|
|
|
@@ -6,10 +6,28 @@
|
|
|
6
6
|
|
|
7
7
|
import type { ClientChunkMeta, ClientChunks } from "../plugin-types.js";
|
|
8
8
|
import { createRangoDebugger, NS } from "../debug.js";
|
|
9
|
+
import { hashRefKey } from "../plugins/client-ref-hashing.js";
|
|
9
10
|
|
|
10
11
|
/** The callback shape @vitejs/plugin-rsc's `clientChunks` option accepts. */
|
|
11
12
|
export type RscClientChunksFn = (meta: ClientChunkMeta) => string | undefined;
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Build-time context the discovery pass populates and the built-in strategy
|
|
16
|
+
* reads. It refines how the catch-all (no route-root marker) modules are grouped
|
|
17
|
+
* without touching marker splits or the shared runtime:
|
|
18
|
+
*
|
|
19
|
+
* - `fallbackRefs`: production hashes of the `"use client"` modules a consumer
|
|
20
|
+
* registered as `errorBoundary`/`notFoundBoundary` fallbacks. Pulled into a
|
|
21
|
+
* dedicated `app-fallback` chunk so the error UI is not co-bundled with the
|
|
22
|
+
* very route code it exists to catch failures for (resilience), and so the
|
|
23
|
+
* chunk it would otherwise sit in gets named after a real module rather than
|
|
24
|
+
* the boundary. Populated by reading each fallback element's client-reference
|
|
25
|
+
* `$$id` during discovery (see discover-routers).
|
|
26
|
+
*/
|
|
27
|
+
export interface ClientChunkContext {
|
|
28
|
+
fallbackRefs: Set<string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
/**
|
|
14
32
|
* Opt-in observability for the built-in strategy. The route-root marker list is
|
|
15
33
|
* intentionally finite (see {@link ROUTE_ROOT_DIRS}); a consumer whose layout
|
|
@@ -101,18 +119,31 @@ const ROUTE_ROOT_DIRS = new Set([
|
|
|
101
119
|
* `app-components`), re-introducing cross-app leakage.
|
|
102
120
|
*
|
|
103
121
|
* Resolution order:
|
|
104
|
-
* 1.
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
122
|
+
* 1. Shared runtime (React / router / node_modules) -> `undefined` (never split).
|
|
123
|
+
* 2. A registered error/notFound fallback (`ctx.fallbackRefs`) -> `app-fallback`,
|
|
124
|
+
* regardless of location, so the error UI is decoupled from the happy path.
|
|
125
|
+
* 3. A {@link ROUTE_ROOT_DIRS} marker with a directory after it -> key on that
|
|
126
|
+
* next segment (the route id), robust to any nesting depth.
|
|
127
|
+
* 4. Otherwise `undefined` (inherit the default `serverChunk` grouping).
|
|
108
128
|
*/
|
|
109
129
|
export function directoryClientChunks(
|
|
110
130
|
meta: ClientChunkMeta,
|
|
131
|
+
ctx?: ClientChunkContext,
|
|
111
132
|
): string | undefined {
|
|
112
133
|
if (isSharedRuntime(meta)) {
|
|
113
134
|
// React / router runtime / node_modules: always shared, expected, uninteresting.
|
|
114
135
|
return undefined;
|
|
115
136
|
}
|
|
137
|
+
// Registered error/notFound fallbacks -> a dedicated chunk. The error UI must
|
|
138
|
+
// not co-bundle with the code it catches failures for, and removing it lets the
|
|
139
|
+
// chunk it would otherwise anchor be named after a real module, not the boundary.
|
|
140
|
+
if (
|
|
141
|
+
ctx?.fallbackRefs.size &&
|
|
142
|
+
ctx.fallbackRefs.has(hashRefKey(meta.normalizedId))
|
|
143
|
+
) {
|
|
144
|
+
debugChunks?.("fallback %s -> app-fallback", meta.normalizedId);
|
|
145
|
+
return "app-fallback";
|
|
146
|
+
}
|
|
116
147
|
const segments = meta.normalizedId.split("/").filter(Boolean);
|
|
117
148
|
const dirCount = segments.length - 1; // exclude the filename
|
|
118
149
|
if (dirCount >= 1) {
|
|
@@ -144,13 +175,16 @@ export function directoryClientChunks(
|
|
|
144
175
|
* grouping.
|
|
145
176
|
*
|
|
146
177
|
* - `false` / `undefined` -> `undefined` (no override).
|
|
147
|
-
* - `true` -> the built-in {@link directoryClientChunks} strategy
|
|
148
|
-
*
|
|
178
|
+
* - `true` -> the built-in {@link directoryClientChunks} strategy,
|
|
179
|
+
* bound to the discovery-populated {@link ClientChunkContext} (fallback chunk).
|
|
180
|
+
* - function -> the user's function, used verbatim (full control; the
|
|
181
|
+
* fallback refinement does not apply — the consumer owns the grouping).
|
|
149
182
|
*/
|
|
150
183
|
export function resolveClientChunks(
|
|
151
184
|
option: ClientChunks | undefined,
|
|
185
|
+
ctx?: ClientChunkContext,
|
|
152
186
|
): RscClientChunksFn | undefined {
|
|
153
187
|
if (!option) return undefined;
|
|
154
|
-
if (option === true) return directoryClientChunks;
|
|
188
|
+
if (option === true) return (meta) => directoryClientChunks(meta, ctx);
|
|
155
189
|
return option;
|
|
156
190
|
}
|
|
@@ -1,78 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* include node's staticPrefix. That happens when a dynamic param collapses the
|
|
10
|
-
* staticPrefix of nested includes onto the parent's (e.g. `/m/:id/edit` -> sp
|
|
11
|
-
* `/m`): precomputing such a leaf under the collapsed prefix would let the
|
|
12
|
-
* ancestor's lazy entry claim a route it cannot register (the route is behind
|
|
13
|
-
* further nested lazy includes), producing a RouteNotFoundError at request time
|
|
14
|
-
* (issue #506). Those routes are resolved via the handler chain instead.
|
|
15
|
-
*/
|
|
16
|
-
export function flattenLeafEntries(
|
|
17
|
-
prefixTree: Record<string, any>,
|
|
18
|
-
routeManifest: Record<string, string>,
|
|
19
|
-
result: Array<{ staticPrefix: string; routes: Record<string, string> }>,
|
|
20
|
-
): void {
|
|
21
|
-
function visit(node: any, ancestorStaticPrefixes: Set<string>): void {
|
|
22
|
-
const children = node.children || {};
|
|
23
|
-
if (
|
|
24
|
-
Object.keys(children).length === 0 &&
|
|
25
|
-
node.routes &&
|
|
26
|
-
node.routes.length > 0
|
|
27
|
-
) {
|
|
28
|
-
// Leaf node. Skip if its staticPrefix collides with an ancestor include
|
|
29
|
-
// node's staticPrefix (dynamic-param collapse) — see doc comment above.
|
|
30
|
-
if (ancestorStaticPrefixes.has(node.staticPrefix)) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
// Collect its routes from the manifest
|
|
34
|
-
const routes: Record<string, string> = {};
|
|
35
|
-
for (const name of node.routes) {
|
|
36
|
-
if (name in routeManifest) {
|
|
37
|
-
routes[name] = routeManifest[name];
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
result.push({ staticPrefix: node.staticPrefix, routes });
|
|
41
|
-
} else {
|
|
42
|
-
// Non-leaf: recurse into children, tracking this node's staticPrefix as
|
|
43
|
-
// an ancestor so a collapsed nested leaf below it is not over-claimed.
|
|
44
|
-
const nextAncestors = new Set(ancestorStaticPrefixes);
|
|
45
|
-
nextAncestors.add(node.staticPrefix);
|
|
46
|
-
for (const child of Object.values(children)) {
|
|
47
|
-
visit(child, nextAncestors);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
for (const node of Object.values(prefixTree)) {
|
|
52
|
-
visit(node, new Set());
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Walk prefix tree to map each route name to its scope's staticPrefix.
|
|
58
|
-
*/
|
|
59
|
-
export function buildRouteToStaticPrefix(
|
|
60
|
-
prefixTree: Record<string, any>,
|
|
61
|
-
result: Record<string, string>,
|
|
62
|
-
): void {
|
|
63
|
-
function visit(node: any): void {
|
|
64
|
-
const sp = node.staticPrefix || "";
|
|
65
|
-
for (const name of node.routes || []) {
|
|
66
|
-
result[name] = sp;
|
|
67
|
-
}
|
|
68
|
-
for (const child of Object.values(node.children || {})) {
|
|
69
|
-
visit(child);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
for (const node of Object.values(prefixTree)) {
|
|
73
|
-
visit(node);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
1
|
+
// Pure prefix-tree walks live in the build layer so runtime code can consume
|
|
2
|
+
// them without importing from vite/. Re-exported here for the vite-side
|
|
3
|
+
// callers (discover-routers, virtual-module-codegen) that already import them
|
|
4
|
+
// from this module.
|
|
5
|
+
export {
|
|
6
|
+
flattenLeafEntries,
|
|
7
|
+
buildRouteToStaticPrefix,
|
|
8
|
+
} from "../../build/prefix-tree-utils.js";
|
|
76
9
|
|
|
77
10
|
/**
|
|
78
11
|
* Wrap a value as `JSON.parse('...')` instead of a JS object literal.
|