@rangojs/router 0.22.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/dist/bin/rango.js +8 -3
  2. package/dist/testing/vitest.js +31 -1
  3. package/dist/types/browser/defer.d.ts +48 -0
  4. package/dist/types/browser/document-revealed.d.ts +17 -0
  5. package/dist/types/browser/event-controller.d.ts +7 -1
  6. package/dist/types/browser/navigation-transaction.d.ts +21 -7
  7. package/dist/types/browser/partial-update.d.ts +34 -5
  8. package/dist/types/browser/react/NavigationProvider.d.ts +13 -3
  9. package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
  10. package/dist/types/browser/response-adapter.d.ts +7 -0
  11. package/dist/types/browser/types.d.ts +19 -9
  12. package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
  13. package/dist/types/build/generate-route-types.d.ts +1 -1
  14. package/dist/types/build/route-types/router-processing.d.ts +1 -1
  15. package/dist/types/build/route-types/scan-filter.d.ts +16 -2
  16. package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
  17. package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
  18. package/dist/types/client-urls/client-urls.d.ts +1 -1
  19. package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
  20. package/dist/types/client-urls/navigation.d.ts +0 -37
  21. package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
  22. package/dist/types/client-urls/server-projection.d.ts +29 -1
  23. package/dist/types/client-urls/types.d.ts +34 -69
  24. package/dist/types/client.d.ts +0 -1
  25. package/dist/types/errors.d.ts +5 -0
  26. package/dist/types/handles/deferred-resolution.d.ts +2 -1
  27. package/dist/types/handles/pushed-by.d.ts +23 -0
  28. package/dist/types/loader-store.d.ts +8 -0
  29. package/dist/types/prefetch-markers.d.ts +11 -0
  30. package/dist/types/route-definition/helpers-types.d.ts +27 -0
  31. package/dist/types/router/is-action.d.ts +2 -1
  32. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
  33. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +241 -0
  34. package/dist/types/server/context.d.ts +21 -1
  35. package/dist/types/server/handle-store.d.ts +9 -1
  36. package/dist/types/server/request-context.d.ts +14 -1
  37. package/dist/types/server.d.ts +1 -0
  38. package/dist/types/testing/flight.entry.d.ts +1 -1
  39. package/dist/types/testing/index.d.ts +3 -3
  40. package/dist/types/testing/internal/action-id.d.ts +10 -1
  41. package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
  42. package/dist/types/testing/internal/test-action.d.ts +8 -0
  43. package/dist/types/testing/run-loader.d.ts +2 -1
  44. package/dist/types/testing/run-revalidate.d.ts +74 -0
  45. package/dist/types/testing/run-transition-when.d.ts +2 -3
  46. package/dist/types/testing/serve-shell-request.d.ts +38 -0
  47. package/dist/types/testing/vitest.d.ts +11 -0
  48. package/dist/types/types/boundaries.d.ts +9 -0
  49. package/dist/types/types/handler-context.d.ts +14 -0
  50. package/dist/types/types/loader-types.d.ts +30 -0
  51. package/dist/types/types/segments.d.ts +21 -4
  52. package/dist/types/urls/path-helper-types.d.ts +25 -4
  53. package/dist/types/urls/type-extraction.d.ts +6 -2
  54. package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
  55. package/dist/types/vite/discovery/state.d.ts +11 -4
  56. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
  57. package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
  58. package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
  59. package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
  60. package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
  61. package/dist/types/vite/router-discovery.d.ts +10 -0
  62. package/dist/vite/index.js +842 -308
  63. package/package.json +1 -1
  64. package/skills/caching/SKILL.md +42 -5
  65. package/skills/catalog.json +2 -2
  66. package/skills/client-urls/SKILL.md +113 -24
  67. package/skills/cloudflare/SKILL.md +14 -9
  68. package/skills/hooks/SKILL.md +11 -3
  69. package/skills/links/SKILL.md +24 -0
  70. package/skills/loader/SKILL.md +192 -1
  71. package/skills/migrate-nextjs/SKILL.md +2 -2
  72. package/skills/migrate-react-router/SKILL.md +10 -7
  73. package/skills/migrate-react-router/data-and-actions.md +1 -1
  74. package/skills/ppr/SKILL.md +13 -0
  75. package/skills/prerender/SKILL.md +22 -8
  76. package/skills/rango/SKILL.md +12 -6
  77. package/skills/route/SKILL.md +139 -0
  78. package/skills/testing/SKILL.md +3 -2
  79. package/skills/testing/client-components.md +1 -1
  80. package/skills/testing/predicates.md +67 -32
  81. package/skills/testing/serve-shell-request.md +88 -18
  82. package/skills/testing/setup.md +2 -2
  83. package/skills/typesafety/route-types.md +2 -2
  84. package/skills/view-transitions/SKILL.md +26 -0
  85. package/src/browser/defer.ts +82 -0
  86. package/src/browser/document-revealed.ts +52 -0
  87. package/src/browser/event-controller.ts +43 -32
  88. package/src/browser/navigation-bridge.ts +64 -14
  89. package/src/browser/navigation-client.ts +48 -36
  90. package/src/browser/navigation-store.ts +13 -0
  91. package/src/browser/navigation-transaction.ts +25 -8
  92. package/src/browser/partial-update.ts +528 -135
  93. package/src/browser/prefetch/fetch.ts +2 -1
  94. package/src/browser/react/NavigationProvider.tsx +47 -28
  95. package/src/browser/react/filter-segment-order.ts +13 -2
  96. package/src/browser/response-adapter.ts +20 -0
  97. package/src/browser/rsc-router.tsx +17 -45
  98. package/src/browser/server-action-bridge.ts +19 -63
  99. package/src/browser/types.ts +19 -9
  100. package/src/browser/validate-redirect-origin.ts +16 -0
  101. package/src/build/generate-route-types.ts +1 -0
  102. package/src/build/route-types/router-processing.ts +3 -6
  103. package/src/build/route-types/scan-filter.ts +37 -10
  104. package/src/cache/cf/cf-cache-store.ts +59 -21
  105. package/src/cache/cf/cf-cache-types.ts +24 -2
  106. package/src/cache/document-cache.ts +22 -1
  107. package/src/client-urls/client-urls.ts +22 -6
  108. package/src/client-urls/hoisted-revalidate.ts +25 -0
  109. package/src/client-urls/navigation.ts +0 -96
  110. package/src/client-urls/revalidate-registry.ts +30 -0
  111. package/src/client-urls/server-projection.ts +151 -65
  112. package/src/client-urls/types.ts +39 -71
  113. package/src/client.tsx +9 -5
  114. package/src/errors.ts +15 -0
  115. package/src/handles/deferred-resolution.ts +28 -5
  116. package/src/handles/pushed-by.ts +122 -0
  117. package/src/loader-store.ts +18 -7
  118. package/src/prefetch-markers.ts +13 -0
  119. package/src/route-content-wrapper.tsx +18 -4
  120. package/src/route-definition/dsl-helpers.ts +27 -4
  121. package/src/route-definition/helpers-types.ts +27 -1
  122. package/src/router/intercept-resolution.ts +6 -4
  123. package/src/router/is-action.ts +2 -1
  124. package/src/router/loader-resolution.ts +83 -4
  125. package/src/router/match-api.ts +36 -1
  126. package/src/router/match-middleware/background-revalidation.ts +11 -1
  127. package/src/router/match-middleware/cache-lookup.ts +29 -6
  128. package/src/router/match-middleware/cache-store.ts +17 -1
  129. package/src/router/match-result.ts +14 -2
  130. package/src/router/navigation-snapshot.ts +2 -1
  131. package/src/router/segment-resolution/fresh.ts +22 -18
  132. package/src/router/segment-resolution/loader-cache.ts +2 -1
  133. package/src/router/segment-resolution/loader-mask.ts +5 -3
  134. package/src/router/segment-resolution/prefetch-deferral.ts +543 -0
  135. package/src/router/segment-resolution/revalidation.ts +169 -60
  136. package/src/rsc/rsc-rendering.ts +50 -7
  137. package/src/rsc/shell-capture.ts +2 -1
  138. package/src/segment-loader-promise.ts +23 -20
  139. package/src/segment-system.tsx +60 -26
  140. package/src/server/context.ts +39 -5
  141. package/src/server/handle-store.ts +104 -8
  142. package/src/server/request-context.ts +21 -1
  143. package/src/server.ts +4 -0
  144. package/src/testing/action-form.ts +2 -11
  145. package/src/testing/e2e/page-helpers.ts +3 -1
  146. package/src/testing/flight.entry.ts +1 -0
  147. package/src/testing/index.ts +6 -3
  148. package/src/testing/internal/action-id.ts +16 -1
  149. package/src/testing/internal/client-urls-projection.ts +70 -0
  150. package/src/testing/internal/test-action.ts +25 -0
  151. package/src/testing/run-loader.ts +2 -1
  152. package/src/testing/run-revalidate.ts +142 -0
  153. package/src/testing/run-transition-when.ts +7 -8
  154. package/src/testing/serve-shell-request.ts +116 -6
  155. package/src/testing/vitest.ts +67 -1
  156. package/src/types/boundaries.ts +12 -0
  157. package/src/types/handler-context.ts +14 -0
  158. package/src/types/loader-types.ts +30 -0
  159. package/src/types/segments.ts +26 -4
  160. package/src/urls/path-helper-types.ts +27 -5
  161. package/src/urls/type-extraction.ts +30 -10
  162. package/src/use-loader.tsx +18 -13
  163. package/src/vite/discovery/client-urls-projection.ts +20 -21
  164. package/src/vite/discovery/discover-routers.ts +94 -26
  165. package/src/vite/discovery/state.ts +20 -4
  166. package/src/vite/discovery/virtual-module-codegen.ts +22 -17
  167. package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
  168. package/src/vite/plugins/hoist/literal.ts +417 -0
  169. package/src/vite/plugins/hoist/scope.ts +370 -0
  170. package/src/vite/plugins/hoist/source.ts +84 -0
  171. package/src/vite/plugins/transition-when-hoist.ts +113 -779
  172. package/src/vite/rango.ts +2 -0
  173. package/src/vite/router-discovery.ts +93 -32
  174. package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
  175. package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
  176. package/dist/types/testing/run-client-revalidate.d.ts +0 -43
  177. package/src/client-urls/revalidate-chain.ts +0 -83
  178. package/src/client-urls/revalidation-protocol.ts +0 -56
  179. package/src/testing/run-client-revalidate.ts +0 -92
@@ -2,8 +2,36 @@ import type { Plugin } from "vite";
2
2
  import { normalizePath, parseAst } from "vite";
3
3
  import MagicString from "magic-string";
4
4
  import { builtinModules } from "node:module";
5
- import { readFile } from "node:fs/promises";
5
+ import { hasDirective } from "@vitejs/plugin-rsc/transforms";
6
+ import { offsetToLineColumn } from "./expose-ids/export-analysis.js";
6
7
  import { TransitionWhenError } from "../../transition-when-ref.js";
8
+ import {
9
+ analyzeScopes,
10
+ collectImports,
11
+ type ImportBinding,
12
+ type Node,
13
+ type Scope,
14
+ } from "./hoist/scope.js";
15
+ import {
16
+ collectFreeReferences,
17
+ directivePrologueEnd,
18
+ emitImports,
19
+ findHelperCalls,
20
+ findLexicalCapture,
21
+ isFunctionLiteral,
22
+ parseLang,
23
+ propertyKeyIs,
24
+ unwrapExpression,
25
+ type LexicalCaptureMessages,
26
+ } from "./hoist/literal.js";
27
+ import {
28
+ loadEmittedOrDisk,
29
+ readSource,
30
+ recordEmitted,
31
+ resolveSourceTarget,
32
+ SOURCE_FILE_RE,
33
+ type EmittedEntry,
34
+ } from "./hoist/source.js";
7
35
 
8
36
  /**
9
37
  * Inline transition({ when }) in urls(): the build hoists the function
@@ -51,397 +79,6 @@ const TRANSITION_WHEN_QUERY = "rango-when";
51
79
  const EXPORT_NAME = "__rango_when";
52
80
  const LOCAL_PREFIX = "__rango_when_";
53
81
 
54
- type Node = any;
55
-
56
- interface ImportBinding {
57
- readonly source: string;
58
- /** "default", "*", or the imported name. */
59
- readonly imported: string;
60
- readonly typeOnly: boolean;
61
- }
62
-
63
- class Scope {
64
- readonly declarations = new Set<string>();
65
- constructor(
66
- readonly parent: Scope | null,
67
- readonly isFunction: boolean,
68
- readonly node: Node,
69
- ) {}
70
- nearestFunction(): Scope {
71
- return this.isFunction || !this.parent
72
- ? this
73
- : this.parent.nearestFunction();
74
- }
75
- lookup(name: string): Scope | null {
76
- if (this.declarations.has(name)) return this;
77
- return this.parent ? this.parent.lookup(name) : null;
78
- }
79
- }
80
-
81
- interface Reference {
82
- readonly node: Node;
83
- readonly scope: Scope;
84
- readonly typeOnly: boolean;
85
- }
86
-
87
- const TYPE_KEYS = new Set([
88
- "typeAnnotation",
89
- "returnType",
90
- "typeParameters",
91
- "typeArguments",
92
- "superTypeArguments",
93
- "implements",
94
- ]);
95
-
96
- /**
97
- * The identifier a JSX element name refers to: a component (`<Local />`) or
98
- * the root of a member name (`<ui.Badge />`). Lowercase and dashed names are
99
- * intrinsic tags, not references; `<this.X />` is checked as `this`.
100
- */
101
- function jsxNameReference(name: Node): Node | null {
102
- if (name?.type === "JSXIdentifier") {
103
- return /^[a-z]/.test(name.name) || name.name.includes("-") ? null : name;
104
- }
105
- let root = name;
106
- while (root?.type === "JSXMemberExpression") root = root.object;
107
- return root?.type === "JSXIdentifier" && root.name !== "this" ? root : null;
108
- }
109
-
110
- function patternNames(pattern: Node, names: string[] = []): string[] {
111
- if (!pattern) return names;
112
- switch (pattern.type) {
113
- case "Identifier":
114
- names.push(pattern.name);
115
- break;
116
- case "ObjectPattern":
117
- for (const prop of pattern.properties) {
118
- patternNames(prop.type === "RestElement" ? prop : prop.value, names);
119
- }
120
- break;
121
- case "ArrayPattern":
122
- for (const element of pattern.elements) patternNames(element, names);
123
- break;
124
- case "RestElement":
125
- patternNames(pattern.argument, names);
126
- break;
127
- case "AssignmentPattern":
128
- patternNames(pattern.left, names);
129
- break;
130
- case "TSParameterProperty":
131
- patternNames(pattern.parameter, names);
132
- break;
133
- }
134
- return names;
135
- }
136
-
137
- /**
138
- * Scope tree and every identifier reference of a module, TS type positions
139
- * flagged. Enough of ES scoping for free-variable analysis of one literal:
140
- * var hoists to the function scope, let/const/class to the block, function
141
- * declarations to their block.
142
- */
143
- function analyzeScopes(program: Node): {
144
- moduleScope: Scope;
145
- references: Reference[];
146
- scopeOf: Map<Node, Scope>;
147
- } {
148
- const moduleScope = new Scope(null, true, program);
149
- const scopeOf = new Map<Node, Scope>([[program, moduleScope]]);
150
- const references: Reference[] = [];
151
-
152
- const declarePattern = (pattern: Node, scope: Scope): void => {
153
- for (const name of patternNames(pattern)) scope.declarations.add(name);
154
- };
155
-
156
- // Binding patterns: identifiers declare; defaults and computed keys refer.
157
- const walkPattern = (pattern: Node, scope: Scope, typeOnly: boolean) => {
158
- if (!pattern) return;
159
- switch (pattern.type) {
160
- case "Identifier":
161
- if (pattern.typeAnnotation) walk(pattern.typeAnnotation, scope, true);
162
- return;
163
- case "ObjectPattern":
164
- for (const prop of pattern.properties) {
165
- if (prop.type === "RestElement") {
166
- walkPattern(prop.argument, scope, typeOnly);
167
- } else {
168
- if (prop.computed) walk(prop.key, scope, typeOnly);
169
- walkPattern(prop.value, scope, typeOnly);
170
- }
171
- }
172
- if (pattern.typeAnnotation) walk(pattern.typeAnnotation, scope, true);
173
- return;
174
- case "ArrayPattern":
175
- for (const element of pattern.elements) {
176
- walkPattern(element, scope, typeOnly);
177
- }
178
- if (pattern.typeAnnotation) walk(pattern.typeAnnotation, scope, true);
179
- return;
180
- case "RestElement":
181
- walkPattern(pattern.argument, scope, typeOnly);
182
- return;
183
- case "AssignmentPattern":
184
- walkPattern(pattern.left, scope, typeOnly);
185
- walk(pattern.right, scope, typeOnly);
186
- return;
187
- case "TSParameterProperty":
188
- walkPattern(pattern.parameter, scope, typeOnly);
189
- return;
190
- default:
191
- walk(pattern, scope, typeOnly);
192
- }
193
- };
194
-
195
- // Hoist function-scoped and block-scoped declarations of a body before its
196
- // statements are walked, so a reference ahead of its declaration resolves.
197
- const hoistDeclarations = (statements: Node[], scope: Scope): void => {
198
- for (const statement of statements) {
199
- const node =
200
- statement.type === "ExportNamedDeclaration" ||
201
- statement.type === "ExportDefaultDeclaration"
202
- ? statement.declaration
203
- : statement;
204
- if (!node) continue;
205
- switch (node.type) {
206
- case "ImportDeclaration":
207
- for (const spec of node.specifiers) {
208
- scope.declarations.add(spec.local.name);
209
- }
210
- break;
211
- case "FunctionDeclaration":
212
- case "ClassDeclaration":
213
- case "TSEnumDeclaration":
214
- case "TSTypeAliasDeclaration":
215
- case "TSInterfaceDeclaration":
216
- case "TSModuleDeclaration":
217
- if (node.id?.type === "Identifier") {
218
- scope.declarations.add(node.id.name);
219
- }
220
- break;
221
- case "VariableDeclaration":
222
- for (const declarator of node.declarations) {
223
- declarePattern(
224
- declarator.id,
225
- node.kind === "var" ? scope.nearestFunction() : scope,
226
- );
227
- }
228
- break;
229
- }
230
- }
231
- };
232
-
233
- const walk = (node: Node, scope: Scope, typeOnly: boolean): void => {
234
- if (!node || typeof node !== "object") return;
235
- if (Array.isArray(node)) {
236
- for (const item of node) walk(item, scope, typeOnly);
237
- return;
238
- }
239
- if (typeof node.type !== "string") return;
240
-
241
- switch (node.type) {
242
- case "Identifier":
243
- references.push({ node, scope, typeOnly });
244
- if (node.typeAnnotation) walk(node.typeAnnotation, scope, true);
245
- return;
246
- case "ImportDeclaration":
247
- case "ExportAllDeclaration":
248
- return;
249
- case "ExportNamedDeclaration":
250
- if (node.declaration) walk(node.declaration, scope, typeOnly);
251
- else if (!node.source) {
252
- for (const spec of node.specifiers) {
253
- walk(spec.local, scope, typeOnly || node.exportKind === "type");
254
- }
255
- }
256
- return;
257
- case "MemberExpression":
258
- walk(node.object, scope, typeOnly);
259
- if (node.computed) walk(node.property, scope, typeOnly);
260
- return;
261
- case "Property":
262
- case "MethodDefinition":
263
- case "PropertyDefinition":
264
- case "TSPropertySignature":
265
- case "TSMethodSignature":
266
- if (node.computed) walk(node.key, scope, typeOnly);
267
- walk(node.value, scope, typeOnly);
268
- if (node.typeAnnotation) walk(node.typeAnnotation, scope, true);
269
- return;
270
- case "LabeledStatement":
271
- walk(node.body, scope, typeOnly);
272
- return;
273
- case "BreakStatement":
274
- case "ContinueStatement":
275
- case "MetaProperty":
276
- return;
277
- case "JSXOpeningElement":
278
- case "JSXClosingElement": {
279
- const ref = jsxNameReference(node.name);
280
- if (ref) references.push({ node: ref, scope, typeOnly });
281
- walk(node.typeArguments, scope, true);
282
- walk(node.attributes, scope, typeOnly);
283
- return;
284
- }
285
- case "TSQualifiedName":
286
- walk(node.left, scope, typeOnly);
287
- return;
288
- case "TSEnumMember":
289
- walk(node.initializer, scope, typeOnly);
290
- return;
291
- case "TSTypeAliasDeclaration":
292
- case "TSInterfaceDeclaration":
293
- walk(node.typeParameters, scope, true);
294
- walk(node.typeAnnotation ?? node.body, scope, true);
295
- walk(node.extends, scope, true);
296
- return;
297
- case "VariableDeclarator":
298
- walkPattern(node.id, scope, typeOnly);
299
- walk(node.init, scope, typeOnly);
300
- return;
301
- case "FunctionDeclaration":
302
- case "FunctionExpression":
303
- case "ArrowFunctionExpression": {
304
- const fnScope = new Scope(scope, true, node);
305
- scopeOf.set(node, fnScope);
306
- if (node.type === "FunctionExpression" && node.id) {
307
- fnScope.declarations.add(node.id.name);
308
- }
309
- for (const param of node.params) declarePattern(param, fnScope);
310
- walk(node.typeParameters, fnScope, true);
311
- for (const param of node.params) walkPattern(param, fnScope, typeOnly);
312
- walk(node.returnType, fnScope, true);
313
- if (node.body?.type === "BlockStatement") {
314
- hoistDeclarations(node.body.body, fnScope);
315
- walk(node.body.body, fnScope, typeOnly);
316
- } else {
317
- walk(node.body, fnScope, typeOnly);
318
- }
319
- return;
320
- }
321
- case "ClassDeclaration":
322
- case "ClassExpression": {
323
- let classScope = scope;
324
- if (node.type === "ClassExpression" && node.id) {
325
- classScope = new Scope(scope, false, node);
326
- classScope.declarations.add(node.id.name);
327
- scopeOf.set(node, classScope);
328
- }
329
- walk(node.superClass, classScope, typeOnly);
330
- walk(node.superTypeArguments, classScope, true);
331
- walk(node.typeParameters, classScope, true);
332
- walk(node.implements, classScope, true);
333
- walk(node.body, classScope, typeOnly);
334
- return;
335
- }
336
- case "BlockStatement":
337
- case "StaticBlock": {
338
- const blockScope = new Scope(scope, false, node);
339
- scopeOf.set(node, blockScope);
340
- hoistDeclarations(node.body, blockScope);
341
- walk(node.body, blockScope, typeOnly);
342
- return;
343
- }
344
- case "ForStatement":
345
- case "ForInStatement":
346
- case "ForOfStatement": {
347
- const loopScope = new Scope(scope, false, node);
348
- scopeOf.set(node, loopScope);
349
- const head = node.type === "ForStatement" ? node.init : node.left;
350
- if (head?.type === "VariableDeclaration") {
351
- hoistDeclarations([head], loopScope);
352
- }
353
- for (const key of ["init", "left", "right", "test", "update", "body"]) {
354
- walk(node[key], loopScope, typeOnly);
355
- }
356
- return;
357
- }
358
- case "SwitchStatement": {
359
- walk(node.discriminant, scope, typeOnly);
360
- const switchScope = new Scope(scope, false, node);
361
- scopeOf.set(node, switchScope);
362
- hoistDeclarations(
363
- node.cases.flatMap((c: Node) => c.consequent),
364
- switchScope,
365
- );
366
- for (const c of node.cases) {
367
- walk(c.test, switchScope, typeOnly);
368
- walk(c.consequent, switchScope, typeOnly);
369
- }
370
- return;
371
- }
372
- case "CatchClause": {
373
- const catchScope = new Scope(scope, false, node);
374
- scopeOf.set(node, catchScope);
375
- if (node.param) {
376
- declarePattern(node.param, catchScope);
377
- walkPattern(node.param, catchScope, typeOnly);
378
- }
379
- walk(node.body, catchScope, typeOnly);
380
- return;
381
- }
382
- }
383
-
384
- for (const key of Object.keys(node)) {
385
- if (key === "type" || key === "start" || key === "end") continue;
386
- walk(node[key], scope, typeOnly || TYPE_KEYS.has(key));
387
- }
388
- };
389
-
390
- hoistDeclarations(program.body, moduleScope);
391
- walk(program.body, moduleScope, false);
392
- return { moduleScope, references, scopeOf };
393
- }
394
-
395
- function collectImports(program: Node): Map<string, ImportBinding> {
396
- const imports = new Map<string, ImportBinding>();
397
- for (const statement of program.body) {
398
- if (statement.type !== "ImportDeclaration") continue;
399
- const declTypeOnly = statement.importKind === "type";
400
- for (const spec of statement.specifiers) {
401
- imports.set(spec.local.name, {
402
- source: statement.source.value,
403
- imported:
404
- spec.type === "ImportDefaultSpecifier"
405
- ? "default"
406
- : spec.type === "ImportNamespaceSpecifier"
407
- ? "*"
408
- : spec.imported.type === "Identifier"
409
- ? spec.imported.name
410
- : String(spec.imported.value),
411
- typeOnly: declTypeOnly || spec.importKind === "type",
412
- });
413
- }
414
- }
415
- return imports;
416
- }
417
-
418
- function hasUseClientDirective(program: Node): boolean {
419
- for (const statement of program.body) {
420
- if (
421
- statement.type !== "ExpressionStatement" ||
422
- typeof statement.directive !== "string"
423
- ) {
424
- return false;
425
- }
426
- if (statement.directive === "use client") return true;
427
- }
428
- return false;
429
- }
430
-
431
- function lineColumn(code: string, offset: number): string {
432
- let line = 1;
433
- let column = 1;
434
- for (let i = 0; i < offset && i < code.length; i++) {
435
- if (code[i] === "\n") {
436
- line++;
437
- column = 1;
438
- } else {
439
- column++;
440
- }
441
- }
442
- return `${line}:${column}`;
443
- }
444
-
445
82
  /**
446
83
  * A `when` site: the `when` property of a router transition() config, and its
447
84
  * value with TS wrappers dropped (a function literal, an identifier, ...).
@@ -452,44 +89,7 @@ interface WhenSite {
452
89
  readonly scope: Scope;
453
90
  }
454
91
 
455
- const 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
- }
92
+ const ROUTER_SOURCES: ReadonlySet<string> = new Set(["@rangojs/router"]);
493
93
 
494
94
  function findWhenSites(
495
95
  program: Node,
@@ -498,244 +98,52 @@ function findWhenSites(
498
98
  imports: Map<string, ImportBinding>,
499
99
  ): WhenSite[] {
500
100
  const sites: WhenSite[] = [];
501
- const 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
- }
101
+ for (const { call, scope } of findHelperCalls(
102
+ program,
103
+ scopeOf,
104
+ moduleScope,
105
+ imports,
106
+ {
107
+ sources: ROUTER_SOURCES,
108
+ builder: "urls",
109
+ helper: "transition",
110
+ directImport: true,
111
+ },
112
+ )) {
113
+ if (call.arguments[0]?.type !== "ObjectExpression") continue;
114
+ for (const prop of call.arguments[0].properties) {
115
+ if (propertyKeyIs(prop, "when")) {
116
+ sites.push({ prop, value: unwrapExpression(prop.value), scope });
603
117
  }
604
118
  }
605
- 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);
119
+ }
611
120
  return sites;
612
121
  }
613
122
 
614
123
  /** Why a lexical-context use in a literal is a capture, by construct. */
615
- const 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.",
124
+ const WHEN_CAPTURE_MESSAGES: LexicalCaptureMessages = {
125
+ lexical: {
126
+ this: "`this` in an arrow is the enclosing server function's `this`.",
127
+ arguments:
128
+ "`arguments` in an arrow is the enclosing server function's `arguments`.",
129
+ super: "`super` refers to the enclosing server object.",
130
+ "new.target":
131
+ "`new.target` in an arrow is the enclosing server function's.",
132
+ },
133
+ captureHint:
134
+ "Read what you need from `to.params`/`to.state`, or export the " +
135
+ 'predicate from a "use client" module.',
136
+ loadsHint:
137
+ "Import client-safe modules statically, or export the predicate from a " +
138
+ '"use client" module.',
139
+ importMetaWhy: "is the server module's.",
140
+ importMetaMemberWhy:
141
+ "is resolved against the server module; only `import.meta.env` is " +
142
+ "available to an inline predicate.",
143
+ dynamicImportWhy:
144
+ "loads a module at run time, which the build cannot check is client-safe.",
621
145
  };
622
146
 
623
- /**
624
- * The first use in a literal of a lexical context it would lose when hoisted
625
- * (`this`, `arguments`, `super`, `new.target` of an enclosing server function
626
- * or object), or of a module-loading expression the build cannot check
627
- * (`import()`, `import.meta` other than `import.meta.env`). A nested
628
- * non-arrow function (or class body) owns its `this`/`arguments`, so uses
629
- * inside one are fine; a method literal owns them too, but its `super`
630
- * loses the object it was defined on.
631
- */
632
- function findLexicalCapture(
633
- literal: Node,
634
- references: readonly Reference[],
635
- code: string,
636
- ): { node: Node; detail: string } | null {
637
- const ownArguments: Node[] = [];
638
- let found: { node: Node; detail: string } | null = null;
639
- const capture = (node: Node, what: string): void => {
640
- found = {
641
- node,
642
- detail:
643
- `${LEXICAL_CAPTURE[what]} Read what you need from ` +
644
- '`to.params`/`to.state`, or export the predicate from a "use client" ' +
645
- "module.",
646
- };
647
- };
648
- const loads = (node: Node, expression: string, why: string): void => {
649
- found = {
650
- node,
651
- detail:
652
- `\`${expression}\` ${why} Import client-safe modules statically, ` +
653
- 'or export the predicate from a "use client" module.',
654
- };
655
- };
656
-
657
- const visit = (node: Node, boundary: Node | null): void => {
658
- if (found || !node || typeof node !== "object") return;
659
- if (Array.isArray(node)) {
660
- for (const item of node) visit(item, boundary);
661
- return;
662
- }
663
- if (typeof node.type !== "string") return;
664
- switch (node.type) {
665
- case "FunctionExpression":
666
- case "FunctionDeclaration":
667
- ownArguments.push(node);
668
- boundary = node;
669
- break;
670
- case "ClassBody":
671
- boundary = node;
672
- break;
673
- case "ThisExpression":
674
- if (!boundary) capture(node, "this");
675
- return;
676
- case "Super":
677
- if (!boundary || boundary === literal) capture(node, "super");
678
- return;
679
- case "JSXMemberExpression": {
680
- let root = node;
681
- while (root.type === "JSXMemberExpression") root = root.object;
682
- if (root.name === "this" && !boundary) capture(root, "this");
683
- return;
684
- }
685
- case "MetaProperty":
686
- if (node.meta.name === "import") {
687
- loads(node, "import.meta", "is the server module's.");
688
- } else if (!boundary) {
689
- capture(node, "new.target");
690
- }
691
- return;
692
- case "MemberExpression":
693
- if (node.object?.type === "MetaProperty") {
694
- if (node.object.meta.name !== "import") break;
695
- if (!node.computed && node.property.name === "env") return;
696
- loads(
697
- node,
698
- node.computed
699
- ? "import.meta[...]"
700
- : `import.meta.${node.property.name}`,
701
- "is resolved against the server module; only `import.meta.env` " +
702
- "is available to an inline predicate.",
703
- );
704
- return;
705
- }
706
- break;
707
- case "ImportExpression":
708
- loads(
709
- node,
710
- `import(${code.slice(node.source.start, node.source.end)})`,
711
- "loads a module at run time, which the build cannot check is " +
712
- "client-safe.",
713
- );
714
- return;
715
- }
716
- for (const key of Object.keys(node)) {
717
- if (key === "type" || key === "start" || key === "end") continue;
718
- if (TYPE_KEYS.has(key)) continue;
719
- visit(node[key], boundary);
720
- }
721
- };
722
- visit(literal, null);
723
- if (found) return found;
724
-
725
- for (const ref of references) {
726
- if (ref.typeOnly || ref.node.name !== "arguments") continue;
727
- if (ref.node.start < literal.start || ref.node.end > literal.end) continue;
728
- const owned = ownArguments.some(
729
- (fn) => fn.start <= ref.node.start && ref.node.end <= fn.end,
730
- );
731
- if (!owned) {
732
- capture(ref.node, "arguments");
733
- return found;
734
- }
735
- }
736
- return null;
737
- }
738
-
739
147
  const NODE_BUILTINS = new Set(builtinModules);
740
148
 
741
149
  /** Specifiers that can never load in the browser. */
@@ -748,7 +156,7 @@ export function isServerOnlySpecifier(source: string): boolean {
748
156
  );
749
157
  }
750
158
 
751
- export interface HoistTransitionWhenDeps {
159
+ interface HoistTransitionWhenDeps {
752
160
  /**
753
161
  * Whether an import the literal re-emits is server-only beyond its
754
162
  * specifier (the plugin resolves it and looks for `import "server-only"`).
@@ -756,7 +164,7 @@ export interface HoistTransitionWhenDeps {
756
164
  isServerOnlyImport?(source: string): Promise<boolean> | boolean;
757
165
  }
758
166
 
759
- export interface HoistTransitionWhenResult {
167
+ interface HoistTransitionWhenResult {
760
168
  readonly code: string;
761
169
  readonly map: ReturnType<MagicString["generateMap"]>;
762
170
  /** Virtual module source per hoisted literal, by index. */
@@ -771,16 +179,10 @@ export interface HoistTransitionWhenResult {
771
179
  const ROUTER_IMPORT_RE = /["']@rangojs\/router["']/;
772
180
  const WHEN_RE = /\bwhen\b/;
773
181
 
774
- function parseLang(file: string): "ts" | "tsx" | "jsx" {
775
- // A .ts file parses as ts: tsx would reject `<T>expr` and `<T>(x) => x`.
776
- if (/\.[cm]?ts$/.test(file)) return "ts";
777
- if (/\.[cm]?jsx?$/.test(file)) return "jsx";
778
- return "tsx";
779
- }
780
-
781
182
  function whenError(file: string, code: string, node: Node, detail: string) {
183
+ const { line, column } = offsetToLineColumn(code, node.start);
782
184
  return new TransitionWhenError(
783
- `[rango] transition({ when }) at ${file}:${lineColumn(code, node.start)}: ` +
185
+ `[rango] transition({ when }) at ${file}:${line}:${column}: ` +
784
186
  `\`when\` runs in the browser; ${detail}`,
785
187
  );
786
188
  }
@@ -804,7 +206,7 @@ export async function hoistTransitionWhens(
804
206
  } catch {
805
207
  return null;
806
208
  }
807
- if (hasUseClientDirective(program)) return null;
209
+ if (hasDirective(program.body, "use client")) return null;
808
210
 
809
211
  const { moduleScope, references, scopeOf } = analyzeScopes(program);
810
212
  const imports = collectImports(program);
@@ -841,78 +243,46 @@ export async function hoistTransitionWhens(
841
243
  }
842
244
  if (!isFunctionLiteral(value)) continue;
843
245
 
844
- const lexical = findLexicalCapture(value, references, code);
246
+ const lexical = findLexicalCapture(
247
+ value,
248
+ references,
249
+ code,
250
+ WHEN_CAPTURE_MESSAGES,
251
+ );
845
252
  if (lexical) throw whenError(file, code, lexical.node, lexical.detail);
846
253
 
847
- // 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
254
+ const free = collectFreeReferences(value, references, moduleScope, imports);
255
+ const binding = free.bindings[0];
256
+ if (binding) {
866
257
  throw whenError(
867
258
  file,
868
259
  code,
869
- ref.node,
870
- `\`${ref.node.name}\` is a server-module binding. Read it from ` +
260
+ binding.node,
261
+ `\`${binding.node.name}\` is a server-module binding. Read it from ` +
871
262
  "`to.params`/`to.state`, or export the predicate from a " +
872
263
  '"use client" module.',
873
264
  );
874
265
  }
875
266
 
876
- // 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) {
267
+ const importLines = await emitImports(
268
+ free.imports,
269
+ imports,
270
+ async (local, imported) => {
881
271
  if (
882
- isServerOnlySpecifier(binding.source) ||
883
- (await deps.isServerOnlyImport?.(binding.source))
272
+ isServerOnlySpecifier(imported.source) ||
273
+ (await deps.isServerOnlyImport?.(imported.source))
884
274
  ) {
885
275
  throw whenError(
886
276
  file,
887
277
  code,
888
278
  value,
889
- `\`${local}\` is imported from "${binding.source}", a server-only ` +
279
+ `\`${local}\` is imported from "${imported.source}", a server-only ` +
890
280
  "module. Import only client-safe modules into an inline " +
891
281
  'predicate, or export it from a "use client" module.',
892
282
  );
893
283
  }
894
- }
895
- 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
- }
284
+ },
285
+ );
916
286
 
917
287
  // A method's FunctionExpression spans from its type parameters or
918
288
  // parameters; the async/generator markers sit on the property.
@@ -924,7 +294,7 @@ export async function hoistTransitionWhens(
924
294
  modules.push(
925
295
  [
926
296
  `"use client";`,
927
- ...[...bySource.values()].flat(),
297
+ ...importLines,
928
298
  `export const ${EXPORT_NAME} = ${source};`,
929
299
  "",
930
300
  ].join("\n"),
@@ -946,16 +316,7 @@ export async function hoistTransitionWhens(
946
316
  if (modules.length === 0) return null;
947
317
 
948
318
  // After the directive prologue, so a "use server"/"use strict" stays first.
949
- 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
- }
319
+ const insertAt = directivePrologueEnd(program);
959
320
  s.appendLeft(insertAt, `${insertAt ? "\n" : ""}${header.join("\n")}\n`);
960
321
 
961
322
  return {
@@ -964,7 +325,6 @@ export async function hoistTransitionWhens(
964
325
  modules,
965
326
  };
966
327
  }
967
-
968
328
  /** The hoisted module index a `?rango-when=N` id names, or null. */
969
329
  export function parseHoistedWhenId(
970
330
  id: string,
@@ -979,37 +339,19 @@ export function parseHoistedWhenId(
979
339
 
980
340
  const SERVER_ONLY_IMPORT_RE = /\bimport\s+["']server-only["']/;
981
341
 
982
- async function readSource(file: string): Promise<string | null> {
983
- try {
984
- return await readFile(file, "utf-8");
985
- } catch {
986
- return null;
987
- }
988
- }
989
-
990
- /** What the RSC transform emitted for a urls file, and the disk source it saw. */
991
- interface EmittedWhens {
992
- readonly disk: string | null;
993
- readonly modules: readonly string[];
994
- }
995
-
996
342
  /**
997
343
  * The plugin: hoists in the RSC environment (where urls() modules are
998
344
  * evaluated) and serves the `?rango-when=N` modules in every environment
999
345
  * (RSC, SSR and client all load them).
1000
346
  *
1001
- * `load` serves the modules the transform emitted, not a 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.
347
+ * `load` serves the modules the transform emitted (loadEmittedOrDisk), not a
348
+ * re-hoist of the file on disk: re-hoisting a disk source an earlier plugin
349
+ * changed can number the literals differently and serve the wrong one for
350
+ * index N. The map is per plugin instance, so every environment of one dev
351
+ * server or build shares it.
1010
352
  */
1011
353
  export function transitionWhenHoistPlugin(): Plugin {
1012
- const emitted = new Map<string, EmittedWhens>();
354
+ const emitted = new Map<string, EmittedEntry<readonly string[]>>();
1013
355
  return {
1014
356
  name: "@rangojs/router:transition-when-hoist",
1015
357
  enforce: "pre",
@@ -1017,15 +359,12 @@ export function transitionWhenHoistPlugin(): Plugin {
1017
359
  async load(id) {
1018
360
  const hoisted = parseHoistedWhenId(id);
1019
361
  if (!hoisted) return;
1020
- const 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;
362
+ const modules = await loadEmittedOrDisk(
363
+ emitted,
364
+ hoisted.file,
365
+ async (disk) =>
366
+ (await hoistTransitionWhens(disk, hoisted.file))?.modules,
367
+ );
1029
368
  const module = modules?.[hoisted.index];
1030
369
  if (module === undefined) {
1031
370
  throw new TransitionWhenError(
@@ -1039,25 +378,20 @@ export function transitionWhenHoistPlugin(): Plugin {
1039
378
  if (this.environment?.name !== "rsc") return;
1040
379
  if (id.includes("/node_modules/") || parseHoistedWhenId(id)) return;
1041
380
  const file = normalizePath(id.split("?", 1)[0]!);
1042
- if (!/\.[cm]?[jt]sx?$/.test(file)) return;
381
+ if (!SOURCE_FILE_RE.test(file)) return;
1043
382
  emitted.delete(file);
1044
383
  const result = await hoistTransitionWhens(code, id, {
1045
384
  isServerOnlyImport: async (source) => {
1046
- const 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
- }
385
+ const target = await resolveSourceTarget(this, source, id, {
386
+ skipNodeModules: true,
387
+ });
388
+ if (target === null) return false;
1052
389
  const text = await readSource(target);
1053
390
  return text !== null && SERVER_ONLY_IMPORT_RE.test(text);
1054
391
  },
1055
392
  });
1056
393
  if (!result) return;
1057
- emitted.set(file, {
1058
- disk: await readSource(file),
1059
- modules: result.modules,
1060
- });
394
+ await recordEmitted(emitted, file, result.modules);
1061
395
  return { code: result.code, map: result.map };
1062
396
  },
1063
397
  };