@rangojs/router 0.32.0 → 0.33.1
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 +3 -2
- package/dist/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10173
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-3HH6BP3K.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-JALTIWLT.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +15 -6
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +5 -2
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +2 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +5 -2
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +43 -7
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +66 -2
- package/dist/types/testing/serve-shell-request.d.ts +6 -3
- package/dist/types/types/handler-context.d.ts +7 -5
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +15 -4
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/vite/index.js +449 -189
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +81 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +18 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +29 -11
- package/skills/comparison/references/framework-comparison.md +13 -8
- package/skills/document-cache/SKILL.md +8 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/outlets.md +4 -0
- package/skills/hooks/state.md +24 -8
- package/skills/intercept/SKILL.md +33 -12
- package/skills/layout/SKILL.md +60 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +26 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +41 -8
- package/skills/testing/client-components.md +29 -13
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +70 -19
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +20 -9
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +8 -1
- package/src/browser/server-action-bridge.ts +16 -11
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +27 -0
- package/src/cache/document-cache.ts +5 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +21 -13
- package/src/router/match-result.ts +39 -87
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +53 -23
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +8 -3
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/request-stages.ts +59 -0
- package/src/rsc/rsc-rendering.ts +49 -33
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +23 -2
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +113 -28
- package/src/testing/dispatch.ts +3 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +269 -4
- package/src/testing/serve-shell-request.ts +6 -3
- package/src/types/handler-context.ts +7 -5
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +15 -4
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
|
@@ -271,7 +271,7 @@ export function createServerActionBridge(
|
|
|
271
271
|
* - Never over an intercept: a request holding no segments drops the page
|
|
272
272
|
* under it (match-result.ts buildMatchResult). Checked when it would run,
|
|
273
273
|
* against the page then on screen; the debt is dropped there.
|
|
274
|
-
* - Never supersedes a navigation the user started
|
|
274
|
+
* - Never supersedes a navigation the user started: it waits for
|
|
275
275
|
* NavigationEntry.settled, then looks again. A navigation that failed
|
|
276
276
|
* keeps its error on screen, and the debt is dropped.
|
|
277
277
|
* - Runs again when it is left before its commit (a navigation, or an action
|
|
@@ -322,7 +322,7 @@ export function createServerActionBridge(
|
|
|
322
322
|
* R6: an action that starts while the follow-up has not committed leaves
|
|
323
323
|
* it, and the batch's end owes it again. Its request may predate that
|
|
324
324
|
* action's write, and its commit could land after the action's own,
|
|
325
|
-
* moving data on screen backwards
|
|
325
|
+
* moving data on screen backwards. Through the controller, so
|
|
326
326
|
* the navigation it started stops being current.
|
|
327
327
|
*/
|
|
328
328
|
function leaveFollowUp(): void {
|
|
@@ -339,7 +339,7 @@ export function createServerActionBridge(
|
|
|
339
339
|
* repairs. Resolves with whether it started and the error of a loader
|
|
340
340
|
* that threw on it. Never starts while a navigation the user started may
|
|
341
341
|
* be superseded (unsettledUserNavigation): its startNavigation would
|
|
342
|
-
* abort it
|
|
342
|
+
* abort it. A repair that cannot start is owed at the batch's
|
|
343
343
|
* end instead (finalizeAction). `run` (the follow-up's) is handed the
|
|
344
344
|
* refetch's navigation, and told once whether it ended left before its
|
|
345
345
|
* commit.
|
|
@@ -885,14 +885,19 @@ export function createServerActionBridge(
|
|
|
885
885
|
throw new Error("No matched segments in response");
|
|
886
886
|
}
|
|
887
887
|
|
|
888
|
-
// Reconcile server segments with cached segments (single source of
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
888
|
+
// Reconcile server segments with cached segments (single source of
|
|
889
|
+
// truth). A page that left commits nothing (navigated-away), and the
|
|
890
|
+
// cache on screen is another page's: reconciling would report missing
|
|
891
|
+
// segments.
|
|
892
|
+
const reconciled = navigatedSince()
|
|
893
|
+
? { segments: [], mainSegments: [], interceptSegments: [] }
|
|
894
|
+
: reconcileSegments({
|
|
895
|
+
actor: "action",
|
|
896
|
+
matched,
|
|
897
|
+
diff: diff || [],
|
|
898
|
+
serverSegments: segments || [],
|
|
899
|
+
cachedSegments,
|
|
900
|
+
});
|
|
896
901
|
const fullSegments = reconciled.segments;
|
|
897
902
|
|
|
898
903
|
const returnData = returnValue?.data;
|
package/src/browser/types.ts
CHANGED
|
@@ -703,7 +703,15 @@ export interface NavigationStore {
|
|
|
703
703
|
*/
|
|
704
704
|
getPageHistoryKeys(historyKey: string): string[];
|
|
705
705
|
markHistoryCacheStale(): void;
|
|
706
|
+
/**
|
|
707
|
+
* Mark history entries and prefetches stale without a broadcast: a cookie
|
|
708
|
+
* read saw another write (rango-state.ts setRangoStateWriteObserver).
|
|
709
|
+
*/
|
|
710
|
+
markCachesStaleAfterWrite(): void;
|
|
711
|
+
/** A server action without a directive: mark stale, soft broadcast. */
|
|
706
712
|
markCacheAsStaleAndBroadcast(): void;
|
|
713
|
+
/** The client seat of invalidateClientCache(): flush, rotate, broadcast. */
|
|
714
|
+
invalidateCacheAndBroadcast(): void;
|
|
707
715
|
clearHistoryCache(): void;
|
|
708
716
|
|
|
709
717
|
// Cross-tab refresh callback (set by navigation bridge)
|
|
@@ -753,6 +761,16 @@ export interface FetchPartialOptions {
|
|
|
753
761
|
* to a prefetch cache or the HTTP cache, and it cancels no prefetch.
|
|
754
762
|
*/
|
|
755
763
|
fill?: boolean;
|
|
764
|
+
/**
|
|
765
|
+
* Whether a prefetch a server action marked stale (prefetch/cache.ts
|
|
766
|
+
* isPrefetchStale) may answer this navigation. Only a click sets it: it
|
|
767
|
+
* revalidates its page once it settled (navigation-bridge.ts navigate,
|
|
768
|
+
* partial-update.ts onStalePrefetch). For any other caller (Back/Forward on
|
|
769
|
+
* a history-cache miss, leaving an intercept) a stale stored entry is a
|
|
770
|
+
* miss, and a stale in-flight one is dropped and fetched again, so no path
|
|
771
|
+
* that never revalidates commits pre-write data.
|
|
772
|
+
*/
|
|
773
|
+
acceptStalePrefetch?: boolean;
|
|
756
774
|
}
|
|
757
775
|
|
|
758
776
|
/**
|
|
@@ -771,6 +789,13 @@ export interface FetchPartialResult {
|
|
|
771
789
|
* fallbacks stream as usual.
|
|
772
790
|
*/
|
|
773
791
|
fullyPrefetched?: boolean;
|
|
792
|
+
/**
|
|
793
|
+
* True when the payload is a prefetch a server action marked stale after it
|
|
794
|
+
* was fetched (prefetch/cache.ts isPrefetchStale). The click commits it as
|
|
795
|
+
* any prefetch; the navigation then revalidates its page once it settled,
|
|
796
|
+
* as Back onto a stale history entry does (navigation-bridge.ts navigate).
|
|
797
|
+
*/
|
|
798
|
+
stalePrefetch?: boolean;
|
|
774
799
|
}
|
|
775
800
|
|
|
776
801
|
/**
|
|
@@ -13,7 +13,8 @@ export type UnresolvableReason =
|
|
|
13
13
|
| "dynamic-expression"
|
|
14
14
|
| "unresolvable-import"
|
|
15
15
|
| "file-not-found"
|
|
16
|
-
| "unresolvable-basename"
|
|
16
|
+
| "unresolvable-basename"
|
|
17
|
+
| "unresolvable-routes";
|
|
17
18
|
|
|
18
19
|
export interface UnresolvableInclude {
|
|
19
20
|
pathPrefix: string;
|
|
@@ -78,14 +79,50 @@ function readSourceMemoized(
|
|
|
78
79
|
return source;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
function isUrlsDefinitionCall(
|
|
82
|
+
export function isUrlsDefinitionCall(
|
|
83
|
+
node: ts.CallExpression,
|
|
84
|
+
aliases?: ReadonlySet<string>,
|
|
85
|
+
): boolean {
|
|
82
86
|
const callee = node.expression;
|
|
83
87
|
return (
|
|
84
88
|
ts.isIdentifier(callee) &&
|
|
85
|
-
(callee.text === "urls" ||
|
|
89
|
+
(callee.text === "urls" ||
|
|
90
|
+
callee.text === "clientUrls" ||
|
|
91
|
+
aliases?.has(callee.text) === true)
|
|
86
92
|
);
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Local names that `import { urls as u, clientUrls as cu } from
|
|
97
|
+
* "@rangojs/router"` (or `/client`) binds to urls()/clientUrls(), so a renamed
|
|
98
|
+
* import is read like the plain name. Other modules' exports are not guessed
|
|
99
|
+
* at: a `urls` from elsewhere is read only under its own name.
|
|
100
|
+
*/
|
|
101
|
+
export function collectUrlsImportAliases(
|
|
102
|
+
sourceFile: ts.SourceFile,
|
|
103
|
+
): ReadonlySet<string> {
|
|
104
|
+
const aliases = new Set<string>();
|
|
105
|
+
for (const stmt of sourceFile.statements) {
|
|
106
|
+
if (
|
|
107
|
+
!ts.isImportDeclaration(stmt) ||
|
|
108
|
+
!ts.isStringLiteral(stmt.moduleSpecifier) ||
|
|
109
|
+
(stmt.moduleSpecifier.text !== "@rangojs/router" &&
|
|
110
|
+
stmt.moduleSpecifier.text !== "@rangojs/router/client")
|
|
111
|
+
) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const bindings = stmt.importClause?.namedBindings;
|
|
115
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
116
|
+
for (const el of bindings.elements) {
|
|
117
|
+
const imported = (el.propertyName ?? el.name).text;
|
|
118
|
+
if (imported === "urls" || imported === "clientUrls") {
|
|
119
|
+
aliases.add(el.name.text);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return aliases;
|
|
124
|
+
}
|
|
125
|
+
|
|
89
126
|
// ---------------------------------------------------------------------------
|
|
90
127
|
// AST-based include() parsing
|
|
91
128
|
// ---------------------------------------------------------------------------
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
resolveImportPath,
|
|
22
22
|
buildCombinedRouteMapWithSearch,
|
|
23
23
|
createScanMemo,
|
|
24
|
+
isUrlsDefinitionCall,
|
|
25
|
+
collectUrlsImportAliases,
|
|
24
26
|
type ScanMemo,
|
|
25
27
|
type UnresolvableInclude,
|
|
26
28
|
} from "./include-resolution.js";
|
|
@@ -46,7 +48,9 @@ const ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
|
|
|
46
48
|
// Global variant for the code-region scan (firstCodeMatchIndex sets lastIndex).
|
|
47
49
|
const ROUTER_CALL_PATTERN_G = /\bcreateRouter\s*[<(]/g;
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
// Test files build their own routers (a Flight test's `*.rsc-test.tsx` usually
|
|
52
|
+
// mounts `createRouter({}).routes(urls(...))`); none is an app's router root.
|
|
53
|
+
export function isRoutableSourceFile(name: string): boolean {
|
|
50
54
|
return (
|
|
51
55
|
(name.endsWith(".ts") ||
|
|
52
56
|
name.endsWith(".tsx") ||
|
|
@@ -54,6 +58,7 @@ function isRoutableSourceFile(name: string): boolean {
|
|
|
54
58
|
name.endsWith(".jsx")) &&
|
|
55
59
|
!name.includes(".gen.") &&
|
|
56
60
|
!name.includes(".test.") &&
|
|
61
|
+
!name.includes(".rsc-test.") &&
|
|
57
62
|
!name.includes(".spec.")
|
|
58
63
|
);
|
|
59
64
|
}
|
|
@@ -205,13 +210,19 @@ function isCreateRouterCall(node: ts.Node): boolean {
|
|
|
205
210
|
}
|
|
206
211
|
|
|
207
212
|
function extractUrlsArgument(
|
|
208
|
-
|
|
213
|
+
arg: ts.Expression,
|
|
209
214
|
sourceFile: ts.SourceFile,
|
|
215
|
+
urlsAliases: ReadonlySet<string>,
|
|
210
216
|
): UrlsExtractionResult | null {
|
|
217
|
+
const node = unwrapTypeOperators(arg);
|
|
211
218
|
if (ts.isIdentifier(node)) {
|
|
212
219
|
return { kind: "variable", name: node.text };
|
|
213
220
|
}
|
|
214
|
-
if (
|
|
221
|
+
if (
|
|
222
|
+
ts.isArrowFunction(node) ||
|
|
223
|
+
ts.isFunctionExpression(node) ||
|
|
224
|
+
(ts.isCallExpression(node) && isUrlsDefinitionCall(node, urlsAliases))
|
|
225
|
+
) {
|
|
215
226
|
return { kind: "inline", block: node.getText(sourceFile) };
|
|
216
227
|
}
|
|
217
228
|
return null;
|
|
@@ -234,24 +245,135 @@ function collectCreateRouterCallChain(
|
|
|
234
245
|
}
|
|
235
246
|
}
|
|
236
247
|
|
|
237
|
-
//
|
|
238
|
-
//
|
|
248
|
+
// Local names a file exports other than by an `export` modifier on their own
|
|
249
|
+
// declaration: `export { a, b as c }`, `export default a`, `export = a` and
|
|
250
|
+
// `export const x = a` (a one-hop alias).
|
|
251
|
+
function collectExportedLocalNames(sourceFile: ts.SourceFile): Set<string> {
|
|
252
|
+
const names = new Set<string>();
|
|
253
|
+
const addIdentifier = (expr: ts.Expression | undefined) => {
|
|
254
|
+
const node = expr && unwrapTypeOperators(expr);
|
|
255
|
+
if (node && ts.isIdentifier(node)) names.add(node.text);
|
|
256
|
+
};
|
|
257
|
+
for (const statement of sourceFile.statements) {
|
|
258
|
+
if (ts.isExportAssignment(statement)) {
|
|
259
|
+
addIdentifier(statement.expression);
|
|
260
|
+
} else if (
|
|
261
|
+
ts.isExportDeclaration(statement) &&
|
|
262
|
+
!statement.moduleSpecifier &&
|
|
263
|
+
!statement.isTypeOnly &&
|
|
264
|
+
statement.exportClause &&
|
|
265
|
+
ts.isNamedExports(statement.exportClause)
|
|
266
|
+
) {
|
|
267
|
+
for (const element of statement.exportClause.elements) {
|
|
268
|
+
if (!element.isTypeOnly) {
|
|
269
|
+
names.add((element.propertyName ?? element.name).text);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} else if (
|
|
273
|
+
ts.isVariableStatement(statement) &&
|
|
274
|
+
hasModifier(statement, ts.SyntaxKind.ExportKeyword)
|
|
275
|
+
) {
|
|
276
|
+
for (const decl of statement.declarationList.declarations) {
|
|
277
|
+
addIdentifier(decl.initializer);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return names;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Whether a createRouter chain is the value a module exports: the initializer
|
|
285
|
+
// of an exported top-level variable (or of one an export list or default
|
|
286
|
+
// export names), or the `export default` / `export =` expression itself,
|
|
287
|
+
// through `as`, `satisfies` and parentheses. A chain inside a function or class
|
|
288
|
+
// body, an argument of another call or a property of an object literal is not
|
|
289
|
+
// exported even when the enclosing declaration is.
|
|
290
|
+
function isExportedChain(
|
|
291
|
+
chainEnd: ts.CallExpression,
|
|
292
|
+
exportedNames: ReadonlySet<string>,
|
|
293
|
+
): boolean {
|
|
294
|
+
let current: ts.Node = chainEnd;
|
|
295
|
+
while (
|
|
296
|
+
current.parent &&
|
|
297
|
+
(ts.isAsExpression(current.parent) ||
|
|
298
|
+
ts.isSatisfiesExpression(current.parent) ||
|
|
299
|
+
ts.isParenthesizedExpression(current.parent))
|
|
300
|
+
) {
|
|
301
|
+
current = current.parent;
|
|
302
|
+
}
|
|
303
|
+
const parent = current.parent;
|
|
304
|
+
if (!parent) return false;
|
|
305
|
+
if (ts.isExportAssignment(parent))
|
|
306
|
+
return parent.parent === current.getSourceFile();
|
|
307
|
+
if (
|
|
308
|
+
!ts.isVariableDeclaration(parent) ||
|
|
309
|
+
parent.initializer !== current ||
|
|
310
|
+
!ts.isIdentifier(parent.name)
|
|
311
|
+
) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
const statement = parent.parent.parent;
|
|
315
|
+
if (
|
|
316
|
+
!ts.isVariableStatement(statement) ||
|
|
317
|
+
!ts.isSourceFile(statement.parent)
|
|
318
|
+
) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
return (
|
|
322
|
+
hasModifier(statement, ts.SyntaxKind.ExportKeyword) ||
|
|
323
|
+
exportedNames.has(parent.name.text)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// The createRouter chain whose urls the route types and basename come from,
|
|
328
|
+
// with its extractions in runtime order. A file may hold several chains (a
|
|
329
|
+
// helper router beside the app's): an exported chain always wins over one that
|
|
330
|
+
// is not exported, so a helper never supplies the app's routes or basename. Only when no chain is exported (a lone router in a test file)
|
|
331
|
+
// does a non-exported one count. Among the candidates, the first (in source
|
|
332
|
+
// order) that mounts a readable urls wins.
|
|
333
|
+
// `unreadable` holds the source of each mount the static parser cannot follow
|
|
334
|
+
// (`.routes(buildUrls())`, `urls: mod.tree`): its routes are missing from the
|
|
335
|
+
// extractions, so detectUnresolvableIncludes reports it rather than letting
|
|
336
|
+
// the gen file silently lose them. A chain with only unreadable
|
|
337
|
+
// mounts is returned only when no candidate has a readable one. An exported
|
|
338
|
+
// chain that mounts nothing still hides the non-exported ones.
|
|
239
339
|
function findRouterUrls(sourceFile: ts.SourceFile): {
|
|
240
340
|
createRouterCall: ts.CallExpression;
|
|
241
341
|
extractions: UrlsExtractionResult[];
|
|
342
|
+
unreadable: string[];
|
|
242
343
|
} | null {
|
|
243
|
-
|
|
344
|
+
type RouterUrls = {
|
|
244
345
|
createRouterCall: ts.CallExpression;
|
|
245
346
|
extractions: UrlsExtractionResult[];
|
|
246
|
-
|
|
347
|
+
unreadable: string[];
|
|
348
|
+
};
|
|
349
|
+
const exported: RouterUrls[] = [];
|
|
350
|
+
const local: RouterUrls[] = [];
|
|
351
|
+
let anyExported = false;
|
|
352
|
+
const seen = new Set<ts.CallExpression>();
|
|
353
|
+
const urlsAliases = collectUrlsImportAliases(sourceFile);
|
|
354
|
+
const exportedNames = collectExportedLocalNames(sourceFile);
|
|
247
355
|
|
|
248
356
|
function visit(node: ts.Node) {
|
|
249
|
-
if (result) return;
|
|
250
357
|
if (ts.isCallExpression(node)) {
|
|
251
358
|
const chain = collectCreateRouterCallChain(node);
|
|
252
|
-
if (chain) {
|
|
359
|
+
if (chain && !seen.has(chain[0])) {
|
|
253
360
|
const extracted: UrlsExtractionResult[] = [];
|
|
361
|
+
const unreadable: string[] = [];
|
|
254
362
|
const createRouterCall = chain[0];
|
|
363
|
+
seen.add(createRouterCall);
|
|
364
|
+
// `mount` wraps the argument's source for the diagnostic; the text is
|
|
365
|
+
// only built for an argument the parser cannot read.
|
|
366
|
+
const addMount = (
|
|
367
|
+
arg: ts.Expression,
|
|
368
|
+
mount: (text: string) => string,
|
|
369
|
+
) => {
|
|
370
|
+
const urls = extractUrlsArgument(arg, sourceFile, urlsAliases);
|
|
371
|
+
if (urls) extracted.push(urls);
|
|
372
|
+
else
|
|
373
|
+
unreadable.push(
|
|
374
|
+
mount(arg.getText(sourceFile).replace(/\s+/g, " ")),
|
|
375
|
+
);
|
|
376
|
+
};
|
|
255
377
|
|
|
256
378
|
for (const callArg of createRouterCall.arguments) {
|
|
257
379
|
if (!ts.isObjectLiteralExpression(callArg)) continue;
|
|
@@ -263,8 +385,7 @@ function findRouterUrls(sourceFile: ts.SourceFile): {
|
|
|
263
385
|
) {
|
|
264
386
|
continue;
|
|
265
387
|
}
|
|
266
|
-
|
|
267
|
-
if (urls) extracted.push(urls);
|
|
388
|
+
addMount(prop.initializer, (text) => `urls: ${text}`);
|
|
268
389
|
break;
|
|
269
390
|
}
|
|
270
391
|
}
|
|
@@ -277,13 +398,17 @@ function findRouterUrls(sourceFile: ts.SourceFile): {
|
|
|
277
398
|
) {
|
|
278
399
|
continue;
|
|
279
400
|
}
|
|
280
|
-
|
|
281
|
-
if (urls) extracted.push(urls);
|
|
401
|
+
addMount(call.arguments[0], (text) => `.routes(${text})`);
|
|
282
402
|
}
|
|
283
403
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
404
|
+
const isExported = isExportedChain(node, exportedNames);
|
|
405
|
+
if (isExported) anyExported = true;
|
|
406
|
+
if (extracted.length > 0 || unreadable.length > 0) {
|
|
407
|
+
(isExported ? exported : local).push({
|
|
408
|
+
createRouterCall,
|
|
409
|
+
extractions: extracted,
|
|
410
|
+
unreadable,
|
|
411
|
+
});
|
|
287
412
|
}
|
|
288
413
|
}
|
|
289
414
|
}
|
|
@@ -291,27 +416,34 @@ function findRouterUrls(sourceFile: ts.SourceFile): {
|
|
|
291
416
|
}
|
|
292
417
|
|
|
293
418
|
visit(sourceFile);
|
|
294
|
-
|
|
419
|
+
const candidates = anyExported ? exported : local;
|
|
420
|
+
return (
|
|
421
|
+
candidates.find((c) => c.extractions.length > 0) ?? candidates[0] ?? null
|
|
422
|
+
);
|
|
295
423
|
}
|
|
296
424
|
|
|
297
|
-
function
|
|
298
|
-
|
|
425
|
+
function parseRouterSource(code: string): ts.SourceFile {
|
|
426
|
+
return ts.createSourceFile(
|
|
299
427
|
"router.tsx",
|
|
300
428
|
code,
|
|
301
429
|
ts.ScriptTarget.Latest,
|
|
302
430
|
true,
|
|
303
431
|
ts.ScriptKind.TSX,
|
|
304
432
|
);
|
|
305
|
-
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function extractAllUrlsFromRouter(code: string): UrlsExtractionResult[] {
|
|
436
|
+
return findRouterUrls(parseRouterSource(code))?.extractions ?? [];
|
|
306
437
|
}
|
|
307
438
|
|
|
308
439
|
/**
|
|
309
440
|
* Extract the url patterns from a router file using AST.
|
|
310
|
-
* Detects
|
|
441
|
+
* Detects these patterns, in either mount (`.routes(x)` or `urls: x`):
|
|
311
442
|
* 1. createRouter(...).routes(variableName)
|
|
312
443
|
* 2. createRouter({ urls: variableName, ... })
|
|
313
444
|
* 3. createRouter(...).routes(({ path, ... }) => [...])
|
|
314
445
|
* 4. createRouter({ urls: ({ path, ... }) => [...], ... })
|
|
446
|
+
* 5. createRouter(...).routes(urls(({ path, ... }) => [...])), and clientUrls()
|
|
315
447
|
* Returns either a variable name or an inline code block.
|
|
316
448
|
*/
|
|
317
449
|
export function extractUrlsFromRouter(
|
|
@@ -322,7 +454,11 @@ export function extractUrlsFromRouter(
|
|
|
322
454
|
}
|
|
323
455
|
|
|
324
456
|
function unwrapTypeOperators(expr: ts.Expression): ts.Expression {
|
|
325
|
-
while (
|
|
457
|
+
while (
|
|
458
|
+
ts.isAsExpression(expr) ||
|
|
459
|
+
ts.isSatisfiesExpression(expr) ||
|
|
460
|
+
ts.isParenthesizedExpression(expr)
|
|
461
|
+
) {
|
|
326
462
|
expr = expr.expression;
|
|
327
463
|
}
|
|
328
464
|
return expr;
|
|
@@ -699,12 +835,21 @@ export function detectUnresolvableIncludes(
|
|
|
699
835
|
return [];
|
|
700
836
|
}
|
|
701
837
|
|
|
702
|
-
const
|
|
703
|
-
if (
|
|
838
|
+
const routerUrls = findRouterUrls(parseRouterSource(source));
|
|
839
|
+
if (!routerUrls) return [];
|
|
704
840
|
|
|
705
841
|
const diagnostics: UnresolvableInclude[] = [];
|
|
842
|
+
for (const mount of routerUrls.unreadable) {
|
|
843
|
+
diagnostics.push({
|
|
844
|
+
pathPrefix: "/",
|
|
845
|
+
namePrefix: null,
|
|
846
|
+
reason: "unresolvable-routes",
|
|
847
|
+
sourceFile: realPath,
|
|
848
|
+
detail: `\`${mount}\` is not a urls() call (as imported from @rangojs/router), a builder function or a variable; its routes would be missing`,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
706
851
|
const memo = createScanMemo();
|
|
707
|
-
for (const extraction of extractions) {
|
|
852
|
+
for (const extraction of routerUrls.extractions) {
|
|
708
853
|
resolveRouterUrlsExtraction(
|
|
709
854
|
realPath,
|
|
710
855
|
source,
|
|
@@ -187,9 +187,10 @@ export const KEY_PART_PREFIX = "key:";
|
|
|
187
187
|
* (CacheScope.resolveKeyFrom, CacheScope.resolvePartition): the `key()`
|
|
188
188
|
* results of the scope's cache() chain, outermost first, then its default
|
|
189
189
|
* parts: the keyGenerator results of enclosing scopes on another store
|
|
190
|
-
* (#974) and, for a route record key, the scope's own
|
|
190
|
+
* (#974) and, for a route record key, the scope's own parts: without a
|
|
191
191
|
* `key()` of its own, its default key (or its store's keyGenerator result,
|
|
192
|
-
* #970
|
|
192
|
+
* #970, then the request kind when that result does not start with it,
|
|
193
|
+
* #1298); with one, the default key's request kind (`doc`, `partial` or
|
|
193
194
|
* `intercept:<intercept>`, routeRecordKind in cache-scope.ts).
|
|
194
195
|
*
|
|
195
196
|
* Key scheme invariant (#975). A `key()` result is often request input,
|
|
@@ -209,7 +210,9 @@ export const KEY_PART_PREFIX = "key:";
|
|
|
209
210
|
* - one `key()` part is the key; two or more parts are joined by `|`,
|
|
210
211
|
* `key()` parts first;
|
|
211
212
|
* - a lone default part stays raw: with no `key()` on the chain the record
|
|
212
|
-
* key is the default key or the store's keyGenerator result, unchanged
|
|
213
|
+
* key is the default key or the store's keyGenerator result, unchanged
|
|
214
|
+
* when that result starts with its request kind, else that result and
|
|
215
|
+
* the kind composed (`items|doc`).
|
|
213
216
|
*
|
|
214
217
|
* Why nothing collides: the router's default keys (`doc:`, `partial:`,
|
|
215
218
|
* `intercept:`, `response:<type>:`) never start with `key:` and hold a `:`.
|
|
@@ -218,11 +221,12 @@ export const KEY_PART_PREFIX = "key:";
|
|
|
218
221
|
* Among composed keys, splitting on `|` recovers each part, a part's kind
|
|
219
222
|
* (a `key:` prefix, or no `:` at all) and its value. A request kind part is
|
|
220
223
|
* a default key's prefix without the URL after it, so it never equals a
|
|
221
|
-
* default key part
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
224
|
+
* default key part. A raw keyGenerator record key starts with its kind and
|
|
225
|
+
* holds a `:`, so it never equals a composed one, and two kinds' prefixes
|
|
226
|
+
* differ; a result without its kind is composed with it, never prefixed,
|
|
227
|
+
* since `doc:` + `x` would be the raw result `doc:x`. Pinned by the
|
|
228
|
+
* collision probes in src/cache/__tests__/cache-scope-chain.test.ts. A
|
|
229
|
+
* keyGenerator result is the store's own namespace, as before.
|
|
226
230
|
*
|
|
227
231
|
* Shared with the testing helper `shellCacheKey`, so a test builds the
|
|
228
232
|
* production partition.
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
runAsBackgroundWork,
|
|
26
26
|
type RequestContext,
|
|
27
27
|
} from "../server/request-context.js";
|
|
28
|
+
import { segmentPinOf } from "../server/context.js";
|
|
28
29
|
import { isUnderTestRunner } from "../runtime-env.js";
|
|
29
30
|
import {
|
|
30
31
|
isTainted,
|
|
@@ -505,8 +506,11 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
505
506
|
if (ctx.body !== undefined || ctx.formData !== undefined) {
|
|
506
507
|
hasBodyCtx = true;
|
|
507
508
|
}
|
|
509
|
+
// Only an argument with a pin (a handler or loader ctx, or its copy)
|
|
510
|
+
// is the caller: a request context before it has no claim.
|
|
508
511
|
if (callerSegmentId === undefined) {
|
|
509
|
-
callerSegmentId =
|
|
512
|
+
callerSegmentId =
|
|
513
|
+
ctx._currentSegmentId === undefined ? undefined : segmentPinOf(ctx);
|
|
510
514
|
claimLoaderPushes = ctx._claimLoaderPushes;
|
|
511
515
|
}
|
|
512
516
|
if (ctx.params && typeof ctx.params === "object") {
|
package/src/cache/cache-scope.ts
CHANGED
|
@@ -235,6 +235,22 @@ function positionalParts(parts: readonly (string | null)[]): string[] {
|
|
|
235
235
|
: parts.map((part) => part ?? "");
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/**
|
|
239
|
+
* A route record key's own parts (CacheScope.resolveKeyFrom): `own`, the
|
|
240
|
+
* scope's default key or its store's keyGenerator result (undefined under a
|
|
241
|
+
* `key()` of its own), then the request `kind` unless `own` starts with
|
|
242
|
+
* `{kind}:`. A default key does, and so does a keyGenerator result built on
|
|
243
|
+
* `defaultKey` (`${defaultKey}|x`), which stays byte for byte. One that
|
|
244
|
+
* ignores `defaultKey` (a constant, the pathname) gave a document, a
|
|
245
|
+
* navigation and each intercept navigation one record, so a document GET
|
|
246
|
+
* rendered an intercept's. The kind is a part, never a `{kind}:` prefix: a
|
|
247
|
+
* prefix would give the results `x` and `doc:x` one record.
|
|
248
|
+
*/
|
|
249
|
+
function ownParts(own: string | undefined, kind: string | undefined): string[] {
|
|
250
|
+
if (own === undefined) return kind === undefined ? [] : [kind];
|
|
251
|
+
return kind === undefined || own.startsWith(`${kind}:`) ? [own] : [own, kind];
|
|
252
|
+
}
|
|
253
|
+
|
|
238
254
|
/**
|
|
239
255
|
* One key resolution per request, resolver (`by`: a `key()` or a store's
|
|
240
256
|
* keyGenerator) and `slot`, held on the request context (`_resolvedCacheKeys`,
|
|
@@ -530,8 +546,10 @@ export class CacheScope {
|
|
|
530
546
|
* replaces the URL part, and its store's keyGenerator does not run. It
|
|
531
547
|
* appends `kind` instead (routeRecordKind): the `key()` decides which
|
|
532
548
|
* requests of one kind share a record, never that a document, a
|
|
533
|
-
* navigation and an intercept navigation do.
|
|
534
|
-
*
|
|
549
|
+
* navigation and an intercept navigation do. A keyGenerator result that
|
|
550
|
+
* does not start with its kind gets `kind` after it too (ownParts). With
|
|
551
|
+
* no partition part it is resolveDefaultKey alone, or that result and
|
|
552
|
+
* `kind` composed. With a `key()` on the chain but no request
|
|
535
553
|
* context it rejects (keyWithoutContextError). `partitionedPrefix`
|
|
536
554
|
* prefixes a key built from partition parts only (the response family's
|
|
537
555
|
* `response:`, which passes no `kind`: a response route answers every
|
|
@@ -556,7 +574,9 @@ export class CacheScope {
|
|
|
556
574
|
if (!requestCtx || !this.mayPartition) {
|
|
557
575
|
return this.keyFns.length > 0
|
|
558
576
|
? Promise.reject(keyWithoutContextError())
|
|
559
|
-
: this.resolveDefaultKey(requestCtx, defaultKey)
|
|
577
|
+
: this.resolveDefaultKey(requestCtx, defaultKey).then((own) =>
|
|
578
|
+
composeCacheKeys([], ownParts(own, kind)),
|
|
579
|
+
);
|
|
560
580
|
}
|
|
561
581
|
return Promise.all([
|
|
562
582
|
this.resolvePartitionParts(requestCtx, defaultKey),
|
|
@@ -565,10 +585,11 @@ export class CacheScope {
|
|
|
565
585
|
: this.resolveDefaultKey(requestCtx, defaultKey),
|
|
566
586
|
]).then(([[keys, storeParts], ownDefault]) => {
|
|
567
587
|
const generated = positionalParts(storeParts);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
588
|
+
const own = ownParts(ownDefault, kind);
|
|
589
|
+
if (keys.length === 0 && generated.length === 0) {
|
|
590
|
+
return composeCacheKeys([], own);
|
|
591
|
+
}
|
|
592
|
+
return partitionedPrefix + composeCacheKeys(keys, [...generated, ...own]);
|
|
572
593
|
});
|
|
573
594
|
}
|
|
574
595
|
|
|
@@ -419,19 +419,23 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
419
419
|
/**
|
|
420
420
|
* Custom key generator applied to all cache operations.
|
|
421
421
|
* Receives the full RequestContext (including env) and the default-generated key.
|
|
422
|
-
*
|
|
422
|
+
* The return value is the cache key (unless route overrides with `key`
|
|
423
|
+
* option), except that a route record keeps its request kind: a result that
|
|
424
|
+
* does not start with `defaultKey`'s prefix (`doc:`, `partial:`,
|
|
425
|
+
* `intercept:<intercept>:`) gets the kind appended (see
|
|
426
|
+
* SegmentCacheStore.keyGenerator).
|
|
423
427
|
*
|
|
424
428
|
* Reserved prefixes: tag-invalidation markers live in the SAME KV namespace as
|
|
425
429
|
* data, keyed `__tag__/<tag>` (and `__tagmarker__/<tag>` for the L1 cache). A
|
|
426
430
|
* returned key must NOT begin with `__tag__/` or `__tagmarker__/`, or it can
|
|
427
431
|
* collide with a tag marker and corrupt invalidation. The documented
|
|
428
|
-
*
|
|
432
|
+
* generators below start with `defaultKey` and are safe.
|
|
429
433
|
*
|
|
430
434
|
* @example Using headers for user segmentation
|
|
431
435
|
* ```typescript
|
|
432
436
|
* keyGenerator: (ctx, defaultKey) => {
|
|
433
437
|
* const segment = ctx.request.headers.get('x-user-segment') || 'default';
|
|
434
|
-
* return `${
|
|
438
|
+
* return `${defaultKey}|${segment}`;
|
|
435
439
|
* }
|
|
436
440
|
* ```
|
|
437
441
|
*
|
|
@@ -439,7 +443,7 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
439
443
|
* ```typescript
|
|
440
444
|
* keyGenerator: (ctx, defaultKey) => {
|
|
441
445
|
* const region = ctx.env.REGION || 'us';
|
|
442
|
-
* return `${
|
|
446
|
+
* return `${defaultKey}|${region}`;
|
|
443
447
|
* }
|
|
444
448
|
* ```
|
|
445
449
|
*
|
|
@@ -447,7 +451,7 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
447
451
|
* ```typescript
|
|
448
452
|
* keyGenerator: (ctx, defaultKey) => {
|
|
449
453
|
* const locale = cookies().get('locale')?.value || 'en';
|
|
450
|
-
* return `${
|
|
454
|
+
* return `${defaultKey}|${locale}`;
|
|
451
455
|
* }
|
|
452
456
|
* ```
|
|
453
457
|
*/
|