@rangojs/router 0.18.0 → 0.19.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/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +2 -0
- package/dist/types/browser/navigation-transaction.d.ts +20 -2
- package/dist/types/browser/partial-update.d.ts +16 -1
- package/dist/types/browser/prefetch/cache.d.ts +6 -6
- package/dist/types/browser/react/Link.d.ts +19 -9
- package/dist/types/browser/react/context.d.ts +2 -2
- package/dist/types/browser/react/location-state-shared.d.ts +95 -56
- package/dist/types/browser/react/location-state.d.ts +1 -1
- package/dist/types/browser/transition-when.d.ts +80 -0
- package/dist/types/browser/types.d.ts +104 -9
- package/dist/types/build/generate-manifest.d.ts +8 -0
- package/dist/types/cache/background-task.d.ts +24 -2
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/cache-key-utils.d.ts +44 -13
- package/dist/types/cache/cache-policy.d.ts +1 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +117 -35
- package/dist/types/cache/cache-tag.d.ts +24 -4
- package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
- package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +40 -8
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-snapshot.d.ts +88 -141
- package/dist/types/cache/tag-invalidation.d.ts +38 -14
- package/dist/types/cache/types.d.ts +68 -61
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
- package/dist/types/client-urls/navigation.d.ts +28 -2
- package/dist/types/client-urls/server-projection.d.ts +5 -2
- package/dist/types/client-urls/types.d.ts +5 -5
- package/dist/types/client.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/route-definition/helpers-types.d.ts +11 -11
- package/dist/types/route-name.d.ts +7 -0
- package/dist/types/router/error-handling.d.ts +1 -1
- package/dist/types/router/intercept-resolution.d.ts +2 -0
- package/dist/types/router/match-context.d.ts +5 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
- package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
- package/dist/types/router/transition-when.d.ts +24 -10
- package/dist/types/rsc/attach-transition-when.d.ts +18 -0
- package/dist/types/rsc/full-payload.d.ts +14 -7
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
- package/dist/types/rsc/shell-capture.d.ts +78 -81
- package/dist/types/rsc/shell-serve.d.ts +19 -48
- package/dist/types/rsc/types.d.ts +2 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +89 -61
- package/dist/types/server/cookie-store.d.ts +30 -18
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +66 -45
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/index.d.ts +3 -2
- package/dist/types/testing/internal/action-id.d.ts +7 -0
- package/dist/types/testing/location-state-key.d.ts +24 -0
- package/dist/types/testing/render-route.d.ts +17 -1
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +7 -6
- package/dist/types/testing/shell-status.d.ts +34 -22
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/segments.d.ts +86 -71
- package/dist/types/urls/path-helper-types.d.ts +3 -4
- package/dist/types/urls/pattern-types.d.ts +3 -4
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/vite/index.js +960 -97
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +76 -49
- package/skills/caching/SKILL.md +167 -69
- package/skills/client-urls/SKILL.md +1 -1
- package/skills/comparison/references/framework-comparison.md +8 -5
- package/skills/document-cache/SKILL.md +24 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +98 -6
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +52 -21
- package/skills/middleware/SKILL.md +18 -0
- package/skills/migrate-nextjs/SKILL.md +3 -3
- package/skills/observability/SKILL.md +6 -5
- package/skills/parallel/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +129 -77
- package/skills/prerender/SKILL.md +3 -3
- package/skills/rango/SKILL.md +25 -6
- package/skills/route/SKILL.md +2 -0
- package/skills/testing/SKILL.md +27 -25
- package/skills/testing/cache-prerender.md +9 -9
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +3 -2
- package/skills/testing/middleware.md +1 -0
- package/skills/testing/predicates.md +49 -49
- package/skills/testing/render-handler.md +1 -0
- package/skills/testing/serve-shell-request.md +14 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +8 -4
- package/skills/typesafety/env-and-bindings.md +8 -0
- package/skills/use-cache/SKILL.md +64 -19
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +52 -1
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +77 -18
- package/src/browser/navigation-client.ts +4 -3
- package/src/browser/navigation-store.ts +43 -1
- package/src/browser/navigation-transaction.ts +31 -11
- package/src/browser/partial-update.ts +146 -35
- package/src/browser/prefetch/cache.ts +6 -6
- package/src/browser/prefetch/fetch.ts +2 -2
- package/src/browser/react/Link.tsx +38 -33
- package/src/browser/react/NavigationProvider.tsx +2 -2
- package/src/browser/react/context.ts +2 -2
- package/src/browser/react/location-state-shared.ts +241 -49
- package/src/browser/react/location-state.ts +5 -2
- package/src/browser/rsc-router.tsx +4 -0
- package/src/browser/server-action-bridge.ts +84 -15
- package/src/browser/transition-when.ts +205 -0
- package/src/browser/types.ts +124 -10
- package/src/build/generate-manifest.ts +77 -1
- package/src/cache/background-task.ts +55 -2
- package/src/cache/cache-exec-scope.ts +48 -1
- package/src/cache/cache-key-utils.ts +56 -16
- package/src/cache/cache-policy.ts +9 -4
- package/src/cache/cache-runtime.ts +197 -144
- package/src/cache/cache-scope.ts +304 -112
- package/src/cache/cache-tag.ts +105 -13
- package/src/cache/cf/cf-cache-store.ts +152 -76
- package/src/cache/cf/cf-shell-frame.ts +2 -2
- package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +52 -9
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-snapshot.ts +130 -348
- package/src/cache/tag-invalidation.ts +57 -51
- package/src/cache/types.ts +71 -67
- package/src/cache/vercel/vercel-cache-store.ts +61 -58
- package/src/client-urls/client-root.tsx +8 -4
- package/src/client-urls/client-urls.ts +8 -6
- package/src/client-urls/navigation.ts +55 -4
- package/src/client-urls/server-projection.ts +31 -10
- package/src/client-urls/types.ts +12 -5
- package/src/client.tsx +2 -0
- package/src/host/cookie-handler.ts +2 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/loader-redirect.tsx +7 -3
- package/src/prerender/build-shell-capture.ts +155 -146
- package/src/route-definition/dsl-helpers.ts +2 -0
- package/src/route-definition/helpers-types.ts +11 -11
- package/src/route-name.ts +12 -0
- package/src/router/content-negotiation.ts +4 -1
- package/src/router/error-handling.ts +5 -2
- package/src/router/handler-context.ts +10 -7
- package/src/router/intercept-resolution.ts +11 -1
- package/src/router/loader-resolution.ts +28 -3
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +62 -34
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +9 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +35 -19
- package/src/router/match-middleware/cache-store.ts +15 -9
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware.ts +2 -5
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +4 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +0 -4
- package/src/router/segment-resolution/loader-cache.ts +150 -25
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +15 -78
- package/src/router/segment-resolution/mask-nested.ts +47 -22
- package/src/router/segment-resolution/revalidation.ts +0 -3
- package/src/router/segment-resolution/view-transition-default.ts +6 -21
- package/src/router/telemetry.ts +6 -3
- package/src/router/transition-when.ts +34 -52
- package/src/router.ts +2 -1
- package/src/rsc/attach-transition-when.ts +47 -0
- package/src/rsc/full-payload.ts +30 -10
- package/src/rsc/handler.ts +12 -10
- package/src/rsc/loader-fetch.ts +2 -1
- package/src/rsc/manifest-init.ts +4 -0
- package/src/rsc/origin-guard.ts +8 -5
- package/src/rsc/progressive-enhancement.ts +7 -30
- package/src/rsc/response-cache-serve.ts +68 -80
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +206 -252
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +22 -20
- package/src/rsc/shell-capture-constants.ts +136 -20
- package/src/rsc/shell-capture.ts +352 -589
- package/src/rsc/shell-serve.ts +73 -109
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +2 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +189 -124
- package/src/server/cookie-store.ts +153 -127
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +123 -84
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/internal/action-id.ts +25 -0
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/location-state-key.ts +45 -0
- package/src/testing/render-handler.ts +1 -1
- package/src/testing/render-route.tsx +109 -16
- package/src/testing/run-client-revalidate.ts +3 -19
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +10 -7
- package/src/testing/shell-status.ts +60 -56
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/index.ts +3 -0
- package/src/types/segments.ts +94 -91
- package/src/urls/path-helper-types.ts +3 -4
- package/src/urls/pattern-types.ts +3 -4
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +0 -2
- package/src/vite/discovery/virtual-module-codegen.ts +16 -6
- package/src/vite/plugins/transition-when-hoist.ts +1064 -0
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +75 -5
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
|
@@ -0,0 +1,1064 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import { normalizePath, parseAst } from "vite";
|
|
3
|
+
import MagicString from "magic-string";
|
|
4
|
+
import { builtinModules } from "node:module";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { TransitionWhenError } from "../../transition-when-ref.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Inline transition({ when }) in urls(): the build hoists the function
|
|
10
|
+
* literal into a virtual "use client" module, so it reaches the browser as a
|
|
11
|
+
* client reference like an exported predicate would.
|
|
12
|
+
*
|
|
13
|
+
* transition({ when: (ctx) => ctx.to.params.tab !== "raw" })
|
|
14
|
+
*
|
|
15
|
+
* becomes, in the urls module,
|
|
16
|
+
*
|
|
17
|
+
* import { __rango_when as __rango_when_0 } from "<file>?rango-when=0";
|
|
18
|
+
* transition({ when: __rango_when_0 })
|
|
19
|
+
*
|
|
20
|
+
* and `<file>?rango-when=0` (a query on the urls file itself, so relative
|
|
21
|
+
* imports resolve as they did there and an edit invalidates it in every
|
|
22
|
+
* environment's graph) is
|
|
23
|
+
*
|
|
24
|
+
* "use client";
|
|
25
|
+
* <the module-level imports the literal references>
|
|
26
|
+
* export const __rango_when = (ctx) => ctx.to.params.tab !== "raw";
|
|
27
|
+
*
|
|
28
|
+
* Keyed on `transition({ when: <function literal> })` where the callee is the
|
|
29
|
+
* router's transition: an import from "@rangojs/router" (renamed or through a
|
|
30
|
+
* namespace), or the helper a router urls() callback receives (destructured,
|
|
31
|
+
* renamed, or as `helpers.transition`). Any other function named
|
|
32
|
+
* `transition` is left alone. There is no function-level "use client" (React
|
|
33
|
+
* defines the directive per module). The literal may be an arrow, a function
|
|
34
|
+
* expression or a `when() {}` method, under TS wrappers (`as`, `satisfies`,
|
|
35
|
+
* `!`, `<T>`), which are dropped.
|
|
36
|
+
*
|
|
37
|
+
* A literal may not capture anything: a free reference to a module binding
|
|
38
|
+
* that is not an import (JSX component names included), to an enclosing
|
|
39
|
+
* function's binding, or an arrow's lexical `this`/`arguments`/`super`/
|
|
40
|
+
* `new.target`, is a LOUD error naming it; so are `import()` and
|
|
41
|
+
* `import.meta` other than `import.meta.env`, whose targets the build cannot
|
|
42
|
+
* check. Referenced imports are re-emitted, so they must be client-safe: a
|
|
43
|
+
* server-only one fails naming the import. An identifier value must be a
|
|
44
|
+
* "use client" import; a local binding fails here, anything else in route
|
|
45
|
+
* discovery. "use client" modules (clientUrls()) are left alone: their `when`
|
|
46
|
+
* is already browser code. Vitest does not run the plugin, so in unit tests
|
|
47
|
+
* the literal stays a plain function.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
const TRANSITION_WHEN_QUERY = "rango-when";
|
|
51
|
+
const EXPORT_NAME = "__rango_when";
|
|
52
|
+
const LOCAL_PREFIX = "__rango_when_";
|
|
53
|
+
|
|
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
|
+
/**
|
|
446
|
+
* A `when` site: the `when` property of a router transition() config, and its
|
|
447
|
+
* value with TS wrappers dropped (a function literal, an identifier, ...).
|
|
448
|
+
*/
|
|
449
|
+
interface WhenSite {
|
|
450
|
+
readonly prop: Node;
|
|
451
|
+
readonly value: Node;
|
|
452
|
+
readonly scope: Scope;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const ROUTER_SOURCE = "@rangojs/router";
|
|
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
|
+
}
|
|
493
|
+
|
|
494
|
+
function findWhenSites(
|
|
495
|
+
program: Node,
|
|
496
|
+
scopeOf: Map<Node, Scope>,
|
|
497
|
+
moduleScope: Scope,
|
|
498
|
+
imports: Map<string, ImportBinding>,
|
|
499
|
+
): WhenSite[] {
|
|
500
|
+
const sites: WhenSite[] = [];
|
|
501
|
+
const helpers = new Map<Scope, HelperBindings>();
|
|
502
|
+
|
|
503
|
+
// The router export a callee names: `urls`, `transition as t`, `Rango.urls`.
|
|
504
|
+
const routerExport = (callee: Node, scope: Scope): string | null => {
|
|
505
|
+
const routerImport = (id: Node): ImportBinding | null => {
|
|
506
|
+
if (id?.type !== "Identifier") return null;
|
|
507
|
+
if (scope.lookup(id.name) !== moduleScope) return null;
|
|
508
|
+
const binding = imports.get(id.name);
|
|
509
|
+
return binding?.source === ROUTER_SOURCE && !binding.typeOnly
|
|
510
|
+
? binding
|
|
511
|
+
: null;
|
|
512
|
+
};
|
|
513
|
+
if (callee.type === "Identifier") {
|
|
514
|
+
const imported = routerImport(callee)?.imported;
|
|
515
|
+
return imported && imported !== "*" && imported !== "default"
|
|
516
|
+
? imported
|
|
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
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
for (const key of Object.keys(node)) {
|
|
606
|
+
if (key === "type" || key === "start" || key === "end") continue;
|
|
607
|
+
visit(node[key], own);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
visit(program.body, moduleScope);
|
|
611
|
+
return sites;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Why a lexical-context use in a literal is a capture, by construct. */
|
|
615
|
+
const LEXICAL_CAPTURE: Record<string, string> = {
|
|
616
|
+
this: "`this` in an arrow is the enclosing server function's `this`.",
|
|
617
|
+
arguments:
|
|
618
|
+
"`arguments` in an arrow is the enclosing server function's `arguments`.",
|
|
619
|
+
super: "`super` refers to the enclosing server object.",
|
|
620
|
+
"new.target": "`new.target` in an arrow is the enclosing server function's.",
|
|
621
|
+
};
|
|
622
|
+
|
|
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
|
+
const NODE_BUILTINS = new Set(builtinModules);
|
|
740
|
+
|
|
741
|
+
/** Specifiers that can never load in the browser. */
|
|
742
|
+
export function isServerOnlySpecifier(source: string): boolean {
|
|
743
|
+
return (
|
|
744
|
+
source === "server-only" ||
|
|
745
|
+
source.startsWith("node:") ||
|
|
746
|
+
source.startsWith("cloudflare:") ||
|
|
747
|
+
NODE_BUILTINS.has(source)
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export interface HoistTransitionWhenDeps {
|
|
752
|
+
/**
|
|
753
|
+
* Whether an import the literal re-emits is server-only beyond its
|
|
754
|
+
* specifier (the plugin resolves it and looks for `import "server-only"`).
|
|
755
|
+
*/
|
|
756
|
+
isServerOnlyImport?(source: string): Promise<boolean> | boolean;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export interface HoistTransitionWhenResult {
|
|
760
|
+
readonly code: string;
|
|
761
|
+
readonly map: ReturnType<MagicString["generateMap"]>;
|
|
762
|
+
/** Virtual module source per hoisted literal, by index. */
|
|
763
|
+
readonly modules: readonly string[];
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Cheap pre-filter before a parse: every hoistable site needs an import from
|
|
767
|
+
// the router (transition itself, or the urls() that hands it out) and a
|
|
768
|
+
// `when` key in some form (`when:`, `"when":`, `when() {}`, `get when()`),
|
|
769
|
+
// possibly under an alias, so no tighter pattern holds. The AST walk is the
|
|
770
|
+
// authority.
|
|
771
|
+
const ROUTER_IMPORT_RE = /["']@rangojs\/router["']/;
|
|
772
|
+
const WHEN_RE = /\bwhen\b/;
|
|
773
|
+
|
|
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
|
+
function whenError(file: string, code: string, node: Node, detail: string) {
|
|
782
|
+
return new TransitionWhenError(
|
|
783
|
+
`[rango] transition({ when }) at ${file}:${lineColumn(code, node.start)}: ` +
|
|
784
|
+
`\`when\` runs in the browser; ${detail}`,
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Hoist every inline `transition({ when: <function literal> })` of a urls
|
|
790
|
+
* module. Returns null when there is nothing to hoist. Throws a
|
|
791
|
+
* TransitionWhenError for a capture, a server-only import or module-loading
|
|
792
|
+
* expression, an accessor `when`, or a local identifier value.
|
|
793
|
+
*/
|
|
794
|
+
export async function hoistTransitionWhens(
|
|
795
|
+
code: string,
|
|
796
|
+
id: string,
|
|
797
|
+
deps: HoistTransitionWhenDeps = {},
|
|
798
|
+
): Promise<HoistTransitionWhenResult | null> {
|
|
799
|
+
if (!ROUTER_IMPORT_RE.test(code) || !WHEN_RE.test(code)) return null;
|
|
800
|
+
const file = normalizePath(id.split("?", 1)[0]!);
|
|
801
|
+
let program: Node;
|
|
802
|
+
try {
|
|
803
|
+
program = parseAst(code, { lang: parseLang(file) });
|
|
804
|
+
} catch {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
if (hasUseClientDirective(program)) return null;
|
|
808
|
+
|
|
809
|
+
const { moduleScope, references, scopeOf } = analyzeScopes(program);
|
|
810
|
+
const imports = collectImports(program);
|
|
811
|
+
const sites = findWhenSites(program, scopeOf, moduleScope, imports);
|
|
812
|
+
if (sites.length === 0) return null;
|
|
813
|
+
|
|
814
|
+
const s = new MagicString(code);
|
|
815
|
+
const modules: string[] = [];
|
|
816
|
+
const header: string[] = [];
|
|
817
|
+
|
|
818
|
+
for (const site of sites) {
|
|
819
|
+
const { prop, value } = site;
|
|
820
|
+
if (prop.kind === "get" || prop.kind === "set") {
|
|
821
|
+
throw whenError(
|
|
822
|
+
file,
|
|
823
|
+
code,
|
|
824
|
+
prop,
|
|
825
|
+
`\`${prop.kind} when()\` is an accessor; \`when\` must be the ` +
|
|
826
|
+
"predicate itself: `when: (ctx) => ...` or `when(ctx) { ... }`.",
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
if (value.type === "Identifier") {
|
|
830
|
+
const decl = site.scope.lookup(value.name);
|
|
831
|
+
if (decl && !(decl === moduleScope && imports.has(value.name))) {
|
|
832
|
+
throw whenError(
|
|
833
|
+
file,
|
|
834
|
+
code,
|
|
835
|
+
value,
|
|
836
|
+
`\`${value.name}\` is a server-module binding. Export the predicate ` +
|
|
837
|
+
'from a "use client" module and import it, or write it inline.',
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
if (!isFunctionLiteral(value)) continue;
|
|
843
|
+
|
|
844
|
+
const lexical = findLexicalCapture(value, references, code);
|
|
845
|
+
if (lexical) throw whenError(file, code, lexical.node, lexical.detail);
|
|
846
|
+
|
|
847
|
+
// Free references of the literal: everything declared outside it.
|
|
848
|
+
const used = new Map<string, boolean>(); // import local name -> value use
|
|
849
|
+
for (const ref of references) {
|
|
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
|
|
866
|
+
throw whenError(
|
|
867
|
+
file,
|
|
868
|
+
code,
|
|
869
|
+
ref.node,
|
|
870
|
+
`\`${ref.node.name}\` is a server-module binding. Read it from ` +
|
|
871
|
+
"`to.params`/`to.state`, or export the predicate from a " +
|
|
872
|
+
'"use client" module.',
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Re-emit the referenced imports, grouped per source.
|
|
877
|
+
const bySource = new Map<string, string[]>();
|
|
878
|
+
for (const [local, valueUse] of used) {
|
|
879
|
+
const binding = imports.get(local)!;
|
|
880
|
+
if (valueUse) {
|
|
881
|
+
if (
|
|
882
|
+
isServerOnlySpecifier(binding.source) ||
|
|
883
|
+
(await deps.isServerOnlyImport?.(binding.source))
|
|
884
|
+
) {
|
|
885
|
+
throw whenError(
|
|
886
|
+
file,
|
|
887
|
+
code,
|
|
888
|
+
value,
|
|
889
|
+
`\`${local}\` is imported from "${binding.source}", a server-only ` +
|
|
890
|
+
"module. Import only client-safe modules into an inline " +
|
|
891
|
+
'predicate, or export it from a "use client" module.',
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const lines = bySource.get(binding.source) ?? [];
|
|
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
|
+
}
|
|
916
|
+
|
|
917
|
+
// A method's FunctionExpression spans from its type parameters or
|
|
918
|
+
// parameters; the async/generator markers sit on the property.
|
|
919
|
+
const source = prop.method
|
|
920
|
+
? `${value.async ? "async " : ""}function${value.generator ? "*" : ""} ` +
|
|
921
|
+
code.slice(value.start, value.end)
|
|
922
|
+
: code.slice(value.start, value.end);
|
|
923
|
+
const index = modules.length;
|
|
924
|
+
modules.push(
|
|
925
|
+
[
|
|
926
|
+
`"use client";`,
|
|
927
|
+
...[...bySource.values()].flat(),
|
|
928
|
+
`export const ${EXPORT_NAME} = ${source};`,
|
|
929
|
+
"",
|
|
930
|
+
].join("\n"),
|
|
931
|
+
);
|
|
932
|
+
const local = `${LOCAL_PREFIX}${index}`;
|
|
933
|
+
// The whole property: drops a method's markers and TS wrappers with it.
|
|
934
|
+
s.overwrite(
|
|
935
|
+
prop.start,
|
|
936
|
+
prop.end,
|
|
937
|
+
`${code.slice(prop.key.start, prop.key.end)}: ${local}`,
|
|
938
|
+
);
|
|
939
|
+
header.push(
|
|
940
|
+
`import { ${EXPORT_NAME} as ${local} } from ${JSON.stringify(
|
|
941
|
+
`${file}?${TRANSITION_WHEN_QUERY}=${index}`,
|
|
942
|
+
)};`,
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (modules.length === 0) return null;
|
|
947
|
+
|
|
948
|
+
// After the directive prologue, so a "use server"/"use strict" stays first.
|
|
949
|
+
let insertAt = 0;
|
|
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
|
+
}
|
|
959
|
+
s.appendLeft(insertAt, `${insertAt ? "\n" : ""}${header.join("\n")}\n`);
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
code: s.toString(),
|
|
963
|
+
map: s.generateMap({ hires: true, source: file }),
|
|
964
|
+
modules,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/** The hoisted module index a `?rango-when=N` id names, or null. */
|
|
969
|
+
export function parseHoistedWhenId(
|
|
970
|
+
id: string,
|
|
971
|
+
): { file: string; index: number } | null {
|
|
972
|
+
const q = id.indexOf("?");
|
|
973
|
+
if (q === -1) return null;
|
|
974
|
+
const params = new URLSearchParams(id.slice(q + 1));
|
|
975
|
+
const raw = params.get(TRANSITION_WHEN_QUERY);
|
|
976
|
+
if (raw === null || !/^\d+$/.test(raw)) return null;
|
|
977
|
+
return { file: id.slice(0, q), index: Number(raw) };
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const SERVER_ONLY_IMPORT_RE = /\bimport\s+["']server-only["']/;
|
|
981
|
+
|
|
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
|
+
/**
|
|
997
|
+
* The plugin: hoists in the RSC environment (where urls() modules are
|
|
998
|
+
* evaluated) and serves the `?rango-when=N` modules in every environment
|
|
999
|
+
* (RSC, SSR and client all load them).
|
|
1000
|
+
*
|
|
1001
|
+
* `load` serves the modules the transform emitted, not a re-hoist of the file
|
|
1002
|
+
* on disk: the transform hoists the pipeline code, which an earlier plugin
|
|
1003
|
+
* may have changed, so re-hoisting the disk source can number the literals
|
|
1004
|
+
* differently and serve the wrong one for index N. The disk source is the
|
|
1005
|
+
* fallback only when the transform has not seen the current file: an
|
|
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.
|
|
1010
|
+
*/
|
|
1011
|
+
export function transitionWhenHoistPlugin(): Plugin {
|
|
1012
|
+
const emitted = new Map<string, EmittedWhens>();
|
|
1013
|
+
return {
|
|
1014
|
+
name: "@rangojs/router:transition-when-hoist",
|
|
1015
|
+
enforce: "pre",
|
|
1016
|
+
|
|
1017
|
+
async load(id) {
|
|
1018
|
+
const hoisted = parseHoistedWhenId(id);
|
|
1019
|
+
if (!hoisted) return;
|
|
1020
|
+
const file = normalizePath(hoisted.file);
|
|
1021
|
+
const disk = await readSource(hoisted.file);
|
|
1022
|
+
const entry = emitted.get(file);
|
|
1023
|
+
const modules =
|
|
1024
|
+
entry && (disk === null || entry.disk === disk)
|
|
1025
|
+
? entry.modules
|
|
1026
|
+
: disk === null
|
|
1027
|
+
? undefined
|
|
1028
|
+
: (await hoistTransitionWhens(disk, hoisted.file))?.modules;
|
|
1029
|
+
const module = modules?.[hoisted.index];
|
|
1030
|
+
if (module === undefined) {
|
|
1031
|
+
throw new TransitionWhenError(
|
|
1032
|
+
`[rango] ${hoisted.file} has no inline transition({ when }) #${hoisted.index}.`,
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
return module;
|
|
1036
|
+
},
|
|
1037
|
+
|
|
1038
|
+
async transform(code, id) {
|
|
1039
|
+
if (this.environment?.name !== "rsc") return;
|
|
1040
|
+
if (id.includes("/node_modules/") || parseHoistedWhenId(id)) return;
|
|
1041
|
+
const file = normalizePath(id.split("?", 1)[0]!);
|
|
1042
|
+
if (!/\.[cm]?[jt]sx?$/.test(file)) return;
|
|
1043
|
+
emitted.delete(file);
|
|
1044
|
+
const result = await hoistTransitionWhens(code, id, {
|
|
1045
|
+
isServerOnlyImport: async (source) => {
|
|
1046
|
+
const resolved = await this.resolve(source, id);
|
|
1047
|
+
if (!resolved || resolved.external) return false;
|
|
1048
|
+
const target = resolved.id.split("?", 1)[0]!;
|
|
1049
|
+
if (target.includes("/node_modules/") || target.startsWith("\0")) {
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
const text = await readSource(target);
|
|
1053
|
+
return text !== null && SERVER_ONLY_IMPORT_RE.test(text);
|
|
1054
|
+
},
|
|
1055
|
+
});
|
|
1056
|
+
if (!result) return;
|
|
1057
|
+
emitted.set(file, {
|
|
1058
|
+
disk: await readSource(file),
|
|
1059
|
+
modules: result.modules,
|
|
1060
|
+
});
|
|
1061
|
+
return { code: result.code, map: result.map };
|
|
1062
|
+
},
|
|
1063
|
+
};
|
|
1064
|
+
}
|