@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.
Files changed (244) hide show
  1. package/dist/types/browser/event-controller.d.ts +3 -3
  2. package/dist/types/browser/history-state.d.ts +2 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  4. package/dist/types/browser/partial-update.d.ts +16 -1
  5. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  6. package/dist/types/browser/react/Link.d.ts +19 -9
  7. package/dist/types/browser/react/context.d.ts +2 -2
  8. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  9. package/dist/types/browser/react/location-state.d.ts +1 -1
  10. package/dist/types/browser/transition-when.d.ts +80 -0
  11. package/dist/types/browser/types.d.ts +104 -9
  12. package/dist/types/build/generate-manifest.d.ts +8 -0
  13. package/dist/types/cache/background-task.d.ts +24 -2
  14. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +44 -13
  16. package/dist/types/cache/cache-policy.d.ts +1 -1
  17. package/dist/types/cache/cache-runtime.d.ts +7 -0
  18. package/dist/types/cache/cache-scope.d.ts +117 -35
  19. package/dist/types/cache/cache-tag.d.ts +24 -4
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
  21. package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
  22. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
  23. package/dist/types/cache/document-cache.d.ts +10 -0
  24. package/dist/types/cache/handle-snapshot.d.ts +40 -8
  25. package/dist/types/cache/invalidation-order.d.ts +23 -0
  26. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +88 -141
  28. package/dist/types/cache/tag-invalidation.d.ts +38 -14
  29. package/dist/types/cache/types.d.ts +68 -61
  30. package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
  31. package/dist/types/client-urls/navigation.d.ts +28 -2
  32. package/dist/types/client-urls/server-projection.d.ts +5 -2
  33. package/dist/types/client-urls/types.d.ts +5 -5
  34. package/dist/types/client.d.ts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  38. package/dist/types/route-name.d.ts +7 -0
  39. package/dist/types/router/error-handling.d.ts +1 -1
  40. package/dist/types/router/intercept-resolution.d.ts +2 -0
  41. package/dist/types/router/match-context.d.ts +5 -0
  42. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  43. package/dist/types/router/router-options.d.ts +4 -1
  44. package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
  45. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  46. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
  47. package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
  48. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  49. package/dist/types/router/transition-when.d.ts +24 -10
  50. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  51. package/dist/types/rsc/full-payload.d.ts +14 -7
  52. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  53. package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
  54. package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
  55. package/dist/types/rsc/shell-capture.d.ts +78 -81
  56. package/dist/types/rsc/shell-serve.d.ts +19 -48
  57. package/dist/types/rsc/types.d.ts +2 -0
  58. package/dist/types/segment-system.d.ts +7 -0
  59. package/dist/types/server/context.d.ts +89 -61
  60. package/dist/types/server/cookie-store.d.ts +30 -18
  61. package/dist/types/server/handle-store.d.ts +52 -1
  62. package/dist/types/server/request-context.d.ts +66 -45
  63. package/dist/types/server/request-headers.d.ts +44 -0
  64. package/dist/types/server.d.ts +1 -0
  65. package/dist/types/testing/e2e/index.d.ts +1 -1
  66. package/dist/types/testing/index.d.ts +3 -2
  67. package/dist/types/testing/internal/action-id.d.ts +7 -0
  68. package/dist/types/testing/location-state-key.d.ts +24 -0
  69. package/dist/types/testing/render-route.d.ts +17 -1
  70. package/dist/types/testing/run-transition-when.d.ts +44 -69
  71. package/dist/types/testing/serve-shell-request.d.ts +7 -6
  72. package/dist/types/testing/shell-status.d.ts +34 -22
  73. package/dist/types/transition-when-ref.d.ts +79 -0
  74. package/dist/types/types/cache-types.d.ts +15 -3
  75. package/dist/types/types/index.d.ts +1 -1
  76. package/dist/types/types/segments.d.ts +86 -71
  77. package/dist/types/urls/path-helper-types.d.ts +3 -4
  78. package/dist/types/urls/pattern-types.d.ts +3 -4
  79. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  80. package/dist/vite/index.js +960 -97
  81. package/package.json +1 -1
  82. package/skills/cache-guide/SKILL.md +76 -49
  83. package/skills/caching/SKILL.md +167 -69
  84. package/skills/client-urls/SKILL.md +1 -1
  85. package/skills/comparison/references/framework-comparison.md +8 -5
  86. package/skills/document-cache/SKILL.md +24 -0
  87. package/skills/hooks/SKILL.md +3 -1
  88. package/skills/hooks/navigation.md +23 -2
  89. package/skills/hooks/state.md +98 -6
  90. package/skills/intercept/SKILL.md +29 -12
  91. package/skills/loader/SKILL.md +52 -21
  92. package/skills/middleware/SKILL.md +18 -0
  93. package/skills/migrate-nextjs/SKILL.md +3 -3
  94. package/skills/observability/SKILL.md +6 -5
  95. package/skills/parallel/SKILL.md +1 -1
  96. package/skills/ppr/SKILL.md +129 -77
  97. package/skills/prerender/SKILL.md +3 -3
  98. package/skills/rango/SKILL.md +25 -6
  99. package/skills/route/SKILL.md +2 -0
  100. package/skills/testing/SKILL.md +27 -25
  101. package/skills/testing/cache-prerender.md +9 -9
  102. package/skills/testing/client-components.md +45 -3
  103. package/skills/testing/loader.md +3 -2
  104. package/skills/testing/middleware.md +1 -0
  105. package/skills/testing/predicates.md +49 -49
  106. package/skills/testing/render-handler.md +1 -0
  107. package/skills/testing/serve-shell-request.md +14 -0
  108. package/skills/testing/server-actions.md +9 -3
  109. package/skills/theme/SKILL.md +8 -4
  110. package/skills/typesafety/env-and-bindings.md +8 -0
  111. package/skills/use-cache/SKILL.md +64 -19
  112. package/skills/view-transitions/SKILL.md +150 -29
  113. package/src/__augment-tests__/augmented.check.ts +23 -0
  114. package/src/browser/event-controller.ts +4 -4
  115. package/src/browser/history-state.ts +52 -1
  116. package/src/browser/link-interceptor.ts +4 -0
  117. package/src/browser/navigation-bridge.ts +77 -18
  118. package/src/browser/navigation-client.ts +4 -3
  119. package/src/browser/navigation-store.ts +43 -1
  120. package/src/browser/navigation-transaction.ts +31 -11
  121. package/src/browser/partial-update.ts +146 -35
  122. package/src/browser/prefetch/cache.ts +6 -6
  123. package/src/browser/prefetch/fetch.ts +2 -2
  124. package/src/browser/react/Link.tsx +38 -33
  125. package/src/browser/react/NavigationProvider.tsx +2 -2
  126. package/src/browser/react/context.ts +2 -2
  127. package/src/browser/react/location-state-shared.ts +241 -49
  128. package/src/browser/react/location-state.ts +5 -2
  129. package/src/browser/rsc-router.tsx +4 -0
  130. package/src/browser/server-action-bridge.ts +84 -15
  131. package/src/browser/transition-when.ts +205 -0
  132. package/src/browser/types.ts +124 -10
  133. package/src/build/generate-manifest.ts +77 -1
  134. package/src/cache/background-task.ts +55 -2
  135. package/src/cache/cache-exec-scope.ts +48 -1
  136. package/src/cache/cache-key-utils.ts +56 -16
  137. package/src/cache/cache-policy.ts +9 -4
  138. package/src/cache/cache-runtime.ts +197 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +105 -13
  141. package/src/cache/cf/cf-cache-store.ts +152 -76
  142. package/src/cache/cf/cf-shell-frame.ts +2 -2
  143. package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
  144. package/src/cache/document-cache.ts +53 -7
  145. package/src/cache/handle-snapshot.ts +52 -9
  146. package/src/cache/invalidation-order.ts +84 -0
  147. package/src/cache/request-tag-mask.ts +147 -0
  148. package/src/cache/shell-snapshot.ts +130 -348
  149. package/src/cache/tag-invalidation.ts +57 -51
  150. package/src/cache/types.ts +71 -67
  151. package/src/cache/vercel/vercel-cache-store.ts +61 -58
  152. package/src/client-urls/client-root.tsx +8 -4
  153. package/src/client-urls/client-urls.ts +8 -6
  154. package/src/client-urls/navigation.ts +55 -4
  155. package/src/client-urls/server-projection.ts +31 -10
  156. package/src/client-urls/types.ts +12 -5
  157. package/src/client.tsx +2 -0
  158. package/src/host/cookie-handler.ts +2 -1
  159. package/src/index.rsc.ts +3 -0
  160. package/src/index.ts +3 -0
  161. package/src/loader-redirect.tsx +7 -3
  162. package/src/prerender/build-shell-capture.ts +155 -146
  163. package/src/route-definition/dsl-helpers.ts +2 -0
  164. package/src/route-definition/helpers-types.ts +11 -11
  165. package/src/route-name.ts +12 -0
  166. package/src/router/content-negotiation.ts +4 -1
  167. package/src/router/error-handling.ts +5 -2
  168. package/src/router/handler-context.ts +10 -7
  169. package/src/router/intercept-resolution.ts +11 -1
  170. package/src/router/loader-resolution.ts +28 -3
  171. package/src/router/logging.ts +5 -3
  172. package/src/router/match-api.ts +62 -34
  173. package/src/router/match-context.ts +5 -0
  174. package/src/router/match-handlers.ts +9 -29
  175. package/src/router/match-middleware/background-revalidation.ts +3 -5
  176. package/src/router/match-middleware/cache-lookup.ts +35 -19
  177. package/src/router/match-middleware/cache-store.ts +15 -9
  178. package/src/router/match-result.ts +1 -0
  179. package/src/router/middleware.ts +2 -5
  180. package/src/router/navigation-snapshot.ts +6 -5
  181. package/src/router/prerender-match.ts +4 -0
  182. package/src/router/request-classification.ts +7 -4
  183. package/src/router/router-options.ts +4 -1
  184. package/src/router/segment-resolution/fresh.ts +0 -4
  185. package/src/router/segment-resolution/loader-cache.ts +150 -25
  186. package/src/router/segment-resolution/loader-mask.ts +3 -4
  187. package/src/router/segment-resolution/loader-snapshot.ts +15 -78
  188. package/src/router/segment-resolution/mask-nested.ts +47 -22
  189. package/src/router/segment-resolution/revalidation.ts +0 -3
  190. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  191. package/src/router/telemetry.ts +6 -3
  192. package/src/router/transition-when.ts +34 -52
  193. package/src/router.ts +2 -1
  194. package/src/rsc/attach-transition-when.ts +47 -0
  195. package/src/rsc/full-payload.ts +30 -10
  196. package/src/rsc/handler.ts +12 -10
  197. package/src/rsc/loader-fetch.ts +2 -1
  198. package/src/rsc/manifest-init.ts +4 -0
  199. package/src/rsc/origin-guard.ts +8 -5
  200. package/src/rsc/progressive-enhancement.ts +7 -30
  201. package/src/rsc/response-cache-serve.ts +68 -80
  202. package/src/rsc/response-route-handler.ts +2 -2
  203. package/src/rsc/rsc-rendering.ts +206 -252
  204. package/src/rsc/server-action.ts +8 -20
  205. package/src/rsc/shell-build-manifest.ts +22 -20
  206. package/src/rsc/shell-capture-constants.ts +136 -20
  207. package/src/rsc/shell-capture.ts +352 -589
  208. package/src/rsc/shell-serve.ts +73 -109
  209. package/src/rsc/ssr-setup.ts +3 -2
  210. package/src/rsc/types.ts +2 -0
  211. package/src/segment-system.tsx +40 -7
  212. package/src/server/context.ts +189 -124
  213. package/src/server/cookie-store.ts +153 -127
  214. package/src/server/handle-store.ts +245 -27
  215. package/src/server/request-context.ts +123 -84
  216. package/src/server/request-headers.ts +134 -0
  217. package/src/server.ts +4 -0
  218. package/src/testing/dispatch.ts +4 -7
  219. package/src/testing/e2e/index.ts +1 -0
  220. package/src/testing/index.ts +5 -0
  221. package/src/testing/internal/action-id.ts +25 -0
  222. package/src/testing/internal/context.ts +1 -1
  223. package/src/testing/location-state-key.ts +45 -0
  224. package/src/testing/render-handler.ts +1 -1
  225. package/src/testing/render-route.tsx +109 -16
  226. package/src/testing/run-client-revalidate.ts +3 -19
  227. package/src/testing/run-loader.ts +5 -1
  228. package/src/testing/run-transition-when.ts +109 -172
  229. package/src/testing/serve-shell-request.ts +10 -7
  230. package/src/testing/shell-status.ts +60 -56
  231. package/src/transition-when-ref.ts +220 -0
  232. package/src/types/cache-types.ts +15 -3
  233. package/src/types/index.ts +3 -0
  234. package/src/types/segments.ts +94 -91
  235. package/src/urls/path-helper-types.ts +3 -4
  236. package/src/urls/pattern-types.ts +3 -4
  237. package/src/vite/discovery/discover-routers.ts +3 -0
  238. package/src/vite/discovery/shell-prerender-phase.ts +0 -2
  239. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  240. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  241. package/src/vite/rango.ts +2 -0
  242. package/src/vite/router-discovery.ts +75 -5
  243. package/dist/types/rsc/transition-gate.d.ts +0 -23
  244. 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
+ }