@rangojs/router 0.0.0-experimental.b02a2fec → 0.0.0-experimental.b30bbf02
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/README.md +112 -17
- package/dist/vite/index.js +1338 -462
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +7 -5
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +33 -20
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +90 -16
- package/skills/loader/SKILL.md +70 -3
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/parallel/SKILL.md +66 -0
- package/skills/rango/SKILL.md +25 -22
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +24 -0
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +3 -1
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +44 -4
- package/src/browser/navigation-bridge.ts +71 -5
- package/src/browser/navigation-client.ts +64 -13
- package/src/browser/navigation-store.ts +25 -1
- package/src/browser/partial-update.ts +34 -3
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +148 -16
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +70 -18
- package/src/browser/react/filter-segment-order.ts +51 -7
- 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 +8 -1
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/rsc-router.tsx +34 -6
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/types.ts +19 -0
- package/src/build/route-trie.ts +50 -24
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/client.tsx +82 -174
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +40 -9
- package/src/outlet-context.ts +1 -1
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +7 -3
- package/src/route-definition/dsl-helpers.ts +175 -23
- package/src/route-definition/helpers-types.ts +63 -14
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-types.ts +7 -0
- package/src/router/handler-context.ts +24 -4
- package/src/router/lazy-includes.ts +6 -6
- package/src/router/loader-resolution.ts +3 -0
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-result.ts +21 -2
- package/src/router/middleware-types.ts +2 -22
- package/src/router/middleware.ts +54 -7
- package/src/router/pattern-matching.ts +87 -17
- package/src/router/revalidation.ts +15 -1
- package/src/router/segment-resolution/fresh.ts +8 -0
- package/src/router/segment-resolution/revalidation.ts +128 -100
- package/src/router/trie-matching.ts +18 -13
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +1 -2
- package/src/rsc/handler.ts +8 -4
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +10 -0
- package/src/rsc/server-action.ts +4 -0
- package/src/rsc/types.ts +6 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +11 -61
- package/src/server/context.ts +26 -3
- package/src/server/request-context.ts +10 -42
- package/src/ssr/index.tsx +5 -1
- package/src/types/handler-context.ts +12 -39
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +17 -1
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +30 -4
- package/src/urls/response-types.ts +2 -10
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +31 -3
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +48 -1
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/plugins/cjs-to-esm.ts +5 -0
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +16 -4
- 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 +52 -28
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +516 -486
- package/src/vite/plugins/performance-tracks.ts +17 -9
- package/src/vite/plugins/use-cache-transform.ts +56 -43
- package/src/vite/plugins/version-injector.ts +37 -11
- package/src/vite/rango.ts +49 -14
- package/src/vite/router-discovery.ts +558 -53
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +20 -6
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
import type { ScopedReverseFunction } from "../reverse.js";
|
|
15
15
|
import type { Theme } from "../theme/types.js";
|
|
16
16
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
17
|
+
import type { RequestScope } from "../types/request-scope.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Get variable function type
|
|
@@ -57,28 +58,7 @@ export interface CookieOptions {
|
|
|
57
58
|
export interface MiddlewareContext<
|
|
58
59
|
TEnv = any,
|
|
59
60
|
TParams = Record<string, string>,
|
|
60
|
-
> {
|
|
61
|
-
/** Original request */
|
|
62
|
-
request: Request;
|
|
63
|
-
|
|
64
|
-
/** Parsed URL (with internal `_rsc*` params stripped) */
|
|
65
|
-
url: URL;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The original request URL with all parameters intact, including
|
|
69
|
-
* internal `_rsc*` transport params.
|
|
70
|
-
*/
|
|
71
|
-
originalUrl: URL;
|
|
72
|
-
|
|
73
|
-
/** URL pathname */
|
|
74
|
-
pathname: string;
|
|
75
|
-
|
|
76
|
-
/** URL search params */
|
|
77
|
-
searchParams: URLSearchParams;
|
|
78
|
-
|
|
79
|
-
/** Platform bindings (Cloudflare, etc.) */
|
|
80
|
-
env: TEnv;
|
|
81
|
-
|
|
61
|
+
> extends RequestScope<TEnv> {
|
|
82
62
|
/** URL params extracted from route/middleware pattern */
|
|
83
63
|
params: TParams;
|
|
84
64
|
|
package/src/router/middleware.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { contextGet, contextSet } from "../context-var.js";
|
|
13
|
+
import { safeDecodeURIComponent } from "./url-params.js";
|
|
14
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
13
15
|
import type {
|
|
14
16
|
CollectedMiddleware,
|
|
15
17
|
MiddlewareCollectableEntry,
|
|
@@ -22,6 +24,7 @@ import { _getRequestContext } from "../server/request-context.js";
|
|
|
22
24
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
23
25
|
import { appendMetric, createMetricsStore } from "./metrics.js";
|
|
24
26
|
import { stripInternalParams } from "./handler-context.js";
|
|
27
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
25
28
|
|
|
26
29
|
// Re-export types and cookie utilities for backward compatibility
|
|
27
30
|
export type {
|
|
@@ -112,7 +115,12 @@ function escapeRegex(str: string): string {
|
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
/**
|
|
115
|
-
* Extract params from a pathname using a pattern's regex and param names
|
|
118
|
+
* Extract params from a pathname using a pattern's regex and param names.
|
|
119
|
+
*
|
|
120
|
+
* Values are URL-decoded so apps see the raw string (e.g. "ivo@example.com")
|
|
121
|
+
* instead of the percent-encoded form ("ivo%40example.com"). This matches the
|
|
122
|
+
* contract assumed by ctx.reverse (which re-encodes) and aligns with
|
|
123
|
+
* Express/React Router/Fastify/Koa.
|
|
116
124
|
*/
|
|
117
125
|
export function extractParams(
|
|
118
126
|
pathname: string,
|
|
@@ -124,7 +132,7 @@ export function extractParams(
|
|
|
124
132
|
|
|
125
133
|
const params: Record<string, string> = {};
|
|
126
134
|
for (let i = 0; i < paramNames.length; i++) {
|
|
127
|
-
params[paramNames[i]] = match[i + 1] || "";
|
|
135
|
+
params[paramNames[i]] = safeDecodeURIComponent(match[i + 1] || "");
|
|
128
136
|
}
|
|
129
137
|
return params;
|
|
130
138
|
}
|
|
@@ -179,14 +187,22 @@ export function createMiddlewareContext<TEnv>(
|
|
|
179
187
|
return responseHolder.response;
|
|
180
188
|
};
|
|
181
189
|
|
|
190
|
+
// Capture reqCtx once: the request-scoped platform fields
|
|
191
|
+
// (originalUrl, executionContext, waitUntil) are immutable per request,
|
|
192
|
+
// so snapshotting beats re-reading ALS on every access. The lazy getters
|
|
193
|
+
// below (routeName, theme, setTheme) stay lazy because those can change
|
|
194
|
+
// during `await next()`.
|
|
195
|
+
const reqCtx = _getRequestContext();
|
|
182
196
|
return {
|
|
183
197
|
request,
|
|
184
198
|
url,
|
|
185
|
-
originalUrl: new URL(request.url),
|
|
199
|
+
originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
|
|
186
200
|
pathname: url.pathname,
|
|
187
201
|
searchParams: url.searchParams,
|
|
188
202
|
env: env as MiddlewareContext<TEnv>["env"],
|
|
189
203
|
params,
|
|
204
|
+
executionContext: reqCtx?.executionContext,
|
|
205
|
+
waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
|
|
190
206
|
// Getter: re-derives from request context on each access so that global
|
|
191
207
|
// middleware sees the matched route name after await next().
|
|
192
208
|
get routeName(): MiddlewareContext<TEnv>["routeName"] {
|
|
@@ -360,6 +376,11 @@ export async function executeMiddleware<TEnv>(
|
|
|
360
376
|
});
|
|
361
377
|
}
|
|
362
378
|
|
|
379
|
+
if (isWebSocketUpgradeResponse(response)) {
|
|
380
|
+
responseHolder.response = response;
|
|
381
|
+
return response;
|
|
382
|
+
}
|
|
383
|
+
|
|
363
384
|
// Clone response with merged headers (mutable for post-next() modifications)
|
|
364
385
|
responseHolder.response = new Response(response.body, {
|
|
365
386
|
status: response.status,
|
|
@@ -426,8 +447,16 @@ export async function executeMiddleware<TEnv>(
|
|
|
426
447
|
try {
|
|
427
448
|
result = await entry.handler(ctx, wrappedNext);
|
|
428
449
|
} catch (error) {
|
|
429
|
-
|
|
430
|
-
|
|
450
|
+
// Thrown Response is short-circuit control flow, not an error.
|
|
451
|
+
// Fall through to the `if (result instanceof Response)` branch below
|
|
452
|
+
// so stub headers and request-context cookies merge as they do for
|
|
453
|
+
// an explicit `return new Response(...)`. Real errors propagate.
|
|
454
|
+
if (error instanceof Response) {
|
|
455
|
+
result = error;
|
|
456
|
+
} else {
|
|
457
|
+
finishMiddleware();
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
431
460
|
}
|
|
432
461
|
finishMiddleware();
|
|
433
462
|
|
|
@@ -451,6 +480,10 @@ export async function executeMiddleware<TEnv>(
|
|
|
451
480
|
// RequestContext stub headers (from ctx.setCookie) into the
|
|
452
481
|
// returned Response so they are not lost.
|
|
453
482
|
if (result instanceof Response) {
|
|
483
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
484
|
+
responseHolder.response = result;
|
|
485
|
+
return result;
|
|
486
|
+
}
|
|
454
487
|
const mergedHeaders = new Headers(result.headers);
|
|
455
488
|
stubResponse.headers.forEach((value, name) => {
|
|
456
489
|
if (name.toLowerCase() === "set-cookie") {
|
|
@@ -527,8 +560,11 @@ export async function executeMiddleware<TEnv>(
|
|
|
527
560
|
// last merge point (e.g. cookies().set() called after await next()).
|
|
528
561
|
// The reqCtx stub may have already been partially merged during finalHandler
|
|
529
562
|
// or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
|
|
563
|
+
//
|
|
564
|
+
// Skip for upgrade responses: upgrade headers are semantically immutable and
|
|
565
|
+
// set-cookie on an upgrade is not meaningful.
|
|
530
566
|
const reqCtx = _getRequestContext();
|
|
531
|
-
if (reqCtx) {
|
|
567
|
+
if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
|
|
532
568
|
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
533
569
|
if (stubCookies.length > 0) {
|
|
534
570
|
const existingCookies = new Set(finalResponse.headers.getSetCookie());
|
|
@@ -613,7 +649,18 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
613
649
|
return next();
|
|
614
650
|
};
|
|
615
651
|
|
|
616
|
-
|
|
652
|
+
let result: Response | void;
|
|
653
|
+
try {
|
|
654
|
+
result = await middleware(ctx, guardedNext);
|
|
655
|
+
} catch (error) {
|
|
656
|
+
// Thrown Response is short-circuit control flow, parity with the
|
|
657
|
+
// explicit-return path below. Real errors propagate.
|
|
658
|
+
if (error instanceof Response) {
|
|
659
|
+
result = error;
|
|
660
|
+
} else {
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
617
664
|
|
|
618
665
|
if (result instanceof Response) {
|
|
619
666
|
earlyResponse = result;
|
|
@@ -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
|
|
@@ -82,6 +83,13 @@ export interface CompiledPattern {
|
|
|
82
83
|
paramNames: string[];
|
|
83
84
|
optionalParams: Set<string>;
|
|
84
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[]>;
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
// Module-level cache for compiled patterns. Route patterns are a finite set
|
|
@@ -142,6 +150,7 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
142
150
|
const segments = parsePattern(normalizedPattern);
|
|
143
151
|
const paramNames: string[] = [];
|
|
144
152
|
const optionalParams = new Set<string>();
|
|
153
|
+
let constraints: Record<string, string[]> | undefined;
|
|
145
154
|
|
|
146
155
|
let regexPattern = "";
|
|
147
156
|
|
|
@@ -152,11 +161,14 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
152
161
|
} else if (segment.type === "param") {
|
|
153
162
|
paramNames.push(segment.value);
|
|
154
163
|
const suffixPattern = segment.suffix ? escapeRegex(segment.suffix) : "";
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
}
|
|
160
172
|
|
|
161
173
|
if (segment.optional) {
|
|
162
174
|
optionalParams.add(segment.value);
|
|
@@ -186,9 +198,35 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
186
198
|
paramNames,
|
|
187
199
|
optionalParams,
|
|
188
200
|
hasTrailingSlash,
|
|
201
|
+
...(constraints ? { constraints } : {}),
|
|
189
202
|
};
|
|
190
203
|
}
|
|
191
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. Absent optionals (key missing or `undefined`) are allowed;
|
|
209
|
+
* `""` is also tolerated as "absent" so user-provided params or fixtures
|
|
210
|
+
* that pass empty strings explicitly behave the same way.
|
|
211
|
+
*/
|
|
212
|
+
function satisfiesConstraints(
|
|
213
|
+
params: Record<string, string>,
|
|
214
|
+
constraints: Record<string, string[]> | undefined,
|
|
215
|
+
): boolean {
|
|
216
|
+
if (!constraints) return true;
|
|
217
|
+
for (const name in constraints) {
|
|
218
|
+
const value = params[name];
|
|
219
|
+
if (
|
|
220
|
+
value !== undefined &&
|
|
221
|
+
value !== "" &&
|
|
222
|
+
!constraints[name].includes(value)
|
|
223
|
+
) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
192
230
|
/**
|
|
193
231
|
* Escape special regex characters in a string
|
|
194
232
|
*/
|
|
@@ -196,6 +234,27 @@ function escapeRegex(str: string): string {
|
|
|
196
234
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
197
235
|
}
|
|
198
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Build the named-params record from a regex match. Optional segments that
|
|
239
|
+
* didn't capture leave the corresponding group `undefined`; we skip those
|
|
240
|
+
* keys so `ctx.params.<name>` reads as `undefined` rather than `""`. This
|
|
241
|
+
* keeps the runtime aligned with the `ExtractParams` type and matches the
|
|
242
|
+
* trie matcher's contract (see `trie-matching.ts:validateAndBuild`).
|
|
243
|
+
*/
|
|
244
|
+
function buildParamsFromMatch(
|
|
245
|
+
match: RegExpExecArray,
|
|
246
|
+
paramNames: string[],
|
|
247
|
+
): Record<string, string> {
|
|
248
|
+
const params: Record<string, string> = {};
|
|
249
|
+
paramNames.forEach((name, index) => {
|
|
250
|
+
const captured = match[index + 1];
|
|
251
|
+
if (captured !== undefined) {
|
|
252
|
+
params[name] = safeDecodeURIComponent(captured);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
return params;
|
|
256
|
+
}
|
|
257
|
+
|
|
199
258
|
/**
|
|
200
259
|
* Extract the static prefix from a route pattern.
|
|
201
260
|
* Returns everything before the first param/wildcard.
|
|
@@ -247,8 +306,10 @@ export function extractStaticPrefix(pattern: string): string {
|
|
|
247
306
|
/**
|
|
248
307
|
* Match a pathname against registered routes
|
|
249
308
|
*
|
|
250
|
-
* Note: Optional params that are absent in the path
|
|
251
|
-
*
|
|
309
|
+
* Note: Optional params that are absent in the path are omitted from the
|
|
310
|
+
* returned `params` (read as `undefined`), matching the trie matcher and
|
|
311
|
+
* the `ExtractParams<"/:locale?/...">` type. Use the pattern definition or
|
|
312
|
+
* `optionalParams` to determine which keys are optional.
|
|
252
313
|
*
|
|
253
314
|
* Trailing slash handling (priority order):
|
|
254
315
|
* 1. Per-route `trailingSlash` config from route()
|
|
@@ -392,8 +453,13 @@ export function findMatch<TEnv>(
|
|
|
392
453
|
fullPattern = entry.prefix + pattern;
|
|
393
454
|
}
|
|
394
455
|
|
|
395
|
-
const {
|
|
396
|
-
|
|
456
|
+
const {
|
|
457
|
+
regex,
|
|
458
|
+
paramNames,
|
|
459
|
+
optionalParams,
|
|
460
|
+
hasTrailingSlash,
|
|
461
|
+
constraints,
|
|
462
|
+
} = getCompiledPattern(fullPattern);
|
|
397
463
|
|
|
398
464
|
// Get trailing slash mode for this route (per-route config or pattern-based)
|
|
399
465
|
const trailingSlashMode: TrailingSlashMode | undefined =
|
|
@@ -410,10 +476,13 @@ export function findMatch<TEnv>(
|
|
|
410
476
|
// Try exact match first
|
|
411
477
|
const match = regex.exec(pathname);
|
|
412
478
|
if (match) {
|
|
413
|
-
const params
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
479
|
+
const params = buildParamsFromMatch(match, paramNames);
|
|
480
|
+
|
|
481
|
+
// Validate constraints against decoded values; a failure falls
|
|
482
|
+
// through to the next route so other patterns can still match.
|
|
483
|
+
if (!satisfiesConstraints(params, constraints)) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
417
486
|
|
|
418
487
|
if (effectiveDebug) {
|
|
419
488
|
debugLog("findMatch", "matched route", {
|
|
@@ -465,10 +534,11 @@ export function findMatch<TEnv>(
|
|
|
465
534
|
// Try alternate pathname (opposite trailing slash)
|
|
466
535
|
const altMatch = regex.exec(alternatePathname);
|
|
467
536
|
if (altMatch) {
|
|
468
|
-
const params
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
537
|
+
const params = buildParamsFromMatch(altMatch, paramNames);
|
|
538
|
+
|
|
539
|
+
if (!satisfiesConstraints(params, constraints)) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
472
542
|
|
|
473
543
|
// Determine redirect behavior based on mode
|
|
474
544
|
if (trailingSlashMode === "ignore") {
|
|
@@ -59,6 +59,14 @@ interface EvaluateRevalidationOptions<TEnv> {
|
|
|
59
59
|
stale?: boolean;
|
|
60
60
|
/** Trace source hint for the revalidation trace */
|
|
61
61
|
traceSource?: RevalidationTraceEntry["source"];
|
|
62
|
+
/**
|
|
63
|
+
* Override the segment-type-derived default. When set, the value is used as
|
|
64
|
+
* the seed `defaultShouldRevalidate` passed to user revalidate fns and the
|
|
65
|
+
* reason flows into the trace. Callers use this when client-knowledge
|
|
66
|
+
* (e.g. parallel slot not in clientSegmentIds) should dictate the seed
|
|
67
|
+
* instead of the params/method-based heuristic.
|
|
68
|
+
*/
|
|
69
|
+
defaultOverride?: { value: boolean; reason: string };
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
/**
|
|
@@ -81,6 +89,7 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
81
89
|
actionContext,
|
|
82
90
|
stale,
|
|
83
91
|
traceSource,
|
|
92
|
+
defaultOverride,
|
|
84
93
|
} = options;
|
|
85
94
|
const nextParams = segment.params || {};
|
|
86
95
|
const paramsChanged = !paramsEqual(nextParams, prevParams);
|
|
@@ -110,7 +119,12 @@ export async function evaluateRevalidation<TEnv>(
|
|
|
110
119
|
let defaultShouldRevalidate: boolean;
|
|
111
120
|
let defaultReason: string;
|
|
112
121
|
|
|
113
|
-
if (
|
|
122
|
+
if (defaultOverride) {
|
|
123
|
+
// Caller injected the seed (e.g. parallel slot not in clientSegmentIds).
|
|
124
|
+
// Skip the type-derived heuristic — caller knows better in this context.
|
|
125
|
+
defaultShouldRevalidate = defaultOverride.value;
|
|
126
|
+
defaultReason = defaultOverride.reason;
|
|
127
|
+
} else if (request.method === "POST") {
|
|
114
128
|
// Actions: revalidate segments that belong to the route, skip parent chain
|
|
115
129
|
if (segment.type === "route") {
|
|
116
130
|
// Route segment always revalidates on actions
|
|
@@ -515,6 +515,14 @@ export async function resolveParallelEntry<TEnv>(
|
|
|
515
515
|
if (handler === undefined) {
|
|
516
516
|
continue;
|
|
517
517
|
}
|
|
518
|
+
// Pin `_currentSegmentId` to the slot's own id so handle pushes from
|
|
519
|
+
// inside the slot handler get their own bucket in the HandleStore.
|
|
520
|
+
// Parent-keying would collapse them into the parent layout's bucket;
|
|
521
|
+
// the partial-update merge then replaces the parent's bucket on a
|
|
522
|
+
// slot-only revalidation and drops layout-pushed Meta/Breadcrumbs.
|
|
523
|
+
// filterSegmentOrder() retains slot ids so the client preserves them.
|
|
524
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
525
|
+
`${parentShortCode}.${slot}`;
|
|
518
526
|
const doneParallelHandler = track(
|
|
519
527
|
`handler:${parallelEntry.id}.${slot}`,
|
|
520
528
|
2,
|