@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c
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/AGENTS.md +4 -0
- package/README.md +172 -50
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +1160 -508
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +17 -16
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +49 -8
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +61 -51
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +91 -17
- package/skills/loader/SKILL.md +107 -24
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/parallel/SKILL.md +185 -0
- package/skills/prerender/SKILL.md +112 -70
- package/skills/rango/SKILL.md +24 -23
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +58 -4
- package/skills/router-setup/SKILL.md +95 -5
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +38 -24
- package/src/__internal.ts +92 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +175 -17
- package/src/browser/navigation-client.ts +177 -44
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +113 -17
- package/src/browser/prefetch/cache.ts +275 -28
- package/src/browser/prefetch/fetch.ts +191 -46
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +98 -14
- package/src/browser/react/NavigationProvider.tsx +89 -14
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/rsc-router.tsx +177 -66
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +73 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +67 -25
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +223 -74
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +455 -15
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +2 -1
- package/src/client.tsx +85 -276
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +0 -3
- package/src/index.rsc.ts +9 -36
- package/src/index.ts +79 -70
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +27 -2
- package/src/route-definition/dsl-helpers.ts +240 -40
- package/src/route-definition/helpers-types.ts +67 -19
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +129 -26
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +10 -7
- package/src/router/loader-resolution.ts +160 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -193
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +94 -17
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +103 -18
- package/src/router/metrics.ts +238 -13
- package/src/router/middleware-types.ts +48 -27
- package/src/router/middleware.ts +201 -86
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +77 -11
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +50 -5
- package/src/router/router-options.ts +50 -19
- package/src/router/segment-resolution/fresh.ts +215 -19
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +454 -301
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/trie-matching.ts +30 -6
- package/src/router/types.ts +1 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +89 -17
- package/src/rsc/handler.ts +563 -364
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +37 -10
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +47 -44
- package/src/rsc/server-action.ts +24 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +11 -1
- package/src/search-params.ts +16 -13
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +109 -23
- package/src/server/context.ts +174 -19
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +218 -65
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +140 -72
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +2 -5
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +18 -16
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +61 -89
- package/src/vite/discovery/discover-routers.ts +7 -4
- package/src/vite/discovery/prerender-collection.ts +162 -88
- package/src/vite/discovery/state.ts +17 -13
- package/src/vite/index.ts +8 -3
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +190 -217
- package/src/vite/router-discovery.ts +241 -45
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/package-resolution.ts +34 -1
- package/src/vite/utils/prerender-utils.ts +97 -5
- package/src/vite/utils/shared-utils.ts +3 -2
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { RouteEntry, TrailingSlashMode } from "../types";
|
|
8
8
|
import type { EntryData } from "../server/context";
|
|
9
9
|
import { debugLog, isRouterDebugEnabled } from "./logging.js";
|
|
10
|
+
import { safeDecodeURIComponent } from "./url-params.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Parsed segment info
|
|
@@ -16,6 +17,7 @@ export interface ParsedSegment {
|
|
|
16
17
|
value: string; // static text, param name, or "*"
|
|
17
18
|
optional: boolean;
|
|
18
19
|
constraint?: string[]; // enum values like ["en", "gb"]
|
|
20
|
+
suffix?: string; // literal text after param in same segment (e.g., ".html")
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -39,11 +41,21 @@ export function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
39
41
|
// - :param(a|b)?
|
|
40
42
|
// - *
|
|
41
43
|
const segmentRegex =
|
|
42
|
-
/\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)
|
|
44
|
+
/\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
|
|
43
45
|
|
|
44
46
|
let match;
|
|
45
47
|
while ((match = segmentRegex.exec(pattern)) !== null) {
|
|
46
|
-
const [
|
|
48
|
+
const [
|
|
49
|
+
,
|
|
50
|
+
,
|
|
51
|
+
paramName,
|
|
52
|
+
,
|
|
53
|
+
constraint,
|
|
54
|
+
optional,
|
|
55
|
+
suffix,
|
|
56
|
+
wildcard,
|
|
57
|
+
staticText,
|
|
58
|
+
] = match;
|
|
47
59
|
|
|
48
60
|
if (wildcard) {
|
|
49
61
|
segments.push({ type: "wildcard", value: "*", optional: false });
|
|
@@ -53,6 +65,7 @@ export function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
53
65
|
value: paramName,
|
|
54
66
|
optional: optional === "?",
|
|
55
67
|
constraint: constraint ? constraint.split("|") : undefined,
|
|
68
|
+
suffix: suffix || undefined,
|
|
56
69
|
});
|
|
57
70
|
} else if (staticText) {
|
|
58
71
|
segments.push({ type: "static", value: staticText, optional: false });
|
|
@@ -70,6 +83,13 @@ export interface CompiledPattern {
|
|
|
70
83
|
paramNames: string[];
|
|
71
84
|
optionalParams: Set<string>;
|
|
72
85
|
hasTrailingSlash: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Param-name → allowed values for constrained params (e.g. `:lang(en|gb)`).
|
|
88
|
+
* Validated against the **decoded** param value after regex extraction so
|
|
89
|
+
* a URL like `/en%20GB` still matches `:lang(en GB)` — matching the trie
|
|
90
|
+
* path's behavior (trie-matching.ts:validateAndBuild).
|
|
91
|
+
*/
|
|
92
|
+
constraints?: Record<string, string[]>;
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
// Module-level cache for compiled patterns. Route patterns are a finite set
|
|
@@ -130,6 +150,7 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
130
150
|
const segments = parsePattern(normalizedPattern);
|
|
131
151
|
const paramNames: string[] = [];
|
|
132
152
|
const optionalParams = new Set<string>();
|
|
153
|
+
let constraints: Record<string, string[]> | undefined;
|
|
133
154
|
|
|
134
155
|
let regexPattern = "";
|
|
135
156
|
|
|
@@ -139,16 +160,22 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
139
160
|
regexPattern += "/(.*)";
|
|
140
161
|
} else if (segment.type === "param") {
|
|
141
162
|
paramNames.push(segment.value);
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
163
|
+
const suffixPattern = segment.suffix ? escapeRegex(segment.suffix) : "";
|
|
164
|
+
// Constrained params capture anything here; the allowed values are
|
|
165
|
+
// checked post-decode in findMatch so URL-encoded constraint values
|
|
166
|
+
// (e.g. `:lang(en GB)` via `/en%20GB`) still match.
|
|
167
|
+
const valuePattern = segment.suffix ? "([^/]+?)" : "([^/]+)";
|
|
168
|
+
|
|
169
|
+
if (segment.constraint) {
|
|
170
|
+
(constraints ??= {})[segment.value] = segment.constraint;
|
|
171
|
+
}
|
|
145
172
|
|
|
146
173
|
if (segment.optional) {
|
|
147
174
|
optionalParams.add(segment.value);
|
|
148
175
|
// Optional: make the whole /segment optional
|
|
149
|
-
regexPattern += `(?:/${valuePattern})?`;
|
|
176
|
+
regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
|
|
150
177
|
} else {
|
|
151
|
-
regexPattern += `/${valuePattern}`;
|
|
178
|
+
regexPattern += `/${valuePattern}${suffixPattern}`;
|
|
152
179
|
}
|
|
153
180
|
} else {
|
|
154
181
|
// Static segment
|
|
@@ -171,9 +198,33 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
171
198
|
paramNames,
|
|
172
199
|
optionalParams,
|
|
173
200
|
hasTrailingSlash,
|
|
201
|
+
...(constraints ? { constraints } : {}),
|
|
174
202
|
};
|
|
175
203
|
}
|
|
176
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Validate decoded params against a compiled pattern's constraints.
|
|
207
|
+
* Returns false if any constrained param has a non-empty value not in the
|
|
208
|
+
* allowed list (empty-string = absent optional, which is allowed).
|
|
209
|
+
*/
|
|
210
|
+
function satisfiesConstraints(
|
|
211
|
+
params: Record<string, string>,
|
|
212
|
+
constraints: Record<string, string[]> | undefined,
|
|
213
|
+
): boolean {
|
|
214
|
+
if (!constraints) return true;
|
|
215
|
+
for (const name in constraints) {
|
|
216
|
+
const value = params[name];
|
|
217
|
+
if (
|
|
218
|
+
value !== undefined &&
|
|
219
|
+
value !== "" &&
|
|
220
|
+
!constraints[name].includes(value)
|
|
221
|
+
) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
177
228
|
/**
|
|
178
229
|
* Escape special regex characters in a string
|
|
179
230
|
*/
|
|
@@ -377,8 +428,13 @@ export function findMatch<TEnv>(
|
|
|
377
428
|
fullPattern = entry.prefix + pattern;
|
|
378
429
|
}
|
|
379
430
|
|
|
380
|
-
const {
|
|
381
|
-
|
|
431
|
+
const {
|
|
432
|
+
regex,
|
|
433
|
+
paramNames,
|
|
434
|
+
optionalParams,
|
|
435
|
+
hasTrailingSlash,
|
|
436
|
+
constraints,
|
|
437
|
+
} = getCompiledPattern(fullPattern);
|
|
382
438
|
|
|
383
439
|
// Get trailing slash mode for this route (per-route config or pattern-based)
|
|
384
440
|
const trailingSlashMode: TrailingSlashMode | undefined =
|
|
@@ -397,9 +453,15 @@ export function findMatch<TEnv>(
|
|
|
397
453
|
if (match) {
|
|
398
454
|
const params: Record<string, string> = {};
|
|
399
455
|
paramNames.forEach((name, index) => {
|
|
400
|
-
params[name] = match[index + 1] ?? "";
|
|
456
|
+
params[name] = safeDecodeURIComponent(match[index + 1] ?? "");
|
|
401
457
|
});
|
|
402
458
|
|
|
459
|
+
// Validate constraints against decoded values; a failure falls
|
|
460
|
+
// through to the next route so other patterns can still match.
|
|
461
|
+
if (!satisfiesConstraints(params, constraints)) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
403
465
|
if (effectiveDebug) {
|
|
404
466
|
debugLog("findMatch", "matched route", {
|
|
405
467
|
routeKey,
|
|
@@ -452,9 +514,13 @@ export function findMatch<TEnv>(
|
|
|
452
514
|
if (altMatch) {
|
|
453
515
|
const params: Record<string, string> = {};
|
|
454
516
|
paramNames.forEach((name, index) => {
|
|
455
|
-
params[name] = altMatch[index + 1] ?? "";
|
|
517
|
+
params[name] = safeDecodeURIComponent(altMatch[index + 1] ?? "");
|
|
456
518
|
});
|
|
457
519
|
|
|
520
|
+
if (!satisfiesConstraints(params, constraints)) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
458
524
|
// Determine redirect behavior based on mode
|
|
459
525
|
if (trailingSlashMode === "ignore") {
|
|
460
526
|
// Match without redirect
|
|
@@ -54,6 +54,9 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
54
54
|
deps: PrerenderMatchDeps<TEnv>,
|
|
55
55
|
buildVars?: Record<string, any>,
|
|
56
56
|
isPassthroughRoute?: boolean,
|
|
57
|
+
buildEnv?: TEnv,
|
|
58
|
+
/** Dev-only: check getParams() for passthrough routes to skip unknown params. */
|
|
59
|
+
devMode?: boolean,
|
|
57
60
|
): Promise<{
|
|
58
61
|
segments: SerializedSegmentData[];
|
|
59
62
|
handles: Record<string, SegmentHandleData>;
|
|
@@ -90,20 +93,106 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
90
93
|
entries.push(entry);
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
// 3b. Dev-mode passthrough shortcut: if the route is a Passthrough route
|
|
97
|
+
// and has getParams(), check if the matched params are in the known list.
|
|
98
|
+
// In production, only known params are pre-rendered; unknown params fall
|
|
99
|
+
// through to the live handler. Mirror that behavior in dev mode to avoid
|
|
100
|
+
// rendering unknown params with build: true.
|
|
101
|
+
// Vars collected from getParams() probe — merged into render context below.
|
|
102
|
+
let devProbeBuildVars: Record<string, any> | undefined;
|
|
103
|
+
|
|
104
|
+
if (devMode && matchedPassthroughRoute) {
|
|
105
|
+
const routeEntry = entries.find(
|
|
106
|
+
(
|
|
107
|
+
e,
|
|
108
|
+
): e is EntryData & {
|
|
109
|
+
type: "route";
|
|
110
|
+
prerenderDef: { getParams: (ctx: any) => Promise<any[]> | any[] };
|
|
111
|
+
} =>
|
|
112
|
+
e.type === "route" &&
|
|
113
|
+
!!(e as any).isPassthrough &&
|
|
114
|
+
!!(e as any).prerenderDef?.getParams,
|
|
115
|
+
);
|
|
116
|
+
if (routeEntry) {
|
|
117
|
+
try {
|
|
118
|
+
const probeBuildVars: Record<string, any> = {};
|
|
119
|
+
const knownParamsList = await routeEntry.prerenderDef.getParams({
|
|
120
|
+
build: true as const,
|
|
121
|
+
dev: true,
|
|
122
|
+
set: ((keyOrVar: any, value: any) => {
|
|
123
|
+
contextSet(probeBuildVars, keyOrVar, value);
|
|
124
|
+
}) as any,
|
|
125
|
+
reverse: createReverseFunction(deps.mergedRouteMap),
|
|
126
|
+
get env() {
|
|
127
|
+
if (buildEnv !== undefined) return buildEnv;
|
|
128
|
+
throw new Error(
|
|
129
|
+
"[rsc-router] ctx.env is not available during dev-mode getParams(). " +
|
|
130
|
+
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
// Compare only the keys returned by getParams — ignore mount params
|
|
135
|
+
// from include() prefixes that aren't part of the handler's params.
|
|
136
|
+
const isKnown = knownParamsList.some((known: Record<string, any>) => {
|
|
137
|
+
const knownKeys = Object.keys(known);
|
|
138
|
+
return knownKeys.every(
|
|
139
|
+
(k) => String(known[k]) === String(matchedParams[k]),
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
if (!isKnown) {
|
|
143
|
+
return {
|
|
144
|
+
segments: [],
|
|
145
|
+
handles: {},
|
|
146
|
+
routeName: matched.routeKey,
|
|
147
|
+
params: matchedParams,
|
|
148
|
+
passthrough: true as const,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Preserve vars set by getParams() for the render context
|
|
152
|
+
if (
|
|
153
|
+
Object.keys(probeBuildVars).length > 0 ||
|
|
154
|
+
Object.getOwnPropertySymbols(probeBuildVars).length > 0
|
|
155
|
+
) {
|
|
156
|
+
devProbeBuildVars = probeBuildVars;
|
|
157
|
+
}
|
|
158
|
+
} catch (err: any) {
|
|
159
|
+
// Mirror production semantics (prerender-collection.ts):
|
|
160
|
+
// Skip errors are intentional — treat as passthrough.
|
|
161
|
+
// All other errors propagate so dev surfaces them.
|
|
162
|
+
if (err?.name === "Skip") {
|
|
163
|
+
return {
|
|
164
|
+
segments: [],
|
|
165
|
+
handles: {},
|
|
166
|
+
routeName: matched.routeKey,
|
|
167
|
+
params: matchedParams,
|
|
168
|
+
passthrough: true as const,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
93
176
|
// 4. Create handle store for collecting handle data
|
|
94
177
|
const handleStore = createHandleStore();
|
|
95
178
|
|
|
96
179
|
// 5. Create a minimal request context with the handle store
|
|
97
|
-
// Shallow-copy getParams vars so each param set is independent
|
|
98
|
-
|
|
180
|
+
// Shallow-copy getParams vars so each param set is independent.
|
|
181
|
+
// In dev mode, merge vars from the getParams() probe if the caller
|
|
182
|
+
// didn't provide buildVars (production passes them from expandPrerenderRoutes).
|
|
183
|
+
const effectiveBuildVars = buildVars ?? devProbeBuildVars;
|
|
184
|
+
const variables: Record<string, any> = effectiveBuildVars
|
|
185
|
+
? { ...effectiveBuildVars }
|
|
186
|
+
: {};
|
|
99
187
|
const stubRes = new Response(null, { status: 200 });
|
|
100
188
|
const minimalRequestContext: RequestContext<TEnv> = {
|
|
101
|
-
env: {} as TEnv,
|
|
189
|
+
env: buildEnv ?? ({} as TEnv),
|
|
102
190
|
request: new Request("http://prerender" + pathname),
|
|
103
191
|
url: new URL("http://prerender" + pathname),
|
|
192
|
+
originalUrl: new URL("http://prerender" + pathname),
|
|
104
193
|
pathname,
|
|
105
194
|
searchParams: new URLSearchParams(),
|
|
106
|
-
|
|
195
|
+
_variables: variables,
|
|
107
196
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
108
197
|
set: ((keyOrVar: any, value: any) => {
|
|
109
198
|
contextSet(variables, keyOrVar, value);
|
|
@@ -116,6 +205,7 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
116
205
|
deleteCookie: () => {},
|
|
117
206
|
header: () => {},
|
|
118
207
|
setStatus: () => {},
|
|
208
|
+
_setStatus: () => {},
|
|
119
209
|
use: (() => {
|
|
120
210
|
throw new Error("use() not available during pre-rendering");
|
|
121
211
|
}) as any,
|
|
@@ -126,6 +216,8 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
126
216
|
_onResponseCallbacks: [],
|
|
127
217
|
setLocationState() {},
|
|
128
218
|
_locationState: undefined,
|
|
219
|
+
_renderBarrier: Promise.resolve(),
|
|
220
|
+
_resolveRenderBarrier: () => {},
|
|
129
221
|
_reportedErrors: new WeakSet<object>(),
|
|
130
222
|
reverse: createReverseFunction(
|
|
131
223
|
deps.mergedRouteMap,
|
|
@@ -138,7 +230,7 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
138
230
|
return runWithRequestContext(minimalRequestContext, async () => {
|
|
139
231
|
// 6. Create prerender context with synthetic URL.
|
|
140
232
|
// Prerender handlers get params, pathname, url, searchParams, search,
|
|
141
|
-
// reverse,
|
|
233
|
+
// reverse, use(handle), and optionally env (when buildEnv is configured).
|
|
142
234
|
const buildCtx = createPrerenderContext<TEnv>(
|
|
143
235
|
matchedParams,
|
|
144
236
|
pathname,
|
|
@@ -146,6 +238,8 @@ export async function matchForPrerender<TEnv = any>(
|
|
|
146
238
|
matched.routeKey,
|
|
147
239
|
variables,
|
|
148
240
|
matchedPassthroughRoute,
|
|
241
|
+
buildEnv,
|
|
242
|
+
devMode,
|
|
149
243
|
);
|
|
150
244
|
|
|
151
245
|
// 7. Wire use() for handles only (loaders throw)
|
|
@@ -318,6 +412,8 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
318
412
|
handlerId: string,
|
|
319
413
|
mergedRouteMap: Record<string, string>,
|
|
320
414
|
routeName?: string,
|
|
415
|
+
buildEnv?: TEnv,
|
|
416
|
+
devMode?: boolean,
|
|
321
417
|
): Promise<{ encoded: string; handles: Record<string, unknown[]> } | null> {
|
|
322
418
|
const syntheticUrl = new URL("http://prerender/");
|
|
323
419
|
const syntheticRequest = new Request(syntheticUrl);
|
|
@@ -328,12 +424,13 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
328
424
|
// Minimal request context so setupBuildUse can find the HandleStore
|
|
329
425
|
const stubRes = new Response(null, { status: 200 });
|
|
330
426
|
const minimalRequestContext: RequestContext<TEnv> = {
|
|
331
|
-
env: {} as TEnv,
|
|
427
|
+
env: buildEnv ?? ({} as TEnv),
|
|
332
428
|
request: syntheticRequest,
|
|
333
429
|
url: syntheticUrl,
|
|
430
|
+
originalUrl: syntheticUrl,
|
|
334
431
|
pathname: "/",
|
|
335
432
|
searchParams: syntheticUrl.searchParams,
|
|
336
|
-
|
|
433
|
+
_variables: {},
|
|
337
434
|
get: () => undefined as any,
|
|
338
435
|
set: () => {},
|
|
339
436
|
params: {},
|
|
@@ -344,6 +441,7 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
344
441
|
deleteCookie: () => {},
|
|
345
442
|
header: () => {},
|
|
346
443
|
setStatus: () => {},
|
|
444
|
+
_setStatus: () => {},
|
|
347
445
|
use: (() => {
|
|
348
446
|
throw new Error("use() not available during static pre-rendering");
|
|
349
447
|
}) as any,
|
|
@@ -354,6 +452,8 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
354
452
|
_onResponseCallbacks: [],
|
|
355
453
|
setLocationState() {},
|
|
356
454
|
_locationState: undefined,
|
|
455
|
+
_renderBarrier: Promise.resolve(),
|
|
456
|
+
_resolveRenderBarrier: () => {},
|
|
357
457
|
_reportedErrors: new WeakSet<object>(),
|
|
358
458
|
reverse: createReverseFunction(
|
|
359
459
|
mergedRouteMap,
|
|
@@ -364,9 +464,13 @@ export async function renderStaticSegment<TEnv = any>(
|
|
|
364
464
|
};
|
|
365
465
|
|
|
366
466
|
return runWithRequestContext(minimalRequestContext, async () => {
|
|
367
|
-
// Static handlers get only reverse
|
|
368
|
-
|
|
369
|
-
|
|
467
|
+
// Static handlers get only reverse, use(handle), and optionally env.
|
|
468
|
+
const buildCtx = createStaticContext<TEnv>(
|
|
469
|
+
mergedRouteMap,
|
|
470
|
+
routeName,
|
|
471
|
+
buildEnv,
|
|
472
|
+
devMode,
|
|
473
|
+
);
|
|
370
474
|
|
|
371
475
|
// Set segment ID so handle pushes are keyed correctly
|
|
372
476
|
(buildCtx as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { traverseBack } from "./pattern-matching.js";
|
|
3
|
-
import { collectRouteMiddleware } from "./middleware.js";
|
|
4
|
-
import {
|
|
5
|
-
parseAcceptTypes,
|
|
6
|
-
RSC_RESPONSE_TYPE,
|
|
7
|
-
pickNegotiateVariant,
|
|
8
|
-
} from "./content-negotiation.js";
|
|
1
|
+
import { negotiateRoute } from "./content-negotiation.js";
|
|
9
2
|
import { runWithRouterLogContext, withRouterLogScope } from "./logging.js";
|
|
10
3
|
import type { EntryData } from "../server/context";
|
|
11
4
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
12
5
|
import type { MiddlewareFn } from "./middleware.js";
|
|
6
|
+
import { resolveRoute } from "./route-snapshot.js";
|
|
13
7
|
|
|
14
8
|
export interface PreviewMatchDeps<TEnv = any> {
|
|
15
9
|
findMatch: (pathname: string) => RouteMatchResult<TEnv> | null;
|
|
@@ -42,110 +36,44 @@ export async function previewMatch<TEnv = any>(
|
|
|
42
36
|
const url = new URL(request.url);
|
|
43
37
|
const pathname = url.pathname;
|
|
44
38
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
// Route resolution via snapshot (lite mode: skip entries/cacheScope
|
|
40
|
+
// since previewMatch only needs matched, manifestEntry, routeMiddleware,
|
|
41
|
+
// and responseType)
|
|
42
|
+
const result = await resolveRoute<TEnv>(pathname, {
|
|
43
|
+
findMatch: deps.findMatch,
|
|
44
|
+
lite: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!result) {
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Skip redirect check - will be handled in full match
|
|
52
|
-
if (
|
|
52
|
+
if (result.type === "redirect") {
|
|
53
53
|
return { routeMiddleware: undefined };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
const manifestEntry
|
|
58
|
-
|
|
59
|
-
matched.routeKey,
|
|
60
|
-
pathname,
|
|
61
|
-
undefined, // No metrics store for preview
|
|
62
|
-
false, // isSSR - doesn't matter for preview
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
// Collect route-level middleware from entry tree
|
|
66
|
-
// Includes middleware from orphan layouts (inline layouts within routes)
|
|
67
|
-
const routeMiddleware = collectRouteMiddleware(
|
|
68
|
-
traverseBack(manifestEntry),
|
|
69
|
-
matched.params,
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
// Check for response type (from trie match or manifest entry)
|
|
73
|
-
const responseType =
|
|
74
|
-
matched.responseType ||
|
|
75
|
-
(manifestEntry.type === "route"
|
|
76
|
-
? manifestEntry.responseType
|
|
77
|
-
: undefined);
|
|
78
|
-
|
|
79
|
-
// Content negotiation: when negotiate variants exist, pick the best
|
|
80
|
-
// handler based on the Accept header. Uses q-values and client order
|
|
81
|
-
// as tiebreaker (matching Express/Hono behavior). RSC routes participate
|
|
82
|
-
// as text/html candidates so browsers naturally get HTML without
|
|
83
|
-
// special-casing.
|
|
84
|
-
if (matched.negotiateVariants && matched.negotiateVariants.length > 0) {
|
|
85
|
-
const acceptEntries = parseAcceptTypes(
|
|
86
|
-
request.headers.get("accept") || "",
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
// Build candidate list preserving definition order.
|
|
90
|
-
// For wildcard (*/*) and no-Accept fallback, the first candidate wins.
|
|
91
|
-
const variants = matched.negotiateVariants;
|
|
92
|
-
let candidates: Array<{ routeKey: string; responseType: string }>;
|
|
93
|
-
if (responseType) {
|
|
94
|
-
// Primary is response-type — include it as a candidate
|
|
95
|
-
candidates = [
|
|
96
|
-
...variants,
|
|
97
|
-
{ routeKey: matched.routeKey, responseType },
|
|
98
|
-
];
|
|
99
|
-
} else {
|
|
100
|
-
// Primary is RSC — insert as text/html candidate in definition order
|
|
101
|
-
const rscCandidate = {
|
|
102
|
-
routeKey: matched.routeKey,
|
|
103
|
-
responseType: RSC_RESPONSE_TYPE,
|
|
104
|
-
};
|
|
105
|
-
candidates = matched.rscFirst
|
|
106
|
-
? [rscCandidate, ...variants]
|
|
107
|
-
: [...variants, rscCandidate];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const variant = pickNegotiateVariant(acceptEntries, candidates);
|
|
56
|
+
const snapshot = result.snapshot;
|
|
57
|
+
const { matched, manifestEntry, routeMiddleware, responseType } =
|
|
58
|
+
snapshot;
|
|
111
59
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// since different variants can have different middleware chains.
|
|
127
|
-
const variantMiddleware = collectRouteMiddleware(
|
|
128
|
-
traverseBack(negotiateEntry),
|
|
129
|
-
matched.params,
|
|
130
|
-
);
|
|
131
|
-
return {
|
|
132
|
-
routeMiddleware:
|
|
133
|
-
variantMiddleware.length > 0 ? variantMiddleware : undefined,
|
|
134
|
-
responseType: variant.responseType,
|
|
135
|
-
handler:
|
|
136
|
-
negotiateEntry.type === "route"
|
|
137
|
-
? negotiateEntry.handler
|
|
138
|
-
: undefined,
|
|
139
|
-
params: matched.params,
|
|
140
|
-
negotiated: true,
|
|
141
|
-
manifestEntry: negotiateEntry,
|
|
142
|
-
routeKey: matched.routeKey,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
60
|
+
const negotiation = await negotiateRoute(request, pathname, snapshot);
|
|
61
|
+
if (negotiation) {
|
|
62
|
+
return {
|
|
63
|
+
routeMiddleware:
|
|
64
|
+
negotiation.routeMiddleware.length > 0
|
|
65
|
+
? negotiation.routeMiddleware
|
|
66
|
+
: undefined,
|
|
67
|
+
responseType: negotiation.responseType,
|
|
68
|
+
handler: negotiation.handler,
|
|
69
|
+
params: matched.params,
|
|
70
|
+
negotiated: true,
|
|
71
|
+
manifestEntry: negotiation.manifestEntry,
|
|
72
|
+
routeKey: matched.routeKey,
|
|
73
|
+
};
|
|
145
74
|
}
|
|
146
75
|
|
|
147
|
-
//
|
|
148
|
-
// negotiated so the handler sets Vary: Accept on the response.
|
|
76
|
+
// No negotiation or RSC won — return default route info
|
|
149
77
|
const hasVariants =
|
|
150
78
|
matched.negotiateVariants && matched.negotiateVariants.length > 0;
|
|
151
79
|
return {
|