@rangojs/router 0.22.1 → 0.23.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 +8 -3
- 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/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +34 -5
- package/dist/types/browser/react/NavigationProvider.d.ts +13 -3
- 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 +19 -9
- 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 +16 -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/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/errors.d.ts +5 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +23 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +241 -0
- 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 +14 -1
- 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 +3 -3
- 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/run-loader.d.ts +2 -1
- 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 +38 -0
- package/dist/types/testing/vitest.d.ts +11 -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/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 +11 -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 +842 -308
- package/package.json +1 -1
- package/skills/caching/SKILL.md +42 -5
- package/skills/catalog.json +2 -2
- package/skills/client-urls/SKILL.md +113 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +11 -3
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +192 -1
- 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 +13 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +12 -6
- package/skills/route/SKILL.md +139 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +1 -1
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/serve-shell-request.md +88 -18
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +43 -32
- package/src/browser/navigation-bridge.ts +64 -14
- 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 +528 -135
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/NavigationProvider.tsx +47 -28
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +19 -63
- package/src/browser/types.ts +19 -9
- 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 +37 -10
- 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.ts +22 -1
- 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/errors.ts +15 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +122 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- 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/router/intercept-resolution.ts +6 -4
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +83 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +11 -1
- package/src/router/match-middleware/cache-lookup.ts +29 -6
- package/src/router/match-middleware/cache-store.ts +17 -1
- package/src/router/match-result.ts +14 -2
- package/src/router/navigation-snapshot.ts +2 -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/prefetch-deferral.ts +543 -0
- package/src/router/segment-resolution/revalidation.ts +169 -60
- package/src/rsc/rsc-rendering.ts +50 -7
- package/src/rsc/shell-capture.ts +2 -1
- 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/handle-store.ts +104 -8
- package/src/server/request-context.ts +21 -1
- package/src/server.ts +4 -0
- package/src/testing/action-form.ts +2 -11
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +6 -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/run-loader.ts +2 -1
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +116 -6
- package/src/testing/vitest.ts +67 -1
- 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/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 +20 -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/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 +93 -32
- 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
|
@@ -2,8 +2,36 @@ import type { Plugin } from "vite";
|
|
|
2
2
|
import { normalizePath, parseAst } from "vite";
|
|
3
3
|
import MagicString from "magic-string";
|
|
4
4
|
import { builtinModules } from "node:module";
|
|
5
|
-
import {
|
|
5
|
+
import { hasDirective } from "@vitejs/plugin-rsc/transforms";
|
|
6
|
+
import { offsetToLineColumn } from "./expose-ids/export-analysis.js";
|
|
6
7
|
import { TransitionWhenError } from "../../transition-when-ref.js";
|
|
8
|
+
import {
|
|
9
|
+
analyzeScopes,
|
|
10
|
+
collectImports,
|
|
11
|
+
type ImportBinding,
|
|
12
|
+
type Node,
|
|
13
|
+
type Scope,
|
|
14
|
+
} from "./hoist/scope.js";
|
|
15
|
+
import {
|
|
16
|
+
collectFreeReferences,
|
|
17
|
+
directivePrologueEnd,
|
|
18
|
+
emitImports,
|
|
19
|
+
findHelperCalls,
|
|
20
|
+
findLexicalCapture,
|
|
21
|
+
isFunctionLiteral,
|
|
22
|
+
parseLang,
|
|
23
|
+
propertyKeyIs,
|
|
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";
|
|
7
35
|
|
|
8
36
|
/**
|
|
9
37
|
* Inline transition({ when }) in urls(): the build hoists the function
|
|
@@ -51,397 +79,6 @@ const TRANSITION_WHEN_QUERY = "rango-when";
|
|
|
51
79
|
const EXPORT_NAME = "__rango_when";
|
|
52
80
|
const LOCAL_PREFIX = "__rango_when_";
|
|
53
81
|
|
|
54
|
-
type Node = any;
|
|
55
|
-
|
|
56
|
-
interface ImportBinding {
|
|
57
|
-
readonly source: string;
|
|
58
|
-
/** "default", "*", or the imported name. */
|
|
59
|
-
readonly imported: string;
|
|
60
|
-
readonly typeOnly: boolean;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
class Scope {
|
|
64
|
-
readonly declarations = new Set<string>();
|
|
65
|
-
constructor(
|
|
66
|
-
readonly parent: Scope | null,
|
|
67
|
-
readonly isFunction: boolean,
|
|
68
|
-
readonly node: Node,
|
|
69
|
-
) {}
|
|
70
|
-
nearestFunction(): Scope {
|
|
71
|
-
return this.isFunction || !this.parent
|
|
72
|
-
? this
|
|
73
|
-
: this.parent.nearestFunction();
|
|
74
|
-
}
|
|
75
|
-
lookup(name: string): Scope | null {
|
|
76
|
-
if (this.declarations.has(name)) return this;
|
|
77
|
-
return this.parent ? this.parent.lookup(name) : null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
interface Reference {
|
|
82
|
-
readonly node: Node;
|
|
83
|
-
readonly scope: Scope;
|
|
84
|
-
readonly typeOnly: boolean;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const TYPE_KEYS = new Set([
|
|
88
|
-
"typeAnnotation",
|
|
89
|
-
"returnType",
|
|
90
|
-
"typeParameters",
|
|
91
|
-
"typeArguments",
|
|
92
|
-
"superTypeArguments",
|
|
93
|
-
"implements",
|
|
94
|
-
]);
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* The identifier a JSX element name refers to: a component (`<Local />`) or
|
|
98
|
-
* the root of a member name (`<ui.Badge />`). Lowercase and dashed names are
|
|
99
|
-
* intrinsic tags, not references; `<this.X />` is checked as `this`.
|
|
100
|
-
*/
|
|
101
|
-
function jsxNameReference(name: Node): Node | null {
|
|
102
|
-
if (name?.type === "JSXIdentifier") {
|
|
103
|
-
return /^[a-z]/.test(name.name) || name.name.includes("-") ? null : name;
|
|
104
|
-
}
|
|
105
|
-
let root = name;
|
|
106
|
-
while (root?.type === "JSXMemberExpression") root = root.object;
|
|
107
|
-
return root?.type === "JSXIdentifier" && root.name !== "this" ? root : null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function patternNames(pattern: Node, names: string[] = []): string[] {
|
|
111
|
-
if (!pattern) return names;
|
|
112
|
-
switch (pattern.type) {
|
|
113
|
-
case "Identifier":
|
|
114
|
-
names.push(pattern.name);
|
|
115
|
-
break;
|
|
116
|
-
case "ObjectPattern":
|
|
117
|
-
for (const prop of pattern.properties) {
|
|
118
|
-
patternNames(prop.type === "RestElement" ? prop : prop.value, names);
|
|
119
|
-
}
|
|
120
|
-
break;
|
|
121
|
-
case "ArrayPattern":
|
|
122
|
-
for (const element of pattern.elements) patternNames(element, names);
|
|
123
|
-
break;
|
|
124
|
-
case "RestElement":
|
|
125
|
-
patternNames(pattern.argument, names);
|
|
126
|
-
break;
|
|
127
|
-
case "AssignmentPattern":
|
|
128
|
-
patternNames(pattern.left, names);
|
|
129
|
-
break;
|
|
130
|
-
case "TSParameterProperty":
|
|
131
|
-
patternNames(pattern.parameter, names);
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
return names;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Scope tree and every identifier reference of a module, TS type positions
|
|
139
|
-
* flagged. Enough of ES scoping for free-variable analysis of one literal:
|
|
140
|
-
* var hoists to the function scope, let/const/class to the block, function
|
|
141
|
-
* declarations to their block.
|
|
142
|
-
*/
|
|
143
|
-
function analyzeScopes(program: Node): {
|
|
144
|
-
moduleScope: Scope;
|
|
145
|
-
references: Reference[];
|
|
146
|
-
scopeOf: Map<Node, Scope>;
|
|
147
|
-
} {
|
|
148
|
-
const moduleScope = new Scope(null, true, program);
|
|
149
|
-
const scopeOf = new Map<Node, Scope>([[program, moduleScope]]);
|
|
150
|
-
const references: Reference[] = [];
|
|
151
|
-
|
|
152
|
-
const declarePattern = (pattern: Node, scope: Scope): void => {
|
|
153
|
-
for (const name of patternNames(pattern)) scope.declarations.add(name);
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
// Binding patterns: identifiers declare; defaults and computed keys refer.
|
|
157
|
-
const walkPattern = (pattern: Node, scope: Scope, typeOnly: boolean) => {
|
|
158
|
-
if (!pattern) return;
|
|
159
|
-
switch (pattern.type) {
|
|
160
|
-
case "Identifier":
|
|
161
|
-
if (pattern.typeAnnotation) walk(pattern.typeAnnotation, scope, true);
|
|
162
|
-
return;
|
|
163
|
-
case "ObjectPattern":
|
|
164
|
-
for (const prop of pattern.properties) {
|
|
165
|
-
if (prop.type === "RestElement") {
|
|
166
|
-
walkPattern(prop.argument, scope, typeOnly);
|
|
167
|
-
} else {
|
|
168
|
-
if (prop.computed) walk(prop.key, scope, typeOnly);
|
|
169
|
-
walkPattern(prop.value, scope, typeOnly);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (pattern.typeAnnotation) walk(pattern.typeAnnotation, scope, true);
|
|
173
|
-
return;
|
|
174
|
-
case "ArrayPattern":
|
|
175
|
-
for (const element of pattern.elements) {
|
|
176
|
-
walkPattern(element, scope, typeOnly);
|
|
177
|
-
}
|
|
178
|
-
if (pattern.typeAnnotation) walk(pattern.typeAnnotation, scope, true);
|
|
179
|
-
return;
|
|
180
|
-
case "RestElement":
|
|
181
|
-
walkPattern(pattern.argument, scope, typeOnly);
|
|
182
|
-
return;
|
|
183
|
-
case "AssignmentPattern":
|
|
184
|
-
walkPattern(pattern.left, scope, typeOnly);
|
|
185
|
-
walk(pattern.right, scope, typeOnly);
|
|
186
|
-
return;
|
|
187
|
-
case "TSParameterProperty":
|
|
188
|
-
walkPattern(pattern.parameter, scope, typeOnly);
|
|
189
|
-
return;
|
|
190
|
-
default:
|
|
191
|
-
walk(pattern, scope, typeOnly);
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
// Hoist function-scoped and block-scoped declarations of a body before its
|
|
196
|
-
// statements are walked, so a reference ahead of its declaration resolves.
|
|
197
|
-
const hoistDeclarations = (statements: Node[], scope: Scope): void => {
|
|
198
|
-
for (const statement of statements) {
|
|
199
|
-
const node =
|
|
200
|
-
statement.type === "ExportNamedDeclaration" ||
|
|
201
|
-
statement.type === "ExportDefaultDeclaration"
|
|
202
|
-
? statement.declaration
|
|
203
|
-
: statement;
|
|
204
|
-
if (!node) continue;
|
|
205
|
-
switch (node.type) {
|
|
206
|
-
case "ImportDeclaration":
|
|
207
|
-
for (const spec of node.specifiers) {
|
|
208
|
-
scope.declarations.add(spec.local.name);
|
|
209
|
-
}
|
|
210
|
-
break;
|
|
211
|
-
case "FunctionDeclaration":
|
|
212
|
-
case "ClassDeclaration":
|
|
213
|
-
case "TSEnumDeclaration":
|
|
214
|
-
case "TSTypeAliasDeclaration":
|
|
215
|
-
case "TSInterfaceDeclaration":
|
|
216
|
-
case "TSModuleDeclaration":
|
|
217
|
-
if (node.id?.type === "Identifier") {
|
|
218
|
-
scope.declarations.add(node.id.name);
|
|
219
|
-
}
|
|
220
|
-
break;
|
|
221
|
-
case "VariableDeclaration":
|
|
222
|
-
for (const declarator of node.declarations) {
|
|
223
|
-
declarePattern(
|
|
224
|
-
declarator.id,
|
|
225
|
-
node.kind === "var" ? scope.nearestFunction() : scope,
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const walk = (node: Node, scope: Scope, typeOnly: boolean): void => {
|
|
234
|
-
if (!node || typeof node !== "object") return;
|
|
235
|
-
if (Array.isArray(node)) {
|
|
236
|
-
for (const item of node) walk(item, scope, typeOnly);
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
if (typeof node.type !== "string") return;
|
|
240
|
-
|
|
241
|
-
switch (node.type) {
|
|
242
|
-
case "Identifier":
|
|
243
|
-
references.push({ node, scope, typeOnly });
|
|
244
|
-
if (node.typeAnnotation) walk(node.typeAnnotation, scope, true);
|
|
245
|
-
return;
|
|
246
|
-
case "ImportDeclaration":
|
|
247
|
-
case "ExportAllDeclaration":
|
|
248
|
-
return;
|
|
249
|
-
case "ExportNamedDeclaration":
|
|
250
|
-
if (node.declaration) walk(node.declaration, scope, typeOnly);
|
|
251
|
-
else if (!node.source) {
|
|
252
|
-
for (const spec of node.specifiers) {
|
|
253
|
-
walk(spec.local, scope, typeOnly || node.exportKind === "type");
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return;
|
|
257
|
-
case "MemberExpression":
|
|
258
|
-
walk(node.object, scope, typeOnly);
|
|
259
|
-
if (node.computed) walk(node.property, scope, typeOnly);
|
|
260
|
-
return;
|
|
261
|
-
case "Property":
|
|
262
|
-
case "MethodDefinition":
|
|
263
|
-
case "PropertyDefinition":
|
|
264
|
-
case "TSPropertySignature":
|
|
265
|
-
case "TSMethodSignature":
|
|
266
|
-
if (node.computed) walk(node.key, scope, typeOnly);
|
|
267
|
-
walk(node.value, scope, typeOnly);
|
|
268
|
-
if (node.typeAnnotation) walk(node.typeAnnotation, scope, true);
|
|
269
|
-
return;
|
|
270
|
-
case "LabeledStatement":
|
|
271
|
-
walk(node.body, scope, typeOnly);
|
|
272
|
-
return;
|
|
273
|
-
case "BreakStatement":
|
|
274
|
-
case "ContinueStatement":
|
|
275
|
-
case "MetaProperty":
|
|
276
|
-
return;
|
|
277
|
-
case "JSXOpeningElement":
|
|
278
|
-
case "JSXClosingElement": {
|
|
279
|
-
const ref = jsxNameReference(node.name);
|
|
280
|
-
if (ref) references.push({ node: ref, scope, typeOnly });
|
|
281
|
-
walk(node.typeArguments, scope, true);
|
|
282
|
-
walk(node.attributes, scope, typeOnly);
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
case "TSQualifiedName":
|
|
286
|
-
walk(node.left, scope, typeOnly);
|
|
287
|
-
return;
|
|
288
|
-
case "TSEnumMember":
|
|
289
|
-
walk(node.initializer, scope, typeOnly);
|
|
290
|
-
return;
|
|
291
|
-
case "TSTypeAliasDeclaration":
|
|
292
|
-
case "TSInterfaceDeclaration":
|
|
293
|
-
walk(node.typeParameters, scope, true);
|
|
294
|
-
walk(node.typeAnnotation ?? node.body, scope, true);
|
|
295
|
-
walk(node.extends, scope, true);
|
|
296
|
-
return;
|
|
297
|
-
case "VariableDeclarator":
|
|
298
|
-
walkPattern(node.id, scope, typeOnly);
|
|
299
|
-
walk(node.init, scope, typeOnly);
|
|
300
|
-
return;
|
|
301
|
-
case "FunctionDeclaration":
|
|
302
|
-
case "FunctionExpression":
|
|
303
|
-
case "ArrowFunctionExpression": {
|
|
304
|
-
const fnScope = new Scope(scope, true, node);
|
|
305
|
-
scopeOf.set(node, fnScope);
|
|
306
|
-
if (node.type === "FunctionExpression" && node.id) {
|
|
307
|
-
fnScope.declarations.add(node.id.name);
|
|
308
|
-
}
|
|
309
|
-
for (const param of node.params) declarePattern(param, fnScope);
|
|
310
|
-
walk(node.typeParameters, fnScope, true);
|
|
311
|
-
for (const param of node.params) walkPattern(param, fnScope, typeOnly);
|
|
312
|
-
walk(node.returnType, fnScope, true);
|
|
313
|
-
if (node.body?.type === "BlockStatement") {
|
|
314
|
-
hoistDeclarations(node.body.body, fnScope);
|
|
315
|
-
walk(node.body.body, fnScope, typeOnly);
|
|
316
|
-
} else {
|
|
317
|
-
walk(node.body, fnScope, typeOnly);
|
|
318
|
-
}
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
case "ClassDeclaration":
|
|
322
|
-
case "ClassExpression": {
|
|
323
|
-
let classScope = scope;
|
|
324
|
-
if (node.type === "ClassExpression" && node.id) {
|
|
325
|
-
classScope = new Scope(scope, false, node);
|
|
326
|
-
classScope.declarations.add(node.id.name);
|
|
327
|
-
scopeOf.set(node, classScope);
|
|
328
|
-
}
|
|
329
|
-
walk(node.superClass, classScope, typeOnly);
|
|
330
|
-
walk(node.superTypeArguments, classScope, true);
|
|
331
|
-
walk(node.typeParameters, classScope, true);
|
|
332
|
-
walk(node.implements, classScope, true);
|
|
333
|
-
walk(node.body, classScope, typeOnly);
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
case "BlockStatement":
|
|
337
|
-
case "StaticBlock": {
|
|
338
|
-
const blockScope = new Scope(scope, false, node);
|
|
339
|
-
scopeOf.set(node, blockScope);
|
|
340
|
-
hoistDeclarations(node.body, blockScope);
|
|
341
|
-
walk(node.body, blockScope, typeOnly);
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
case "ForStatement":
|
|
345
|
-
case "ForInStatement":
|
|
346
|
-
case "ForOfStatement": {
|
|
347
|
-
const loopScope = new Scope(scope, false, node);
|
|
348
|
-
scopeOf.set(node, loopScope);
|
|
349
|
-
const head = node.type === "ForStatement" ? node.init : node.left;
|
|
350
|
-
if (head?.type === "VariableDeclaration") {
|
|
351
|
-
hoistDeclarations([head], loopScope);
|
|
352
|
-
}
|
|
353
|
-
for (const key of ["init", "left", "right", "test", "update", "body"]) {
|
|
354
|
-
walk(node[key], loopScope, typeOnly);
|
|
355
|
-
}
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
case "SwitchStatement": {
|
|
359
|
-
walk(node.discriminant, scope, typeOnly);
|
|
360
|
-
const switchScope = new Scope(scope, false, node);
|
|
361
|
-
scopeOf.set(node, switchScope);
|
|
362
|
-
hoistDeclarations(
|
|
363
|
-
node.cases.flatMap((c: Node) => c.consequent),
|
|
364
|
-
switchScope,
|
|
365
|
-
);
|
|
366
|
-
for (const c of node.cases) {
|
|
367
|
-
walk(c.test, switchScope, typeOnly);
|
|
368
|
-
walk(c.consequent, switchScope, typeOnly);
|
|
369
|
-
}
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
case "CatchClause": {
|
|
373
|
-
const catchScope = new Scope(scope, false, node);
|
|
374
|
-
scopeOf.set(node, catchScope);
|
|
375
|
-
if (node.param) {
|
|
376
|
-
declarePattern(node.param, catchScope);
|
|
377
|
-
walkPattern(node.param, catchScope, typeOnly);
|
|
378
|
-
}
|
|
379
|
-
walk(node.body, catchScope, typeOnly);
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
for (const key of Object.keys(node)) {
|
|
385
|
-
if (key === "type" || key === "start" || key === "end") continue;
|
|
386
|
-
walk(node[key], scope, typeOnly || TYPE_KEYS.has(key));
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
hoistDeclarations(program.body, moduleScope);
|
|
391
|
-
walk(program.body, moduleScope, false);
|
|
392
|
-
return { moduleScope, references, scopeOf };
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function collectImports(program: Node): Map<string, ImportBinding> {
|
|
396
|
-
const imports = new Map<string, ImportBinding>();
|
|
397
|
-
for (const statement of program.body) {
|
|
398
|
-
if (statement.type !== "ImportDeclaration") continue;
|
|
399
|
-
const declTypeOnly = statement.importKind === "type";
|
|
400
|
-
for (const spec of statement.specifiers) {
|
|
401
|
-
imports.set(spec.local.name, {
|
|
402
|
-
source: statement.source.value,
|
|
403
|
-
imported:
|
|
404
|
-
spec.type === "ImportDefaultSpecifier"
|
|
405
|
-
? "default"
|
|
406
|
-
: spec.type === "ImportNamespaceSpecifier"
|
|
407
|
-
? "*"
|
|
408
|
-
: spec.imported.type === "Identifier"
|
|
409
|
-
? spec.imported.name
|
|
410
|
-
: String(spec.imported.value),
|
|
411
|
-
typeOnly: declTypeOnly || spec.importKind === "type",
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
return imports;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function hasUseClientDirective(program: Node): boolean {
|
|
419
|
-
for (const statement of program.body) {
|
|
420
|
-
if (
|
|
421
|
-
statement.type !== "ExpressionStatement" ||
|
|
422
|
-
typeof statement.directive !== "string"
|
|
423
|
-
) {
|
|
424
|
-
return false;
|
|
425
|
-
}
|
|
426
|
-
if (statement.directive === "use client") return true;
|
|
427
|
-
}
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function lineColumn(code: string, offset: number): string {
|
|
432
|
-
let line = 1;
|
|
433
|
-
let column = 1;
|
|
434
|
-
for (let i = 0; i < offset && i < code.length; i++) {
|
|
435
|
-
if (code[i] === "\n") {
|
|
436
|
-
line++;
|
|
437
|
-
column = 1;
|
|
438
|
-
} else {
|
|
439
|
-
column++;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return `${line}:${column}`;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
82
|
/**
|
|
446
83
|
* A `when` site: the `when` property of a router transition() config, and its
|
|
447
84
|
* value with TS wrappers dropped (a function literal, an identifier, ...).
|
|
@@ -452,44 +89,7 @@ interface WhenSite {
|
|
|
452
89
|
readonly scope: Scope;
|
|
453
90
|
}
|
|
454
91
|
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
const EXPRESSION_WRAPPERS = new Set([
|
|
458
|
-
"TSAsExpression",
|
|
459
|
-
"TSSatisfiesExpression",
|
|
460
|
-
"TSNonNullExpression",
|
|
461
|
-
"TSTypeAssertion",
|
|
462
|
-
"ParenthesizedExpression",
|
|
463
|
-
]);
|
|
464
|
-
|
|
465
|
-
function unwrapExpression(node: Node): Node {
|
|
466
|
-
while (node && EXPRESSION_WRAPPERS.has(node.type)) node = node.expression;
|
|
467
|
-
return node;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function isFunctionLiteral(node: Node): boolean {
|
|
471
|
-
return (
|
|
472
|
-
node?.type === "ArrowFunctionExpression" ||
|
|
473
|
-
node?.type === "FunctionExpression"
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function propertyKeyIs(prop: Node, name: string): boolean {
|
|
478
|
-
return (
|
|
479
|
-
prop?.type === "Property" &&
|
|
480
|
-
!prop.computed &&
|
|
481
|
-
((prop.key.type === "Identifier" && prop.key.name === name) ||
|
|
482
|
-
(prop.key.type === "Literal" && prop.key.value === name))
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/** What a urls() callback's first parameter binds the transition helper to. */
|
|
487
|
-
interface HelperBindings {
|
|
488
|
-
/** `({ transition })`, `({ transition: t })`. */
|
|
489
|
-
readonly transitions: Set<string>;
|
|
490
|
-
/** `(helpers) => helpers.transition(...)`. */
|
|
491
|
-
readonly objects: Set<string>;
|
|
492
|
-
}
|
|
92
|
+
const ROUTER_SOURCES: ReadonlySet<string> = new Set(["@rangojs/router"]);
|
|
493
93
|
|
|
494
94
|
function findWhenSites(
|
|
495
95
|
program: Node,
|
|
@@ -498,244 +98,52 @@ function findWhenSites(
|
|
|
498
98
|
imports: Map<string, ImportBinding>,
|
|
499
99
|
): WhenSite[] {
|
|
500
100
|
const sites: WhenSite[] = [];
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
: null;
|
|
518
|
-
}
|
|
519
|
-
if (
|
|
520
|
-
callee.type === "MemberExpression" &&
|
|
521
|
-
!callee.computed &&
|
|
522
|
-
callee.property.type === "Identifier" &&
|
|
523
|
-
routerImport(callee.object)?.imported === "*"
|
|
524
|
-
) {
|
|
525
|
-
return callee.property.name;
|
|
526
|
-
}
|
|
527
|
-
return null;
|
|
528
|
-
};
|
|
529
|
-
|
|
530
|
-
const registerUrlsCallback = (callback: Node): void => {
|
|
531
|
-
const fnScope = scopeOf.get(callback);
|
|
532
|
-
let param = callback.params[0];
|
|
533
|
-
if (param?.type === "AssignmentPattern") param = param.left;
|
|
534
|
-
if (!fnScope || !param) return;
|
|
535
|
-
const bindings: HelperBindings = {
|
|
536
|
-
transitions: new Set(),
|
|
537
|
-
objects: new Set(),
|
|
538
|
-
};
|
|
539
|
-
if (param.type === "Identifier") bindings.objects.add(param.name);
|
|
540
|
-
if (param.type === "ObjectPattern") {
|
|
541
|
-
for (const prop of param.properties) {
|
|
542
|
-
if (!propertyKeyIs(prop, "transition")) continue;
|
|
543
|
-
const target =
|
|
544
|
-
prop.value.type === "AssignmentPattern"
|
|
545
|
-
? prop.value.left
|
|
546
|
-
: prop.value;
|
|
547
|
-
if (target.type === "Identifier") bindings.transitions.add(target.name);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
helpers.set(fnScope, bindings);
|
|
551
|
-
};
|
|
552
|
-
|
|
553
|
-
const isRouterTransition = (callee: Node, scope: Scope): boolean => {
|
|
554
|
-
if (routerExport(callee, scope) === "transition") return true;
|
|
555
|
-
if (callee.type === "Identifier") {
|
|
556
|
-
const decl = scope.lookup(callee.name);
|
|
557
|
-
return !!decl && !!helpers.get(decl)?.transitions.has(callee.name);
|
|
558
|
-
}
|
|
559
|
-
if (
|
|
560
|
-
callee.type === "MemberExpression" &&
|
|
561
|
-
!callee.computed &&
|
|
562
|
-
callee.property.type === "Identifier" &&
|
|
563
|
-
callee.property.name === "transition" &&
|
|
564
|
-
callee.object.type === "Identifier"
|
|
565
|
-
) {
|
|
566
|
-
const decl = scope.lookup(callee.object.name);
|
|
567
|
-
return !!decl && !!helpers.get(decl)?.objects.has(callee.object.name);
|
|
568
|
-
}
|
|
569
|
-
return false;
|
|
570
|
-
};
|
|
571
|
-
|
|
572
|
-
const visit = (node: Node, scope: Scope): void => {
|
|
573
|
-
if (!node || typeof node !== "object") return;
|
|
574
|
-
if (Array.isArray(node)) {
|
|
575
|
-
for (const item of node) visit(item, scope);
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
if (typeof node.type !== "string") return;
|
|
579
|
-
const own = scopeOf.get(node) ?? scope;
|
|
580
|
-
if (node.type === "CallExpression") {
|
|
581
|
-
// Registered before the callback body is visited, which is where its
|
|
582
|
-
// transition() calls are.
|
|
583
|
-
const callback = node.arguments[0];
|
|
584
|
-
if (
|
|
585
|
-
isFunctionLiteral(callback) &&
|
|
586
|
-
routerExport(node.callee, own) === "urls"
|
|
587
|
-
) {
|
|
588
|
-
registerUrlsCallback(callback);
|
|
589
|
-
}
|
|
590
|
-
if (
|
|
591
|
-
node.arguments[0]?.type === "ObjectExpression" &&
|
|
592
|
-
isRouterTransition(node.callee, own)
|
|
593
|
-
) {
|
|
594
|
-
for (const prop of node.arguments[0].properties) {
|
|
595
|
-
if (propertyKeyIs(prop, "when")) {
|
|
596
|
-
sites.push({
|
|
597
|
-
prop,
|
|
598
|
-
value: unwrapExpression(prop.value),
|
|
599
|
-
scope: own,
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
}
|
|
101
|
+
for (const { call, scope } of findHelperCalls(
|
|
102
|
+
program,
|
|
103
|
+
scopeOf,
|
|
104
|
+
moduleScope,
|
|
105
|
+
imports,
|
|
106
|
+
{
|
|
107
|
+
sources: ROUTER_SOURCES,
|
|
108
|
+
builder: "urls",
|
|
109
|
+
helper: "transition",
|
|
110
|
+
directImport: true,
|
|
111
|
+
},
|
|
112
|
+
)) {
|
|
113
|
+
if (call.arguments[0]?.type !== "ObjectExpression") continue;
|
|
114
|
+
for (const prop of call.arguments[0].properties) {
|
|
115
|
+
if (propertyKeyIs(prop, "when")) {
|
|
116
|
+
sites.push({ prop, value: unwrapExpression(prop.value), scope });
|
|
603
117
|
}
|
|
604
118
|
}
|
|
605
|
-
|
|
606
|
-
if (key === "type" || key === "start" || key === "end") continue;
|
|
607
|
-
visit(node[key], own);
|
|
608
|
-
}
|
|
609
|
-
};
|
|
610
|
-
visit(program.body, moduleScope);
|
|
119
|
+
}
|
|
611
120
|
return sites;
|
|
612
121
|
}
|
|
613
122
|
|
|
614
123
|
/** Why a lexical-context use in a literal is a capture, by construct. */
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
124
|
+
const WHEN_CAPTURE_MESSAGES: LexicalCaptureMessages = {
|
|
125
|
+
lexical: {
|
|
126
|
+
this: "`this` in an arrow is the enclosing server function's `this`.",
|
|
127
|
+
arguments:
|
|
128
|
+
"`arguments` in an arrow is the enclosing server function's `arguments`.",
|
|
129
|
+
super: "`super` refers to the enclosing server object.",
|
|
130
|
+
"new.target":
|
|
131
|
+
"`new.target` in an arrow is the enclosing server function's.",
|
|
132
|
+
},
|
|
133
|
+
captureHint:
|
|
134
|
+
"Read what you need from `to.params`/`to.state`, or export the " +
|
|
135
|
+
'predicate from a "use client" module.',
|
|
136
|
+
loadsHint:
|
|
137
|
+
"Import client-safe modules statically, or export the predicate from a " +
|
|
138
|
+
'"use client" module.',
|
|
139
|
+
importMetaWhy: "is the server module's.",
|
|
140
|
+
importMetaMemberWhy:
|
|
141
|
+
"is resolved against the server module; only `import.meta.env` is " +
|
|
142
|
+
"available to an inline predicate.",
|
|
143
|
+
dynamicImportWhy:
|
|
144
|
+
"loads a module at run time, which the build cannot check is client-safe.",
|
|
621
145
|
};
|
|
622
146
|
|
|
623
|
-
/**
|
|
624
|
-
* The first use in a literal of a lexical context it would lose when hoisted
|
|
625
|
-
* (`this`, `arguments`, `super`, `new.target` of an enclosing server function
|
|
626
|
-
* or object), or of a module-loading expression the build cannot check
|
|
627
|
-
* (`import()`, `import.meta` other than `import.meta.env`). A nested
|
|
628
|
-
* non-arrow function (or class body) owns its `this`/`arguments`, so uses
|
|
629
|
-
* inside one are fine; a method literal owns them too, but its `super`
|
|
630
|
-
* loses the object it was defined on.
|
|
631
|
-
*/
|
|
632
|
-
function findLexicalCapture(
|
|
633
|
-
literal: Node,
|
|
634
|
-
references: readonly Reference[],
|
|
635
|
-
code: string,
|
|
636
|
-
): { node: Node; detail: string } | null {
|
|
637
|
-
const ownArguments: Node[] = [];
|
|
638
|
-
let found: { node: Node; detail: string } | null = null;
|
|
639
|
-
const capture = (node: Node, what: string): void => {
|
|
640
|
-
found = {
|
|
641
|
-
node,
|
|
642
|
-
detail:
|
|
643
|
-
`${LEXICAL_CAPTURE[what]} Read what you need from ` +
|
|
644
|
-
'`to.params`/`to.state`, or export the predicate from a "use client" ' +
|
|
645
|
-
"module.",
|
|
646
|
-
};
|
|
647
|
-
};
|
|
648
|
-
const loads = (node: Node, expression: string, why: string): void => {
|
|
649
|
-
found = {
|
|
650
|
-
node,
|
|
651
|
-
detail:
|
|
652
|
-
`\`${expression}\` ${why} Import client-safe modules statically, ` +
|
|
653
|
-
'or export the predicate from a "use client" module.',
|
|
654
|
-
};
|
|
655
|
-
};
|
|
656
|
-
|
|
657
|
-
const visit = (node: Node, boundary: Node | null): void => {
|
|
658
|
-
if (found || !node || typeof node !== "object") return;
|
|
659
|
-
if (Array.isArray(node)) {
|
|
660
|
-
for (const item of node) visit(item, boundary);
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
if (typeof node.type !== "string") return;
|
|
664
|
-
switch (node.type) {
|
|
665
|
-
case "FunctionExpression":
|
|
666
|
-
case "FunctionDeclaration":
|
|
667
|
-
ownArguments.push(node);
|
|
668
|
-
boundary = node;
|
|
669
|
-
break;
|
|
670
|
-
case "ClassBody":
|
|
671
|
-
boundary = node;
|
|
672
|
-
break;
|
|
673
|
-
case "ThisExpression":
|
|
674
|
-
if (!boundary) capture(node, "this");
|
|
675
|
-
return;
|
|
676
|
-
case "Super":
|
|
677
|
-
if (!boundary || boundary === literal) capture(node, "super");
|
|
678
|
-
return;
|
|
679
|
-
case "JSXMemberExpression": {
|
|
680
|
-
let root = node;
|
|
681
|
-
while (root.type === "JSXMemberExpression") root = root.object;
|
|
682
|
-
if (root.name === "this" && !boundary) capture(root, "this");
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
case "MetaProperty":
|
|
686
|
-
if (node.meta.name === "import") {
|
|
687
|
-
loads(node, "import.meta", "is the server module's.");
|
|
688
|
-
} else if (!boundary) {
|
|
689
|
-
capture(node, "new.target");
|
|
690
|
-
}
|
|
691
|
-
return;
|
|
692
|
-
case "MemberExpression":
|
|
693
|
-
if (node.object?.type === "MetaProperty") {
|
|
694
|
-
if (node.object.meta.name !== "import") break;
|
|
695
|
-
if (!node.computed && node.property.name === "env") return;
|
|
696
|
-
loads(
|
|
697
|
-
node,
|
|
698
|
-
node.computed
|
|
699
|
-
? "import.meta[...]"
|
|
700
|
-
: `import.meta.${node.property.name}`,
|
|
701
|
-
"is resolved against the server module; only `import.meta.env` " +
|
|
702
|
-
"is available to an inline predicate.",
|
|
703
|
-
);
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
break;
|
|
707
|
-
case "ImportExpression":
|
|
708
|
-
loads(
|
|
709
|
-
node,
|
|
710
|
-
`import(${code.slice(node.source.start, node.source.end)})`,
|
|
711
|
-
"loads a module at run time, which the build cannot check is " +
|
|
712
|
-
"client-safe.",
|
|
713
|
-
);
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
for (const key of Object.keys(node)) {
|
|
717
|
-
if (key === "type" || key === "start" || key === "end") continue;
|
|
718
|
-
if (TYPE_KEYS.has(key)) continue;
|
|
719
|
-
visit(node[key], boundary);
|
|
720
|
-
}
|
|
721
|
-
};
|
|
722
|
-
visit(literal, null);
|
|
723
|
-
if (found) return found;
|
|
724
|
-
|
|
725
|
-
for (const ref of references) {
|
|
726
|
-
if (ref.typeOnly || ref.node.name !== "arguments") continue;
|
|
727
|
-
if (ref.node.start < literal.start || ref.node.end > literal.end) continue;
|
|
728
|
-
const owned = ownArguments.some(
|
|
729
|
-
(fn) => fn.start <= ref.node.start && ref.node.end <= fn.end,
|
|
730
|
-
);
|
|
731
|
-
if (!owned) {
|
|
732
|
-
capture(ref.node, "arguments");
|
|
733
|
-
return found;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
return null;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
147
|
const NODE_BUILTINS = new Set(builtinModules);
|
|
740
148
|
|
|
741
149
|
/** Specifiers that can never load in the browser. */
|
|
@@ -748,7 +156,7 @@ export function isServerOnlySpecifier(source: string): boolean {
|
|
|
748
156
|
);
|
|
749
157
|
}
|
|
750
158
|
|
|
751
|
-
|
|
159
|
+
interface HoistTransitionWhenDeps {
|
|
752
160
|
/**
|
|
753
161
|
* Whether an import the literal re-emits is server-only beyond its
|
|
754
162
|
* specifier (the plugin resolves it and looks for `import "server-only"`).
|
|
@@ -756,7 +164,7 @@ export interface HoistTransitionWhenDeps {
|
|
|
756
164
|
isServerOnlyImport?(source: string): Promise<boolean> | boolean;
|
|
757
165
|
}
|
|
758
166
|
|
|
759
|
-
|
|
167
|
+
interface HoistTransitionWhenResult {
|
|
760
168
|
readonly code: string;
|
|
761
169
|
readonly map: ReturnType<MagicString["generateMap"]>;
|
|
762
170
|
/** Virtual module source per hoisted literal, by index. */
|
|
@@ -771,16 +179,10 @@ export interface HoistTransitionWhenResult {
|
|
|
771
179
|
const ROUTER_IMPORT_RE = /["']@rangojs\/router["']/;
|
|
772
180
|
const WHEN_RE = /\bwhen\b/;
|
|
773
181
|
|
|
774
|
-
function parseLang(file: string): "ts" | "tsx" | "jsx" {
|
|
775
|
-
// A .ts file parses as ts: tsx would reject `<T>expr` and `<T>(x) => x`.
|
|
776
|
-
if (/\.[cm]?ts$/.test(file)) return "ts";
|
|
777
|
-
if (/\.[cm]?jsx?$/.test(file)) return "jsx";
|
|
778
|
-
return "tsx";
|
|
779
|
-
}
|
|
780
|
-
|
|
781
182
|
function whenError(file: string, code: string, node: Node, detail: string) {
|
|
183
|
+
const { line, column } = offsetToLineColumn(code, node.start);
|
|
782
184
|
return new TransitionWhenError(
|
|
783
|
-
`[rango] transition({ when }) at ${file}:${
|
|
185
|
+
`[rango] transition({ when }) at ${file}:${line}:${column}: ` +
|
|
784
186
|
`\`when\` runs in the browser; ${detail}`,
|
|
785
187
|
);
|
|
786
188
|
}
|
|
@@ -804,7 +206,7 @@ export async function hoistTransitionWhens(
|
|
|
804
206
|
} catch {
|
|
805
207
|
return null;
|
|
806
208
|
}
|
|
807
|
-
if (
|
|
209
|
+
if (hasDirective(program.body, "use client")) return null;
|
|
808
210
|
|
|
809
211
|
const { moduleScope, references, scopeOf } = analyzeScopes(program);
|
|
810
212
|
const imports = collectImports(program);
|
|
@@ -841,78 +243,46 @@ export async function hoistTransitionWhens(
|
|
|
841
243
|
}
|
|
842
244
|
if (!isFunctionLiteral(value)) continue;
|
|
843
245
|
|
|
844
|
-
const lexical = findLexicalCapture(
|
|
246
|
+
const lexical = findLexicalCapture(
|
|
247
|
+
value,
|
|
248
|
+
references,
|
|
249
|
+
code,
|
|
250
|
+
WHEN_CAPTURE_MESSAGES,
|
|
251
|
+
);
|
|
845
252
|
if (lexical) throw whenError(file, code, lexical.node, lexical.detail);
|
|
846
253
|
|
|
847
|
-
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
if (ref.node.start < value.start || ref.node.end > value.end) continue;
|
|
851
|
-
const decl = ref.scope.lookup(ref.node.name);
|
|
852
|
-
if (!decl) continue; // a global (window, URL, console, ...)
|
|
853
|
-
if (
|
|
854
|
-
decl.node.start >= value.start &&
|
|
855
|
-
decl.node.end <= value.end &&
|
|
856
|
-
decl !== moduleScope
|
|
857
|
-
) {
|
|
858
|
-
continue; // declared inside the literal
|
|
859
|
-
}
|
|
860
|
-
if (decl === moduleScope && imports.has(ref.node.name)) {
|
|
861
|
-
const valueUse = !ref.typeOnly && !imports.get(ref.node.name)!.typeOnly;
|
|
862
|
-
used.set(ref.node.name, (used.get(ref.node.name) ?? false) || valueUse);
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
if (ref.typeOnly) continue; // a local type: erased
|
|
254
|
+
const free = collectFreeReferences(value, references, moduleScope, imports);
|
|
255
|
+
const binding = free.bindings[0];
|
|
256
|
+
if (binding) {
|
|
866
257
|
throw whenError(
|
|
867
258
|
file,
|
|
868
259
|
code,
|
|
869
|
-
|
|
870
|
-
`\`${
|
|
260
|
+
binding.node,
|
|
261
|
+
`\`${binding.node.name}\` is a server-module binding. Read it from ` +
|
|
871
262
|
"`to.params`/`to.state`, or export the predicate from a " +
|
|
872
263
|
'"use client" module.',
|
|
873
264
|
);
|
|
874
265
|
}
|
|
875
266
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
if (valueUse) {
|
|
267
|
+
const importLines = await emitImports(
|
|
268
|
+
free.imports,
|
|
269
|
+
imports,
|
|
270
|
+
async (local, imported) => {
|
|
881
271
|
if (
|
|
882
|
-
isServerOnlySpecifier(
|
|
883
|
-
(await deps.isServerOnlyImport?.(
|
|
272
|
+
isServerOnlySpecifier(imported.source) ||
|
|
273
|
+
(await deps.isServerOnlyImport?.(imported.source))
|
|
884
274
|
) {
|
|
885
275
|
throw whenError(
|
|
886
276
|
file,
|
|
887
277
|
code,
|
|
888
278
|
value,
|
|
889
|
-
`\`${local}\` is imported from "${
|
|
279
|
+
`\`${local}\` is imported from "${imported.source}", a server-only ` +
|
|
890
280
|
"module. Import only client-safe modules into an inline " +
|
|
891
281
|
'predicate, or export it from a "use client" module.',
|
|
892
282
|
);
|
|
893
283
|
}
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
const typePrefix = valueUse ? "" : "type ";
|
|
897
|
-
if (binding.imported === "*") {
|
|
898
|
-
lines.push(
|
|
899
|
-
`import ${typePrefix}* as ${local} from ${JSON.stringify(binding.source)};`,
|
|
900
|
-
);
|
|
901
|
-
} else if (binding.imported === "default") {
|
|
902
|
-
lines.push(
|
|
903
|
-
`import ${typePrefix}${local} from ${JSON.stringify(binding.source)};`,
|
|
904
|
-
);
|
|
905
|
-
} else {
|
|
906
|
-
const spec =
|
|
907
|
-
binding.imported === local
|
|
908
|
-
? local
|
|
909
|
-
: `${JSON.stringify(binding.imported)} as ${local}`;
|
|
910
|
-
lines.push(
|
|
911
|
-
`import ${typePrefix}{ ${spec} } from ${JSON.stringify(binding.source)};`,
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
bySource.set(binding.source, lines);
|
|
915
|
-
}
|
|
284
|
+
},
|
|
285
|
+
);
|
|
916
286
|
|
|
917
287
|
// A method's FunctionExpression spans from its type parameters or
|
|
918
288
|
// parameters; the async/generator markers sit on the property.
|
|
@@ -924,7 +294,7 @@ export async function hoistTransitionWhens(
|
|
|
924
294
|
modules.push(
|
|
925
295
|
[
|
|
926
296
|
`"use client";`,
|
|
927
|
-
...
|
|
297
|
+
...importLines,
|
|
928
298
|
`export const ${EXPORT_NAME} = ${source};`,
|
|
929
299
|
"",
|
|
930
300
|
].join("\n"),
|
|
@@ -946,16 +316,7 @@ export async function hoistTransitionWhens(
|
|
|
946
316
|
if (modules.length === 0) return null;
|
|
947
317
|
|
|
948
318
|
// After the directive prologue, so a "use server"/"use strict" stays first.
|
|
949
|
-
|
|
950
|
-
for (const statement of program.body) {
|
|
951
|
-
if (
|
|
952
|
-
statement.type !== "ExpressionStatement" ||
|
|
953
|
-
typeof statement.directive !== "string"
|
|
954
|
-
) {
|
|
955
|
-
break;
|
|
956
|
-
}
|
|
957
|
-
insertAt = statement.end;
|
|
958
|
-
}
|
|
319
|
+
const insertAt = directivePrologueEnd(program);
|
|
959
320
|
s.appendLeft(insertAt, `${insertAt ? "\n" : ""}${header.join("\n")}\n`);
|
|
960
321
|
|
|
961
322
|
return {
|
|
@@ -964,7 +325,6 @@ export async function hoistTransitionWhens(
|
|
|
964
325
|
modules,
|
|
965
326
|
};
|
|
966
327
|
}
|
|
967
|
-
|
|
968
328
|
/** The hoisted module index a `?rango-when=N` id names, or null. */
|
|
969
329
|
export function parseHoistedWhenId(
|
|
970
330
|
id: string,
|
|
@@ -979,37 +339,19 @@ export function parseHoistedWhenId(
|
|
|
979
339
|
|
|
980
340
|
const SERVER_ONLY_IMPORT_RE = /\bimport\s+["']server-only["']/;
|
|
981
341
|
|
|
982
|
-
async function readSource(file: string): Promise<string | null> {
|
|
983
|
-
try {
|
|
984
|
-
return await readFile(file, "utf-8");
|
|
985
|
-
} catch {
|
|
986
|
-
return null;
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
/** What the RSC transform emitted for a urls file, and the disk source it saw. */
|
|
991
|
-
interface EmittedWhens {
|
|
992
|
-
readonly disk: string | null;
|
|
993
|
-
readonly modules: readonly string[];
|
|
994
|
-
}
|
|
995
|
-
|
|
996
342
|
/**
|
|
997
343
|
* The plugin: hoists in the RSC environment (where urls() modules are
|
|
998
344
|
* evaluated) and serves the `?rango-when=N` modules in every environment
|
|
999
345
|
* (RSC, SSR and client all load them).
|
|
1000
346
|
*
|
|
1001
|
-
* `load` serves the modules the transform emitted, not a
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1006
|
-
* environment loading before the RSC transform ran (the order environments
|
|
1007
|
-
* ask in is not fixed), or an edit the RSC environment has not re-transformed
|
|
1008
|
-
* yet (serving the last transform would then be stale). The map is per
|
|
1009
|
-
* plugin instance, so every environment of one dev server or build shares it.
|
|
347
|
+
* `load` serves the modules the transform emitted (loadEmittedOrDisk), not a
|
|
348
|
+
* re-hoist of the file on disk: re-hoisting a disk source an earlier plugin
|
|
349
|
+
* changed can number the literals differently and serve the wrong one for
|
|
350
|
+
* index N. The map is per plugin instance, so every environment of one dev
|
|
351
|
+
* server or build shares it.
|
|
1010
352
|
*/
|
|
1011
353
|
export function transitionWhenHoistPlugin(): Plugin {
|
|
1012
|
-
const emitted = new Map<string,
|
|
354
|
+
const emitted = new Map<string, EmittedEntry<readonly string[]>>();
|
|
1013
355
|
return {
|
|
1014
356
|
name: "@rangojs/router:transition-when-hoist",
|
|
1015
357
|
enforce: "pre",
|
|
@@ -1017,15 +359,12 @@ export function transitionWhenHoistPlugin(): Plugin {
|
|
|
1017
359
|
async load(id) {
|
|
1018
360
|
const hoisted = parseHoistedWhenId(id);
|
|
1019
361
|
if (!hoisted) return;
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
: disk === null
|
|
1027
|
-
? undefined
|
|
1028
|
-
: (await hoistTransitionWhens(disk, hoisted.file))?.modules;
|
|
362
|
+
const modules = await loadEmittedOrDisk(
|
|
363
|
+
emitted,
|
|
364
|
+
hoisted.file,
|
|
365
|
+
async (disk) =>
|
|
366
|
+
(await hoistTransitionWhens(disk, hoisted.file))?.modules,
|
|
367
|
+
);
|
|
1029
368
|
const module = modules?.[hoisted.index];
|
|
1030
369
|
if (module === undefined) {
|
|
1031
370
|
throw new TransitionWhenError(
|
|
@@ -1039,25 +378,20 @@ export function transitionWhenHoistPlugin(): Plugin {
|
|
|
1039
378
|
if (this.environment?.name !== "rsc") return;
|
|
1040
379
|
if (id.includes("/node_modules/") || parseHoistedWhenId(id)) return;
|
|
1041
380
|
const file = normalizePath(id.split("?", 1)[0]!);
|
|
1042
|
-
if (
|
|
381
|
+
if (!SOURCE_FILE_RE.test(file)) return;
|
|
1043
382
|
emitted.delete(file);
|
|
1044
383
|
const result = await hoistTransitionWhens(code, id, {
|
|
1045
384
|
isServerOnlyImport: async (source) => {
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
if (target
|
|
1050
|
-
return false;
|
|
1051
|
-
}
|
|
385
|
+
const target = await resolveSourceTarget(this, source, id, {
|
|
386
|
+
skipNodeModules: true,
|
|
387
|
+
});
|
|
388
|
+
if (target === null) return false;
|
|
1052
389
|
const text = await readSource(target);
|
|
1053
390
|
return text !== null && SERVER_ONLY_IMPORT_RE.test(text);
|
|
1054
391
|
},
|
|
1055
392
|
});
|
|
1056
393
|
if (!result) return;
|
|
1057
|
-
emitted
|
|
1058
|
-
disk: await readSource(file),
|
|
1059
|
-
modules: result.modules,
|
|
1060
|
-
});
|
|
394
|
+
await recordEmitted(emitted, file, result.modules);
|
|
1061
395
|
return { code: result.code, map: result.map };
|
|
1062
396
|
},
|
|
1063
397
|
};
|