@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126
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 +6 -4
- package/dist/bin/rango.js +3 -4
- package/dist/vite/index.js +315 -68
- package/package.json +19 -18
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/hooks/SKILL.md +2 -2
- package/skills/route/SKILL.md +6 -0
- package/skills/server-actions/SKILL.md +25 -1
- package/skills/testing/SKILL.md +17 -17
- package/skills/testing/cache-prerender.md +29 -3
- package/skills/testing/flight.md +13 -10
- package/skills/testing/render-handler.md +3 -0
- package/skills/testing/server-tree.md +1 -1
- package/skills/testing/setup.md +1 -1
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +10 -0
- package/src/browser/event-controller.ts +1 -83
- package/src/browser/navigation-store-handle.ts +3 -4
- package/src/browser/navigation-store.ts +0 -39
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +23 -84
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +2 -23
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -45
- package/src/browser/react/location-state-shared.ts +0 -13
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-router.ts +2 -1
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/rsc-router.tsx +10 -3
- package/src/browser/server-action-bridge.ts +51 -3
- package/src/browser/types.ts +23 -5
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/index.ts +8 -9
- package/src/build/route-trie.ts +46 -11
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/router-processing.ts +0 -8
- package/src/cache/cache-policy.ts +0 -54
- package/src/cache/cache-runtime.ts +48 -24
- package/src/cache/cache-scope.ts +0 -27
- package/src/cache/cache-tag.ts +0 -37
- package/src/cache/cf/cf-cache-store.ts +72 -45
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +10 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +0 -52
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/types.ts +0 -98
- package/src/client.rsc.tsx +4 -22
- package/src/client.tsx +19 -32
- package/src/context-var.ts +12 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +2 -12
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +6 -0
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +0 -16
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +27 -2
- package/src/index.ts +7 -0
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +4 -15
- package/src/loader.ts +3 -9
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +23 -30
- package/src/prerender.ts +34 -0
- package/src/redirect-origin.ts +100 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +1 -44
- package/src/route-definition/dsl-helpers.ts +7 -19
- package/src/route-definition/helpers-types.ts +3 -3
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-map-builder.ts +0 -16
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -31
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +25 -23
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +1 -25
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +0 -43
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +96 -179
- package/src/router/match-middleware/cache-store.ts +0 -31
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +1 -52
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +77 -60
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +5 -56
- package/src/router/prerender-match.ts +56 -51
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +14 -62
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +10 -0
- package/src/router/segment-resolution/fresh.ts +25 -57
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +35 -23
- package/src/router/segment-resolution/revalidation.ts +188 -283
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +0 -22
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +66 -45
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +8 -11
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +20 -4
- package/src/rsc/helpers.ts +71 -3
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/origin-guard.ts +9 -15
- package/src/rsc/progressive-enhancement.ts +10 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-route-handler.ts +23 -18
- package/src/rsc/rsc-rendering.ts +2 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +34 -29
- package/src/rsc/types.ts +6 -3
- package/src/search-params.ts +0 -16
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +79 -88
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +29 -92
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +2 -27
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +1 -24
- package/src/testing/dispatch.ts +43 -6
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/flight-matchers.ts +0 -13
- package/src/testing/flight-normalize.ts +3 -30
- package/src/testing/flight.ts +46 -48
- package/src/testing/generated-routes.ts +1 -41
- package/src/testing/index.ts +1 -21
- package/src/testing/internal/context.ts +3 -45
- package/src/testing/internal/seed-vars.ts +0 -26
- package/src/testing/render-handler.ts +31 -61
- package/src/testing/render-route.tsx +75 -103
- package/src/testing/run-loader.ts +0 -96
- package/src/testing/run-middleware.ts +0 -26
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/error-types.ts +25 -89
- package/src/types/global-namespace.ts +4 -14
- package/src/types/handler-context.ts +28 -9
- package/src/types/index.ts +0 -10
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +0 -13
- package/src/urls/include-helper.ts +0 -4
- package/src/urls/index.ts +0 -6
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/path-helper.ts +0 -54
- package/src/urls/urls-function.ts +0 -13
- package/src/use-loader.tsx +0 -186
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/discover-routers.ts +28 -18
- package/src/vite/discovery/prerender-collection.ts +2 -4
- package/src/vite/discovery/state.ts +5 -0
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +35 -9
- package/src/vite/plugins/cjs-to-esm.ts +0 -11
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +0 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
- package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +10 -0
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/version-injector.ts +0 -20
- package/src/vite/plugins/version-plugin.ts +1 -49
- package/src/vite/plugins/virtual-entries.ts +0 -15
- package/src/vite/rango.ts +2 -108
- package/src/vite/router-discovery.ts +9 -1
- package/src/vite/utils/ast-handler-extract.ts +0 -16
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +0 -4
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -35
- package/src/vite/utils/shared-utils.ts +3 -35
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
isBrowserDebugEnabled,
|
|
24
24
|
startBrowserTransaction,
|
|
25
25
|
} from "./logging.js";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
validateRedirectOrigin,
|
|
28
|
+
validateExternalRedirect,
|
|
29
|
+
} from "./validate-redirect-origin.js";
|
|
27
30
|
import {
|
|
28
31
|
extractRscHeaderUrl,
|
|
29
32
|
emptyResponse,
|
|
@@ -83,6 +86,9 @@ export function createServerActionBridge(
|
|
|
83
86
|
|
|
84
87
|
// SPA-navigate when onNavigate is set, else hard-reload. state is omitted (not
|
|
85
88
|
// passed as undefined) to match the header path's prior call shape.
|
|
89
|
+
// Callers pass an already same-origin-validated url; the hard-reload fallback
|
|
90
|
+
// re-validates defensively so this leaf cannot become an open redirect if a
|
|
91
|
+
// future caller forgets (the SPA path validates inside the navigation bridge).
|
|
86
92
|
async function dispatchRedirect(url: string, state?: unknown): Promise<void> {
|
|
87
93
|
if (onNavigate) {
|
|
88
94
|
await onNavigate(url, {
|
|
@@ -91,7 +97,10 @@ export function createServerActionBridge(
|
|
|
91
97
|
_skipCache: true,
|
|
92
98
|
});
|
|
93
99
|
} else {
|
|
94
|
-
window.location.
|
|
100
|
+
const safe = validateRedirectOrigin(url, window.location.origin);
|
|
101
|
+
if (safe) {
|
|
102
|
+
window.location.href = safe;
|
|
103
|
+
}
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
106
|
|
|
@@ -161,6 +170,15 @@ export function createServerActionBridge(
|
|
|
161
170
|
// Whether the action's response carried the keepClientCache() directive.
|
|
162
171
|
// Set when the response arrives; gates the deferred invalidation below.
|
|
163
172
|
let keepCache = false;
|
|
173
|
+
// Whether a Response actually settled from the network (the server saw the
|
|
174
|
+
// request). Set true as the first statement in the fetch .then() below.
|
|
175
|
+
// Gates the automatic invalidation: a pre-dispatch failure (encodeReply
|
|
176
|
+
// throw or a fetch rejection — server unreachable/DNS/connection refused)
|
|
177
|
+
// leaves this false, so finalizeAction() must NOT invalidate or broadcast —
|
|
178
|
+
// nothing reached the server, so nothing could have mutated. A failed Flight
|
|
179
|
+
// DECODE after the response arrived keeps it true (the mutation may have
|
|
180
|
+
// committed, so invalidating the now-possibly-stale client cache is correct).
|
|
181
|
+
let responseReceived = false;
|
|
164
182
|
// Single deferred invalidation + fence release, run exactly ONCE however the
|
|
165
183
|
// action terminates (normal, redirect, error, abort, intercept, concurrent).
|
|
166
184
|
// This replaces main's eager clear at action start: every directive-free
|
|
@@ -176,7 +194,10 @@ export function createServerActionBridge(
|
|
|
176
194
|
actionFinalized = true;
|
|
177
195
|
// finally so a throw in invalidation cannot leak the fence (latch is set).
|
|
178
196
|
try {
|
|
179
|
-
|
|
197
|
+
// responseReceived gates the automatic invalidation: a pre-dispatch
|
|
198
|
+
// failure (serialize throw / fetch reject) never reached the server, so
|
|
199
|
+
// marking the cache stale + broadcasting cross-tab would be spurious.
|
|
200
|
+
if (responseReceived && !keepCache && !skipInvalidation) {
|
|
180
201
|
store.markCacheAsStaleAndBroadcast();
|
|
181
202
|
}
|
|
182
203
|
} finally {
|
|
@@ -263,6 +284,12 @@ export function createServerActionBridge(
|
|
|
263
284
|
body: encodedBody,
|
|
264
285
|
signal: fetchAbort.signal,
|
|
265
286
|
}).then(async (response) => {
|
|
287
|
+
// A settled fetch promise means the request reached the server and a
|
|
288
|
+
// Response came back (true for 2xx, 4xx, AND 5xx — fetch only rejects
|
|
289
|
+
// on network-layer failure, never on HTTP status). Record it as the
|
|
290
|
+
// first statement so every downstream terminal can invalidate; a
|
|
291
|
+
// pre-dispatch failure never gets here and stays gated out.
|
|
292
|
+
responseReceived = true;
|
|
266
293
|
// Response arrived — disconnect fetch abort from handle abort so
|
|
267
294
|
// abortAllActions() doesn't disrupt the in-progress Flight stream.
|
|
268
295
|
handle.signal.removeEventListener("abort", onHandleAbort);
|
|
@@ -399,6 +426,27 @@ export function createServerActionBridge(
|
|
|
399
426
|
// Check handle.signal.aborted to avoid redirecting from a stale action
|
|
400
427
|
// when the user has already navigated away.
|
|
401
428
|
if (metadata?.redirect && !handle.signal.aborted) {
|
|
429
|
+
// Explicit off-host redirect (redirect(url, { external: true })):
|
|
430
|
+
// hard-navigate, but still scheme-validate (http/https only). external
|
|
431
|
+
// waives the same-origin check, NOT scheme safety, so a forged payload
|
|
432
|
+
// carrying a javascript:/data: URL cannot script via location.assign.
|
|
433
|
+
if (metadata.redirect.external) {
|
|
434
|
+
const externalUrl = validateExternalRedirect(
|
|
435
|
+
metadata.redirect.url,
|
|
436
|
+
window.location.origin,
|
|
437
|
+
);
|
|
438
|
+
if (!externalUrl) {
|
|
439
|
+
log("blocked external action redirect payload", {
|
|
440
|
+
url: metadata.redirect.url,
|
|
441
|
+
});
|
|
442
|
+
handle.complete(returnValue?.data);
|
|
443
|
+
return returnValue?.data;
|
|
444
|
+
}
|
|
445
|
+
log("external action redirect", { url: externalUrl });
|
|
446
|
+
handle.complete(returnValue?.data);
|
|
447
|
+
window.location.assign(externalUrl);
|
|
448
|
+
return returnValue?.data;
|
|
449
|
+
}
|
|
402
450
|
const redirectUrl = validateRedirectOrigin(
|
|
403
451
|
metadata.redirect.url,
|
|
404
452
|
window.location.origin,
|
package/src/browser/types.ts
CHANGED
|
@@ -14,7 +14,6 @@ import type { RenderSegmentsOptions } from "../segment-system.js";
|
|
|
14
14
|
export interface RscPayload<TMetadata = RscMetadata> {
|
|
15
15
|
metadata?: TMetadata;
|
|
16
16
|
returnValue?: ActionResult;
|
|
17
|
-
formState?: unknown;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
/**
|
|
@@ -91,8 +90,12 @@ export interface RscMetadata {
|
|
|
91
90
|
basename?: string;
|
|
92
91
|
/** Whether connection warmup is enabled */
|
|
93
92
|
warmupEnabled?: boolean;
|
|
94
|
-
/**
|
|
95
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Server-side redirect with optional state (for partial requests).
|
|
95
|
+
* `external: true` (from redirect(url, { external: true })) tells the client
|
|
96
|
+
* to hard-navigate to an off-host target instead of validating same-origin.
|
|
97
|
+
*/
|
|
98
|
+
redirect?: { url: string; external?: boolean };
|
|
96
99
|
/** Server-set location state to include in history.pushState */
|
|
97
100
|
locationState?: Record<string, unknown>;
|
|
98
101
|
}
|
|
@@ -193,6 +196,15 @@ export interface TrackedActionState {
|
|
|
193
196
|
result: unknown | null;
|
|
194
197
|
}
|
|
195
198
|
|
|
199
|
+
/**
|
|
200
|
+
* The value returned by {@link useAction} when called without a selector.
|
|
201
|
+
*
|
|
202
|
+
* This is the stable, public name for the action-state shape; consumers can
|
|
203
|
+
* name it in their own signatures (e.g. a wrapper hook). It aliases the
|
|
204
|
+
* internal {@link TrackedActionState}.
|
|
205
|
+
*/
|
|
206
|
+
export type ActionState = TrackedActionState;
|
|
207
|
+
|
|
196
208
|
/**
|
|
197
209
|
* Listener for action state changes
|
|
198
210
|
*
|
|
@@ -330,8 +342,14 @@ export interface RouterInstance {
|
|
|
330
342
|
replace(url: string, options?: RouterNavigateOptions): Promise<void>;
|
|
331
343
|
/** Refresh the current route (re-fetch server data, preserve client state) */
|
|
332
344
|
refresh(): Promise<void>;
|
|
333
|
-
/**
|
|
334
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Prefetch a URL for faster client-side transition.
|
|
347
|
+
*
|
|
348
|
+
* Pass `{ key: ":source" }` to source-scope the prefetch cache entry (parity
|
|
349
|
+
* with `<Link prefetchKey=":source">`) when the target's response can differ
|
|
350
|
+
* by source page.
|
|
351
|
+
*/
|
|
352
|
+
prefetch(url: string, options?: { key?: ":source" }): void;
|
|
335
353
|
/** Go back in browser history */
|
|
336
354
|
back(): void;
|
|
337
355
|
/** Go forward in browser history */
|
|
@@ -1,29 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveSameOriginRedirect,
|
|
3
|
+
resolveExternalRedirect,
|
|
4
|
+
} from "../redirect-origin.js";
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Validate that a client-consumed redirect URL (from headers or Flight payload)
|
|
3
8
|
* targets the same origin as the current page. Prevents open-redirect attacks
|
|
4
9
|
* via crafted responses.
|
|
5
10
|
*
|
|
11
|
+
* Thin wrapper over the shared {@link resolveSameOriginRedirect} rule (also used
|
|
12
|
+
* by the server guard in `rsc/redirect-guard.ts`) so client and server enforce
|
|
13
|
+
* the identical same-origin contract. Adds the client-side `console.error` on a
|
|
14
|
+
* block; the resolver itself stays pure.
|
|
15
|
+
*
|
|
6
16
|
* @returns The canonical (normalized) URL string on success, or null if blocked.
|
|
7
17
|
*/
|
|
8
18
|
export function validateRedirectOrigin(
|
|
9
19
|
url: string,
|
|
10
20
|
currentOrigin: string,
|
|
11
21
|
): string | null {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const resolved = resolveSameOriginRedirect(url, currentOrigin);
|
|
23
|
+
if (resolved === null) {
|
|
24
|
+
console.error(
|
|
25
|
+
`[rango] Redirect blocked: cross-origin or invalid target "${url}"`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return resolved;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validate an explicit off-origin redirect (`redirect(url, { external: true })`)
|
|
33
|
+
* the client is about to hard-navigate to via `window.location.assign()`.
|
|
34
|
+
*
|
|
35
|
+
* Thin wrapper over the shared {@link resolveExternalRedirect} rule (also used by
|
|
36
|
+
* the server guard in `rsc/redirect-guard.ts`) so client and server enforce the
|
|
37
|
+
* identical contract: `external` allows an off-origin target but only an
|
|
38
|
+
* http(s) scheme. This stops a forged or mistaken external payload carrying a
|
|
39
|
+
* `javascript:`/`data:` URL from turning `location.assign` into a scriptable
|
|
40
|
+
* navigation. Adds the client-side `console.error` on a block; the resolver
|
|
41
|
+
* itself stays pure.
|
|
42
|
+
*
|
|
43
|
+
* @returns The normalized URL string on success, or null if blocked.
|
|
44
|
+
*/
|
|
45
|
+
export function validateExternalRedirect(
|
|
46
|
+
url: string,
|
|
47
|
+
currentOrigin: string,
|
|
48
|
+
): string | null {
|
|
49
|
+
const resolved = resolveExternalRedirect(url, currentOrigin);
|
|
50
|
+
if (resolved === null) {
|
|
51
|
+
console.error(
|
|
52
|
+
`[rango] External redirect blocked: non-http(s) target "${url}"`,
|
|
53
|
+
);
|
|
28
54
|
}
|
|
55
|
+
return resolved;
|
|
29
56
|
}
|
package/src/build/index.ts
CHANGED
|
@@ -22,15 +22,14 @@ export {
|
|
|
22
22
|
type GeneratedManifest,
|
|
23
23
|
} from "./generate-manifest.js";
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
// buildRouteTrie / buildPerRouterTrie / collectFallbackClientRefs and the
|
|
26
|
+
// TrieNode/TrieLeaf types are NOT exported here: they are build-pipeline
|
|
27
|
+
// internals, not public API. Their only build-time consumer (the Vite
|
|
28
|
+
// discovery pass) imports them directly from source via a relative path
|
|
29
|
+
// (vite/discovery/discover-routers.ts), and the runtime RSC realm likewise
|
|
30
|
+
// imports route-trie.js directly (rsc/manifest-init.ts). Keeping them off the
|
|
31
|
+
// public ./build surface (#569 decision 6) means consumers can't mistake them
|
|
32
|
+
// for intended API. generateManifest* / route-types / hashParams stay public.
|
|
34
33
|
export {
|
|
35
34
|
writePerModuleRouteTypes,
|
|
36
35
|
extractRoutesFromSource,
|
package/src/build/route-trie.ts
CHANGED
|
@@ -22,9 +22,6 @@ export interface TrieLeaf {
|
|
|
22
22
|
sp: string;
|
|
23
23
|
/** Ancestry shortCodes from root to route [M0L0, M0L0L0, M0L0L0R499] */
|
|
24
24
|
a: string[];
|
|
25
|
-
/** Optional param names declared on the route. Absent params are
|
|
26
|
-
* omitted from the matched params record (read as `undefined`). */
|
|
27
|
-
op?: string[];
|
|
28
25
|
/** Constraint validation: paramName -> allowed values */
|
|
29
26
|
cv?: Record<string, string[]>;
|
|
30
27
|
/** Ordered param names for this route (positional) */
|
|
@@ -63,6 +60,9 @@ export interface TrieNode {
|
|
|
63
60
|
* @param routeAncestry - Map of route name to ancestry shortCodes
|
|
64
61
|
* @param routeToStaticPrefix - Map of route name to its entry's staticPrefix
|
|
65
62
|
* @param routeTrailingSlash - Optional map of route name to trailing slash mode
|
|
63
|
+
* @param prerenderRouteNames - Optional set of prerendered route names (sets leaf.pr)
|
|
64
|
+
* @param passthroughRouteNames - Optional set of passthrough route names (sets leaf.pt)
|
|
65
|
+
* @param responseTypeRoutes - Optional map of route name to response type (sets leaf.rt)
|
|
66
66
|
*/
|
|
67
67
|
export function buildRouteTrie(
|
|
68
68
|
routeManifest: Record<string, string>,
|
|
@@ -97,9 +97,49 @@ export function buildRouteTrie(
|
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
sortSuffixParams(root);
|
|
100
101
|
return root;
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Sort every node's suffix-param map (`node.xp`) by descending suffix length so
|
|
106
|
+
* the matcher tries the most specific suffix first. Overlapping suffixes like
|
|
107
|
+
* `.min.js` and `.js` must resolve by specificity, not route declaration order:
|
|
108
|
+
* a request for `/app.min.js` should match `:file.min.js`, not `:file.js`.
|
|
109
|
+
*
|
|
110
|
+
* This started as a bug — `walkTrie` iterates `node.xp` in object order and
|
|
111
|
+
* returns the first suffix the segment ends with, so the winner depended on
|
|
112
|
+
* which route was declared first. Sorting at build time fixes it allocation-free
|
|
113
|
+
* on the match hot path: the serialized production trie preserves this key order
|
|
114
|
+
* through JSON.parse, so dev (per-request rebuild) and production match
|
|
115
|
+
* identically. Array.prototype.sort is stable (ES2019+), so equal-length
|
|
116
|
+
* suffixes keep their declaration order — the router's existing tiebreak.
|
|
117
|
+
*/
|
|
118
|
+
function sortSuffixParams(node: TrieNode): void {
|
|
119
|
+
if (node.xp) {
|
|
120
|
+
const sorted: Record<string, { n: string; c: TrieNode }> = {};
|
|
121
|
+
for (const suffix of Object.keys(node.xp).sort(
|
|
122
|
+
(a, b) => b.length - a.length,
|
|
123
|
+
)) {
|
|
124
|
+
sorted[suffix] = node.xp[suffix];
|
|
125
|
+
}
|
|
126
|
+
node.xp = sorted;
|
|
127
|
+
}
|
|
128
|
+
if (node.s) {
|
|
129
|
+
for (const child of Object.values(node.s)) {
|
|
130
|
+
sortSuffixParams(child);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (node.p) {
|
|
134
|
+
sortSuffixParams(node.p.c);
|
|
135
|
+
}
|
|
136
|
+
if (node.xp) {
|
|
137
|
+
for (const child of Object.values(node.xp)) {
|
|
138
|
+
sortSuffixParams(child.c);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
103
143
|
/**
|
|
104
144
|
* Build a per-router trie from a generated manifest. This is the single
|
|
105
145
|
* construction path shared by build/discovery (discover-routers.ts, serialized
|
|
@@ -155,18 +195,14 @@ function insertRoute(
|
|
|
155
195
|
node: TrieNode,
|
|
156
196
|
segments: ParsedSegment[],
|
|
157
197
|
index: number,
|
|
158
|
-
leaf: Omit<TrieLeaf, "
|
|
198
|
+
leaf: Omit<TrieLeaf, "cv" | "pa">,
|
|
159
199
|
): void {
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
const optionalParams: string[] = [];
|
|
200
|
+
// cv (full constraint map) is route-level and identical on every terminal,
|
|
201
|
+
// so compute it once on the shared base.
|
|
163
202
|
const constraints: Record<string, string[]> = {};
|
|
164
203
|
|
|
165
204
|
for (const seg of segments) {
|
|
166
205
|
if (seg.type === "param") {
|
|
167
|
-
if (seg.optional) {
|
|
168
|
-
optionalParams.push(seg.value);
|
|
169
|
-
}
|
|
170
206
|
if (seg.constraint) {
|
|
171
207
|
constraints[seg.value] = seg.constraint;
|
|
172
208
|
}
|
|
@@ -175,7 +211,6 @@ function insertRoute(
|
|
|
175
211
|
|
|
176
212
|
const leafBase: Omit<TrieLeaf, "pa"> = {
|
|
177
213
|
...leaf,
|
|
178
|
-
...(optionalParams.length > 0 ? { op: optionalParams } : {}),
|
|
179
214
|
...(Object.keys(constraints).length > 0 ? { cv: constraints } : {}),
|
|
180
215
|
};
|
|
181
216
|
|
|
@@ -37,12 +37,15 @@ export function formatRouteEntry(
|
|
|
37
37
|
): string {
|
|
38
38
|
const hasSearch = search && Object.keys(search).length > 0;
|
|
39
39
|
|
|
40
|
+
// JSON.stringify the pattern and search values so backslashes and quotes in a
|
|
41
|
+
// route pattern (e.g. a custom regex constraint) survive interpolation into
|
|
42
|
+
// both the type-level string and the runtime NamedRoutes value.
|
|
40
43
|
if (!hasSearch) {
|
|
41
|
-
return ` ${key}:
|
|
44
|
+
return ` ${key}: ${JSON.stringify(pattern)},`;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
const searchBody = Object.entries(search!)
|
|
45
|
-
.map(([k, v]) => `${k}:
|
|
48
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
|
|
46
49
|
.join(", ");
|
|
47
|
-
return ` ${key}: { path:
|
|
50
|
+
return ` ${key}: { path: ${JSON.stringify(pattern)}, search: { ${searchBody} } },`;
|
|
48
51
|
}
|
|
@@ -617,9 +617,6 @@ export function writeCombinedRouteTypes(
|
|
|
617
617
|
? readFileSync(outPath, "utf-8")
|
|
618
618
|
: null;
|
|
619
619
|
|
|
620
|
-
// When the static parser can't extract routes (e.g. callback-style urls()),
|
|
621
|
-
// write an empty placeholder so the build-time transform's injected import
|
|
622
|
-
// resolves. Runtime discovery will overwrite this with the real routes.
|
|
623
620
|
if (Object.keys(result.routes).length === 0) {
|
|
624
621
|
if (!existing) {
|
|
625
622
|
const emptySource = generateRouteTypesSource({});
|
|
@@ -635,11 +632,6 @@ export function writeCombinedRouteTypes(
|
|
|
635
632
|
hasSearchSchemas ? result.searchSchemas : undefined,
|
|
636
633
|
);
|
|
637
634
|
if (existing !== source) {
|
|
638
|
-
// On initial dev startup, don't overwrite a file from runtime discovery
|
|
639
|
-
// (which has all dynamic routes) with a smaller set from the static
|
|
640
|
-
// parser. The static parser can't see routes generated by Array.from()
|
|
641
|
-
// or other dynamic code. During HMR (file watcher), always write so
|
|
642
|
-
// newly added routes appear immediately.
|
|
643
635
|
if (opts?.preserveIfLarger && existing) {
|
|
644
636
|
const existingCount = countPublicRouteEntries(existing);
|
|
645
637
|
const newCount = Object.keys(result.routes).filter(
|
|
@@ -69,24 +69,6 @@ export function computeExpiration(
|
|
|
69
69
|
return { staleAt, expiresAt };
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
// ============================================================================
|
|
73
|
-
// Cache Key Resolution
|
|
74
|
-
// ============================================================================
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Resolve cache key using the 3-tier priority:
|
|
78
|
-
* 1. keyFn (full override from route/loader cache options)
|
|
79
|
-
* 2. store.keyGenerator (modifies default key)
|
|
80
|
-
* 3. defaultKey (used when neither keyFn nor keyGenerator is provided)
|
|
81
|
-
*
|
|
82
|
-
* Errors from keyFn and store.keyGenerator propagate to the caller.
|
|
83
|
-
* Cache identity is correctness-critical: if explicit key logic throws,
|
|
84
|
-
* silently remapping to a different key could cause cache collisions or
|
|
85
|
-
* serve stale/wrong data. Callers must handle the error or let it surface.
|
|
86
|
-
*
|
|
87
|
-
* Uses _getRequestContext (non-throwing) so that calls outside ALS
|
|
88
|
-
* (e.g. build-time) gracefully fall back to defaultKey.
|
|
89
|
-
*/
|
|
90
72
|
export async function resolveCacheKey(
|
|
91
73
|
keyFn: ((ctx: RequestContext) => string | Promise<string>) | undefined,
|
|
92
74
|
store: SegmentCacheStore | null,
|
|
@@ -95,34 +77,17 @@ export async function resolveCacheKey(
|
|
|
95
77
|
): Promise<string> {
|
|
96
78
|
const requestCtx = _getRequestContext();
|
|
97
79
|
|
|
98
|
-
// Priority 1: Route/loader-level key function (full override)
|
|
99
80
|
if (keyFn && requestCtx) {
|
|
100
81
|
return await keyFn(requestCtx);
|
|
101
82
|
}
|
|
102
83
|
|
|
103
|
-
// Priority 2: Store-level keyGenerator (modifies default key)
|
|
104
84
|
if (store?.keyGenerator && requestCtx) {
|
|
105
85
|
return await store.keyGenerator(requestCtx, defaultKey);
|
|
106
86
|
}
|
|
107
87
|
|
|
108
|
-
// Priority 3: Default key (no custom key logic provided)
|
|
109
88
|
return defaultKey;
|
|
110
89
|
}
|
|
111
90
|
|
|
112
|
-
// ============================================================================
|
|
113
|
-
// Cache Tag Resolution
|
|
114
|
-
// ============================================================================
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Resolve cache tags from a tags option (static array or function of ctx).
|
|
118
|
-
*
|
|
119
|
-
* Fails open: a thrown tag callback falls back to no tags rather than
|
|
120
|
-
* aborting the request. Tags are additive metadata (not identity), so a
|
|
121
|
-
* missing tag does not cause cache collisions, only a missed invalidation.
|
|
122
|
-
*
|
|
123
|
-
* Shared by the cache() DSL (cache-scope) and loader caching (loader-cache)
|
|
124
|
-
* so tag resolution behaves identically across every cache axis.
|
|
125
|
-
*/
|
|
126
91
|
export function resolveTagsOption<TEnv>(
|
|
127
92
|
tags: string[] | ((ctx: RequestContext<TEnv>) => string[]) | undefined,
|
|
128
93
|
ctx: RequestContext<TEnv> | undefined,
|
|
@@ -131,10 +96,6 @@ export function resolveTagsOption<TEnv>(
|
|
|
131
96
|
if (!tags) return undefined;
|
|
132
97
|
if (typeof tags === "function") {
|
|
133
98
|
if (!ctx) {
|
|
134
|
-
// A dynamic tags function needs the request context to run. Without it
|
|
135
|
-
// (e.g. resolved outside a request, at build/prerender time) the entry is
|
|
136
|
-
// cached UNTAGGED and can never be invalidated - surface that rather than
|
|
137
|
-
// silently dropping the tags, matching the thrown-callback branch below.
|
|
138
99
|
console.warn(
|
|
139
100
|
`[${label}] Dynamic tags function present but no request context; ` +
|
|
140
101
|
`caching without tags (this entry will not be tag-invalidatable).`,
|
|
@@ -167,25 +128,10 @@ function normalizeTagList(tags: string[]): string[] | undefined {
|
|
|
167
128
|
return out.length > 0 ? out : undefined;
|
|
168
129
|
}
|
|
169
130
|
|
|
170
|
-
// ============================================================================
|
|
171
|
-
// Cache Store Resolution
|
|
172
|
-
// ============================================================================
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Resolve cache store from the 2-tier priority:
|
|
176
|
-
* 1. Explicit store from cache options
|
|
177
|
-
* 2. App-level store from request context
|
|
178
|
-
*/
|
|
179
131
|
export function resolveCacheStore(
|
|
180
132
|
explicitStore: SegmentCacheStore | undefined,
|
|
181
133
|
): SegmentCacheStore | null {
|
|
182
134
|
if (explicitStore) {
|
|
183
|
-
// Register explicit per-scope stores so updateTag()/revalidateTag() can
|
|
184
|
-
// reach them. This is the single chokepoint every cache axis (segment,
|
|
185
|
-
// response, loader) resolves through, so registering here covers them all
|
|
186
|
-
// eagerly - no dependence on whether a tagged write has happened yet. The
|
|
187
|
-
// app-level store is intentionally not registered (always reachable via
|
|
188
|
-
// ctx._cacheStore).
|
|
189
135
|
registerExplicitTaggedStore(explicitStore);
|
|
190
136
|
return explicitStore;
|
|
191
137
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
createClientTemporaryReferenceSet,
|
|
22
22
|
} from "@vitejs/plugin-rsc/rsc";
|
|
23
23
|
import { getRequestContext } from "../server/request-context.js";
|
|
24
|
+
import { isUnderTestRunner } from "../runtime-env.js";
|
|
24
25
|
import {
|
|
25
26
|
isTainted,
|
|
26
27
|
CACHED_FN_SYMBOL,
|
|
@@ -46,6 +47,7 @@ import {
|
|
|
46
47
|
runWithCacheTagScope,
|
|
47
48
|
} from "./cache-tag.js";
|
|
48
49
|
import { reportCacheError } from "./cache-error.js";
|
|
50
|
+
import type { CacheItemResult } from "./types.js";
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
53
|
* Convert encodeReply result to a stable string key.
|
|
@@ -58,6 +60,10 @@ async function replyToCacheKey(encoded: string | FormData): Promise<string> {
|
|
|
58
60
|
return text;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
// Cached-fn ids already warned about running uncached under a test runner, so
|
|
64
|
+
// the test-ergonomics warning fires once per fn rather than once per call.
|
|
65
|
+
const warnedUncachedUnderTest = new Set<string>();
|
|
66
|
+
|
|
61
67
|
// ============================================================================
|
|
62
68
|
// Core: registerCachedFunction
|
|
63
69
|
// ============================================================================
|
|
@@ -84,7 +90,28 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
84
90
|
// cacheTag() call inside the function degrades to a no-op rather than
|
|
85
91
|
// throwing "must be called inside a use cache function" - adopting cacheTag()
|
|
86
92
|
// must not hard-fail in apps/tests without an item-capable cache configured.
|
|
93
|
+
// Note: the INSIDE_CACHE_EXEC guard (cookies()/headers()/ctx.set() rejection)
|
|
94
|
+
// is intentionally NOT stamped here. It is a cached-path-only check; in the
|
|
95
|
+
// bypass the body actually executes, so the guarded side effects take effect
|
|
96
|
+
// and nothing is lost on a (non-existent) hit. Same applies to the
|
|
97
|
+
// non-serializable-args bypass below.
|
|
87
98
|
if (!store?.getItem) {
|
|
99
|
+
// Test-ergonomics guard: under a test runner, a "use cache" function that
|
|
100
|
+
// executes with no item-capable store seeded is exercising the UNCACHED
|
|
101
|
+
// path — a green test that proves nothing about caching. Warn once per fn
|
|
102
|
+
// id so the author knows to seed a cacheStore. Advisory (never throws), so
|
|
103
|
+
// a test that DELIBERATELY runs uncached is unaffected. Gated on the test
|
|
104
|
+
// runner (process.env.VITEST, not folded) so production never evaluates it.
|
|
105
|
+
if (isUnderTestRunner() && !warnedUncachedUnderTest.has(id)) {
|
|
106
|
+
warnedUncachedUnderTest.add(id);
|
|
107
|
+
console.warn(
|
|
108
|
+
`[rango] "use cache" function "${id}" executed but no cacheStore was ` +
|
|
109
|
+
`seeded; the cached path is NOT under test (it ran uncached). Pass ` +
|
|
110
|
+
`{ cacheStore, cacheProfiles } to runLoader/runMiddleware/renderHandler/` +
|
|
111
|
+
`runInRequestContext (or configure createRouter({ cache }) for dispatch) ` +
|
|
112
|
+
`to exercise it.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
88
115
|
const scoped = runWithCacheTagScope(() => fn.apply(this, args));
|
|
89
116
|
const result = await scoped.result;
|
|
90
117
|
// Still record the runtime tags into the request set so a cacheTag() in an
|
|
@@ -185,23 +212,29 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
185
212
|
// Cache lookup
|
|
186
213
|
const cached = await store.getItem(cacheKey);
|
|
187
214
|
|
|
215
|
+
// Serve a cached entry on the hit path: deserialize the stored value,
|
|
216
|
+
// replay handle data (gated on tainted args), and surface the entry's tags
|
|
217
|
+
// to the request set (the function did not re-run, so its runtime cacheTag()
|
|
218
|
+
// tags are only available from the stored entry). Shared by the fresh-hit
|
|
219
|
+
// and stale-hit branches; the only divergence is the stale branch scheduling
|
|
220
|
+
// background revalidation, which it does after this returns.
|
|
221
|
+
const serveCached = async (entry: CacheItemResult): Promise<any> => {
|
|
222
|
+
const result = await deserializeResult(entry.value);
|
|
223
|
+
if (entry.handles && hasTaintedArgs) {
|
|
224
|
+
const handleStore = requestCtx?._handleStore;
|
|
225
|
+
if (handleStore) {
|
|
226
|
+
const r = await decodeHandles(entry.handles);
|
|
227
|
+
if (r) restoreHandles(r, handleStore);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
recordRequestTags(entry.tags, requestCtx);
|
|
231
|
+
return result;
|
|
232
|
+
};
|
|
233
|
+
|
|
188
234
|
if (cached && !cached.shouldRevalidate) {
|
|
189
235
|
// Fresh hit: deserialize and return
|
|
190
236
|
try {
|
|
191
|
-
|
|
192
|
-
// Restore handle data if present
|
|
193
|
-
if (cached.handles && hasTaintedArgs) {
|
|
194
|
-
const handleStore = requestCtx?._handleStore;
|
|
195
|
-
if (handleStore) {
|
|
196
|
-
const r = await decodeHandles(cached.handles);
|
|
197
|
-
if (r) restoreHandles(r, handleStore);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
// Surface the hit's tags to the request set so a document built from a
|
|
201
|
-
// cached item is still tagged (the function did not re-run, so its
|
|
202
|
-
// runtime cacheTag() tags are only available from the stored entry).
|
|
203
|
-
recordRequestTags(cached.tags, requestCtx);
|
|
204
|
-
return result;
|
|
237
|
+
return await serveCached(cached);
|
|
205
238
|
} catch (error) {
|
|
206
239
|
// The stored value is corrupt/partial (failed RSC deserialize). Report
|
|
207
240
|
// it, then fall through to fresh execution - the miss path below re-runs
|
|
@@ -217,16 +250,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
217
250
|
if (cached?.shouldRevalidate) {
|
|
218
251
|
// Stale hit: return stale value, revalidate in background
|
|
219
252
|
try {
|
|
220
|
-
const result = await
|
|
221
|
-
if (cached.handles && hasTaintedArgs) {
|
|
222
|
-
const handleStore = requestCtx?._handleStore;
|
|
223
|
-
if (handleStore) {
|
|
224
|
-
const r = await decodeHandles(cached.handles);
|
|
225
|
-
if (r) restoreHandles(r, handleStore);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
// Tag the request with the stale entry's tags (see fresh-hit note).
|
|
229
|
-
recordRequestTags(cached.tags, requestCtx);
|
|
253
|
+
const result = await serveCached(cached);
|
|
230
254
|
// Background revalidation — must capture handles if tainted args present.
|
|
231
255
|
// Use an isolated handle store so background pushes don't pollute the
|
|
232
256
|
// live response or throw LateHandlePushError on the completed store.
|
package/src/cache/cache-scope.ts
CHANGED
|
@@ -34,12 +34,6 @@ import {
|
|
|
34
34
|
} from "./cache-policy.js";
|
|
35
35
|
import type { RequestContext } from "../server/request-context.js";
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
* Resolve tags for a cache() boundary from its config (static array or
|
|
39
|
-
* function of ctx). Thin wrapper over the shared resolveTagsOption so the
|
|
40
|
-
* cache() DSL and loader caching resolve tags identically.
|
|
41
|
-
* @internal
|
|
42
|
-
*/
|
|
43
37
|
export function resolveCacheTags(
|
|
44
38
|
config: PartialCacheOptions | false,
|
|
45
39
|
ctx: RequestContext | undefined,
|
|
@@ -54,17 +48,6 @@ function debugCacheLog(message: string): void {
|
|
|
54
48
|
}
|
|
55
49
|
}
|
|
56
50
|
|
|
57
|
-
// ============================================================================
|
|
58
|
-
// Key Generation (internal)
|
|
59
|
-
// ============================================================================
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Generate cache key base from host, pathname, route params, and search params.
|
|
63
|
-
* Host is included to prevent cross-host cache collisions on shared stores.
|
|
64
|
-
* Route params and search params are sorted alphabetically for deterministic keys.
|
|
65
|
-
* Internal _rsc* and __* query params are excluded.
|
|
66
|
-
* @internal
|
|
67
|
-
*/
|
|
68
51
|
function getCacheKeyBase(
|
|
69
52
|
host: string,
|
|
70
53
|
pathname: string,
|
|
@@ -80,16 +63,6 @@ function getCacheKeyBase(
|
|
|
80
63
|
return key;
|
|
81
64
|
}
|
|
82
65
|
|
|
83
|
-
/**
|
|
84
|
-
* Generate default cache key for a route request.
|
|
85
|
-
* Includes pathname, route params, and user-facing search params for
|
|
86
|
-
* correct scoping. Internal _rsc* params are excluded.
|
|
87
|
-
* Includes request type prefix since they produce different segment sets:
|
|
88
|
-
* - doc: document requests (full page load)
|
|
89
|
-
* - partial: navigation requests (client-side navigation)
|
|
90
|
-
* - intercept: intercept navigation (modal/overlay routes)
|
|
91
|
-
* @internal
|
|
92
|
-
*/
|
|
93
66
|
function getDefaultRouteCacheKey(
|
|
94
67
|
pathname: string,
|
|
95
68
|
params?: Record<string, string>,
|