vite-plugin-taro 0.6.0 → 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 (68) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/node/plugins/h5/transform-app.js +1 -1
  4. package/dist/node/plugins/wx/dev/plugins.js +1 -1
  5. package/dist/node/plugins/wx/dev/wx-dev-options.js +22 -1
  6. package/dist/node/plugins/wx/module/chunk-path.d.ts +8 -0
  7. package/dist/node/plugins/wx/module/chunk-path.js +18 -0
  8. package/dist/node/plugins/wx/{module.js → module/module.js} +2 -2
  9. package/dist/node/plugins/wx/native/create-native-component-output.d.ts +3 -1
  10. package/dist/node/plugins/wx/native/create-native-component-output.js +3 -5
  11. package/dist/node/plugins/wx/output/files.d.ts +3 -2
  12. package/dist/node/plugins/wx/output/files.js +2 -2
  13. package/dist/node/plugins/wx/output/json.d.ts +1 -1
  14. package/dist/node/plugins/wx/output/json.js +1 -1
  15. package/dist/node/plugins/wx/placer/placement.d.ts +51 -0
  16. package/dist/node/plugins/wx/placer/placement.js +245 -0
  17. package/dist/node/plugins/wx/placer/placer.d.ts +76 -0
  18. package/dist/node/plugins/wx/placer/placer.js +147 -0
  19. package/dist/node/plugins/wx/plugins.js +17 -17
  20. package/dist/node/plugins/wx/render/capsule.d.ts +35 -3
  21. package/dist/node/plugins/wx/render/capsule.js +53 -12
  22. package/dist/node/plugins/wx/render/native.d.ts +20 -2
  23. package/dist/node/plugins/wx/render/native.js +539 -35
  24. package/dist/node/plugins/wx/render/system-js/string-editor.d.ts +24 -0
  25. package/dist/node/plugins/wx/render/system-js/string-editor.js +93 -0
  26. package/dist/node/plugins/wx/render/system-js/system-js.d.ts +23 -0
  27. package/dist/node/plugins/wx/render/system-js/system-js.js +602 -0
  28. package/dist/node/plugins/wx/render/transport.d.ts +7 -3
  29. package/dist/node/plugins/wx/render/transport.js +19 -10
  30. package/dist/node/plugins/wx/resolve/resolver.js +1 -1
  31. package/dist/node/plugins/wx/resolve/specialize-bootstrap.js +1 -1
  32. package/dist/node/plugins/wx/resolve/specialize-page-capsule.js +1 -1
  33. package/dist/node/utils/modules.d.ts +0 -2
  34. package/dist/node/utils/modules.js +0 -7
  35. package/dist/node/utils/transform.d.ts +0 -3
  36. package/dist/node/utils/transform.js +0 -23
  37. package/package.json +8 -6
  38. package/src/node/plugins/h5/transform-app.ts +1 -1
  39. package/src/node/plugins/wx/dev/plugins.ts +1 -1
  40. package/src/node/plugins/wx/dev/wx-dev-options.ts +22 -1
  41. package/src/node/plugins/wx/module/chunk-path.ts +22 -0
  42. package/src/node/plugins/wx/{module.ts → module/module.ts} +2 -2
  43. package/src/node/plugins/wx/native/create-native-component-output.ts +6 -5
  44. package/src/node/plugins/wx/output/files.ts +5 -3
  45. package/src/node/plugins/wx/output/json.ts +1 -2
  46. package/src/node/plugins/wx/placer/placement.ts +364 -0
  47. package/src/node/plugins/wx/placer/placer.ts +179 -0
  48. package/src/node/plugins/wx/plugins.ts +19 -19
  49. package/src/node/plugins/wx/render/capsule.ts +53 -17
  50. package/src/node/plugins/wx/render/native.ts +670 -55
  51. package/src/node/plugins/wx/render/system-js/string-editor.ts +115 -0
  52. package/src/node/plugins/wx/render/system-js/system-js.ts +831 -0
  53. package/src/node/plugins/wx/render/transport.ts +25 -9
  54. package/src/node/plugins/wx/resolve/resolver.ts +1 -1
  55. package/src/node/plugins/wx/resolve/specialize-bootstrap.ts +1 -1
  56. package/src/node/plugins/wx/resolve/specialize-page-capsule.ts +1 -1
  57. package/src/node/utils/modules.ts +0 -8
  58. package/src/node/utils/transform.ts +0 -30
  59. package/dist/node/plugins/wx/placement/placer.d.ts +0 -78
  60. package/dist/node/plugins/wx/placement/placer.js +0 -158
  61. package/dist/node/plugins/wx/placement/plan.d.ts +0 -46
  62. package/dist/node/plugins/wx/placement/plan.js +0 -210
  63. package/dist/node/plugins/wx/render/capsule-wrapper.d.ts +0 -3
  64. package/dist/node/plugins/wx/render/capsule-wrapper.js +0 -64
  65. package/src/node/plugins/wx/placement/placer.ts +0 -187
  66. package/src/node/plugins/wx/placement/plan.ts +0 -306
  67. package/src/node/plugins/wx/render/capsule-wrapper.ts +0 -86
  68. /package/dist/node/plugins/wx/{module.d.ts → module/module.d.ts} +0 -0
@@ -1,11 +1,92 @@
1
- import { type PluginObj, type PluginTarget, types } from '@babel/core'
2
- import transformModulesCommonjs from '@babel/plugin-transform-modules-commonjs'
1
+ /**
2
+ * Final Rolldown ESM to native WX CommonJS renderer.
3
+ *
4
+ * The WX build intentionally has two cooperating JavaScript execution domains:
5
+ *
6
+ * - Native and amphibious chunks are entered by WeChat through synchronous CommonJS `require`. They install infrastructure
7
+ * and call the native App, Page, and Component lifecycle APIs.
8
+ * - Capsule chunks retain ESM linking behavior behind the bundled SystemJS runtime. They contain the application graph and
9
+ * may use cycles, live bindings, dynamic imports, and top-level await.
10
+ *
11
+ * Rolldown still emits one ESM chunk graph before that runtime split is materialized. WeChat cannot execute those final ESM
12
+ * imports directly, so this renderer translates chunks classified as native or amphibious into CommonJS while preserving the
13
+ * ESM behavior observable at their boundary. Ordinary native dependencies become `require` namespace cells; capsule imports
14
+ * become synchronous `global.System.importSync` lookups; and exports are published through the CommonJS `exports` object.
15
+ *
16
+ * This is deliberately a final-chunk compiler, not a general source-module compiler. Rolldown has already lowered TypeScript,
17
+ * bundled source modules, selected chunk boundaries, and normalized the remaining imports and exports. Restricting the input
18
+ * grammar lets the hot startup path use one Oxc analysis plus local range edits instead of Babel parsing, cloning, traversing,
19
+ * and regenerating each complete chunk. Unsupported future Rolldown forms fail the build rather than being approximated.
20
+ *
21
+ * Babel's CommonJS transform remains beside this implementation only as a differential-test oracle. Those tests compare
22
+ * runtime observations for import interop, receiver semantics, live export writes, declaration timing, and top-level `this`.
23
+ */
24
+ import type {
25
+ AssignmentTarget,
26
+ ExportSpecifier,
27
+ ImportDeclaration,
28
+ ModuleExportName,
29
+ Node,
30
+ Program,
31
+ VariableDeclaration
32
+ } from '@oxc-project/types'
33
+ import { isReferenceIdentifier, ScopeTracker, walk } from 'oxc-walker'
34
+ import { type ExistingRawSourceMap, RolldownMagicString } from 'rolldown'
35
+ import { parseSync } from 'rolldown/utils'
3
36
  import type { Rolldown } from 'vite'
4
- import { resolveChunkReference } from '../../../utils/modules.ts'
5
- import { type AstTransformResult, transformWithBabel } from '../../../utils/transform.ts'
6
- import { getWxEntryRole } from '../module.ts'
37
+ import type { AstTransformResult } from '../../../utils/transform.ts'
38
+ import { resolveLogicalChunkReference, resolvePhysicalChunkReference } from '../module/chunk-path.ts'
39
+ import { getWxEntryRole } from '../module/module.ts'
7
40
 
8
- /** Renders a native module while activating its statically imported capsules through SystemJS. */
41
+ type ImportBinding = Readonly<{
42
+ imported: string | null
43
+ local: string
44
+ namespace: string
45
+ }>
46
+
47
+ type ImportedReferenceContext = 'plain' | 'unbound'
48
+
49
+ type ImportInterop = 'none' | 'default' | 'namespace'
50
+
51
+ type ImportModel = Readonly<{
52
+ bindings: readonly ImportBinding[]
53
+ capsuleBinding: Readonly<{ imported: string; local: string; logicalId: string }> | null
54
+ interop: ImportInterop
55
+ namespace: string
56
+ reference: string
57
+ }>
58
+
59
+ type NativeModuleModel = Readonly<{
60
+ defaultInterop: string
61
+ exportNamesByLocal: ReadonlyMap<string, readonly string[]>
62
+ imports: readonly ImportModel[]
63
+ importBindingsByLocal: ReadonlyMap<string, ImportBinding>
64
+ namespaceInterop: string
65
+ postfixTemp: string
66
+ program: Program
67
+ scopes: ScopeTracker
68
+ usesPostfixTemp: boolean
69
+ }>
70
+
71
+ /**
72
+ * Materializes one final native/amphibious Rolldown chunk as executable WX CommonJS.
73
+ *
74
+ * The transform performs four semantic operations:
75
+ *
76
+ * 1. Static ESM imports are hoisted into source-order `require` calls. Named imports remain property reads from the required
77
+ * namespace so they observe current values. Default and namespace imports receive Babel-compatible CommonJS interop.
78
+ * 2. An import whose target owns a capsule entry is not passed to native `require`. It becomes
79
+ * `global.System.importSync(logicalChunkId)`, synchronously linking the capsule before the native lifecycle call.
80
+ * 3. Local exports are published at declaration and mutation points. Imported re-exports use getters, while assignments and
81
+ * updates notify every alias without changing expression completion values or accidentally matching shadowed bindings.
82
+ * 4. ESM top-level `this` becomes `undefined`. Direct imported calls and tags are explicitly unbound so converting an import
83
+ * into a namespace property does not introduce a false CommonJS receiver.
84
+ *
85
+ * Static dependency loading is emitted before the untouched module body because ESM evaluates dependencies before body
86
+ * statements regardless of where import declarations appear textually. Generated helper names are allocated against every
87
+ * source identifier. Source maps use the same range edits when requested; WX development disables them and keeps this path
88
+ * focused on startup latency.
89
+ */
9
90
  export function renderNative({
10
91
  code,
11
92
  chunk,
@@ -17,68 +98,602 @@ export function renderNative({
17
98
  chunks: Readonly<Record<string, Rolldown.RenderedChunk>>
18
99
  sourcemap: boolean
19
100
  }): AstTransformResult {
20
- return transformWithBabel(
21
- code,
22
- chunk.fileName,
23
- [connectNativeCapsulesPlugin(chunk.fileName, chunks) as PluginTarget, transformModulesCommonjs as PluginTarget],
24
- sourcemap
25
- )
101
+ const parsed = parseSync(chunk.fileName, code)
102
+ if (parsed.errors.length > 0) {
103
+ const diagnostics = parsed.errors.map((error) => error.message).join('; ')
104
+ throw new Error(`Failed to parse ${chunk.fileName} with Oxc: ${diagnostics}`)
105
+ }
106
+
107
+ // Analysis owns grammar validation, scope resolution, helper allocation, and physical-to-logical capsule classification.
108
+ // It completes before an editor exists, so failure cannot leak a partially rewritten chunk into Rolldown's output graph.
109
+ const model = analyzeNativeModule(parsed.program, chunk, chunks)
110
+ const editor = new RolldownMagicString(code, { filename: chunk.fileName })
111
+
112
+ // Expression edits split untouched source ranges first. Declaration replacement runs afterwards because MagicString must
113
+ // not split a range after that complete range has already been overwritten or removed.
114
+ rewriteExpressionSemantics(editor, model, chunk.fileName)
115
+ rewriteTopLevelThis(editor, model.program)
116
+ rewriteModuleDeclarations(editor, model)
117
+
118
+ const hasExports = model.exportNamesByLocal.size > 0
119
+ const helpers = renderInteropHelpers(model)
120
+ const postfixTemp = model.usesPostfixTemp ? `var ${model.postfixTemp};` : ''
121
+ const imports = renderImports(model)
122
+ if (hasExports) {
123
+ editor.prepend(
124
+ `"use strict";Object.defineProperty(exports,"__esModule",{value:true});${helpers}${postfixTemp}${imports}`
125
+ )
126
+ } else {
127
+ editor.prepend(`"use strict";${helpers}${postfixTemp}${imports}`)
128
+ }
129
+
130
+ return {
131
+ code: editor.toString(),
132
+ map: sourcemap ? createSourceMap(editor, chunk.fileName) : null
133
+ }
26
134
  }
27
135
 
28
- /** Converts only cross-runtime static imports; ordinary native dependencies remain CommonJS imports. */
29
- function connectNativeCapsulesPlugin(
30
- fileName: string,
136
+ /**
137
+ * Builds the immutable semantic model consumed by all rewrite passes.
138
+ *
139
+ * Import classification needs the complete rendered chunk graph: a relative path alone cannot reveal whether native require
140
+ * should execute the target or SystemJS should link it as a capsule. ScopeTracker is frozen after declaration collection so
141
+ * later reference walks can distinguish module cells from same-named parameters and nested locals.
142
+ */
143
+ function analyzeNativeModule(
144
+ program: Program,
145
+ chunk: Rolldown.RenderedChunk,
31
146
  chunks: Readonly<Record<string, Rolldown.RenderedChunk>>
32
- ): PluginObj {
33
- return {
34
- name: 'vpt:connect-native-capsules',
35
- visitor: {
36
- ImportDeclaration(importPath) {
37
- const reference = importPath.node.source.value
38
- if (!reference.startsWith('./') && !reference.startsWith('../')) {
39
- return
147
+ ): NativeModuleModel {
148
+ const identifierNames = collectIdentifierNames(program)
149
+ const exportNamesByLocal = new Map<string, string[]>()
150
+ const imports: ImportModel[] = []
151
+ const importBindingsByLocal = new Map<string, ImportBinding>()
152
+ let hasDirectEval = false
153
+
154
+ walk(program, {
155
+ enter(node) {
156
+ if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'eval') {
157
+ hasDirectEval = true
158
+ }
159
+ }
160
+ })
161
+ if (hasDirectEval) throw unsupported(chunk.fileName, 'direct eval')
162
+
163
+ for (const node of program.body) {
164
+ switch (node.type) {
165
+ case 'ImportDeclaration': {
166
+ requirePlainImport(node, chunk.fileName)
167
+ const capsule = getImportedCapsule(chunk.fileName, node.source.value, chunks)
168
+ if (capsule) {
169
+ const [specifier] = node.specifiers
170
+ if (node.specifiers.length !== 1 || !specifier || specifier.type === 'ImportNamespaceSpecifier') {
171
+ throw new Error(
172
+ `Expected one capsule value import from ${capsule.fileName} in ${chunk.fileName}`
173
+ )
174
+ }
175
+ imports.push({
176
+ bindings: [],
177
+ capsuleBinding: {
178
+ imported:
179
+ specifier.type === 'ImportDefaultSpecifier'
180
+ ? 'default'
181
+ : moduleExportName(specifier.imported),
182
+ local: specifier.local.name,
183
+ logicalId: resolveLogicalChunkReference(chunk.fileName, node.source.value)
184
+ },
185
+ interop: 'none',
186
+ namespace: '',
187
+ reference: node.source.value
188
+ })
189
+ continue
40
190
  }
41
191
 
42
- const chunkId = resolveChunkReference(fileName, reference)
43
- const importedChunk = chunks[chunkId]
44
- if (!importedChunk || getWxEntryRole(importedChunk) !== 'capsule') {
45
- return
192
+ const namespace = takeGeneratedName('__nativeImport', identifierNames)
193
+ const bindings = node.specifiers.map((specifier): ImportBinding => {
194
+ const binding = {
195
+ imported:
196
+ specifier.type === 'ImportNamespaceSpecifier'
197
+ ? null
198
+ : specifier.type === 'ImportDefaultSpecifier'
199
+ ? 'default'
200
+ : moduleExportName(specifier.imported),
201
+ local: specifier.local.name,
202
+ namespace
203
+ }
204
+ importBindingsByLocal.set(binding.local, binding)
205
+ return binding
206
+ })
207
+ imports.push({
208
+ bindings,
209
+ capsuleBinding: null,
210
+ interop: importInterop(node),
211
+ namespace,
212
+ reference: node.source.value
213
+ })
214
+ break
215
+ }
216
+ case 'ExportNamedDeclaration':
217
+ collectFinalExports(node, exportNamesByLocal, chunk.fileName)
218
+ break
219
+ case 'ExportDefaultDeclaration':
220
+ case 'ExportAllDeclaration':
221
+ throw unsupported(chunk.fileName, `source-level ${node.type}`)
222
+ }
223
+ }
224
+
225
+ const defaultInterop = takeGeneratedName('__nativeDefault', identifierNames)
226
+ const namespaceInterop = takeGeneratedName('__nativeNamespace', identifierNames)
227
+ const postfixTemp = takeGeneratedName('__nativePostfix', identifierNames)
228
+ const scopes = new ScopeTracker({ preserveExitedScopes: true })
229
+ walk(program, { scopeTracker: scopes })
230
+ scopes.freeze()
231
+ const usesPostfixTemp = hasPostfixExportUpdate(program, scopes, exportNamesByLocal)
232
+
233
+ return {
234
+ defaultInterop,
235
+ exportNamesByLocal,
236
+ imports,
237
+ importBindingsByLocal,
238
+ namespaceInterop,
239
+ postfixTemp,
240
+ program,
241
+ scopes,
242
+ usesPostfixTemp
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Removes syntax that WX cannot parse and adds declaration-time CommonJS publication without regenerating other source.
248
+ * Import execution itself is emitted in the generated header; removing declarations here retains comments and all unrelated
249
+ * Rolldown output byte-for-byte. Final export lists publish only imported getters because local cells were already instrumented
250
+ * at their declarations and writes.
251
+ */
252
+ function rewriteModuleDeclarations(editor: RolldownMagicString, model: NativeModuleModel): void {
253
+ for (const node of model.program.body) {
254
+ switch (node.type) {
255
+ case 'ImportDeclaration':
256
+ // Static imports execute before every module body statement regardless of their textual position.
257
+ editor.remove(node.start, node.end)
258
+ break
259
+ case 'ExportNamedDeclaration':
260
+ if (node.declaration) {
261
+ // Strip only the export keyword; declaration publication is handled at its initializer or end boundary.
262
+ editor.remove(node.start, node.declaration.start)
263
+ rewriteExportedDeclaration(editor, node.declaration, model.exportNamesByLocal)
264
+ } else {
265
+ editor.overwrite(node.start, node.end, renderExportList(node.specifiers, model))
46
266
  }
267
+ break
268
+ case 'VariableDeclaration':
269
+ publishVariableInitializers(editor, node, model.exportNamesByLocal)
270
+ break
271
+ case 'FunctionDeclaration':
272
+ if (node.id) publishAfter(editor, node.end, node.id.name, model.exportNamesByLocal)
273
+ break
274
+ case 'ClassDeclaration':
275
+ if (node.id) publishAfter(editor, node.end, node.id.name, model.exportNamesByLocal)
276
+ break
277
+ }
278
+ }
279
+ }
47
280
 
48
- const [specifier] = importPath.node.specifiers
49
- if (
50
- importPath.node.specifiers.length !== 1 ||
51
- !specifier ||
52
- types.isImportNamespaceSpecifier(specifier)
53
- ) {
54
- throw new Error(`Expected one capsule value import from ${chunkId} in ${fileName}`)
281
+ /**
282
+ * Rewrites imported references and live-export mutations in one scope-aware O(n) pass.
283
+ *
284
+ * A named import such as `fn` becomes a namespace property read. In call or tag position it is wrapped as `(0, ns.fn)` to
285
+ * preserve ESM's unbound receiver; in `new fn()` it remains `ns.fn` because construction has no method receiver. Exported
286
+ * writes are instrumented only when ScopeTracker resolves the target to the root module declaration.
287
+ */
288
+ function rewriteExpressionSemantics(editor: RolldownMagicString, model: NativeModuleModel, filename: string): void {
289
+ // This mutable traversal stack exists only to see through explicit ParenthesizedExpression nodes around imported calls.
290
+ const ancestors: Node[] = []
291
+ walk(model.program, {
292
+ scopeTracker: model.scopes,
293
+ enter(node, parent) {
294
+ if (
295
+ node.type === 'Identifier' &&
296
+ parent?.type !== 'ImportDeclaration' &&
297
+ isReferenceIdentifier(node, parent)
298
+ ) {
299
+ const binding = model.importBindingsByLocal.get(node.name)
300
+ const declaration = binding ? model.scopes.getDeclaration(node.name) : null
301
+ if (binding && declaration?.type === 'Import') {
302
+ const context = importedReferenceContext(node, parent, ancestors)
303
+ const imported = importedExpression(binding)
304
+ const replacement = context === 'unbound' ? `(0,${imported})` : imported
305
+ // Replacing `{ imported }` with `{ imported: namespace.imported }` preserves shorthand property keys.
306
+ editor.overwrite(
307
+ node.start,
308
+ node.end,
309
+ parent?.type === 'Property' && parent.shorthand && parent.value === node
310
+ ? `${node.name}:${replacement}`
311
+ : replacement
312
+ )
55
313
  }
314
+ }
315
+
316
+ switch (node.type) {
317
+ case 'AssignmentExpression':
318
+ rewriteExportAssignment(editor, node.left, node.start, model, filename)
319
+ break
320
+ case 'UpdateExpression':
321
+ rewriteExportUpdate(editor, node, model)
322
+ break
323
+ case 'ForInStatement':
324
+ case 'ForOfStatement':
325
+ if (node.left.type !== 'VariableDeclaration') {
326
+ requireNoExportedPattern(node.left, model, filename)
327
+ }
328
+ break
329
+ }
330
+ ancestors.push(node)
331
+ },
332
+ leave() {
333
+ ancestors.pop()
334
+ }
335
+ })
336
+ }
337
+
338
+ /** Prefixes a direct exported assignment; assignment operators already preserve their own completion value. */
339
+ function rewriteExportAssignment(
340
+ editor: RolldownMagicString,
341
+ target: AssignmentTarget,
342
+ start: number,
343
+ model: NativeModuleModel,
344
+ filename: string
345
+ ): void {
346
+ if (target.type === 'Identifier') {
347
+ const names = exportedRootNames(target.name, model)
348
+ if (names.length > 0) editor.prependLeft(start, exportAssignmentPrefix(names))
349
+ return
350
+ }
351
+ requireNoExportedPattern(target, model, filename)
352
+ }
353
+
354
+ /** Publishes the updated cell while retaining the distinct prefix/postfix expression result. */
355
+ function rewriteExportUpdate(
356
+ editor: RolldownMagicString,
357
+ update: Extract<Node, { type: 'UpdateExpression' }>,
358
+ model: NativeModuleModel
359
+ ): void {
360
+ if (update.argument.type !== 'Identifier') return
361
+ const names = exportedRootNames(update.argument.name, model)
362
+ if (names.length === 0) return
363
+ if (update.prefix) {
364
+ editor.prependLeft(update.start, exportAssignmentPrefix(names))
365
+ return
366
+ }
367
+
368
+ editor.prependLeft(update.start, `(${model.postfixTemp}=`)
369
+ editor.appendRight(update.end, `,${exportAssignmentExpression(names, update.argument.name)},${model.postfixTemp})`)
370
+ }
371
+
372
+ /** Rejects destructuring writes because wrapping them would change their completion value. */
373
+ function requireNoExportedPattern(target: AssignmentTarget, model: NativeModuleModel, filename: string): void {
374
+ const exported = bindingNames(target).filter((name) => exportedRootNames(name, model).length > 0)
375
+ if (exported.length > 0) {
376
+ throw unsupported(filename, `destructuring write to exported binding ${JSON.stringify(exported[0])}`)
377
+ }
378
+ }
379
+
380
+ function exportedRootNames(local: string, model: NativeModuleModel): readonly string[] {
381
+ const names = model.exportNamesByLocal.get(local) ?? []
382
+ if (names.length === 0) return names
383
+ return model.scopes.getDeclaration(local)?.scope === '' ? names : []
384
+ }
385
+
386
+ /** Detects whether the generated declaration header needs one postfix completion-value cell. */
387
+ function hasPostfixExportUpdate(
388
+ program: Program,
389
+ scopes: ScopeTracker,
390
+ exportNamesByLocal: ReadonlyMap<string, readonly string[]>
391
+ ): boolean {
392
+ let found = false
393
+ walk(program, {
394
+ scopeTracker: scopes,
395
+ enter(node) {
396
+ if (found || node.type !== 'UpdateExpression' || node.prefix || node.argument.type !== 'Identifier') return
397
+ const names = exportNamesByLocal.get(node.argument.name) ?? []
398
+ if (names.length > 0 && scopes.getDeclaration(node.argument.name)?.scope === '') found = true
399
+ }
400
+ })
401
+ return found
402
+ }
403
+
404
+ /** Rewrites ESM top-level this, including arrow functions that lexically inherit it. */
405
+ function rewriteTopLevelThis(editor: RolldownMagicString, program: Program): void {
406
+ let thisBoundaryDepth = 0
407
+ walk(program, {
408
+ enter(node) {
409
+ if (isThisBoundary(node)) thisBoundaryDepth += 1
410
+ if (node.type === 'ThisExpression' && thisBoundaryDepth === 0)
411
+ editor.overwrite(node.start, node.end, 'void 0')
412
+ },
413
+ leave(node) {
414
+ if (isThisBoundary(node)) thisBoundaryDepth -= 1
415
+ }
416
+ })
417
+ }
418
+
419
+ /** Records aliases from Rolldown's normalized terminal `export { ... }` declaration. */
420
+ function collectFinalExports(
421
+ declaration: Extract<Program['body'][number], { type: 'ExportNamedDeclaration' }>,
422
+ exportNamesByLocal: Map<string, string[]>,
423
+ filename: string
424
+ ): void {
425
+ if (declaration.source || declaration.attributes.length > 0 || declaration.exportKind === 'type') {
426
+ throw unsupported(filename, 're-exports, export attributes, or type-only exports')
427
+ }
428
+ if (declaration.declaration) {
429
+ for (const local of declarationBindingNames(declaration.declaration, filename)) {
430
+ const names = exportNamesByLocal.get(local) ?? []
431
+ names.push(local)
432
+ if (!exportNamesByLocal.has(local)) exportNamesByLocal.set(local, names)
433
+ }
434
+ return
435
+ }
436
+ for (const specifier of declaration.specifiers) {
437
+ const local = moduleExportName(specifier.local)
438
+ const names = exportNamesByLocal.get(local) ?? []
439
+ names.push(moduleExportName(specifier.exported))
440
+ if (!exportNamesByLocal.has(local)) exportNamesByLocal.set(local, names)
441
+ }
442
+ }
56
443
 
57
- const imported = types.isImportDefaultSpecifier(specifier)
58
- ? types.identifier('default')
59
- : specifier.imported
60
- const importedConfig = types.memberExpression(
61
- createSyncImport(chunkId),
62
- types.cloneNode(imported),
63
- types.isStringLiteral(imported)
64
- )
65
- importPath.replaceWith(
66
- types.variableDeclaration('const', [
67
- types.variableDeclarator(types.cloneNode(specifier.local), importedConfig)
68
- ])
69
- )
444
+ /** Applies publication edits to a declaration wrapped by `export`. */
445
+ function rewriteExportedDeclaration(
446
+ editor: RolldownMagicString,
447
+ declaration: NonNullable<Extract<Program['body'][number], { type: 'ExportNamedDeclaration' }>['declaration']>,
448
+ exportNamesByLocal: ReadonlyMap<string, readonly string[]>
449
+ ): void {
450
+ switch (declaration.type) {
451
+ case 'VariableDeclaration':
452
+ publishVariableInitializers(editor, declaration, exportNamesByLocal)
453
+ break
454
+ case 'FunctionDeclaration':
455
+ case 'ClassDeclaration':
456
+ if (declaration.id) publishAfter(editor, declaration.end, declaration.id.name, exportNamesByLocal)
457
+ break
458
+ }
459
+ }
460
+
461
+ /** Emits execute-time export assignments, using getters for imported cells to preserve live reads. */
462
+ function renderExportList(specifiers: readonly ExportSpecifier[], model: NativeModuleModel): string {
463
+ return specifiers
464
+ .map((specifier) => {
465
+ const local = moduleExportName(specifier.local)
466
+ const exported = moduleExportName(specifier.exported)
467
+ const imported = model.importBindingsByLocal.get(local)
468
+ return imported
469
+ ? `Object.defineProperty(exports,${JSON.stringify(exported)},{enumerable:true,get:function(){return ${importedExpression(imported)}}});`
470
+ : ''
471
+ })
472
+ .join('')
473
+ }
474
+
475
+ /** Publishes top-level initialized variables at their original initialization point. */
476
+ function publishVariableInitializers(
477
+ editor: RolldownMagicString,
478
+ declaration: VariableDeclaration,
479
+ exportNamesByLocal: ReadonlyMap<string, readonly string[]>
480
+ ): void {
481
+ for (const declarator of declaration.declarations) {
482
+ if (declarator.id.type !== 'Identifier') {
483
+ const exported = bindingNames(declarator.id).filter((name) => exportNamesByLocal.has(name))
484
+ if (exported.length > 0) throw unsupported('native chunk', 'exported destructuring declaration')
485
+ continue
486
+ }
487
+ const names = exportNamesByLocal.get(declarator.id.name) ?? []
488
+ if (names.length === 0) continue
489
+ if (declarator.init) {
490
+ editor.prependLeft(declarator.init.start, exportAssignmentPrefix(names))
491
+ } else {
492
+ editor.appendRight(declarator.end, `=${exportAssignmentExpression(names, 'void 0')}`)
493
+ }
494
+ }
495
+ }
496
+
497
+ /** Publishes declaration values that become initialized only after their declaration executes. */
498
+ function publishAfter(
499
+ editor: RolldownMagicString,
500
+ end: number,
501
+ local: string,
502
+ exportNamesByLocal: ReadonlyMap<string, readonly string[]>
503
+ ): void {
504
+ const names = exportNamesByLocal.get(local) ?? []
505
+ if (names.length > 0) editor.appendRight(end, `;${exportAssignmentExpression(names, local)};`)
506
+ }
507
+
508
+ function exportAssignmentPrefix(names: readonly string[]): string {
509
+ return names.reduce((prefix, name) => `exports[${JSON.stringify(name)}]=${prefix}`, '')
510
+ }
511
+
512
+ function exportAssignmentExpression(names: readonly string[], value: string): string {
513
+ return `${exportAssignmentPrefix(names)}${value}`
514
+ }
515
+
516
+ /**
517
+ * Imported functions are ESM references, not namespace methods. A direct call must therefore discard the generated
518
+ * namespace receiver with `(0, value)()`. The same comma expression preserves unbound optional calls and tag invocation;
519
+ * constructors remain plain references because `new` does not derive a receiver from a member expression.
520
+ */
521
+ function importedReferenceContext(
522
+ node: Extract<Node, { type: 'Identifier' }>,
523
+ parent: Node | null,
524
+ ancestors: readonly Node[]
525
+ ): ImportedReferenceContext {
526
+ let expression: Node = node
527
+ let semanticParent = parent
528
+ let ancestorIndex = ancestors.length - 1
529
+ while (semanticParent?.type === 'ParenthesizedExpression' && semanticParent.expression === expression) {
530
+ expression = semanticParent
531
+ ancestorIndex -= 1
532
+ semanticParent = ancestors[ancestorIndex] ?? null
533
+ }
534
+ if (semanticParent?.type === 'CallExpression' && semanticParent.callee === expression) return 'unbound'
535
+ if (semanticParent?.type === 'NewExpression' && semanticParent.callee === expression) return 'plain'
536
+ if (semanticParent?.type === 'TaggedTemplateExpression' && semanticParent.tag === expression) return 'unbound'
537
+ return 'plain'
538
+ }
539
+
540
+ function importedExpression(binding: ImportBinding): string {
541
+ return binding.imported === null ? binding.namespace : memberExpression(binding.namespace, binding.imported)
542
+ }
543
+
544
+ function memberExpression(object: string, property: string): string {
545
+ return /^[$A-Z_a-z][$\w]*$/.test(property) ? `${object}.${property}` : `${object}[${JSON.stringify(property)}]`
546
+ }
547
+
548
+ function getImportedCapsule(
549
+ fileName: string,
550
+ reference: string,
551
+ chunks: Readonly<Record<string, Rolldown.RenderedChunk>>
552
+ ): Rolldown.RenderedChunk | undefined {
553
+ if (!reference.startsWith('./') && !reference.startsWith('../')) return undefined
554
+ const imported = chunks[resolvePhysicalChunkReference(fileName, reference)]
555
+ return imported && getWxEntryRole(imported) === 'capsule' ? imported : undefined
556
+ }
557
+
558
+ /**
559
+ * Renders the dependency header in original import order.
560
+ *
561
+ * Side-effect imports always remain standalone `require` calls. Ordinary value imports share one required namespace per final
562
+ * import declaration, while cross-domain capsule imports synchronously ask SystemJS for the namespace under its package-neutral
563
+ * logical ID. Physical package placement remains the transport's responsibility.
564
+ */
565
+ function renderImports(model: NativeModuleModel): string {
566
+ return model.imports
567
+ .map((importModel) => {
568
+ if (importModel.capsuleBinding) {
569
+ const { imported, local, logicalId } = importModel.capsuleBinding
570
+ const namespace = `global.System.importSync(${JSON.stringify(logicalId)})`
571
+ return `var ${local}=${memberExpression(namespace, imported)};`
70
572
  }
573
+
574
+ const requireCall = `require(${JSON.stringify(importModel.reference)})`
575
+ if (importModel.bindings.length === 0) return `${requireCall};`
576
+ const importedValue =
577
+ importModel.interop === 'default'
578
+ ? `${model.defaultInterop}(${requireCall})`
579
+ : importModel.interop === 'namespace'
580
+ ? `${model.namespaceInterop}(${requireCall})`
581
+ : requireCall
582
+ return `var ${importModel.namespace}=${importedValue};`
583
+ })
584
+ .join('')
585
+ }
586
+
587
+ /** Selects Babel-compatible wrapping for default and namespace imports from CommonJS values. */
588
+ function importInterop(declaration: ImportDeclaration): ImportInterop {
589
+ if (declaration.specifiers.some((specifier) => specifier.type === 'ImportNamespaceSpecifier')) return 'namespace'
590
+ if (declaration.specifiers.some((specifier) => specifier.type === 'ImportDefaultSpecifier')) return 'default'
591
+ return 'none'
592
+ }
593
+
594
+ /** Emits interop helpers only when an analyzed import needs them. */
595
+ function renderInteropHelpers(model: NativeModuleModel): string {
596
+ const kinds = new Set(model.imports.map(({ interop }) => interop))
597
+ const defaultHelper = kinds.has('default')
598
+ ? `function ${model.defaultInterop}(value){return value&&value.__esModule?value:{default:value};}`
599
+ : ''
600
+ const namespaceHelper = kinds.has('namespace')
601
+ ? `function ${model.namespaceInterop}(value){if(value&&value.__esModule)return value;var namespace={default:value};if(value!==null&&(typeof value==="object"||typeof value==="function")){for(var key in value)if(key!=="default"&&Object.prototype.hasOwnProperty.call(value,key))Object.defineProperty(namespace,key,{enumerable:true,get:function(key){return function(){return value[key]}}(key)});}return namespace;}`
602
+ : ''
603
+ return `${defaultHelper}${namespaceHelper}`
604
+ }
605
+
606
+ function requirePlainImport(declaration: ImportDeclaration, filename: string): void {
607
+ if (declaration.phase || declaration.attributes.length > 0 || declaration.importKind === 'type') {
608
+ throw unsupported(filename, 'import phases, attributes, or type-only imports')
609
+ }
610
+ }
611
+
612
+ /** Collects every occupied identifier before helper allocation. */
613
+ function collectIdentifierNames(program: Program): Set<string> {
614
+ const names = new Set<string>()
615
+ walk(program, {
616
+ enter(node) {
617
+ if (node.type === 'Identifier') names.add(node.name)
71
618
  }
619
+ })
620
+ return names
621
+ }
622
+
623
+ function takeGeneratedName(base: string, used: Set<string>): string {
624
+ let suffix = 0
625
+ let candidate = base
626
+ while (used.has(candidate)) {
627
+ suffix += 1
628
+ candidate = `${base}${suffix}`
72
629
  }
630
+ used.add(candidate)
631
+ return candidate
73
632
  }
74
633
 
75
- /** Creates one synchronous lookup through the canonical output chunk ID. */
76
- function createSyncImport(chunkId: string): types.CallExpression {
77
- return types.callExpression(
78
- types.memberExpression(
79
- types.memberExpression(types.identifier('global'), types.identifier('System')),
80
- types.identifier('importSync')
81
- ),
82
- [types.stringLiteral(chunkId)]
634
+ function moduleExportName(name: ModuleExportName): string {
635
+ return name.type === 'Literal' ? String(name.value) : name.name
636
+ }
637
+
638
+ function declarationBindingNames(declaration: Node, filename: string): string[] {
639
+ switch (declaration.type) {
640
+ case 'VariableDeclaration':
641
+ return declaration.declarations.flatMap(({ id }) => bindingNames(id))
642
+ case 'FunctionDeclaration':
643
+ case 'ClassDeclaration':
644
+ if (declaration.id) return [declaration.id.name]
645
+ throw unsupported(filename, 'anonymous declaration export')
646
+ default:
647
+ throw unsupported(filename, `declaration export ${declaration.type}`)
648
+ }
649
+ }
650
+
651
+ function bindingNames(pattern: Node): string[] {
652
+ switch (pattern.type) {
653
+ case 'Identifier':
654
+ return [pattern.name]
655
+ case 'AssignmentPattern':
656
+ return bindingNames(pattern.left)
657
+ case 'ArrayPattern':
658
+ return pattern.elements.flatMap((element) => (element ? bindingNames(element) : []))
659
+ case 'ObjectPattern':
660
+ return pattern.properties.flatMap((property) =>
661
+ property.type === 'Property' ? bindingNames(property.value) : bindingNames(property.argument)
662
+ )
663
+ case 'RestElement':
664
+ return bindingNames(pattern.argument)
665
+ default:
666
+ return []
667
+ }
668
+ }
669
+
670
+ function isThisBoundary(node: Node): boolean {
671
+ return (
672
+ node.type === 'FunctionDeclaration' ||
673
+ node.type === 'FunctionExpression' ||
674
+ node.type === 'ClassDeclaration' ||
675
+ node.type === 'ClassExpression'
83
676
  )
84
677
  }
678
+
679
+ function unsupported(filename: string, construct: string): Error {
680
+ return new Error(`Unsupported final Rolldown native chunk ${filename}: ${construct}`)
681
+ }
682
+
683
+ function createSourceMap(editor: RolldownMagicString, filename: string): ExistingRawSourceMap {
684
+ const generated = editor.generateMap({
685
+ file: filename,
686
+ hires: 'boundary',
687
+ includeContent: true,
688
+ source: filename
689
+ })
690
+ return {
691
+ version: generated.version,
692
+ file: generated.file,
693
+ sources: generated.sources,
694
+ sourcesContent: generated.sourcesContent,
695
+ names: generated.names,
696
+ mappings: generated.mappings,
697
+ ...(generated.x_google_ignoreList ? { x_google_ignoreList: generated.x_google_ignoreList } : {})
698
+ }
699
+ }