@rangojs/router 0.0.0-experimental.115 → 0.0.0-experimental.117
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 +1 -1
- package/skills/api-client/SKILL.md +211 -0
- package/skills/loader/SKILL.md +17 -17
- 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/browser/navigation-client.ts +56 -68
- package/src/browser/prefetch/cache.ts +58 -27
- package/src/browser/prefetch/fetch.ts +92 -33
- package/src/browser/response-adapter.ts +7 -1
- package/src/browser/rsc-router.tsx +5 -0
- 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/loader-resolution.ts +63 -34
- 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/server/context.ts +32 -0
- package/src/server/request-context.ts +47 -9
- package/src/types/loader-types.ts +6 -3
- 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
package/src/rsc/manifest-init.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
setRouteTrie,
|
|
14
14
|
setRouterManifest,
|
|
15
15
|
setRouterTrie,
|
|
16
|
+
setRouterPrecomputedEntries,
|
|
16
17
|
} from "../route-map-builder.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -36,47 +37,13 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
36
37
|
undefined,
|
|
37
38
|
router.basename ? { urlPrefix: router.basename } : undefined,
|
|
38
39
|
);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const routeToStaticPrefix: Record<string, string> = {};
|
|
47
|
-
for (const name of Object.keys(generated.routeManifest)) {
|
|
48
|
-
routeToStaticPrefix[name] = "";
|
|
49
|
-
}
|
|
50
|
-
// Override with prefix from include() entries so the trie
|
|
51
|
-
// returns the correct sp for lazy entry lookup in findMatch.
|
|
52
|
-
// Walk recursively to include routes in nested includes.
|
|
53
|
-
if (generated.prefixTree) {
|
|
54
|
-
const visitPrefixNode = (node: any): void => {
|
|
55
|
-
const sp = node.staticPrefix || "";
|
|
56
|
-
for (const route of node.routes || []) {
|
|
57
|
-
routeToStaticPrefix[route] = sp;
|
|
58
|
-
}
|
|
59
|
-
for (const child of Object.values(node.children || {})) {
|
|
60
|
-
visitPrefixNode(child);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
for (const node of Object.values(generated.prefixTree)) {
|
|
64
|
-
visitPrefixNode(node);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const trie = buildRouteTrie(
|
|
68
|
-
generated.routeManifest,
|
|
69
|
-
generated._routeAncestry,
|
|
70
|
-
routeToStaticPrefix,
|
|
71
|
-
generated.routeTrailingSlash,
|
|
72
|
-
generated.prerenderRoutes
|
|
73
|
-
? new Set(generated.prerenderRoutes)
|
|
74
|
-
: undefined,
|
|
75
|
-
generated.passthroughRoutes
|
|
76
|
-
? new Set(generated.passthroughRoutes)
|
|
77
|
-
: undefined,
|
|
78
|
-
generated.responseTypeRoutes,
|
|
79
|
-
);
|
|
40
|
+
// Build the trie through the SAME shared helper the production discovery uses
|
|
41
|
+
// (discover-routers.ts), so the dev runtime-rebuilt trie and the prod
|
|
42
|
+
// serialized trie cannot drift. buildPerRouterTrie returns null when there
|
|
43
|
+
// are no routes.
|
|
44
|
+
const { buildPerRouterTrie } = await import("../build/route-trie.js");
|
|
45
|
+
const trie = buildPerRouterTrie(generated);
|
|
46
|
+
if (trie) {
|
|
80
47
|
setRouterTrie(router.id, trie);
|
|
81
48
|
// Set global trie only if not already set by another router
|
|
82
49
|
if (!getRouteTrie()) {
|
|
@@ -84,6 +51,26 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
84
51
|
}
|
|
85
52
|
}
|
|
86
53
|
setRouterManifest(router.id, generated.routeManifest);
|
|
54
|
+
|
|
55
|
+
// Match the production discovery path: precompute leaf-include entries so the
|
|
56
|
+
// match-time shortcut in evaluateLazyEntry applies in dev/Cloudflare too.
|
|
57
|
+
// Without this, dev re-runs each matched leaf include's handler at match time
|
|
58
|
+
// (evaluateLazyEntry) AND again at render time (loadManifest); with it, the
|
|
59
|
+
// match-time run is skipped and the handler runs once per first request.
|
|
60
|
+
// Identical route ownership to the handler path (the shortcut is guarded by
|
|
61
|
+
// the same prefixIsShared and #506 checks production uses).
|
|
62
|
+
const { flattenLeafEntries } = await import("../build/prefix-tree-utils.js");
|
|
63
|
+
const precomputed: Array<{
|
|
64
|
+
staticPrefix: string;
|
|
65
|
+
routes: Record<string, string>;
|
|
66
|
+
}> = [];
|
|
67
|
+
flattenLeafEntries(
|
|
68
|
+
generated.prefixTree,
|
|
69
|
+
generated.routeManifest,
|
|
70
|
+
precomputed,
|
|
71
|
+
);
|
|
72
|
+
setRouterPrecomputedEntries(router.id, precomputed);
|
|
73
|
+
|
|
87
74
|
// Merge into global manifest (needed for reverse/href across routers)
|
|
88
75
|
const existing = hasCachedManifest() ? getGlobalRouteMap() : {};
|
|
89
76
|
setCachedManifest({ ...existing, ...generated.routeManifest });
|
|
@@ -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/server/context.ts
CHANGED
|
@@ -805,3 +805,35 @@ export function runInsideLoaderScope<T>(fn: () => T): T {
|
|
|
805
805
|
export function runInsideLoaderBodyScope<T>(fn: () => T): T {
|
|
806
806
|
return loaderBodyScopeALS.run({ active: true }, fn);
|
|
807
807
|
}
|
|
808
|
+
|
|
809
|
+
// Scope for handle PUSH CALLBACKS (push(() => ...), including async ones).
|
|
810
|
+
// A push callback's value is stored as-is; if it is a promise it is NOT tracked
|
|
811
|
+
// by handleStore.settled and does not block segment resolution, so a
|
|
812
|
+
// ctx.use(loader) made from inside such a callback can never form a rendered()
|
|
813
|
+
// deadlock. This is an ALS (not a plain boolean) so the exemption survives the
|
|
814
|
+
// callback's own awaits — an async push callback that resumes after `await`
|
|
815
|
+
// still reads as "inside a push callback" and stays out of the deadlock guard.
|
|
816
|
+
const PUSH_CALLBACK_SCOPE_KEY = Symbol.for(
|
|
817
|
+
"rangojs-router:push-callback-scope",
|
|
818
|
+
);
|
|
819
|
+
const pushCallbackScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
820
|
+
globalThis as any
|
|
821
|
+
)[PUSH_CALLBACK_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Check if the current execution is inside a handle push callback (sync or an
|
|
825
|
+
* async callback's continuation). Used by the handler-to-loader deadlock guard
|
|
826
|
+
* to exempt push-callback continuations.
|
|
827
|
+
*/
|
|
828
|
+
export function isInsidePushCallbackScope(): boolean {
|
|
829
|
+
return pushCallbackScopeALS.getStore()?.active === true;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Run `fn` inside a push-callback scope. Wraps the invocation of a handle push
|
|
834
|
+
* callback so that any ctx.use(loader) it makes — including after one of its own
|
|
835
|
+
* awaits — is exempt from the deadlock guard.
|
|
836
|
+
*/
|
|
837
|
+
export function runInsidePushCallbackScope<T>(fn: () => T): T {
|
|
838
|
+
return pushCallbackScopeALS.run({ active: true }, fn);
|
|
839
|
+
}
|
|
@@ -273,7 +273,9 @@ export interface RequestContext<
|
|
|
273
273
|
|
|
274
274
|
/**
|
|
275
275
|
* @internal Set to true when the matched entry tree contains any `loading()`
|
|
276
|
-
* entries (streaming).
|
|
276
|
+
* entries (streaming). On a streaming tree rendered() waits for the streaming
|
|
277
|
+
* handlers to settle (via handleStore.settled) before resolving, and the
|
|
278
|
+
* deadlock guard state is kept live until that wait completes.
|
|
277
279
|
*/
|
|
278
280
|
_treeHasStreaming?: boolean;
|
|
279
281
|
|
|
@@ -297,6 +299,18 @@ export interface RequestContext<
|
|
|
297
299
|
*/
|
|
298
300
|
_renderBarrierHandleSnapshot?: HandleData;
|
|
299
301
|
|
|
302
|
+
/**
|
|
303
|
+
* @internal The deadlock guard window is closed (no further handler-awaits-
|
|
304
|
+
* loader cycle is possible). For non-streaming trees this is set when the
|
|
305
|
+
* barrier resolves. For streaming trees the window stays open until
|
|
306
|
+
* handleStore.settled — rendered() keeps waiting past the barrier and a
|
|
307
|
+
* loading() handler can still resume and await a still-waiting loader — so it
|
|
308
|
+
* is set only after settled. The guard (loader-resolution `setupLoaderAccess`)
|
|
309
|
+
* reads this instead of `_renderBarrierSegmentOrder` so it does not go blind
|
|
310
|
+
* during the streaming settle wait.
|
|
311
|
+
*/
|
|
312
|
+
_renderBarrierGuardClosed?: boolean;
|
|
313
|
+
|
|
300
314
|
/** @internal Per-request error dedup set for onError reporting */
|
|
301
315
|
_reportedErrors: WeakSet<object>;
|
|
302
316
|
|
|
@@ -355,6 +369,7 @@ export type PublicRequestContext<
|
|
|
355
369
|
| "_renderBarrierWaiters"
|
|
356
370
|
| "_handlerLoaderDeps"
|
|
357
371
|
| "_renderBarrierHandleSnapshot"
|
|
372
|
+
| "_renderBarrierGuardClosed"
|
|
358
373
|
| "_reportBackgroundError"
|
|
359
374
|
| "_debugPerformance"
|
|
360
375
|
| "_metricsStore"
|
|
@@ -797,14 +812,37 @@ export function createRequestContext<TEnv>(
|
|
|
797
812
|
.filter((s) => s.type !== "loader")
|
|
798
813
|
.map((s) => s.id);
|
|
799
814
|
ctx._renderBarrierSegmentOrder = segOrder;
|
|
800
|
-
|
|
801
|
-
//
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
815
|
+
|
|
816
|
+
// Closing the guard window means no handler can still form a deadlock cycle
|
|
817
|
+
// with a rendered() loader: drop the dependency-tracking state and mark it
|
|
818
|
+
// closed. WHEN this runs is the only streaming/non-streaming difference.
|
|
819
|
+
const closeGuard = () => {
|
|
820
|
+
ctx._renderBarrierWaiters = undefined;
|
|
821
|
+
ctx._handlerLoaderDeps = undefined;
|
|
822
|
+
ctx._renderBarrierGuardClosed = true;
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
if (ctx._treeHasStreaming) {
|
|
826
|
+
// Streaming: rendered() keeps waiting on handleStore.settled past this
|
|
827
|
+
// point, and loading() handlers are still in flight. The eager snapshot
|
|
828
|
+
// here would be incomplete, so leave it unset — rendered() builds and
|
|
829
|
+
// caches the complete one after settled. Keep the guard window OPEN so a
|
|
830
|
+
// handler that resumes and awaits a still-waiting rendered() loader is
|
|
831
|
+
// still caught; close it once settled (every tracked handler has finished
|
|
832
|
+
// then, so none can await a loader anymore). settled resolves after
|
|
833
|
+
// rendered() seals; if no loader used rendered(), nothing seals and the
|
|
834
|
+
// (empty) guard state is simply GC'd at request end.
|
|
835
|
+
handleStore.settled.then(closeGuard);
|
|
836
|
+
} else {
|
|
837
|
+
// Non-streaming: all handlers have settled by now. Build and cache the
|
|
838
|
+
// snapshot so loader ctx.use(handle) calls don't rebuild it, and close the
|
|
839
|
+
// guard window immediately.
|
|
840
|
+
ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
|
|
841
|
+
handleStore,
|
|
842
|
+
segOrder,
|
|
843
|
+
);
|
|
844
|
+
closeGuard();
|
|
845
|
+
}
|
|
808
846
|
if (resolveBarrier) resolveBarrier();
|
|
809
847
|
};
|
|
810
848
|
Object.defineProperty(ctx, "_renderBarrier", {
|
|
@@ -72,9 +72,12 @@ export type LoaderContext<
|
|
|
72
72
|
* **Experimental.** Wait for all non-loader segments to settle.
|
|
73
73
|
*
|
|
74
74
|
* After the returned promise resolves, handle data is available via
|
|
75
|
-
* `ctx.use(handle)`.
|
|
76
|
-
* trees
|
|
77
|
-
*
|
|
75
|
+
* `ctx.use(handle)`. Supported in DSL loaders, including on streaming
|
|
76
|
+
* trees that use `loading()` — the barrier waits for the streaming
|
|
77
|
+
* handlers to finish pushing before it resolves. Throws if called from a
|
|
78
|
+
* handler-invoked loader, or if a handler is already awaiting this loader
|
|
79
|
+
* via `ctx.use()` (that would deadlock — use a loader-to-loader
|
|
80
|
+
* dependency instead).
|
|
78
81
|
*
|
|
79
82
|
* @example
|
|
80
83
|
* ```typescript
|
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.
|