@rangojs/router 0.22.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rango.js +10 -4
- package/dist/testing/vitest.js +31 -1
- package/dist/types/browser/defer.d.ts +48 -0
- package/dist/types/browser/document-revealed.d.ts +17 -0
- package/dist/types/browser/event-controller.d.ts +7 -1
- package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +39 -8
- package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
- package/dist/types/browser/react/context.d.ts +3 -2
- package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
- package/dist/types/browser/response-adapter.d.ts +7 -0
- package/dist/types/browser/types.d.ts +24 -10
- package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/router-processing.d.ts +1 -1
- package/dist/types/build/route-types/scan-filter.d.ts +22 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
- package/dist/types/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- package/dist/types/client-urls/client-urls.d.ts +1 -1
- package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
- package/dist/types/client-urls/navigation.d.ts +0 -37
- package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
- package/dist/types/client-urls/server-projection.d.ts +29 -1
- package/dist/types/client-urls/types.d.ts +34 -69
- package/dist/types/client.d.ts +0 -1
- package/dist/types/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +31 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/route-definition/rewrite.d.ts +56 -0
- package/dist/types/route-map-builder.d.ts +7 -2
- package/dist/types/router/basename.d.ts +7 -0
- package/dist/types/router/handler-context.d.ts +1 -5
- package/dist/types/router/internal-params.d.ts +8 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/middleware-types.d.ts +15 -0
- package/dist/types/router/revalidation.d.ts +1 -2
- package/dist/types/router/route-trie-builder.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
- package/dist/types/rsc/full-payload.d.ts +11 -0
- package/dist/types/rsc/handler.d.ts +33 -0
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/loader-fetch.d.ts +16 -0
- package/dist/types/rsc/rewrite-passes.d.ts +74 -0
- package/dist/types/rsc/ssr-setup.d.ts +3 -1
- package/dist/types/server/context.d.ts +21 -1
- package/dist/types/server/handle-store.d.ts +9 -1
- package/dist/types/server/request-context.d.ts +52 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +4 -4
- package/dist/types/testing/internal/action-id.d.ts +10 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
- package/dist/types/testing/internal/test-action.d.ts +8 -0
- package/dist/types/testing/render-route.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-middleware.d.ts +20 -0
- package/dist/types/testing/run-revalidate.d.ts +74 -0
- package/dist/types/testing/run-transition-when.d.ts +2 -3
- package/dist/types/testing/serve-shell-request.d.ts +40 -1
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/boundaries.d.ts +9 -0
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +30 -0
- package/dist/types/types/request-scope.d.ts +7 -0
- package/dist/types/types/segments.d.ts +21 -4
- package/dist/types/urls/path-helper-types.d.ts +25 -4
- package/dist/types/urls/type-extraction.d.ts +6 -2
- package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
- package/dist/types/vite/discovery/state.d.ts +14 -4
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
- package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
- package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
- package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
- package/dist/types/vite/router-discovery.d.ts +10 -0
- package/dist/vite/index.js +974 -379
- package/package.json +1 -1
- package/skills/caching/SKILL.md +54 -5
- package/skills/catalog.json +3 -3
- package/skills/client-urls/SKILL.md +197 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +16 -3
- package/skills/hooks/data.md +3 -1
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +199 -2
- package/skills/middleware/SKILL.md +183 -2
- package/skills/migrate-nextjs/SKILL.md +2 -2
- package/skills/migrate-react-router/SKILL.md +10 -7
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/ppr/SKILL.md +22 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +13 -7
- package/skills/route/SKILL.md +139 -0
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +2 -1
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +107 -19
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/use-cache/SKILL.md +4 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +46 -32
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +82 -16
- package/src/browser/navigation-client.ts +48 -36
- package/src/browser/navigation-store.ts +13 -0
- package/src/browser/navigation-transaction.ts +25 -8
- package/src/browser/partial-update.ts +554 -146
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +88 -49
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +25 -63
- package/src/browser/types.ts +24 -10
- package/src/browser/validate-redirect-origin.ts +16 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/router-processing.ts +3 -6
- package/src/build/route-types/scan-filter.ts +45 -11
- package/src/cache/cf/cf-cache-store.ts +59 -21
- package/src/cache/cf/cf-cache-types.ts +24 -2
- package/src/cache/document-cache-policy.ts +131 -0
- package/src/cache/document-cache.ts +21 -107
- package/src/client-urls/client-urls.ts +22 -6
- package/src/client-urls/hoisted-revalidate.ts +25 -0
- package/src/client-urls/navigation.ts +0 -96
- package/src/client-urls/revalidate-registry.ts +30 -0
- package/src/client-urls/server-projection.ts +151 -65
- package/src/client-urls/types.ts +39 -71
- package/src/client.tsx +9 -5
- package/src/context-var.ts +32 -2
- package/src/errors.ts +34 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +148 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- package/src/route-content-wrapper.tsx +18 -4
- package/src/route-definition/dsl-helpers.ts +27 -4
- package/src/route-definition/helpers-types.ts +27 -1
- package/src/route-definition/redirect.ts +2 -15
- package/src/route-definition/rewrite.ts +146 -0
- package/src/route-map-builder.ts +14 -11
- package/src/router/basename.ts +20 -0
- package/src/router/handler-context.ts +3 -13
- package/src/router/intercept-resolution.ts +35 -16
- package/src/router/internal-params.ts +16 -0
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +85 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +16 -4
- package/src/router/match-middleware/cache-lookup.ts +35 -7
- package/src/router/match-middleware/cache-store.ts +15 -1
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/match-result.ts +14 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -1
- package/src/router/segment-resolution/fresh.ts +22 -18
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/router/segment-resolution/loader-mask.ts +5 -3
- package/src/router/segment-resolution/loader-snapshot.ts +18 -4
- package/src/router/segment-resolution/mask-nested.ts +8 -4
- package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
- package/src/router/segment-resolution/revalidation.ts +215 -82
- package/src/rsc/full-payload.ts +37 -5
- package/src/rsc/handler.ts +82 -55
- package/src/rsc/helpers.ts +24 -0
- package/src/rsc/loader-fetch.ts +79 -24
- package/src/rsc/response-cache-serve.ts +18 -2
- package/src/rsc/response-route-handler.ts +4 -7
- package/src/rsc/rewrite-passes.ts +203 -0
- package/src/rsc/rsc-rendering.ts +54 -9
- package/src/rsc/shell-capture.ts +2 -1
- package/src/rsc/ssr-setup.ts +11 -5
- package/src/segment-loader-promise.ts +23 -20
- package/src/segment-system.tsx +60 -26
- package/src/server/context.ts +39 -5
- package/src/server/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +152 -25
- package/src/server/request-context.ts +175 -64
- package/src/server/request-headers.ts +4 -14
- package/src/server/rewrite-trail.ts +190 -0
- package/src/server/slot-accessors.ts +57 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +118 -16
- package/src/testing/action-form.ts +2 -11
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +7 -3
- package/src/testing/internal/action-id.ts +16 -1
- package/src/testing/internal/client-urls-projection.ts +70 -0
- package/src/testing/internal/test-action.ts +25 -0
- package/src/testing/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +4 -1
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +131 -8
- package/src/testing/vitest.ts +67 -1
- package/src/thenable-status.ts +11 -0
- package/src/types/boundaries.ts +12 -0
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +30 -0
- package/src/types/request-scope.ts +8 -0
- package/src/types/segments.ts +26 -4
- package/src/urls/path-helper-types.ts +27 -5
- package/src/urls/type-extraction.ts +30 -10
- package/src/use-loader.tsx +18 -13
- package/src/vite/discovery/client-urls-projection.ts +20 -21
- package/src/vite/discovery/discover-routers.ts +94 -26
- package/src/vite/discovery/state.ts +28 -4
- package/src/vite/discovery/virtual-module-codegen.ts +22 -17
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
- package/src/vite/plugins/expose-internal-ids.ts +2 -3
- package/src/vite/plugins/hoist/literal.ts +417 -0
- package/src/vite/plugins/hoist/scope.ts +370 -0
- package/src/vite/plugins/hoist/source.ts +84 -0
- package/src/vite/plugins/transition-when-hoist.ts +113 -779
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +116 -53
- package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
- package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
- package/dist/types/testing/run-client-revalidate.d.ts +0 -43
- package/src/client-urls/revalidate-chain.ts +0 -83
- package/src/client-urls/revalidation-protocol.ts +0 -56
- package/src/testing/run-client-revalidate.ts +0 -92
package/src/browser/types.ts
CHANGED
|
@@ -85,7 +85,11 @@ export interface RscMetadata {
|
|
|
85
85
|
* positions and must complete at the handler barrier.
|
|
86
86
|
*/
|
|
87
87
|
handlesLate?: AsyncGenerator<HandleData, void, unknown>;
|
|
88
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Handle data of the history-cache entry the update renders from (a
|
|
90
|
+
* back/forward restore, or a link to a cached page). It replaces the handle
|
|
91
|
+
* state, and `handles`, when present, merge onto it.
|
|
92
|
+
*/
|
|
89
93
|
cachedHandleData?: HandleData;
|
|
90
94
|
/**
|
|
91
95
|
* RSC version string from the server.
|
|
@@ -308,7 +312,12 @@ export interface HistoryEntryMemory {
|
|
|
308
312
|
export interface NavigationUpdate {
|
|
309
313
|
root: ReactNode | Promise<ReactNode>;
|
|
310
314
|
metadata: RscMetadata;
|
|
311
|
-
/**
|
|
315
|
+
/**
|
|
316
|
+
* The scroll decision of the navigation transaction this update commits,
|
|
317
|
+
* applied after React commits it. Absent on every update that is not a
|
|
318
|
+
* navigation's (server action, error, `prefetch: false` fill): such an
|
|
319
|
+
* update says nothing about scroll, and leaves a pending decision alone.
|
|
320
|
+
*/
|
|
312
321
|
scroll?: {
|
|
313
322
|
/** For back/forward: restore saved position */
|
|
314
323
|
restore?: boolean;
|
|
@@ -637,6 +646,12 @@ export interface NavigationStore {
|
|
|
637
646
|
stale?: boolean,
|
|
638
647
|
handlesPending?: boolean,
|
|
639
648
|
): void;
|
|
649
|
+
/**
|
|
650
|
+
* The history keys whose entries show the page `historyKey`'s entry shows:
|
|
651
|
+
* that entry and the copies a shallow navigation made of it, which share its
|
|
652
|
+
* segments array. `[historyKey]` when it has no entry.
|
|
653
|
+
*/
|
|
654
|
+
getPageHistoryKeys(historyKey: string): string[];
|
|
640
655
|
markHistoryCacheStale(): void;
|
|
641
656
|
markCacheAsStaleAndBroadcast(): void;
|
|
642
657
|
clearHistoryCache(): void;
|
|
@@ -671,13 +686,6 @@ export interface FetchPartialOptions {
|
|
|
671
686
|
signal?: AbortSignal;
|
|
672
687
|
/** If true, this is a stale cache revalidation request - server should force revalidators */
|
|
673
688
|
staleRevalidation?: boolean;
|
|
674
|
-
/**
|
|
675
|
-
* Encoded client-run per-loader revalidation decisions
|
|
676
|
-
* (clientUrls revalidate() predicates executed in the browser); sent as
|
|
677
|
-
* X-Rango-Client-Reval and honored only by materialized client-urls loader
|
|
678
|
-
* stubs. Null/absent = locked server defaults.
|
|
679
|
-
*/
|
|
680
|
-
clientRevalidation?: string | null;
|
|
681
689
|
interceptSourceUrl?: string;
|
|
682
690
|
/** RSC version for cache invalidation detection */
|
|
683
691
|
version?: string;
|
|
@@ -685,6 +693,12 @@ export interface FetchPartialOptions {
|
|
|
685
693
|
routerId?: string;
|
|
686
694
|
/** If true, this is an HMR refetch - server should invalidate manifest cache */
|
|
687
695
|
hmr?: boolean;
|
|
696
|
+
/**
|
|
697
|
+
* The fill request of an adopted prefetch that carried deferred segments
|
|
698
|
+
* (`prefetch: false`): marked `_rsc_fill`, never answered from or written
|
|
699
|
+
* to a prefetch cache or the HTTP cache, and it cancels no prefetch.
|
|
700
|
+
*/
|
|
701
|
+
fill?: boolean;
|
|
688
702
|
}
|
|
689
703
|
|
|
690
704
|
/**
|
|
@@ -785,7 +799,7 @@ export type { ResolvedSegment };
|
|
|
785
799
|
|
|
786
800
|
/**
|
|
787
801
|
* Token for tracking an active stream.
|
|
788
|
-
* Call end() when the stream completes.
|
|
802
|
+
* Call end() when the stream completes. Only the first call counts.
|
|
789
803
|
*/
|
|
790
804
|
export interface StreamingToken {
|
|
791
805
|
end(): void;
|
|
@@ -54,3 +54,19 @@ export function validateExternalRedirect(
|
|
|
54
54
|
}
|
|
55
55
|
return resolved;
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The target of a redirect a Flight payload carries (`metadata.redirect`), or
|
|
60
|
+
* null when the client refuses it. An explicit off-host one
|
|
61
|
+
* (`redirect(url, { external: true })`) is scheme-validated only:
|
|
62
|
+
* `external` waives the same-origin check the app opted out of, not scheme
|
|
63
|
+
* safety. Every other one must stay on this origin.
|
|
64
|
+
*/
|
|
65
|
+
export function validatePayloadRedirect(redirect: {
|
|
66
|
+
url: string;
|
|
67
|
+
external?: boolean;
|
|
68
|
+
}): string | null {
|
|
69
|
+
return redirect.external
|
|
70
|
+
? validateExternalRedirect(redirect.url, window.location.origin)
|
|
71
|
+
: validateRedirectOrigin(redirect.url, window.location.origin);
|
|
72
|
+
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "node:path";
|
|
15
15
|
import ts from "typescript";
|
|
16
16
|
import { generateRouteTypesSource } from "./codegen.js";
|
|
17
|
-
import type
|
|
17
|
+
import { isUniversallySkippedScanDir, type ScanFilter } from "./scan-filter.js";
|
|
18
18
|
import { firstCodeMatchIndex } from "./source-scan.js";
|
|
19
19
|
import {
|
|
20
20
|
resolveImportedVariable,
|
|
@@ -58,12 +58,9 @@ function isRoutableSourceFile(name: string): boolean {
|
|
|
58
58
|
|
|
59
59
|
function isExcludedScanDir(name: string): boolean {
|
|
60
60
|
return (
|
|
61
|
-
name
|
|
62
|
-
name === "dist" ||
|
|
63
|
-
name === "coverage" ||
|
|
61
|
+
isUniversallySkippedScanDir(name) ||
|
|
64
62
|
name === "__tests__" ||
|
|
65
|
-
name === "__mocks__"
|
|
66
|
-
name.startsWith(".")
|
|
63
|
+
name === "__mocks__"
|
|
67
64
|
);
|
|
68
65
|
}
|
|
69
66
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, relative } from "node:path";
|
|
1
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
2
2
|
import { readdirSync } from "node:fs";
|
|
3
3
|
// @ts-ignore -- picomatch ships no .d.ts; types are trivial
|
|
4
4
|
import picomatch from "picomatch";
|
|
@@ -44,8 +44,49 @@ export function createScanFilter(
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* Directory names no discovery scan enters: findTsFiles() and the router scans
|
|
48
|
+
* (findRouterFiles/findHostRouterFiles, router-processing.ts) all build their
|
|
49
|
+
* skip rule on this, so a file below one is never discovered. Dot-directories
|
|
50
|
+
* cover a nested git worktree, a tool cache, or a test's working copy of
|
|
51
|
+
* another app.
|
|
52
|
+
*/
|
|
53
|
+
export function isUniversallySkippedScanDir(name: string): boolean {
|
|
54
|
+
return (
|
|
55
|
+
name === "node_modules" ||
|
|
56
|
+
name === "dist" ||
|
|
57
|
+
name === "coverage" ||
|
|
58
|
+
name.startsWith(".")
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether a file under `root` sits in a directory every discovery scan skips
|
|
64
|
+
* (isUniversallySkippedScanDir). The dev route-file watcher uses it to ignore
|
|
65
|
+
* files discovery would never find. A path outside `root` is false.
|
|
66
|
+
*/
|
|
67
|
+
export function isInSkippedScanDir(
|
|
68
|
+
root: string,
|
|
69
|
+
absolutePath: string,
|
|
70
|
+
): boolean {
|
|
71
|
+
const rel = relative(root, absolutePath);
|
|
72
|
+
if (isAbsolute(rel)) return false;
|
|
73
|
+
const dirs = rel.split(/[\\/]/).slice(0, -1);
|
|
74
|
+
if (dirs[0] === "..") return false;
|
|
75
|
+
return dirs.some(isUniversallySkippedScanDir);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Directory names the source-file scans skip: findTsFiles() and the build
|
|
80
|
+
* loader pre-scan (expose-internal-ids.ts). The isUniversallySkippedScanDir()
|
|
81
|
+
* set plus `build`, which the router scans still enter.
|
|
82
|
+
*/
|
|
83
|
+
export function isSkippedSourceScanDir(name: string): boolean {
|
|
84
|
+
return isUniversallySkippedScanDir(name) || name === "build";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Recursively find .ts/.tsx files under a directory, skipping the directories
|
|
89
|
+
* isSkippedSourceScanDir() names and .gen. files.
|
|
49
90
|
*/
|
|
50
91
|
export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
|
|
51
92
|
const results: string[] = [];
|
|
@@ -61,14 +102,7 @@ export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
|
|
|
61
102
|
for (const entry of entries) {
|
|
62
103
|
const fullPath = join(dir, entry.name);
|
|
63
104
|
if (entry.isDirectory()) {
|
|
64
|
-
if (
|
|
65
|
-
entry.name === "node_modules" ||
|
|
66
|
-
entry.name.startsWith(".") ||
|
|
67
|
-
entry.name === "dist" ||
|
|
68
|
-
entry.name === "build" ||
|
|
69
|
-
entry.name === "coverage"
|
|
70
|
-
)
|
|
71
|
-
continue;
|
|
105
|
+
if (isSkippedSourceScanDir(entry.name)) continue;
|
|
72
106
|
results.push(...findTsFiles(fullPath, filter));
|
|
73
107
|
} else if (
|
|
74
108
|
(entry.name.endsWith(".ts") ||
|
|
@@ -15,6 +15,9 @@ declare global {
|
|
|
15
15
|
* L2 (KV): Global, persistent, ~50ms reads. Auto-warms cold colos.
|
|
16
16
|
*
|
|
17
17
|
* Read flow: L1 hit → serve | L1 miss → L2 hit → serve + promote to L1 | both miss → render
|
|
18
|
+
* With `kvFallThrough`, an L1 copy a tag marker rejects reads as
|
|
19
|
+
* an L1 miss: L2 is checked against the same markers (see get(),
|
|
20
|
+
* "Tag invalidation"). Off by default: the rejected copy is a miss.
|
|
18
21
|
* Write flow: L1 write + L2 write (both via waitUntil)
|
|
19
22
|
*
|
|
20
23
|
* Features:
|
|
@@ -388,7 +391,7 @@ type OpenedShellFrame =
|
|
|
388
391
|
| { status: "corrupt"; error: Error; head?: undefined }
|
|
389
392
|
| { status: "expired"; head: ShellFrameHead }
|
|
390
393
|
| { status: "timeout"; head?: undefined }
|
|
391
|
-
| { status: "invalidated"; head
|
|
394
|
+
| { status: "invalidated"; head: ShellFrameHead };
|
|
392
395
|
|
|
393
396
|
/** The `[CFCacheStore][shell]` trace's timings, from a read's stats. */
|
|
394
397
|
function debugTimings(
|
|
@@ -531,6 +534,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
531
534
|
private readonly tagPurge?: (cacheTags: string[]) => Promise<void>;
|
|
532
535
|
private readonly tagInvalidationTtl?: number;
|
|
533
536
|
private readonly tagCacheTtl: number;
|
|
537
|
+
private readonly kvFallThrough: boolean;
|
|
534
538
|
|
|
535
539
|
constructor(options: CFCacheStoreOptions<TEnv>) {
|
|
536
540
|
if (!options.ctx) {
|
|
@@ -605,6 +609,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
605
609
|
this.keyGenerator = options.keyGenerator;
|
|
606
610
|
this.waitUntil = (fn) => options.ctx.waitUntil(fn());
|
|
607
611
|
this.kv = options.kv;
|
|
612
|
+
this.kvFallThrough = options.kvFallThrough === true && !!options.kv;
|
|
608
613
|
// Without KV, isTagsInvalidatedSince has no durable history — it answers
|
|
609
614
|
// from the per-request mask at best. Runtime shells tolerate that (purge
|
|
610
615
|
// eviction + ttl/swr bound the staleness), but an immutable TAGGED
|
|
@@ -1210,16 +1215,17 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1210
1215
|
return this.kvGetSegment(key, { suppressRevalidate: true });
|
|
1211
1216
|
}
|
|
1212
1217
|
|
|
1213
|
-
// Tag invalidation:
|
|
1214
|
-
// cached is
|
|
1215
|
-
//
|
|
1216
|
-
//
|
|
1217
|
-
//
|
|
1218
|
-
//
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
1221
|
-
//
|
|
1222
|
-
//
|
|
1218
|
+
// Tag invalidation: a copy whose tags were invalidated after it was
|
|
1219
|
+
// cached is never served. Without `kvFallThrough` it is a miss and this
|
|
1220
|
+
// colo re-renders. With it, it reads as an L1 miss, because the tiers can
|
|
1221
|
+
// diverge: another colo may have written KV after the marker (its own
|
|
1222
|
+
// re-render, or a router.prerender() warm after updateTag()), and this
|
|
1223
|
+
// colo then serves that entry instead of rendering it again.
|
|
1224
|
+
// kvGetSegment checks the KV entry against the same markers (this
|
|
1225
|
+
// request's memo: no second marker read) and promotes a valid one over
|
|
1226
|
+
// this copy. When KV still holds the twin written with this copy, the
|
|
1227
|
+
// read costs one KV get and misses as before. The request that
|
|
1228
|
+
// invalidated the tag itself keeps the plain miss (declinesKvFallThrough).
|
|
1223
1229
|
const tagInfo = this.readTagInfo(response.headers);
|
|
1224
1230
|
// Measure the marker-resolution tail (memo -> L1 marker cache -> KV) only
|
|
1225
1231
|
// when debug is on, so the hot path pays nothing. It is the serial read
|
|
@@ -1241,7 +1247,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1241
1247
|
matchMs,
|
|
1242
1248
|
markerMs,
|
|
1243
1249
|
});
|
|
1244
|
-
return
|
|
1250
|
+
return this.declinesKvFallThrough(tagInfo.tags, tagInfo.taggedAt)
|
|
1251
|
+
? null
|
|
1252
|
+
: this.kvGetSegment(key);
|
|
1245
1253
|
}
|
|
1246
1254
|
|
|
1247
1255
|
// Read status headers
|
|
@@ -1524,7 +1532,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1524
1532
|
return this.kvGetResponse(key);
|
|
1525
1533
|
}
|
|
1526
1534
|
|
|
1527
|
-
// Tag invalidation check
|
|
1535
|
+
// Tag invalidation check: a rejected copy reads as an L1 miss (see get()).
|
|
1528
1536
|
const tagInfo = this.readTagInfo(response.headers);
|
|
1529
1537
|
if (
|
|
1530
1538
|
await this.isL1Invalidated(
|
|
@@ -1533,7 +1541,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1533
1541
|
response.headers,
|
|
1534
1542
|
)
|
|
1535
1543
|
) {
|
|
1536
|
-
return
|
|
1544
|
+
return this.declinesKvFallThrough(tagInfo.tags, tagInfo.taggedAt)
|
|
1545
|
+
? null
|
|
1546
|
+
: this.kvGetResponse(key);
|
|
1537
1547
|
}
|
|
1538
1548
|
|
|
1539
1549
|
// Check staleness
|
|
@@ -1801,8 +1811,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1801
1811
|
return this.kvGetItem(key, { suppressRevalidate: true });
|
|
1802
1812
|
}
|
|
1803
1813
|
|
|
1804
|
-
// Tag invalidation check
|
|
1805
|
-
// marker-resolution tail only under debug
|
|
1814
|
+
// Tag invalidation check: a rejected copy reads as an L1 miss (see
|
|
1815
|
+
// get()). Measure the marker-resolution tail only under debug.
|
|
1806
1816
|
const tagInfo = this.readTagInfo(response.headers);
|
|
1807
1817
|
const markerStart = this.debug ? Date.now() : 0;
|
|
1808
1818
|
const invalidated = await this.isL1Invalidated(
|
|
@@ -1821,7 +1831,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1821
1831
|
matchMs,
|
|
1822
1832
|
markerMs,
|
|
1823
1833
|
});
|
|
1824
|
-
return
|
|
1834
|
+
return this.declinesKvFallThrough(tagInfo.tags, tagInfo.taggedAt)
|
|
1835
|
+
? null
|
|
1836
|
+
: this.kvGetItem(key);
|
|
1825
1837
|
}
|
|
1826
1838
|
|
|
1827
1839
|
const staleAt = Number(
|
|
@@ -2233,7 +2245,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2233
2245
|
tier: "l1",
|
|
2234
2246
|
stats,
|
|
2235
2247
|
});
|
|
2236
|
-
|
|
2248
|
+
// As get(): KV may hold a capture from after the marker.
|
|
2249
|
+
return this.declinesKvFallThrough(opened.head.t, opened.head.ta)
|
|
2250
|
+
? null
|
|
2251
|
+
: kvFallback("invalidated");
|
|
2237
2252
|
}
|
|
2238
2253
|
const read = this.shellDocumentRead(
|
|
2239
2254
|
key,
|
|
@@ -2325,7 +2340,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2325
2340
|
* dropped and read from the store (SWR recapture scheduling and each mode's
|
|
2326
2341
|
* invalidation semantics stay with the store read). That read keeps the
|
|
2327
2342
|
* caller's `tagHints`: cfTagHints may have evicted the key while the memo
|
|
2328
|
-
* still holds it.
|
|
2343
|
+
* still holds it. A shell a tag marker rejects is dropped and read from
|
|
2344
|
+
* the store too, which may hold a capture from after the marker (see
|
|
2345
|
+
* get(), "Tag invalidation"), unless the request's own mask rejected it
|
|
2346
|
+
* (declinesKvFallThrough).
|
|
2329
2347
|
*/
|
|
2330
2348
|
private async readMemoizedShell(
|
|
2331
2349
|
key: string,
|
|
@@ -2360,7 +2378,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2360
2378
|
tier: "memo",
|
|
2361
2379
|
stats,
|
|
2362
2380
|
});
|
|
2363
|
-
return
|
|
2381
|
+
return this.declinesKvFallThrough(head.t, head.ta)
|
|
2382
|
+
? null
|
|
2383
|
+
: this.readShellDocumentWithin(key, options, snapshotBudget);
|
|
2364
2384
|
}
|
|
2365
2385
|
if (stats) stats.memo = { hit: true, bytes: cfShellMemo.size };
|
|
2366
2386
|
this.recordShellHit(key, head.t, stats);
|
|
@@ -2472,7 +2492,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2472
2492
|
}
|
|
2473
2493
|
if (invalidated) {
|
|
2474
2494
|
reader.cancel();
|
|
2475
|
-
return { status: "invalidated" };
|
|
2495
|
+
return { status: "invalidated", head: value.head };
|
|
2476
2496
|
}
|
|
2477
2497
|
return value;
|
|
2478
2498
|
}
|
|
@@ -3341,6 +3361,24 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
3341
3361
|
return false;
|
|
3342
3362
|
}
|
|
3343
3363
|
|
|
3364
|
+
/**
|
|
3365
|
+
* Whether a rejected L1 copy skips the KV read: always unless the
|
|
3366
|
+
* `kvFallThrough` option is on, and with it on when this request's own
|
|
3367
|
+
* updateTag()/revalidateTag() (its mask, request-tag-mask.ts) rejects an
|
|
3368
|
+
* entry tagged at `taggedAt`. KV holds a newer entry only if something
|
|
3369
|
+
* rendered after this request's invalidation, while the request re-renders
|
|
3370
|
+
* every entry it masked, so the read would add one KV get per masked entry
|
|
3371
|
+
* to that re-render.
|
|
3372
|
+
* @internal
|
|
3373
|
+
*/
|
|
3374
|
+
private declinesKvFallThrough(
|
|
3375
|
+
tags: string[] | undefined,
|
|
3376
|
+
taggedAt: number | undefined,
|
|
3377
|
+
): boolean {
|
|
3378
|
+
if (!this.kvFallThrough) return true;
|
|
3379
|
+
return maskedForRequest(_getRequestContext(), this, tags, taggedAt ?? 0);
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3344
3382
|
/**
|
|
3345
3383
|
* Synthetic Cache API request for a tag's L1-cached invalidation marker.
|
|
3346
3384
|
* Unversioned for the same reason as tagMarkerKey; the cache namespace
|
|
@@ -66,8 +66,11 @@ export interface CFCacheReadDebugEvent {
|
|
|
66
66
|
* - kv-timeout: the L2/KV read budget fired (read abandoned, NOT a genuine
|
|
67
67
|
* absence -- distinct from kv-miss so a degradation signal is separable)
|
|
68
68
|
* - tag-invalidated: a live L1/KV entry whose cache tags were invalidated
|
|
69
|
-
* after it was written --
|
|
70
|
-
*
|
|
69
|
+
* after it was written -- never served (the tag-invalidation read path,
|
|
70
|
+
* distinct from a plain miss). With `kvFallThrough` on, an L1 copy then
|
|
71
|
+
* reads L2/KV, so a kv-* event follows unless the request invalidated the
|
|
72
|
+
* tag itself; from L2/KV it is a miss and the next render re-populates
|
|
73
|
+
* the entry
|
|
71
74
|
* - error: the read threw
|
|
72
75
|
*/
|
|
73
76
|
outcome:
|
|
@@ -383,6 +386,25 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
383
386
|
*/
|
|
384
387
|
kvReadTimeoutMs?: number;
|
|
385
388
|
|
|
389
|
+
/**
|
|
390
|
+
* After `updateTag()`/`revalidateTag()`, a location whose own L1 copy a tag
|
|
391
|
+
* marker rejects reads KV next, and serves the entry another location (or a
|
|
392
|
+
* `router.prerender()` warm) wrote after the invalidation, instead of
|
|
393
|
+
* rendering again. Covers `cache()` records, `"use cache"` results, cached
|
|
394
|
+
* responses and `ppr` shells. The request that invalidated the tag still
|
|
395
|
+
* re-renders its own entries without the read.
|
|
396
|
+
*
|
|
397
|
+
* Cost: one KV get per rejected copy per location after each invalidation
|
|
398
|
+
* (150 ms added wait, measured in #1069 locally at 50 ms per KV read on a
|
|
399
|
+
* document with a shell, a record and one `"use cache"` result, every copy
|
|
400
|
+
* rejected; not measured on a deployed worker), paid even when KV holds only
|
|
401
|
+
* the old twin. Worth it where re-rendering costs more than a KV read
|
|
402
|
+
* (several locations, expensive renders). Needs `kv`; ignored without it.
|
|
403
|
+
*
|
|
404
|
+
* Defaults to false: a rejected L1 copy is a miss and the location renders.
|
|
405
|
+
*/
|
|
406
|
+
kvFallThrough?: boolean;
|
|
407
|
+
|
|
386
408
|
/**
|
|
387
409
|
* Per-isolate memos ({@link StoreMemoOptions}), one set per isolate and
|
|
388
410
|
* shared by every CFCacheStore in it. `shellMs` (default 2000) and
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document cache storage policy: which responses the document cache stores.
|
|
3
|
+
* A leaf (types and the per-client signal only), so request-scoped code can
|
|
4
|
+
* ask whether a render will be stored (documentCacheStoresRender) without
|
|
5
|
+
* importing the cache middleware.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { hasPerClientSignal } from "../browser/cookie-name.js";
|
|
9
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
10
|
+
|
|
11
|
+
export interface CacheDirectives {
|
|
12
|
+
sMaxAge?: number;
|
|
13
|
+
staleWhileRevalidate?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse Cache-Control header for s-maxage and stale-while-revalidate directives
|
|
18
|
+
*/
|
|
19
|
+
function parseCacheControl(header: string | null): CacheDirectives | null {
|
|
20
|
+
if (!header) return null;
|
|
21
|
+
|
|
22
|
+
// RFC 7234: in a SHARED cache, `private` and `no-store` forbid storage and
|
|
23
|
+
// MUST win over `s-maxage` even though `private, s-maxage` is contradictory.
|
|
24
|
+
// The document cache is a shared app store, so refuse both regardless of any
|
|
25
|
+
// s-maxage / stale-while-revalidate also present. Match standalone directive
|
|
26
|
+
// tokens (start/end, whitespace, comma, semicolon, or `=` bounded), not a
|
|
27
|
+
// substring, so a value containing "private" cannot false-veto.
|
|
28
|
+
if (/(^|[\s,;])(private|no-store)(?=$|[\s,;=])/i.test(header)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// RFC 7234 §5.2.2.2: a shared cache MUST NOT serve a stored `no-cache`
|
|
33
|
+
// response without successful origin validation. This store's hit path has no
|
|
34
|
+
// validation step, so serving a stored no-cache response within s-maxage
|
|
35
|
+
// would hand the client content the origin marked must-revalidate. Refuse to
|
|
36
|
+
// store it. Only UNqualified `no-cache` (no `=`) vetoes — the field-name-
|
|
37
|
+
// scoped `no-cache="set-cookie"` form IS storable per the RFC, so the `=`
|
|
38
|
+
// boundary is excluded from the lookahead (unlike private/no-store above).
|
|
39
|
+
if (/(^|[\s,;])no-cache(?=$|[\s,;])/i.test(header)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const directives: CacheDirectives = {};
|
|
44
|
+
|
|
45
|
+
// Parse s-maxage
|
|
46
|
+
const sMaxAgeMatch = header.match(/s-maxage\s*=\s*(\d+)/i);
|
|
47
|
+
if (sMaxAgeMatch) {
|
|
48
|
+
directives.sMaxAge = parseInt(sMaxAgeMatch[1], 10);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Parse stale-while-revalidate
|
|
52
|
+
const swrMatch = header.match(/stale-while-revalidate\s*=\s*(\d+)/i);
|
|
53
|
+
if (swrMatch) {
|
|
54
|
+
directives.staleWhileRevalidate = parseInt(swrMatch[1], 10);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Only return if we have at least s-maxage (required for document caching)
|
|
58
|
+
if (directives.sMaxAge !== undefined) {
|
|
59
|
+
return directives;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if response should be cached based on Cache-Control headers
|
|
67
|
+
*/
|
|
68
|
+
export function shouldCacheResponse(
|
|
69
|
+
response: Response,
|
|
70
|
+
requestCtx?: Pick<
|
|
71
|
+
RequestContext,
|
|
72
|
+
"_payloadVisitorTheme" | "_payloadDeferred" | "_payloadVisitorLocation"
|
|
73
|
+
>,
|
|
74
|
+
): CacheDirectives | null {
|
|
75
|
+
// Only cache successful responses
|
|
76
|
+
if (response.status !== 200) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Never cache a per-client signal into a SHARED response store. A Set-Cookie
|
|
81
|
+
// (e.g. a rango state rotation from invalidateClientCache(), or any cookie a
|
|
82
|
+
// loader set) would be replayed to every client on a hit — pinning them to
|
|
83
|
+
// one value and even rolling a rotated client back to a prior one. The
|
|
84
|
+
// x-rango-keep-cache directive header is the mirror image: a replayed "keep"
|
|
85
|
+
// would suppress invalidation for every replayed client. Refuse both.
|
|
86
|
+
if (hasPerClientSignal(response.headers)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The payload carries the visitor's theme (#978): the render ran before the
|
|
91
|
+
// response opted in (a Cache-Control written after next()), so
|
|
92
|
+
// payloadInitialTheme could not render the default.
|
|
93
|
+
if (requestCtx?._payloadVisitorTheme) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A rewritten request's render shows the visitor's URL (renderedLocation,
|
|
98
|
+
// full-payload.ts); this entry is keyed by the rewrite target.
|
|
99
|
+
if (requestCtx?._payloadVisitorLocation) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// prefetch: false. The payload carries deferred units: it answers one
|
|
104
|
+
// page's prefetch and nothing else. parseCacheControl refuses the
|
|
105
|
+
// Cache-Control it is sent with (rsc-rendering.ts NOT_REUSED), but a
|
|
106
|
+
// middleware can replace that after next(), and a navigation answered
|
|
107
|
+
// with this body would show a fallback no fill ever replaces.
|
|
108
|
+
if (requestCtx?._payloadDeferred) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const cacheControl = response.headers.get("Cache-Control");
|
|
113
|
+
return parseCacheControl(cacheControl);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether this cache will store the response the current render produces, as
|
|
118
|
+
* far as the render can tell: the middleware runs it (a MISS or a stale
|
|
119
|
+
* refresh) and the response stub already opts in. payloadInitialTheme
|
|
120
|
+
* (rsc/full-payload.ts) then renders the no-cookie default theme (#978). A
|
|
121
|
+
* Cache-Control written after next() is not on the stub yet; for that render
|
|
122
|
+
* shouldCacheResponse refuses a payload marked `_payloadVisitorTheme`.
|
|
123
|
+
*/
|
|
124
|
+
export function documentCacheStoresRender(
|
|
125
|
+
reqCtx: Pick<RequestContext, "_documentCacheRender" | "res">,
|
|
126
|
+
): boolean {
|
|
127
|
+
return (
|
|
128
|
+
reqCtx._documentCacheRender === true &&
|
|
129
|
+
shouldCacheResponse(reqCtx.res) !== null
|
|
130
|
+
);
|
|
131
|
+
}
|