vite-plugin-taro 0.6.1 → 0.6.2

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 (47) hide show
  1. package/dist/node/plugins/h5/transform-app.js +1 -1
  2. package/dist/node/plugins/wx/dev/plugins.js +1 -1
  3. package/dist/node/plugins/wx/dev/wx-dev-options.js +1 -1
  4. package/dist/node/plugins/wx/{module.js → module/module.js} +2 -2
  5. package/dist/node/plugins/wx/placer/placement.d.ts +1 -0
  6. package/dist/node/plugins/wx/placer/placement.js +1 -1
  7. package/dist/node/plugins/wx/placer/placer.d.ts +17 -1
  8. package/dist/node/plugins/wx/placer/placer.js +25 -1
  9. package/dist/node/plugins/wx/plugins.js +1 -1
  10. package/dist/node/plugins/wx/render/capsule.d.ts +35 -3
  11. package/dist/node/plugins/wx/render/capsule.js +53 -12
  12. package/dist/node/plugins/wx/render/native.d.ts +20 -2
  13. package/dist/node/plugins/wx/render/native.js +539 -35
  14. package/dist/node/plugins/wx/render/system-js/string-editor.d.ts +24 -0
  15. package/dist/node/plugins/wx/render/system-js/string-editor.js +93 -0
  16. package/dist/node/plugins/wx/render/system-js/system-js.d.ts +23 -0
  17. package/dist/node/plugins/wx/render/system-js/system-js.js +602 -0
  18. package/dist/node/plugins/wx/render/transport.js +3 -3
  19. package/dist/node/plugins/wx/resolve/resolver.js +1 -1
  20. package/dist/node/plugins/wx/resolve/specialize-bootstrap.js +1 -1
  21. package/dist/node/plugins/wx/resolve/specialize-page-capsule.js +1 -1
  22. package/dist/node/utils/transform.d.ts +0 -3
  23. package/dist/node/utils/transform.js +0 -23
  24. package/package.json +8 -6
  25. package/src/node/plugins/h5/transform-app.ts +1 -1
  26. package/src/node/plugins/wx/dev/plugins.ts +1 -1
  27. package/src/node/plugins/wx/dev/wx-dev-options.ts +1 -1
  28. package/src/node/plugins/wx/{module.ts → module/module.ts} +2 -2
  29. package/src/node/plugins/wx/placer/placement.ts +1 -1
  30. package/src/node/plugins/wx/placer/placer.ts +27 -2
  31. package/src/node/plugins/wx/plugins.ts +1 -1
  32. package/src/node/plugins/wx/render/capsule.ts +53 -17
  33. package/src/node/plugins/wx/render/native.ts +670 -55
  34. package/src/node/plugins/wx/render/system-js/string-editor.ts +115 -0
  35. package/src/node/plugins/wx/render/system-js/system-js.ts +831 -0
  36. package/src/node/plugins/wx/render/transport.ts +3 -3
  37. package/src/node/plugins/wx/resolve/resolver.ts +1 -1
  38. package/src/node/plugins/wx/resolve/specialize-bootstrap.ts +1 -1
  39. package/src/node/plugins/wx/resolve/specialize-page-capsule.ts +1 -1
  40. package/src/node/utils/transform.ts +0 -30
  41. package/dist/node/plugins/wx/render/capsule-wrapper.d.ts +0 -3
  42. package/dist/node/plugins/wx/render/capsule-wrapper.js +0 -64
  43. package/src/node/plugins/wx/render/capsule-wrapper.ts +0 -86
  44. /package/dist/node/plugins/wx/{chunk-path.d.ts → module/chunk-path.d.ts} +0 -0
  45. /package/dist/node/plugins/wx/{chunk-path.js → module/chunk-path.js} +0 -0
  46. /package/dist/node/plugins/wx/{module.d.ts → module/module.d.ts} +0 -0
  47. /package/src/node/plugins/wx/{chunk-path.ts → module/chunk-path.ts} +0 -0
@@ -0,0 +1,831 @@
1
+ /**
2
+ * WX final-chunk ESM to SystemJS compiler.
3
+ *
4
+ * This is deliberately not a general source-module transformer. It consumes JavaScript already normalized and bundled by
5
+ * Rolldown, preserves the original source with range edits, and rejects module forms outside that final-chunk grammar. Keep
6
+ * the Babel differential tests beside this file: Babel defines the expected SystemJS publication timing and runtime behavior.
7
+ */
8
+ import type {
9
+ AssignmentTarget,
10
+ BindingPattern,
11
+ Class,
12
+ ExportSpecifier,
13
+ ImportDeclaration,
14
+ ModuleExportName,
15
+ Node,
16
+ Function as OxcFunction,
17
+ Program,
18
+ VariableDeclaration
19
+ } from '@oxc-project/types'
20
+ import { ScopeTracker, walk } from 'oxc-walker'
21
+ import { type ExistingRawSourceMap, RolldownMagicString } from 'rolldown'
22
+ import { parseSync } from 'rolldown/utils'
23
+ import { StringEditor } from './string-editor.ts'
24
+
25
+ export type SystemJsOutputFormat = 'system-register' | 'commonjs-registration'
26
+ export type SystemJsReferenceKind = 'static' | 'dynamic'
27
+
28
+ export type TransformSystemJsOptions = Readonly<{
29
+ /** Final JavaScript emitted by Rolldown with ES module syntax still present. */
30
+ code: string
31
+ filename: string
32
+ format: SystemJsOutputFormat
33
+ sourcemap: boolean
34
+ /** Converts physical Rolldown references into the logical IDs used by the WX capsule registry. */
35
+ resolveReference(reference: string, kind: SystemJsReferenceKind): string
36
+ }>
37
+
38
+ export type TransformSystemJsResult = Readonly<{
39
+ code: string
40
+ map: ExistingRawSourceMap | null
41
+ }>
42
+
43
+ type SourceEditor = {
44
+ readonly original: string
45
+ appendLeft(position: number, content: string): unknown
46
+ appendRight(position: number, content: string): unknown
47
+ overwrite(start: number, end: number, content: string): unknown
48
+ prependLeft(position: number, content: string): unknown
49
+ remove(start: number, end: number): unknown
50
+ }
51
+
52
+ type GeneratedNames = Readonly<{
53
+ context: string
54
+ exportBinding: string
55
+ dependencyPrefix: string
56
+ }>
57
+
58
+ type ImportBinding = Readonly<{
59
+ imported: string | null
60
+ local: string
61
+ }>
62
+
63
+ type MutableDependency = {
64
+ source: string
65
+ imports: ImportBinding[]
66
+ }
67
+
68
+ type HoistedVariable = Readonly<{
69
+ declaration: VariableDeclaration
70
+ isForIterationBinding: boolean
71
+ }>
72
+
73
+ /** Immutable facts shared by the declaration and expression rewrite passes. */
74
+ type ModuleModel = Readonly<{
75
+ dependencies: readonly MutableDependency[]
76
+ exportNamesByLocal: ReadonlyMap<string, readonly string[]>
77
+ functions: readonly OxcFunction[]
78
+ generatedNames: GeneratedNames
79
+ hasTopLevelAwait: boolean
80
+ hoistedVariables: readonly HoistedVariable[]
81
+ importBindings: ReadonlySet<string>
82
+ outerBindings: ReadonlySet<string>
83
+ program: Program
84
+ scopes: ScopeTracker
85
+ }>
86
+
87
+ /**
88
+ * Converts one final Rolldown ES chunk into System.register data without rebuilding its complete AST through Babel.
89
+ *
90
+ * The compiler intentionally accepts Rolldown's normalized final-chunk grammar rather than arbitrary source modules. Every
91
+ * unsupported module declaration fails before source edits begin, so a future Rolldown output change cannot be miscompiled.
92
+ */
93
+ export function transformSystemJs(options: TransformSystemJsOptions): TransformSystemJsResult {
94
+ const parseResult = parseSync(options.filename, options.code)
95
+ if (parseResult.errors.length > 0) {
96
+ const diagnostics = parseResult.errors.map((error) => error.message).join('; ')
97
+ throw new Error(`Failed to parse ${options.filename} with Oxc: ${diagnostics}`)
98
+ }
99
+
100
+ // Analysis and validation complete before an editor can publish output. A thrown unsupported-form error therefore fails
101
+ // the build transaction instead of returning a partially transformed capsule.
102
+ const model = analyzeModule(parseResult.program, options.filename)
103
+ if (!options.sourcemap) {
104
+ const editor = new StringEditor(options.code)
105
+ applyProgramEdits(editor, model)
106
+ applyExpressionEdits(editor, model, options)
107
+ return { code: assembleUnmappedRegistration(editor, model, options), map: null }
108
+ }
109
+
110
+ const editor = new RolldownMagicString(options.code, { filename: options.filename })
111
+ applyProgramEdits(editor, model)
112
+ applyExpressionEdits(editor, model, options)
113
+ assembleMappedRegistration(editor, model, options)
114
+
115
+ return {
116
+ code: editor.toString(),
117
+ map: createSourceMap(editor, options.filename)
118
+ }
119
+ }
120
+
121
+ /** Collects immutable compilation facts before any source range is changed. */
122
+ function analyzeModule(program: Program, filename: string): ModuleModel {
123
+ // These journals are mutable only during this one linear analysis pass; all are exposed as readonly compilation facts.
124
+ const identifierNames = new Set<string>()
125
+ const exportNamesByLocal = new Map<string, string[]>()
126
+ const dependencyBySource = new Map<string, MutableDependency>()
127
+ const functions: OxcFunction[] = []
128
+ const hoistedVariables: HoistedVariable[] = []
129
+ const importBindings = new Set<string>()
130
+ const outerBindings = new Set<string>()
131
+ // Boundary depth excludes declarations and await expressions owned by nested functions or classes from module analysis.
132
+ let moduleBoundaryDepth = 0
133
+ let hasDirectEval = false
134
+ let hasTopLevelAwait = false
135
+
136
+ walk(program, {
137
+ enter(node, parent) {
138
+ if (node.type === 'Identifier') identifierNames.add(node.name)
139
+ if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'eval') {
140
+ hasDirectEval = true
141
+ }
142
+ if (isModuleBoundary(node)) moduleBoundaryDepth += 1
143
+ if (node.type === 'AwaitExpression' && moduleBoundaryDepth === 0) hasTopLevelAwait = true
144
+ if (node.type === 'VariableDeclaration' && node.kind === 'var' && moduleBoundaryDepth === 0) {
145
+ const isForIterationBinding =
146
+ (parent?.type === 'ForInStatement' || parent?.type === 'ForOfStatement') && parent.left === node
147
+ hoistedVariables.push({ declaration: node, isForIterationBinding })
148
+ bindingNamesFromDeclaration(node).forEach((name) => {
149
+ outerBindings.add(name)
150
+ })
151
+ }
152
+ },
153
+ leave(node) {
154
+ if (isModuleBoundary(node)) moduleBoundaryDepth -= 1
155
+ }
156
+ })
157
+
158
+ // Final Rolldown chunks normally expose imports and exports as direct Program children. Declaration exports and
159
+ // re-exports are intentionally not normalized here because accepting them would expand the semantic surface needlessly.
160
+ for (const node of program.body) {
161
+ switch (node.type) {
162
+ case 'ImportDeclaration':
163
+ collectImport(node, dependencyBySource, importBindings, outerBindings, filename)
164
+ break
165
+ case 'ExportNamedDeclaration':
166
+ collectExports(node, exportNamesByLocal, filename)
167
+ break
168
+ case 'ExportDefaultDeclaration':
169
+ case 'ExportAllDeclaration':
170
+ throw unsupported(filename, `source-level ${node.type}`)
171
+ case 'VariableDeclaration':
172
+ requireSupportedVariableKind(node, filename)
173
+ node.declarations
174
+ .flatMap((declaration) => bindingNames(declaration.id))
175
+ .forEach((name) => {
176
+ outerBindings.add(name)
177
+ })
178
+ break
179
+ case 'FunctionDeclaration':
180
+ if (!node.id || !node.body || node.declare) {
181
+ throw unsupported(filename, 'anonymous or ambient function declaration')
182
+ }
183
+ functions.push(node)
184
+ outerBindings.add(node.id.name)
185
+ break
186
+ case 'ClassDeclaration':
187
+ if (!node.id || node.declare) throw unsupported(filename, 'anonymous or ambient class declaration')
188
+ outerBindings.add(node.id.name)
189
+ break
190
+ }
191
+ }
192
+
193
+ requireNoDirectEval(hasDirectEval, filename)
194
+ const generatedNames = createGeneratedNames(identifierNames, dependencyBySource.size)
195
+ validateExportBindings(exportNamesByLocal, outerBindings, filename)
196
+ const scopes = createFrozenScopes(program)
197
+
198
+ return {
199
+ dependencies: [...dependencyBySource.values()],
200
+ exportNamesByLocal,
201
+ functions,
202
+ generatedNames,
203
+ hasTopLevelAwait,
204
+ hoistedVariables,
205
+ importBindings,
206
+ outerBindings,
207
+ program,
208
+ scopes
209
+ }
210
+ }
211
+
212
+ /** Merges same-source imports because one SystemJS dependency has exactly one setter. */
213
+ function collectImport(
214
+ declaration: ImportDeclaration,
215
+ dependencyBySource: Map<string, MutableDependency>,
216
+ importBindings: Set<string>,
217
+ outerBindings: Set<string>,
218
+ filename: string
219
+ ): void {
220
+ if (declaration.phase || declaration.attributes.length > 0 || declaration.importKind === 'type') {
221
+ throw unsupported(filename, 'import phases, attributes, or type-only imports')
222
+ }
223
+
224
+ const source = declaration.source.value
225
+ // This map owns one mutable import list per source while declarations are folded; duplicate imports retain source order.
226
+ const dependency = dependencyBySource.get(source) ?? { source, imports: [] }
227
+ if (!dependencyBySource.has(source)) dependencyBySource.set(source, dependency)
228
+
229
+ for (const specifier of declaration.specifiers) {
230
+ importBindings.add(specifier.local.name)
231
+ outerBindings.add(specifier.local.name)
232
+ switch (specifier.type) {
233
+ case 'ImportDefaultSpecifier':
234
+ dependency.imports.push({ imported: 'default', local: specifier.local.name })
235
+ break
236
+ case 'ImportNamespaceSpecifier':
237
+ dependency.imports.push({ imported: null, local: specifier.local.name })
238
+ break
239
+ case 'ImportSpecifier':
240
+ if (specifier.importKind === 'type') throw unsupported(filename, 'type-only import specifier')
241
+ dependency.imports.push({ imported: moduleExportName(specifier.imported), local: specifier.local.name })
242
+ break
243
+ }
244
+ }
245
+ }
246
+
247
+ /** Records every public alias attached to one local binding. */
248
+ function collectExports(
249
+ declaration: Extract<Program['body'][number], { type: 'ExportNamedDeclaration' }>,
250
+ exportNamesByLocal: Map<string, string[]>,
251
+ filename: string
252
+ ): void {
253
+ if (
254
+ declaration.declaration ||
255
+ declaration.source ||
256
+ declaration.attributes.length > 0 ||
257
+ declaration.exportKind === 'type'
258
+ ) {
259
+ throw unsupported(filename, 'declaration exports, re-exports, export attributes, or type-only exports')
260
+ }
261
+
262
+ for (const specifier of declaration.specifiers) {
263
+ if (specifier.exportKind === 'type') throw unsupported(filename, 'type-only export specifier')
264
+ const local = moduleExportName(specifier.local)
265
+ const exported = moduleExportName(specifier.exported)
266
+ // Aliases are accumulated in declaration order because nested live-binding calls must publish in the same order as Babel.
267
+ const names = exportNamesByLocal.get(local) ?? []
268
+ names.push(exported)
269
+ if (!exportNamesByLocal.has(local)) exportNamesByLocal.set(local, names)
270
+ }
271
+ }
272
+
273
+ /** Rejects exports outside the direct normalized module cells supported by final Rolldown chunks. */
274
+ function validateExportBindings(
275
+ exportNamesByLocal: ReadonlyMap<string, readonly string[]>,
276
+ outerBindings: ReadonlySet<string>,
277
+ filename: string
278
+ ): void {
279
+ for (const local of exportNamesByLocal.keys()) {
280
+ if (!outerBindings.has(local)) {
281
+ throw unsupported(filename, `export of non-module binding ${JSON.stringify(local)}`)
282
+ }
283
+ }
284
+ }
285
+
286
+ /** Applies declaration-level edits whose semantics are known from direct Program children. */
287
+ function applyProgramEdits(editor: SourceEditor, model: ModuleModel): void {
288
+ for (const node of model.program.body) {
289
+ switch (node.type) {
290
+ case 'ImportDeclaration':
291
+ editor.remove(node.start, node.end)
292
+ break
293
+ case 'ExportNamedDeclaration':
294
+ editor.overwrite(node.start, node.end, renderImportedExports(node.specifiers, model))
295
+ break
296
+ case 'VariableDeclaration':
297
+ transformTopLevelVariables(editor, node, model.generatedNames.exportBinding, model.exportNamesByLocal)
298
+ break
299
+ case 'ClassDeclaration':
300
+ transformTopLevelClass(editor, node, model.generatedNames.exportBinding, model.exportNamesByLocal)
301
+ break
302
+ }
303
+ }
304
+
305
+ const directDeclarations = new Set(
306
+ model.program.body.flatMap((node) => (node.type === 'VariableDeclaration' ? [node] : []))
307
+ )
308
+ model.hoistedVariables
309
+ .filter(({ declaration }) => !directDeclarations.has(declaration))
310
+ .forEach(({ declaration, isForIterationBinding }) => {
311
+ transformNestedHoistedVariables(
312
+ editor,
313
+ declaration,
314
+ isForIterationBinding,
315
+ model.generatedNames.exportBinding,
316
+ model.exportNamesByLocal
317
+ )
318
+ })
319
+ }
320
+
321
+ /** Changes module variables into assignments to declaration-scope cells shared with setters and hoisted functions. */
322
+ function transformTopLevelVariables(
323
+ editor: SourceEditor,
324
+ declaration: VariableDeclaration,
325
+ exportBinding: string,
326
+ exportNamesByLocal: ReadonlyMap<string, readonly string[]>
327
+ ): void {
328
+ const [first] = declaration.declarations
329
+ if (!first) {
330
+ editor.remove(declaration.start, declaration.end)
331
+ return
332
+ }
333
+
334
+ // `const value = init` becomes `(value = init)`: the declaration cell itself is emitted once in the registration scope.
335
+ editor.overwrite(declaration.start, first.start, '(')
336
+ editor.appendLeft(statementTerminatorStart(editor.original, declaration.end), ')')
337
+ transformVariableInitializers(editor, declaration, exportBinding, exportNamesByLocal)
338
+ }
339
+
340
+ /** Hoists module-scoped var declarations nested in statements without changing their control-flow position. */
341
+ function transformNestedHoistedVariables(
342
+ editor: SourceEditor,
343
+ declaration: VariableDeclaration,
344
+ isForIterationBinding: boolean,
345
+ exportBinding: string,
346
+ exportNamesByLocal: ReadonlyMap<string, readonly string[]>
347
+ ): void {
348
+ const [first] = declaration.declarations
349
+ if (!first) {
350
+ editor.remove(declaration.start, declaration.end)
351
+ return
352
+ }
353
+
354
+ editor.overwrite(declaration.start, first.start, isForIterationBinding ? '' : '(')
355
+ if (!isForIterationBinding) editor.appendLeft(statementTerminatorStart(editor.original, declaration.end), ')')
356
+ transformVariableInitializers(editor, declaration, exportBinding, exportNamesByLocal)
357
+ }
358
+
359
+ /** Publishes initialized exported cells at the exact initializer evaluation point. */
360
+ function transformVariableInitializers(
361
+ editor: SourceEditor,
362
+ declaration: VariableDeclaration,
363
+ exportBinding: string,
364
+ exportNamesByLocal: ReadonlyMap<string, readonly string[]>
365
+ ): void {
366
+ for (const declarator of declaration.declarations) {
367
+ if (!declarator.init) continue
368
+ const exportedBindings = bindingNames(declarator.id).flatMap((name) =>
369
+ (exportNamesByLocal.get(name) ?? []).map((exported) => ({ exported, local: name }))
370
+ )
371
+ if (exportedBindings.length === 0) continue
372
+
373
+ if (declarator.id.type === 'Identifier') {
374
+ // Nested calls publish every alias while preserving the initializer's completion value.
375
+ const names = exportNamesByLocal.get(declarator.id.name) ?? []
376
+ editor.prependLeft(declarator.start, exportExpressionPrefix(exportBinding, names, ''))
377
+ editor.appendRight(declarator.end, exportExpressionSuffix(names))
378
+ continue
379
+ }
380
+
381
+ editor.prependLeft(declarator.start, '(')
382
+ editor.appendRight(
383
+ declarator.end,
384
+ `,${exportedBindings.map(({ exported, local }) => exportCall(exportBinding, exported, local)).join(',')})`
385
+ )
386
+ }
387
+ }
388
+
389
+ /** Turns a class declaration into the execute-time assignment required by cyclic SystemJS linking. */
390
+ function transformTopLevelClass(
391
+ editor: SourceEditor,
392
+ declaration: Class,
393
+ exportBinding: string,
394
+ exportNamesByLocal: ReadonlyMap<string, readonly string[]>
395
+ ): void {
396
+ if (!declaration.id) return
397
+ const names = exportNamesByLocal.get(declaration.id.name) ?? []
398
+ editor.prependLeft(declaration.start, exportExpressionPrefix(exportBinding, names, `${declaration.id.name}=`))
399
+ editor.appendRight(declaration.end, exportExpressionSuffix(names))
400
+ }
401
+
402
+ /** Rewrites expression-level module semantics in one scope-aware O(n) traversal. */
403
+ function applyExpressionEdits(editor: SourceEditor, model: ModuleModel, options: TransformSystemJsOptions): void {
404
+ const scopes = model.scopes
405
+ // Function and class depth are mutable traversal cursors used only to identify lexical module `this` and top-level await.
406
+ let thisBoundaryDepth = 0
407
+
408
+ walk(model.program, {
409
+ scopeTracker: scopes,
410
+ enter(node) {
411
+ if (isThisBoundary(node)) thisBoundaryDepth += 1
412
+
413
+ switch (node.type) {
414
+ case 'ImportExpression':
415
+ // SystemJS owns dynamic loading; only string literals are canonicalized at build time.
416
+ if (node.options || node.phase)
417
+ throw unsupported(options.filename, 'dynamic import options or phases')
418
+ editor.overwrite(node.start, node.source.start, `${model.generatedNames.context}.import(`)
419
+ if (node.source.type === 'Literal' && typeof node.source.value === 'string') {
420
+ editor.overwrite(
421
+ node.source.start,
422
+ node.source.end,
423
+ JSON.stringify(options.resolveReference(node.source.value, 'dynamic'))
424
+ )
425
+ }
426
+ break
427
+ case 'MetaProperty':
428
+ if (node.meta.name === 'import' && node.property.name === 'meta') {
429
+ editor.overwrite(node.start, node.end, `${model.generatedNames.context}.meta`)
430
+ }
431
+ break
432
+ case 'ThisExpression':
433
+ // ESM top-level `this` is undefined. Arrow functions inherit it, while normal functions/classes do not.
434
+ if (thisBoundaryDepth === 0) editor.overwrite(node.start, node.end, 'void 0')
435
+ break
436
+ case 'AssignmentExpression':
437
+ transformAssignment(editor, node.left, node.start, node.end, scopes, model, options.filename)
438
+ break
439
+ case 'UpdateExpression':
440
+ transformUpdate(editor, node, scopes, model)
441
+ break
442
+ case 'ForInStatement':
443
+ case 'ForOfStatement':
444
+ if (node.left.type !== 'VariableDeclaration') {
445
+ requireNoExportedPattern(node.left, scopes, model.exportNamesByLocal, options.filename)
446
+ }
447
+ break
448
+ }
449
+ },
450
+ leave(node) {
451
+ if (isThisBoundary(node)) thisBoundaryDepth -= 1
452
+ }
453
+ })
454
+ }
455
+
456
+ /** Wraps one assignment when its target is an exported root binding. */
457
+ function transformAssignment(
458
+ editor: SourceEditor,
459
+ target: AssignmentTarget,
460
+ start: number,
461
+ end: number,
462
+ scopes: ScopeTracker,
463
+ model: ModuleModel,
464
+ filename: string
465
+ ): void {
466
+ if (target.type === 'Identifier') {
467
+ const names = exportedRootNames(target.name, scopes, model.exportNamesByLocal)
468
+ if (names.length === 0) return
469
+ editor.prependLeft(start, exportExpressionPrefix(model.generatedNames.exportBinding, names, ''))
470
+ editor.appendRight(end, exportExpressionSuffix(names))
471
+ return
472
+ }
473
+
474
+ requireNoExportedPattern(target, scopes, model.exportNamesByLocal, filename)
475
+ }
476
+
477
+ /** Preserves prefix and postfix update values while publishing the next live binding. */
478
+ function transformUpdate(
479
+ editor: SourceEditor,
480
+ update: Extract<Node, { type: 'UpdateExpression' }>,
481
+ scopes: ScopeTracker,
482
+ model: ModuleModel
483
+ ): void {
484
+ if (update.argument.type !== 'Identifier') return
485
+ const names = exportedRootNames(update.argument.name, scopes, model.exportNamesByLocal)
486
+ if (names.length === 0) return
487
+
488
+ if (update.prefix) {
489
+ editor.prependLeft(update.start, exportExpressionPrefix(model.generatedNames.exportBinding, names, ''))
490
+ editor.appendRight(update.end, exportExpressionSuffix(names))
491
+ return
492
+ }
493
+
494
+ // A postfix expression must return the old value, so publish the computed next cell before evaluating the original update.
495
+ const nextValue = `+${update.argument.name}${update.operator[0]}1`
496
+ editor.prependLeft(
497
+ update.start,
498
+ `(${nestedExportExpression(model.generatedNames.exportBinding, names, nextValue)},`
499
+ )
500
+ editor.appendRight(update.end, ')')
501
+ }
502
+
503
+ /** Fails rather than silently changing the completion value of an exported destructuring assignment. */
504
+ function requireNoExportedPattern(
505
+ target: AssignmentTarget,
506
+ scopes: ScopeTracker,
507
+ exportNamesByLocal: ReadonlyMap<string, readonly string[]>,
508
+ filename: string
509
+ ): void {
510
+ const exported = assignmentNames(target).filter(
511
+ (name) => exportedRootNames(name, scopes, exportNamesByLocal).length > 0
512
+ )
513
+ if (exported.length > 0) {
514
+ throw unsupported(filename, `destructuring write to exported binding ${JSON.stringify(exported[0])}`)
515
+ }
516
+ }
517
+
518
+ type RegistrationShell = Readonly<{ prefix: string; suffix: string }>
519
+
520
+ /** Renders the no-map fast path through one source-order edit journal. */
521
+ function assembleUnmappedRegistration(
522
+ editor: StringEditor,
523
+ model: ModuleModel,
524
+ options: TransformSystemJsOptions
525
+ ): string {
526
+ const shell = createRegistrationShell(model, options)
527
+ const hoistedFunctions = model.functions
528
+ .map((declaration) => editor.render(declaration.start, declaration.end))
529
+ .join('')
530
+ model.functions.forEach((declaration) => {
531
+ editor.remove(declaration.start, declaration.end)
532
+ })
533
+ return `${shell.prefix}${editor.render(0, editor.original.length)}}};${hoistedFunctions}${shell.suffix}`
534
+ }
535
+
536
+ /** Places mapped hoisted functions after the declaration return while retaining every original source segment. */
537
+ function assembleMappedRegistration(
538
+ editor: RolldownMagicString,
539
+ model: ModuleModel,
540
+ options: TransformSystemJsOptions
541
+ ): void {
542
+ const shell = createRegistrationShell(model, options)
543
+ // Relocation preserves source-map ownership for large hoisted function bodies when callers explicitly request maps.
544
+ for (const declaration of model.functions) editor.move(declaration.start, declaration.end, editor.original.length)
545
+
546
+ editor.prepend(shell.prefix)
547
+ editor.prependLeft(editor.original.length, '}};')
548
+ editor.append(shell.suffix)
549
+ }
550
+
551
+ /** Creates the generated registration boundary shared by mapped and unmapped rendering. */
552
+ function createRegistrationShell(model: ModuleModel, options: TransformSystemJsOptions): RegistrationShell {
553
+ const dependencies = model.dependencies.map((dependency) => options.resolveReference(dependency.source, 'static'))
554
+ const setters = model.dependencies.map((dependency, index) => renderSetter(dependency, index, model.generatedNames))
555
+ const earlyExports = renderEarlyExports(model)
556
+ const declarations = model.outerBindings.size > 0 ? `var ${[...model.outerBindings].join(',')};` : ''
557
+ const execute = model.hasTopLevelAwait ? 'async function' : 'function'
558
+ // Imports, hoisted cells, and cyclically visible exports belong to declaration/link time. Original executable statements
559
+ // remain inside execute(), which becomes async only when the module itself owns a top-level await.
560
+ const declarationStart = `function(${model.generatedNames.exportBinding},${model.generatedNames.context}){"use strict";${declarations}${earlyExports}return {setters:[${setters.join(',')}],execute:${execute}(){`
561
+
562
+ return options.format === 'system-register'
563
+ ? {
564
+ prefix: `System.register(${JSON.stringify(dependencies)},${declarationStart}`,
565
+ suffix: '})'
566
+ }
567
+ : {
568
+ prefix: `module.exports=[${JSON.stringify(dependencies)},${declarationStart}`,
569
+ suffix: '}]'
570
+ }
571
+ }
572
+
573
+ /** Produces declaration-time exports for functions and uninitialized variables, matching cyclic ESM availability. */
574
+ function renderEarlyExports(model: ModuleModel): string {
575
+ const functionNames = new Set(
576
+ model.functions.flatMap((declaration) => (declaration.id ? [declaration.id.name] : []))
577
+ )
578
+ const directVariables = model.program.body.flatMap((node) => (node.type === 'VariableDeclaration' ? [node] : []))
579
+ const nestedHoistedVariables = model.hoistedVariables
580
+ .map(({ declaration }) => declaration)
581
+ .filter((declaration) => !directVariables.includes(declaration))
582
+ const variablesInSourceOrder = [...directVariables, ...nestedHoistedVariables].sort(
583
+ (left, right) => left.start - right.start
584
+ )
585
+ const entries: Array<Readonly<{ exported: string; value: string }>> = []
586
+
587
+ for (const [local, exportedNames] of model.exportNamesByLocal) {
588
+ if (!functionNames.has(local)) continue
589
+ for (const exported of exportedNames) entries.push({ exported, value: local })
590
+ }
591
+ for (const declaration of variablesInSourceOrder) {
592
+ for (const declarator of declaration.declarations) {
593
+ if (declarator.init) continue
594
+ for (const local of bindingNames(declarator.id)) {
595
+ for (const exported of model.exportNamesByLocal.get(local) ?? []) {
596
+ entries.push({ exported, value: 'void 0' })
597
+ }
598
+ }
599
+ }
600
+ }
601
+
602
+ if (entries.length === 0) return ''
603
+ if (entries.length === 1) {
604
+ const [entry] = entries
605
+ return exportCallWith(model.generatedNames.exportBinding, entry.exported, entry.value)
606
+ }
607
+
608
+ const properties = entries.map(({ exported, value }) => `[${JSON.stringify(exported)}]:${value}`).join(',')
609
+ return `${model.generatedNames.exportBinding}({${properties}});`
610
+ }
611
+
612
+ /** Keeps Babel's execute-time publication point for imported locals named by a final export list. */
613
+ function renderImportedExports(specifiers: readonly ExportSpecifier[], model: ModuleModel): string {
614
+ return specifiers
615
+ .filter((specifier) => model.importBindings.has(moduleExportName(specifier.local)))
616
+ .map((specifier) => {
617
+ const local = moduleExportName(specifier.local)
618
+ return exportCallWith(model.generatedNames.exportBinding, moduleExportName(specifier.exported), local)
619
+ })
620
+ .join('')
621
+ }
622
+
623
+ /** Creates one dependency setter with the import cells consumed by execute-time code. */
624
+ function renderSetter(dependency: MutableDependency, index: number, names: GeneratedNames): string {
625
+ const moduleName = `${names.dependencyPrefix}${index}`
626
+ const statements = dependency.imports.flatMap((binding) => {
627
+ const importedValue = binding.imported === null ? moduleName : memberExpression(moduleName, binding.imported)
628
+ return [`${binding.local}=${importedValue};`]
629
+ })
630
+ return `function(${moduleName}){${statements.join('')}}`
631
+ }
632
+
633
+ /** Selects aliases only when the current traversal reference resolves to the root module declaration. */
634
+ function exportedRootNames(
635
+ local: string,
636
+ scopes: ScopeTracker,
637
+ exportNamesByLocal: ReadonlyMap<string, readonly string[]>
638
+ ): readonly string[] {
639
+ const names = exportNamesByLocal.get(local) ?? []
640
+ if (names.length === 0) return names
641
+ const declaration = scopes.getDeclaration(local)
642
+ return declaration?.scope === '' ? names : []
643
+ }
644
+
645
+ /** Builds the opening calls around an expression that follows directly in the original source. */
646
+ function exportExpressionPrefix(exportBinding: string, names: readonly string[], expressionPrefix: string): string {
647
+ if (names.length === 0) return expressionPrefix
648
+ return names.reduce((prefix, exported) => `${exportCallPrefix(exportBinding, exported)}${prefix}`, expressionPrefix)
649
+ }
650
+
651
+ /** Closes nested export calls opened by exportExpressionPrefix. */
652
+ function exportExpressionSuffix(names: readonly string[]): string {
653
+ return ')'.repeat(names.length)
654
+ }
655
+
656
+ /** Renders nested export calls around a generated expression. */
657
+ function nestedExportExpression(exportBinding: string, names: readonly string[], expression: string): string {
658
+ return names.reduce((value, exported) => `${exportCallPrefix(exportBinding, exported)}${value})`, expression)
659
+ }
660
+
661
+ /** Opens an export notification around an original expression whose source text follows. */
662
+ function exportCallPrefix(exportBinding: string, exported: string): string {
663
+ return `${exportBinding}(${JSON.stringify(exported)},`
664
+ }
665
+
666
+ /** Emits an export notification expression without imposing statement boundaries. */
667
+ function exportCall(exportBinding: string, exported: string, value: string): string {
668
+ return `${exportBinding}(${JSON.stringify(exported)},${value})`
669
+ }
670
+
671
+ /** Emits a complete export notification statement. */
672
+ function exportCallWith(exportBinding: string, exported: string, value: string): string {
673
+ return `${exportBinding}(${JSON.stringify(exported)},${value});`
674
+ }
675
+
676
+ /** Creates collision-free identifiers without requiring Babel's scope allocator. */
677
+ function createGeneratedNames(identifierNames: ReadonlySet<string>, dependencyCount: number): GeneratedNames {
678
+ // The used-name set is locally mutable because each selected helper reserves its name for the next helper.
679
+ const used = new Set(identifierNames)
680
+ const take = (base: string): string => {
681
+ // The suffix cursor advances only on an actual source collision and never escapes this allocation call.
682
+ let suffix = 0
683
+ let candidate = base
684
+ while (used.has(candidate)) {
685
+ suffix += 1
686
+ candidate = `${base}${suffix}`
687
+ }
688
+ used.add(candidate)
689
+ return candidate
690
+ }
691
+
692
+ const takeDependencyPrefix = (base: string): string => {
693
+ // Prefix selection tests every concrete setter argument because the unsuffixed base may itself be collision-free.
694
+ let suffix = 0
695
+ let candidate = base
696
+ while (
697
+ Array.from({ length: dependencyCount }, (_, index) => `${candidate}${index}`).some((name) => used.has(name))
698
+ ) {
699
+ suffix += 1
700
+ candidate = `${base}${suffix}`
701
+ }
702
+ Array.from({ length: dependencyCount }, (_, index) => `${candidate}${index}`).forEach((name) => {
703
+ used.add(name)
704
+ })
705
+ return candidate
706
+ }
707
+
708
+ return {
709
+ context: take('__systemContext'),
710
+ exportBinding: take('__systemExport'),
711
+ dependencyPrefix: takeDependencyPrefix('__systemDependency')
712
+ }
713
+ }
714
+
715
+ /** Builds and freezes complete lexical declarations for scope-correct write detection. */
716
+ function createFrozenScopes(program: Program): ScopeTracker {
717
+ // ScopeTracker is intentionally mutable during its declaration pass, then frozen before every query traversal.
718
+ const scopes = new ScopeTracker({ preserveExitedScopes: true })
719
+ walk(program, { scopeTracker: scopes })
720
+ scopes.freeze()
721
+ return scopes
722
+ }
723
+
724
+ /** Normalizes identifier and quoted module names to the runtime property string. */
725
+ function moduleExportName(name: ModuleExportName): string {
726
+ return name.type === 'Literal' ? String(name.value) : name.name
727
+ }
728
+
729
+ /** Uses dot access only when the imported name is a valid identifier. */
730
+ function memberExpression(object: string, property: string): string {
731
+ return /^[$A-Z_a-z][$\w]*$/.test(property) ? `${object}.${property}` : `${object}[${JSON.stringify(property)}]`
732
+ }
733
+
734
+ function bindingNamesFromDeclaration(declaration: VariableDeclaration): string[] {
735
+ return declaration.declarations.flatMap((declarator) => bindingNames(declarator.id))
736
+ }
737
+
738
+ function bindingNames(pattern: BindingPattern): string[] {
739
+ return patternNames(pattern)
740
+ }
741
+
742
+ function assignmentNames(pattern: AssignmentTarget): string[] {
743
+ return patternNames(pattern)
744
+ }
745
+
746
+ /** Extracts identifiers from binding and assignment patterns without treating computed keys as targets. */
747
+ function patternNames(pattern: Node): string[] {
748
+ switch (pattern.type) {
749
+ case 'Identifier':
750
+ return [pattern.name]
751
+ case 'AssignmentPattern':
752
+ return patternNames(pattern.left)
753
+ case 'ArrayPattern':
754
+ return pattern.elements.flatMap((element) => (element ? patternNames(element) : []))
755
+ case 'ObjectPattern':
756
+ return pattern.properties.flatMap((property) =>
757
+ property.type === 'Property' ? patternNames(property.value) : patternNames(property.argument)
758
+ )
759
+ case 'RestElement':
760
+ return patternNames(pattern.argument)
761
+ case 'TSAsExpression':
762
+ case 'TSSatisfiesExpression':
763
+ case 'TSNonNullExpression':
764
+ case 'TSTypeAssertion':
765
+ return patternNames(pattern.expression)
766
+ default:
767
+ return []
768
+ }
769
+ }
770
+
771
+ function isFunction(node: Node): node is OxcFunction | Extract<Node, { type: 'ArrowFunctionExpression' }> {
772
+ return (
773
+ node.type === 'FunctionDeclaration' ||
774
+ node.type === 'FunctionExpression' ||
775
+ node.type === 'ArrowFunctionExpression' ||
776
+ node.type === 'TSEmptyBodyFunctionExpression'
777
+ )
778
+ }
779
+
780
+ function isModuleBoundary(node: Node): boolean {
781
+ return isFunction(node) || node.type === 'ClassDeclaration' || node.type === 'ClassExpression'
782
+ }
783
+
784
+ function isThisBoundary(node: Node): boolean {
785
+ return (
786
+ (isFunction(node) && node.type !== 'ArrowFunctionExpression') ||
787
+ node.type === 'ClassDeclaration' ||
788
+ node.type === 'ClassExpression'
789
+ )
790
+ }
791
+
792
+ /** Babel rejects direct eval for SystemJS because renaming declaration cells cannot update evaluated source. */
793
+ function requireNoDirectEval(hasDirectEval: boolean, filename: string): void {
794
+ if (hasDirectEval) throw unsupported(filename, 'direct eval')
795
+ }
796
+
797
+ function requireSupportedVariableKind(declaration: VariableDeclaration, filename: string): void {
798
+ if (declaration.kind === 'using' || declaration.kind === 'await using' || declaration.declare) {
799
+ throw unsupported(filename, 'using or ambient variable declaration')
800
+ }
801
+ }
802
+
803
+ /** Places a closing edit before an optional semicolon owned by the declaration node. */
804
+ function statementTerminatorStart(code: string, end: number): number {
805
+ return code[end - 1] === ';' ? end - 1 : end
806
+ }
807
+
808
+ /** Makes a future Rolldown grammar change fail closed with the affected chunk named. */
809
+ function unsupported(filename: string, construct: string): Error {
810
+ return new Error(`Unsupported final Rolldown chunk ${filename}: ${construct}`)
811
+ }
812
+
813
+ /** Produces a Vite-compatible raw map that retains the complete pre-transform chunk as source content. */
814
+ function createSourceMap(editor: RolldownMagicString, filename: string): ExistingRawSourceMap {
815
+ const generated = editor.generateMap({
816
+ file: filename,
817
+ hires: 'boundary',
818
+ includeContent: true,
819
+ source: filename
820
+ })
821
+
822
+ return {
823
+ version: generated.version,
824
+ file: generated.file,
825
+ sources: generated.sources,
826
+ sourcesContent: generated.sourcesContent,
827
+ names: generated.names,
828
+ mappings: generated.mappings,
829
+ ...(generated.x_google_ignoreList ? { x_google_ignoreList: generated.x_google_ignoreList } : {})
830
+ }
831
+ }