@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
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import { normalizePath, parseAst } from "vite";
|
|
3
|
+
import MagicString from "magic-string";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { relative } from "node:path";
|
|
6
|
+
import { hasDirective } from "@vitejs/plugin-rsc/transforms";
|
|
7
|
+
import { HOISTED_REVALIDATE_KEY } from "../../client-urls/hoisted-revalidate.js";
|
|
8
|
+
import { hasUseClientDirective } from "../utils/directive-prologue.js";
|
|
9
|
+
import { offsetToLineColumn } from "./expose-ids/export-analysis.js";
|
|
10
|
+
import {
|
|
11
|
+
analyzeScopes,
|
|
12
|
+
collectImports,
|
|
13
|
+
TYPE_KEYS,
|
|
14
|
+
type ImportBinding,
|
|
15
|
+
type Node,
|
|
16
|
+
} from "./hoist/scope.js";
|
|
17
|
+
import {
|
|
18
|
+
collectFreeReferences,
|
|
19
|
+
emitImports,
|
|
20
|
+
findHelperCalls,
|
|
21
|
+
findLexicalCapture,
|
|
22
|
+
isFunctionLiteral,
|
|
23
|
+
parseLang,
|
|
24
|
+
unwrapExpression,
|
|
25
|
+
type LexicalCaptureMessages,
|
|
26
|
+
} from "./hoist/literal.js";
|
|
27
|
+
import {
|
|
28
|
+
loadEmittedOrDisk,
|
|
29
|
+
readSource,
|
|
30
|
+
recordEmitted,
|
|
31
|
+
resolveSourceTarget,
|
|
32
|
+
SOURCE_FILE_RE,
|
|
33
|
+
type EmittedEntry,
|
|
34
|
+
} from "./hoist/source.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* clientUrls() loader `revalidate()` runs on the server, with the arguments a
|
|
38
|
+
* urls() loader's predicate gets. The predicate is declared in a
|
|
39
|
+
* "use client" module, so the build moves it into a server module (the
|
|
40
|
+
* reverse of transition-when-hoist.ts):
|
|
41
|
+
*
|
|
42
|
+
* loader(CartLoader, () => [
|
|
43
|
+
* revalidate((ctx) => ctx.isAction(Cart) ? ctx.actionResult?.ok : undefined),
|
|
44
|
+
* ])
|
|
45
|
+
*
|
|
46
|
+
* becomes, in the client and SSR copies of the module,
|
|
47
|
+
*
|
|
48
|
+
* loader(CartLoader, () => [revalidate({ $$rangoRevalidate: "<key>" })])
|
|
49
|
+
*
|
|
50
|
+
* and `<file>?rango-revalidate` (a query on the file itself, so relative
|
|
51
|
+
* imports resolve as they did there and an edit invalidates it) is
|
|
52
|
+
*
|
|
53
|
+
* import { registerClientUrlRevalidators } from "@rangojs/router/server";
|
|
54
|
+
* <the module-level imports the predicates reference>
|
|
55
|
+
* registerClientUrlRevalidators({ "<key>": (ctx) => ... });
|
|
56
|
+
*
|
|
57
|
+
* plugin-rsc replaces the RSC copy of a "use client" module with client
|
|
58
|
+
* reference proxies and drops everything else, so a post transform appends
|
|
59
|
+
* `import "<file>?rango-revalidate"` to the proxy: the predicates register
|
|
60
|
+
* when the group's module loads on the server (a lazy include stays lazy).
|
|
61
|
+
* Route discovery serializes the SSR copy, so the projection carries the
|
|
62
|
+
* keys; materialization (client-urls/server-projection.ts) attaches the
|
|
63
|
+
* registered predicates to the loader stubs.
|
|
64
|
+
*
|
|
65
|
+
* A key hashes the project-relative file and the predicate's syntax tree
|
|
66
|
+
* (formatting, comments and TypeScript excluded, so a re-printing transform
|
|
67
|
+
* in one environment keeps it): two environments that saw a different
|
|
68
|
+
* predicate fail loudly at materialization instead of pairing a loader with
|
|
69
|
+
* the wrong one.
|
|
70
|
+
*
|
|
71
|
+
* Keyed on calls of the `revalidate` helper a clientUrls() builder callback
|
|
72
|
+
* receives (destructured, renamed, or as `helpers.revalidate`), with
|
|
73
|
+
* clientUrls imported from "@rangojs/router/client". The argument may
|
|
74
|
+
* reference imports and globals only: a binding of the module or of an
|
|
75
|
+
* enclosing function, `this`/`arguments`/`super`/`new.target`, `import()`,
|
|
76
|
+
* `import.meta` other than `import.meta.env`, a browser-only global, an
|
|
77
|
+
* `async` or generator function, or an import of a "use client" module is a
|
|
78
|
+
* LOUD error naming file:line. Imports stay in the "use client" module too
|
|
79
|
+
* (they are not pruned). Vitest does not run the plugin, so in unit tests the
|
|
80
|
+
* predicate stays a plain function.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
const CLIENT_URLS_REVALIDATE_QUERY = "rango-revalidate";
|
|
84
|
+
const REGISTER = "__rango_registerClientUrlRevalidators";
|
|
85
|
+
|
|
86
|
+
const CLIENT_SOURCES: ReadonlySet<string> = new Set(["@rangojs/router/client"]);
|
|
87
|
+
|
|
88
|
+
const BROWSER_GLOBALS: ReadonlySet<string> = new Set([
|
|
89
|
+
"window",
|
|
90
|
+
"document",
|
|
91
|
+
"navigator",
|
|
92
|
+
"localStorage",
|
|
93
|
+
"sessionStorage",
|
|
94
|
+
"location",
|
|
95
|
+
"history",
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
const NOT_A_FUNCTION = new Set([
|
|
99
|
+
"ObjectExpression",
|
|
100
|
+
"ArrayExpression",
|
|
101
|
+
"Literal",
|
|
102
|
+
"TemplateLiteral",
|
|
103
|
+
"ClassExpression",
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const CAPTURE_MESSAGES: LexicalCaptureMessages = {
|
|
107
|
+
lexical: {
|
|
108
|
+
this: "`this` in an arrow is the enclosing function's `this`.",
|
|
109
|
+
arguments: "`arguments` in an arrow is the enclosing function's.",
|
|
110
|
+
super: "`super` refers to the enclosing object.",
|
|
111
|
+
"new.target": "`new.target` in an arrow is the enclosing function's.",
|
|
112
|
+
},
|
|
113
|
+
captureHint: "Read what you need from the predicate's argument.",
|
|
114
|
+
loadsHint: "Import server-safe modules statically.",
|
|
115
|
+
importMetaWhy: 'is the "use client" module\'s.',
|
|
116
|
+
importMetaMemberWhy:
|
|
117
|
+
'is resolved against the "use client" module; only `import.meta.env` ' +
|
|
118
|
+
"is available to a hoisted predicate.",
|
|
119
|
+
dynamicImportWhy:
|
|
120
|
+
"loads a module at run time, which the build cannot check is server-safe.",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Cheap pre-filter: every site needs a "use client" module (the directive's
|
|
124
|
+
// text), clientUrls imported from the router, and a `revalidate` helper. The
|
|
125
|
+
// AST walk is the authority.
|
|
126
|
+
const CLIENT_URLS_RE = /\bclientUrls\b/;
|
|
127
|
+
const CLIENT_IMPORT_RE = /["']@rangojs\/router\/client["']/;
|
|
128
|
+
const REVALIDATE_RE = /\brevalidate\b/;
|
|
129
|
+
|
|
130
|
+
function mayHoist(code: string): boolean {
|
|
131
|
+
return (
|
|
132
|
+
code.includes("use client") &&
|
|
133
|
+
CLIENT_URLS_RE.test(code) &&
|
|
134
|
+
CLIENT_IMPORT_RE.test(code) &&
|
|
135
|
+
REVALIDATE_RE.test(code)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function revalidateError(
|
|
140
|
+
file: string,
|
|
141
|
+
code: string,
|
|
142
|
+
node: Node,
|
|
143
|
+
detail: string,
|
|
144
|
+
): Error {
|
|
145
|
+
const { line, column } = offsetToLineColumn(code, node.start);
|
|
146
|
+
return new Error(
|
|
147
|
+
`[rango] clientUrls() revalidate() at ${file}:${line}:${column}: ` +
|
|
148
|
+
`revalidate() runs on the server; ${detail}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Fields that change with formatting, comments or TS stripping. */
|
|
153
|
+
const FINGERPRINT_SKIP: ReadonlySet<string> = new Set([
|
|
154
|
+
"type",
|
|
155
|
+
"start",
|
|
156
|
+
"end",
|
|
157
|
+
"loc",
|
|
158
|
+
"range",
|
|
159
|
+
"raw",
|
|
160
|
+
"extra",
|
|
161
|
+
"shorthand",
|
|
162
|
+
// TS-only flags (`let x!: T`), lost with the types.
|
|
163
|
+
"definite",
|
|
164
|
+
...TYPE_KEYS,
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The predicate's syntax tree, serialized without what a re-printing
|
|
169
|
+
* transform changes: whitespace, comments, parentheses, quotes, shorthand
|
|
170
|
+
* properties and TypeScript. plugin-react's React Compiler re-prints a
|
|
171
|
+
* "use client" module in the client and SSR environments only, so a key over
|
|
172
|
+
* the source text would differ from the RSC environment's.
|
|
173
|
+
*/
|
|
174
|
+
function fingerprint(node: Node): string {
|
|
175
|
+
const out: string[] = [];
|
|
176
|
+
const visit = (value: unknown): void => {
|
|
177
|
+
if (value === null || value === undefined) return;
|
|
178
|
+
if (Array.isArray(value)) {
|
|
179
|
+
out.push("[");
|
|
180
|
+
for (const item of value) visit(item);
|
|
181
|
+
out.push("]");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (typeof value !== "object") {
|
|
185
|
+
out.push(JSON.stringify(typeof value === "bigint" ? `${value}n` : value));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
let entry = value as Node;
|
|
189
|
+
if (typeof entry.type === "string") {
|
|
190
|
+
entry = unwrapExpression(entry);
|
|
191
|
+
// Type-only syntax; an instantiation `f<T>` keeps its expression.
|
|
192
|
+
if (entry.type.startsWith("TS")) {
|
|
193
|
+
visit(entry.expression);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
out.push(entry.type, "(");
|
|
197
|
+
// A tag sees the raw text: String.raw`\x41` is not String.raw`A`.
|
|
198
|
+
if (entry.type === "TemplateElement") {
|
|
199
|
+
out.push(JSON.stringify(entry.value.raw));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const key of Object.keys(entry)) {
|
|
203
|
+
if (FINGERPRINT_SKIP.has(key)) continue;
|
|
204
|
+
// `x?: T` loses its `?` with its type; `a?.b` keeps it.
|
|
205
|
+
if (key === "optional" && entry.type === "Identifier") continue;
|
|
206
|
+
out.push(key);
|
|
207
|
+
visit(entry[key]);
|
|
208
|
+
}
|
|
209
|
+
if (typeof entry.type === "string") out.push(")");
|
|
210
|
+
};
|
|
211
|
+
visit(node);
|
|
212
|
+
return out.join(" ");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function keyOf(relativeFile: string, predicate: Node): string {
|
|
216
|
+
return createHash("sha256")
|
|
217
|
+
.update(relativeFile)
|
|
218
|
+
.update("\0")
|
|
219
|
+
.update(fingerprint(predicate))
|
|
220
|
+
.digest("hex")
|
|
221
|
+
.slice(0, 12);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The key of the predicate `source` declared in `relativeFile` (relative to
|
|
226
|
+
* the project root): a hash of the file and the predicate's syntax, so the
|
|
227
|
+
* same predicate gets one key in every environment and however it is
|
|
228
|
+
* formatted.
|
|
229
|
+
*/
|
|
230
|
+
export function revalidateKey(relativeFile: string, source: string): string {
|
|
231
|
+
const program: Node = parseAst(`(${source});`, { lang: "tsx" });
|
|
232
|
+
return keyOf(relativeFile, unwrapExpression(program.body[0].expression));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
interface HoistClientUrlsRevalidateDeps {
|
|
236
|
+
/** Project root: keys hash the file relative to it. */
|
|
237
|
+
readonly root: string;
|
|
238
|
+
/**
|
|
239
|
+
* Whether a module a predicate imports is a "use client" module beyond its
|
|
240
|
+
* specifier (the plugin resolves it and reads its directive prologue).
|
|
241
|
+
*/
|
|
242
|
+
isClientModule?(source: string): Promise<boolean> | boolean;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
interface HoistClientUrlsRevalidateResult {
|
|
246
|
+
/** The module with each predicate replaced by its marker. */
|
|
247
|
+
readonly code: string;
|
|
248
|
+
readonly map: ReturnType<MagicString["generateMap"]>;
|
|
249
|
+
/** The server module that registers every hoisted predicate. */
|
|
250
|
+
readonly module: string;
|
|
251
|
+
/** The keys, in declaration order, duplicates removed. */
|
|
252
|
+
readonly keys: readonly string[];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Hoist every clientUrls() `revalidate()` predicate of a "use client"
|
|
257
|
+
* module. Returns null when there is none. Throws, naming file:line, for a
|
|
258
|
+
* predicate that is not pure.
|
|
259
|
+
*/
|
|
260
|
+
export async function hoistClientUrlsRevalidates(
|
|
261
|
+
code: string,
|
|
262
|
+
id: string,
|
|
263
|
+
deps: HoistClientUrlsRevalidateDeps,
|
|
264
|
+
): Promise<HoistClientUrlsRevalidateResult | null> {
|
|
265
|
+
if (!mayHoist(code)) return null;
|
|
266
|
+
const file = normalizePath(id.split("?", 1)[0]!);
|
|
267
|
+
let program: Node;
|
|
268
|
+
try {
|
|
269
|
+
program = parseAst(code, { lang: parseLang(file) });
|
|
270
|
+
} catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
if (!hasDirective(program.body, "use client")) return null;
|
|
274
|
+
|
|
275
|
+
const { moduleScope, references, scopeOf } = analyzeScopes(program);
|
|
276
|
+
const imports = collectImports(program);
|
|
277
|
+
const calls = findHelperCalls(program, scopeOf, moduleScope, imports, {
|
|
278
|
+
sources: CLIENT_SOURCES,
|
|
279
|
+
builder: "clientUrls",
|
|
280
|
+
helper: "revalidate",
|
|
281
|
+
directImport: false,
|
|
282
|
+
});
|
|
283
|
+
if (calls.length === 0) return null;
|
|
284
|
+
|
|
285
|
+
const relativeFile = normalizePath(relative(deps.root, file));
|
|
286
|
+
const s = new MagicString(code);
|
|
287
|
+
const used = new Map<string, boolean>();
|
|
288
|
+
const entries = new Map<string, string>();
|
|
289
|
+
|
|
290
|
+
for (const { call } of calls) {
|
|
291
|
+
const [arg, extra] = call.arguments;
|
|
292
|
+
if (!arg || extra || arg.type === "SpreadElement") {
|
|
293
|
+
throw revalidateError(
|
|
294
|
+
file,
|
|
295
|
+
code,
|
|
296
|
+
call,
|
|
297
|
+
"`revalidate()` takes exactly one predicate.",
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const value = unwrapExpression(arg);
|
|
301
|
+
if (NOT_A_FUNCTION.has(value.type)) {
|
|
302
|
+
throw revalidateError(
|
|
303
|
+
file,
|
|
304
|
+
code,
|
|
305
|
+
value,
|
|
306
|
+
"`revalidate()` expects a predicate function.",
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (isFunctionLiteral(value) && (value.async || value.generator)) {
|
|
310
|
+
throw revalidateError(
|
|
311
|
+
file,
|
|
312
|
+
code,
|
|
313
|
+
value,
|
|
314
|
+
`a${value.async ? "n `async`" : " generator"} function returns ` +
|
|
315
|
+
`${value.async ? "a Promise" : "an iterator"}, which the router ` +
|
|
316
|
+
"ignores: return a boolean, `{ defaultShouldRevalidate }`, or " +
|
|
317
|
+
"nothing.",
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const lexical = findLexicalCapture(
|
|
322
|
+
value,
|
|
323
|
+
references,
|
|
324
|
+
code,
|
|
325
|
+
CAPTURE_MESSAGES,
|
|
326
|
+
);
|
|
327
|
+
if (lexical)
|
|
328
|
+
throw revalidateError(file, code, lexical.node, lexical.detail);
|
|
329
|
+
|
|
330
|
+
const free = collectFreeReferences(value, references, moduleScope, imports);
|
|
331
|
+
const binding = free.bindings[0];
|
|
332
|
+
if (binding) {
|
|
333
|
+
const name = binding.node.name;
|
|
334
|
+
throw revalidateError(
|
|
335
|
+
file,
|
|
336
|
+
code,
|
|
337
|
+
binding.node,
|
|
338
|
+
binding.scope.lookup(name) === moduleScope
|
|
339
|
+
? `\`${name}\` is declared in this "use client" module. Move the ` +
|
|
340
|
+
'predicate to its own module (without "use client") and ' +
|
|
341
|
+
"import it, or write it inline."
|
|
342
|
+
: `\`${name}\` is a binding of the enclosing function. Read what ` +
|
|
343
|
+
"you need from the predicate's argument (`currentParams`, " +
|
|
344
|
+
"`nextUrl`, `context`, `actionResult`, ...).",
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
const browserGlobal = free.globals.find((ref) =>
|
|
348
|
+
BROWSER_GLOBALS.has(ref.node.name),
|
|
349
|
+
);
|
|
350
|
+
if (browserGlobal) {
|
|
351
|
+
throw revalidateError(
|
|
352
|
+
file,
|
|
353
|
+
code,
|
|
354
|
+
browserGlobal.node,
|
|
355
|
+
`\`${browserGlobal.node.name}\` is a browser global. Put the input ` +
|
|
356
|
+
"on the request (a cookie or a search param) and read it from the " +
|
|
357
|
+
"predicate's argument, or refresh the loader with useRefreshLoaders.",
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const unchecked = [...free.imports].filter(
|
|
362
|
+
([local, valueUse]) => valueUse && !used.get(local),
|
|
363
|
+
);
|
|
364
|
+
const clientOnly = await Promise.all(
|
|
365
|
+
unchecked.map(([local]) => isClientOnly(imports.get(local)!, deps)),
|
|
366
|
+
);
|
|
367
|
+
const offender = unchecked.find((_, i) => clientOnly[i]);
|
|
368
|
+
if (offender) {
|
|
369
|
+
const [local] = offender;
|
|
370
|
+
throw revalidateError(
|
|
371
|
+
file,
|
|
372
|
+
code,
|
|
373
|
+
value,
|
|
374
|
+
`\`${local}\` is imported from "${imports.get(local)!.source}", a ` +
|
|
375
|
+
'client-only module: on the server a "use client" module\'s ' +
|
|
376
|
+
"exports are client references and cannot be called. Import the " +
|
|
377
|
+
'predicate\'s inputs from a module without "use client".',
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
for (const [local, valueUse] of free.imports) {
|
|
381
|
+
used.set(local, (used.get(local) ?? false) || valueUse);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const source = code.slice(value.start, value.end);
|
|
385
|
+
const key = keyOf(relativeFile, value);
|
|
386
|
+
entries.set(key, source);
|
|
387
|
+
s.overwrite(
|
|
388
|
+
arg.start,
|
|
389
|
+
arg.end,
|
|
390
|
+
`{ ${HOISTED_REVALIDATE_KEY}: ${JSON.stringify(key)} }`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const importLines = await emitImports(used, imports, () => {});
|
|
395
|
+
const module = [
|
|
396
|
+
`import { registerClientUrlRevalidators as ${REGISTER} } from "@rangojs/router/server";`,
|
|
397
|
+
...importLines,
|
|
398
|
+
`${REGISTER}({`,
|
|
399
|
+
...[...entries].map(
|
|
400
|
+
([key, source]) => ` ${JSON.stringify(key)}: ${source},`,
|
|
401
|
+
),
|
|
402
|
+
`});`,
|
|
403
|
+
"",
|
|
404
|
+
].join("\n");
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
code: s.toString(),
|
|
408
|
+
map: s.generateMap({ hires: true, source: file }),
|
|
409
|
+
module,
|
|
410
|
+
keys: [...entries.keys()],
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function isClientOnly(
|
|
415
|
+
binding: ImportBinding,
|
|
416
|
+
deps: HoistClientUrlsRevalidateDeps,
|
|
417
|
+
): Promise<boolean> {
|
|
418
|
+
return (
|
|
419
|
+
binding.source === "client-only" ||
|
|
420
|
+
Boolean(await deps.isClientModule?.(binding.source))
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** The file a `?rango-revalidate` id names, or null. */
|
|
425
|
+
export function parseHoistedRevalidateId(id: string): string | null {
|
|
426
|
+
const q = id.indexOf("?");
|
|
427
|
+
if (q === -1) return null;
|
|
428
|
+
const params = new URLSearchParams(id.slice(q + 1));
|
|
429
|
+
return params.has(CLIENT_URLS_REVALIDATE_QUERY) ? id.slice(0, q) : null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// The hook filters below are conservative screens (a superset of what the
|
|
433
|
+
// handlers accept): the handlers keep their exact checks.
|
|
434
|
+
const HOISTED_ID_RE = new RegExp(
|
|
435
|
+
`[?&]${CLIENT_URLS_REVALIDATE_QUERY}(?:[=&]|$)`,
|
|
436
|
+
);
|
|
437
|
+
const SOURCE_ID_RE = /\.[cm]?[jt]sx?(?:\?|$)/;
|
|
438
|
+
const NODE_MODULES_RE = /\/node_modules\//;
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* The plugins: a pre transform that hoists in every environment (markers in
|
|
442
|
+
* the client and SSR copies, the server module recorded from the RSC
|
|
443
|
+
* transform) and serves `?rango-revalidate`, and a post transform that
|
|
444
|
+
* appends its import to the RSC client-reference proxy.
|
|
445
|
+
*
|
|
446
|
+
* `load` serves the module the RSC transform emitted, which hoisted the
|
|
447
|
+
* pipeline code; the disk source is re-hoisted only when the transform has
|
|
448
|
+
* not seen the current file (loadEmittedOrDisk). A file whose edit removed
|
|
449
|
+
* every predicate serves an empty module to a proxy that still imports it.
|
|
450
|
+
*
|
|
451
|
+
* The transform's code filter skips a module that never mentions clientUrls,
|
|
452
|
+
* so a file that stops doing so would keep its `emitted` entry: watchChange
|
|
453
|
+
* drops it, together with the cached "use client" answer for an edited
|
|
454
|
+
* dependency (isClientModule memoizes per resolved file; buildStart resets
|
|
455
|
+
* it).
|
|
456
|
+
*/
|
|
457
|
+
export function clientUrlsRevalidateHoistPlugin(): Plugin[] {
|
|
458
|
+
const emitted = new Map<string, EmittedEntry<string>>();
|
|
459
|
+
const clientModules = new Map<string, Promise<boolean>>();
|
|
460
|
+
let root = process.cwd();
|
|
461
|
+
|
|
462
|
+
const isClientFile = (file: string): Promise<boolean> => {
|
|
463
|
+
let known = clientModules.get(file);
|
|
464
|
+
if (!known) {
|
|
465
|
+
known = readSource(file).then(
|
|
466
|
+
(text) =>
|
|
467
|
+
text !== null &&
|
|
468
|
+
text.includes("use client") &&
|
|
469
|
+
hasUseClientDirective(text),
|
|
470
|
+
);
|
|
471
|
+
clientModules.set(file, known);
|
|
472
|
+
}
|
|
473
|
+
return known;
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
return [
|
|
477
|
+
{
|
|
478
|
+
name: "@rangojs/router:client-urls-revalidate-hoist",
|
|
479
|
+
enforce: "pre",
|
|
480
|
+
|
|
481
|
+
configResolved(config) {
|
|
482
|
+
root = config.root;
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
buildStart() {
|
|
486
|
+
clientModules.clear();
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
watchChange(id) {
|
|
490
|
+
const file = normalizePath(id);
|
|
491
|
+
clientModules.delete(file);
|
|
492
|
+
emitted.delete(file);
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
load: {
|
|
496
|
+
filter: { id: HOISTED_ID_RE },
|
|
497
|
+
async handler(id) {
|
|
498
|
+
const hoistedFile = parseHoistedRevalidateId(id);
|
|
499
|
+
if (hoistedFile === null) return;
|
|
500
|
+
const module = await loadEmittedOrDisk(
|
|
501
|
+
emitted,
|
|
502
|
+
hoistedFile,
|
|
503
|
+
async (disk) =>
|
|
504
|
+
(await hoistClientUrlsRevalidates(disk, hoistedFile, { root }))
|
|
505
|
+
?.module,
|
|
506
|
+
);
|
|
507
|
+
return module ?? "export {};\n";
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
|
|
511
|
+
transform: {
|
|
512
|
+
filter: {
|
|
513
|
+
id: {
|
|
514
|
+
include: SOURCE_ID_RE,
|
|
515
|
+
exclude: [NODE_MODULES_RE, HOISTED_ID_RE],
|
|
516
|
+
},
|
|
517
|
+
code: CLIENT_URLS_RE,
|
|
518
|
+
},
|
|
519
|
+
async handler(code, id) {
|
|
520
|
+
if (id.includes("/node_modules/") || parseHoistedRevalidateId(id)) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const file = normalizePath(id.split("?", 1)[0]!);
|
|
524
|
+
if (!SOURCE_FILE_RE.test(file)) return;
|
|
525
|
+
const rsc = this.environment?.name === "rsc";
|
|
526
|
+
if (rsc) emitted.delete(file);
|
|
527
|
+
const result = await hoistClientUrlsRevalidates(code, id, {
|
|
528
|
+
root,
|
|
529
|
+
isClientModule: rsc
|
|
530
|
+
? async (source) => {
|
|
531
|
+
const target = await resolveSourceTarget(this, source, id, {
|
|
532
|
+
skipNodeModules: false,
|
|
533
|
+
});
|
|
534
|
+
return target !== null && (await isClientFile(target));
|
|
535
|
+
}
|
|
536
|
+
: undefined,
|
|
537
|
+
});
|
|
538
|
+
if (!result) return;
|
|
539
|
+
if (rsc) await recordEmitted(emitted, file, result.module);
|
|
540
|
+
return { code: result.code, map: result.map };
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: "@rangojs/router:client-urls-revalidate-register",
|
|
546
|
+
enforce: "post",
|
|
547
|
+
applyToEnvironment: (env) => env.name === "rsc",
|
|
548
|
+
|
|
549
|
+
transform: {
|
|
550
|
+
filter: { code: "registerClientReference" },
|
|
551
|
+
handler(code, id) {
|
|
552
|
+
if (parseHoistedRevalidateId(id)) return;
|
|
553
|
+
const file = normalizePath(id.split("?", 1)[0]!);
|
|
554
|
+
if (!emitted.has(file)) return;
|
|
555
|
+
const s = new MagicString(code);
|
|
556
|
+
s.append(
|
|
557
|
+
`\nimport ${JSON.stringify(`${file}?${CLIENT_URLS_REVALIDATE_QUERY}`)};\n`,
|
|
558
|
+
);
|
|
559
|
+
return { code: s.toString(), map: s.generateMap({ hires: true }) };
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
];
|
|
564
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
transformInlineHandlers,
|
|
13
13
|
type VirtualHandlerEntry,
|
|
14
14
|
} from "../utils/ast-handler-extract.js";
|
|
15
|
+
import { isSkippedSourceScanDir } from "../../build/route-types/scan-filter.js";
|
|
15
16
|
|
|
16
17
|
import type {
|
|
17
18
|
HandlerTransformConfig,
|
|
@@ -297,8 +298,6 @@ ${lazyImports.join(",\n")}
|
|
|
297
298
|
|
|
298
299
|
const fs = await import("node:fs/promises");
|
|
299
300
|
|
|
300
|
-
const SKIP_DIRS = new Set(["node_modules", "dist", "build", "coverage"]);
|
|
301
|
-
|
|
302
301
|
async function scanDir(dir: string): Promise<string[]> {
|
|
303
302
|
const results: string[] = [];
|
|
304
303
|
try {
|
|
@@ -306,7 +305,7 @@ ${lazyImports.join(",\n")}
|
|
|
306
305
|
for (const entry of entries) {
|
|
307
306
|
const fullPath = path.join(dir, entry.name);
|
|
308
307
|
if (entry.isDirectory()) {
|
|
309
|
-
if (!
|
|
308
|
+
if (!isSkippedSourceScanDir(entry.name)) {
|
|
310
309
|
results.push(...(await scanDir(fullPath)));
|
|
311
310
|
}
|
|
312
311
|
} else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|