vite-plugin-taro 0.6.14 → 0.6.16

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 (29) hide show
  1. package/README.en.md +3 -0
  2. package/README.md +3 -0
  3. package/dist/node/plugins/conditional/conditional-directives.js +3 -5
  4. package/dist/node/plugins/h5/create-stencil-client-adapter.js +5 -4
  5. package/dist/node/plugins/h5/plugins.d.ts +7 -0
  6. package/dist/node/plugins/h5/plugins.js +20 -15
  7. package/dist/node/plugins/wx/native/native-component-interface.js +2 -1
  8. package/dist/node/plugins/wx/output/templates.d.ts +107 -0
  9. package/dist/node/plugins/wx/output/templates.js +12 -12
  10. package/dist/node/plugins/wx/render/native.js +8 -14
  11. package/dist/node/plugins/wx/render/system-js/string-editor.js +2 -1
  12. package/dist/node/plugins/wx/render/system-js/system-js.js +22 -36
  13. package/dist/node/plugins/wx/styles/plugins.d.ts +3 -0
  14. package/dist/node/plugins/wx/styles/plugins.js +20 -17
  15. package/dist/node/utils/oxc-transform.js +1 -2
  16. package/dist/node/utils/transform.js +1 -4
  17. package/package.json +27 -53
  18. package/src/node/plugins/conditional/conditional-directives.ts +4 -6
  19. package/src/node/plugins/h5/create-stencil-client-adapter.ts +5 -4
  20. package/src/node/plugins/h5/plugins.ts +21 -16
  21. package/src/node/plugins/wx/native/native-component-interface.ts +3 -2
  22. package/src/node/plugins/wx/output/templates.ts +13 -13
  23. package/src/node/plugins/wx/render/native.ts +8 -13
  24. package/src/node/plugins/wx/render/system-js/string-editor.ts +3 -2
  25. package/src/node/plugins/wx/render/system-js/system-js.ts +22 -32
  26. package/src/node/plugins/wx/styles/plugins.ts +25 -18
  27. package/src/node/utils/oxc-transform.ts +1 -2
  28. package/src/node/utils/transform.ts +1 -5
  29. package/src/node/babel-plugins.d.ts +0 -20
package/README.en.md CHANGED
@@ -1,7 +1,10 @@
1
1
  # VPT
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/vite-plugin-taro.svg)](https://www.npmjs.com/package/vite-plugin-taro)
4
+ [![npm downloads](https://img.shields.io/npm/dm/vite-plugin-taro.svg)](https://www.npmjs.com/package/vite-plugin-taro)
4
5
  ![Vite](https://img.shields.io/npm/dependency-version/vite-plugin-taro/peer/vite?label=Vite)
6
+ [![CI](https://github.com/sep2/vite-plugin-taro/actions/workflows/windows.yml/badge.svg?branch=main)](https://github.com/sep2/vite-plugin-taro/actions/workflows/windows.yml)
7
+ [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/sep2/vite-plugin-taro/actions/workflows/coverage.yml)
5
8
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
9
 
7
10
  [简体中文](README.md) | English
package/README.md CHANGED
@@ -1,7 +1,10 @@
1
1
  # VPT
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/vite-plugin-taro.svg)](https://www.npmjs.com/package/vite-plugin-taro)
4
+ [![npm downloads](https://img.shields.io/npm/dm/vite-plugin-taro.svg)](https://www.npmjs.com/package/vite-plugin-taro)
4
5
  ![Vite](https://img.shields.io/npm/dependency-version/vite-plugin-taro/peer/vite?label=Vite)
6
+ [![CI](https://github.com/sep2/vite-plugin-taro/actions/workflows/windows.yml/badge.svg?branch=main)](https://github.com/sep2/vite-plugin-taro/actions/workflows/windows.yml)
7
+ [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/sep2/vite-plugin-taro/actions/workflows/coverage.yml)
5
8
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
9
 
7
10
  简体中文 | [English](README.en.md)
@@ -22,7 +22,7 @@ function isConditionalDirectiveSource(id) {
22
22
  }
23
23
  /** Keeps only the source branches active for one build target. */
24
24
  function transformConditionalDirectives(code, target) {
25
- const lines = code.match(/[^\n]*(?:\n|$)/g) ?? [];
25
+ const lines = Array.from(code.matchAll(/[^\n]*(?:\n|$)/g), (match) => match[0]);
26
26
  const frames = [];
27
27
  let transformed = '';
28
28
  for (const line of lines) {
@@ -64,15 +64,13 @@ function parseDirective(line) {
64
64
  if (!match) {
65
65
  return;
66
66
  }
67
+ // The regular expression constrains this capture to the complete directive union.
67
68
  const name = match[1];
68
69
  if (name === 'if' || name === 'elif') {
69
70
  throw new Error(`vpt no longer supports #${name}; use #ifdef, #ifndef, or #else`);
70
71
  }
71
- if (name !== 'ifdef' && name !== 'ifndef' && name !== 'else' && name !== 'endif') {
72
- return;
73
- }
74
72
  return {
75
73
  name,
76
- target: match[2]?.replace(/\*\/$/, '').trim() ?? ''
74
+ target: match[2].replace(/\*\/$/, '').trim()
77
75
  };
78
76
  }
@@ -69,7 +69,6 @@ function createStencilVisitor(editor) {
69
69
  // generated-AST type safety, so a broad match could silently alter unrelated runtime
70
70
  // behavior when Stencil changes its implementation.
71
71
  const [style, query] = node.arguments;
72
- const selector = query?.type === 'CallExpression' ? query.arguments[0] : undefined;
73
72
  if (node.arguments.length !== 2 ||
74
73
  style?.type !== 'Identifier' ||
75
74
  style.name !== 'styleElm' ||
@@ -79,9 +78,11 @@ function createStencilVisitor(editor) {
79
78
  query.callee.object.type !== 'Identifier' ||
80
79
  query.callee.object.name !== 'styleContainerNode' ||
81
80
  query.callee.property.type !== 'Identifier' ||
82
- query.callee.property.name !== 'querySelector' ||
83
- selector?.type !== 'Literal' ||
84
- selector.value !== 'link') {
81
+ query.callee.property.name !== 'querySelector') {
82
+ return;
83
+ }
84
+ const [selector] = query.arguments;
85
+ if (selector?.type !== 'Literal' || selector.value !== 'link') {
85
86
  return;
86
87
  }
87
88
  // Replace only the second argument expression. Preserving the surrounding Stencil
@@ -12,3 +12,10 @@ export declare function createH5TargetPlugins(options: VptOptions): PluginOption
12
12
  * neither branch and remain entirely outside Babel's parser and generator.
13
13
  */
14
14
  export declare const h5TaroApiTransformCodeFilter: RegExp;
15
+ /** Babel preset body shared by Rolldown's filtered adapter and direct semantic tests. */
16
+ export declare function h5TaroApiPreset(): {
17
+ plugins: [string, {
18
+ packageName: string;
19
+ definition: any;
20
+ }][];
21
+ };
@@ -1,3 +1,4 @@
1
+ import { createRequire } from 'node:module';
1
2
  import babel, { defineRolldownBabelPreset } from '@rolldown/plugin-babel';
2
3
  import { WeappTailwindcss } from 'weapp-tailwindcss/vite';
3
4
  import { esTarget } from '../../utils/constant.js';
@@ -126,24 +127,28 @@ function createH5IndexHtmlTags() {
126
127
  * neither branch and remain entirely outside Babel's parser and generator.
127
128
  */
128
129
  export const h5TaroApiTransformCodeFilter = /virtual:taro\/api|\baria[A-Z]/;
130
+ /** Babel preset body shared by Rolldown's filtered adapter and direct semantic tests. */
131
+ export function h5TaroApiPreset() {
132
+ // Resolve Taro's private transform from the H5 package that owns it instead of promoting it into VPT's Babel graph.
133
+ const h5PlatformRequire = createRequire(packageRequire.resolve('@tarojs/plugin-platform-h5/package.json'));
134
+ const transformTaroApiPath = h5PlatformRequire.resolve('babel-plugin-transform-taroapi');
135
+ const definition = packageRequire(packageRequire.resolve('@tarojs/plugin-platform-h5/dist/definition.json'));
136
+ return {
137
+ plugins: [
138
+ [
139
+ transformTaroApiPath,
140
+ {
141
+ packageName: clientTaroApiId,
142
+ definition
143
+ }
144
+ ]
145
+ ]
146
+ };
147
+ }
129
148
  /** Creates a filterable Babel preset containing Taro's upstream, scope-aware API transform. */
130
149
  function createH5TaroApiPreset() {
131
- const transformTaroApiPath = packageRequire.resolve('babel-plugin-transform-taroapi');
132
- const definition = packageRequire(packageRequire.resolve('@tarojs/plugin-platform-h5/dist/definition.json'));
133
150
  return defineRolldownBabelPreset({
134
- preset: function h5TaroApiPreset() {
135
- return {
136
- plugins: [
137
- [
138
- transformTaroApiPath,
139
- {
140
- packageName: clientTaroApiId,
141
- definition
142
- }
143
- ]
144
- ]
145
- };
146
- },
151
+ preset: h5TaroApiPreset,
147
152
  rolldown: {
148
153
  // @rolldown/plugin-babel can lift a preset filter into its native transform hook. The Taro plugin must be
149
154
  // nested in this preset rather than passed through Babel's top-level `plugins`: explicit plugins may apply
@@ -141,8 +141,9 @@ function readInterfaceFields(typeArguments, declaredInterfaces, buildError) {
141
141
  if (typeArguments.params.length !== 1) {
142
142
  throw buildError('Native component interface must be one TypeScript object type');
143
143
  }
144
+ // The length check above proves the one parameter exists; ESTree models the array element as optional generically.
144
145
  const firstType = typeArguments.params[0];
145
- const members = firstType ? resolveInterfaceMembers(firstType, declaredInterfaces) : undefined;
146
+ const members = resolveInterfaceMembers(firstType, declaredInterfaces);
146
147
  if (!members) {
147
148
  throw buildError('Native component interface must be inline or declared in the same module');
148
149
  }
@@ -14,4 +14,111 @@ export declare function createTemplateAssets({ bundle, options, subpackages, nat
14
14
  nativeComponents: readonly NativeComponentRegistration[];
15
15
  isProduction: boolean;
16
16
  }): Rolldown.EmittedAsset[];
17
+ /**
18
+ * Adapts Taro's stock template builder to the WXML half of WX App wrapping without changing its recursive renderer.
19
+ *
20
+ * End-to-end contract
21
+ * -------------------
22
+ * React and Taro retain one in-memory ownership tree:
23
+ *
24
+ * App React root
25
+ * -> App host records
26
+ * -> vpt_page_outlet host at App {children}
27
+ * -> independently scheduled Taro Page roots
28
+ *
29
+ * The patched WX document makes the singleton App host a TaroRootElement. App host mutations therefore batch under app.*
30
+ * and fan out to every mounted native Page. Each Page root remains its own TaroRootElement and emits only page.*. The React
31
+ * host renderer marks the outlet and its App ancestors with an ordinary compact `vo` prop after each commit, while patched
32
+ * hydrate() stops at vpt_page_outlet. Page roots remain attached for React Context, lifecycle, events, removal, refs, effects,
33
+ * and HMR without entering app data.
34
+ *
35
+ * Build-time output
36
+ * -----------------
37
+ * createTemplateAssets asks one builder for shared base.wxml, utils.wxs, comp.wxml, custom-wrapper.wxml, and each Page WXML.
38
+ * This adapter specializes the products that own the native join:
39
+ *
40
+ * - shared base.wxml receives branch-local slot forwarding at comp boundaries plus vpt_fragment and vpt_page_outlet
41
+ * template definitions;
42
+ * - each Page WXML replaces Taro's root:root entry with one generic comp bound to app and one caller-owned taro_tmpl bound
43
+ * to page as that component's default slot.
44
+ *
45
+ * utils.wxs remains Taro's normal compact-node dispatcher. comp.wxml and custom-wrapper.wxml retain Taro's generic compact
46
+ * rendering contracts; their JSON companions register recursive boundaries and native components, while runtime configs
47
+ * retain standard event dispatch. CustomWrapper keeps Taro's Page-local ownership model and is not made slot-transparent
48
+ * for App wrapping.
49
+ * There is one shared template namespace, no Page-specific base file, no App/Page mode property, and no Page object threaded
50
+ * through App template data.
51
+ *
52
+ * First native Page
53
+ * -----------------
54
+ * createPageConfig starts the native Page with:
55
+ *
56
+ * app = { nn: 'vpt_fragment', cn: [] }
57
+ * page = { cn: [] }
58
+ *
59
+ * nn lets unchanged comp dispatch one input object even though App JSX may produce one or many root hosts. The record
60
+ * is WXML-only rather than a Taro host, so it needs no sid. After React commits the Page root below the outlet, the framework
61
+ * queues a lazy hydrate(AppRoot).cn value beside the Page root's already-pending page.* payloads. Taro drains both through the
62
+ * native Page's existing first setData, making App wrapping and Page content appear atomically.
63
+ *
64
+ * Native WXML execution
65
+ * ---------------------
66
+ * Data/slot ownership and named-template ownership intentionally travel through different scopes:
67
+ *
68
+ * Page WXML (owns app, page, Page eh, and Page-content light DOM)
69
+ * -> <comp i="{{app}}"> (crosses into a virtual custom-component scope)
70
+ * -> comp.wxml (owns App eh, imports unchanged utils.wxs and shared base.wxml)
71
+ * -> tmpl_0_vpt_fragment (iterates the real App compact roots in app.cn)
72
+ * -> stock Taro templates (render App hosts and recurse through their cn arrays)
73
+ * -> native recursion boundary <comp ...>
74
+ * -> forwards <slot /> only when this compact subtree root has i.vo
75
+ * -> tmpl_0_vpt_page_outlet at React's exact {children} position
76
+ * -> <slot />
77
+ * -> caller-owned <template is="taro_tmpl" data="{{root:page}}" />
78
+ * -> stock Taro templates render this native Page's page.cn records
79
+ *
80
+ * The virtual comp and both private templates add no native layout node. `vo` is part of the existing compact node i, not a
81
+ * component property or template context. App events execute through comp.eh; slotted Page events retain the native Page's
82
+ * eh. Both resolve the original Taro sid through the same event source.
83
+ *
84
+ * Named-template scope
85
+ * --------------------
86
+ * Slots transfer caller-owned light DOM only. They do not transfer the caller's named-template table or WXS modules. App
87
+ * dispatch runs inside comp.wxml, so vpt_fragment and vpt_page_outlet must live in base.wxml imported by that component.
88
+ * Putting those definitions in buildPageTemplate produces Page WXML that compiles, but component runtime dispatch fails with
89
+ * `Template tmpl_0_vpt_fragment not found`. Shared base.wxml makes the names visible in the root and every recursive
90
+ * component scope and emits them once rather than once per Page.
91
+ *
92
+ * Projection-spine ownership
93
+ * --------------------------
94
+ * React's host renderer runs after the final commit tree exists and caches the outlet-to-root Taro host ancestor array. It
95
+ * skips the unchanged root-side suffix, gives old leaf-side nodes the ordinary host prop vo=false, and gives new ones vo=true.
96
+ * Taro's existing lazy structural hydration runs later and therefore serializes those props without projection-specific
97
+ * scheduler or hydrate behavior. If React replaces the outlet host while moving it, the renderer finds the unique new marker
98
+ * once and caches its new ancestor array. At each depth-reset comp boundary, i.vo makes forwarding an O(1) local decision;
99
+ * WXML never searches descendants, and Page trees instantiate no unnamed slot.
100
+ *
101
+ * Steady-state updates and navigation
102
+ * -----------------------------------
103
+ * A page.* setData updates only the caller-owned Page template inside the slot. app is not passed through that template and
104
+ * comp.i does not change, so Page updates cannot invalidate App recursion. Ordinary app.* payloads and outlet-spine marker
105
+ * changes remain in Taro's granular batch before it fans out to every retained native Page. Adding or removing a React Page
106
+ * root mutates the outlet only in memory, and the runtime suppresses that marker's native child update. A newly pushed Page
107
+ * receives the latest complete App snapshot in its initial batch, while existing and hidden Pages require no navigation
108
+ * synchronization.
109
+ *
110
+ * Method responsibilities
111
+ * -----------------------
112
+ * 1. buildBaseTemplate preserves stock host templates, makes each depth-reset comp boundary slot-transparent through i.vo,
113
+ * and adds the two private definitions to the shared namespace.
114
+ * 2. buildPageTemplate owns only the Page boundary: app binding, page binding, and the single Page-content slot.
115
+ * 3. buildXScript delegates unchanged because routing metadata already travels inside compact node i.
116
+ * 4. buildBaseComponentTemplate delegates unchanged so recursive comp remains generic and feature-independent.
117
+ * 5. buildCustomComponentTemplate delegates unchanged so CustomWrapper renders compact children in its own native scope.
118
+ *
119
+ * H5 never calls this WX output builder. Its App continues to receive ordinary Fragment children and none of these native
120
+ * data roots, templates, custom-component boundaries, or slot rules enter the browser build.
121
+ */
122
+ /** Replaces one pinned Taro fragment and rejects absent or duplicated upstream contracts. */
123
+ export declare function replaceExactlyOnce(source: string, current: string, replacement: string, description: string): string;
17
124
  export {};
@@ -142,28 +142,28 @@ export function createTemplateAssets({ bundle, options, subpackages, nativeCompo
142
142
  * H5 never calls this WX output builder. Its App continues to receive ordinary Fragment children and none of these native
143
143
  * data roots, templates, custom-component boundaries, or slot rules enter the browser build.
144
144
  */
145
+ /** Replaces one pinned Taro fragment and rejects absent or duplicated upstream contracts. */
146
+ export function replaceExactlyOnce(source, current, replacement, description) {
147
+ const firstIndex = source.indexOf(current);
148
+ const duplicateIndex = firstIndex === -1 ? -1 : source.indexOf(current, firstIndex + current.length);
149
+ if (firstIndex === -1 || duplicateIndex !== -1) {
150
+ throw new Error(`Expected one ${description}, found ${firstIndex === -1 ? 0 : 'multiple'}`);
151
+ }
152
+ return `${source.slice(0, firstIndex)}${replacement}${source.slice(firstIndex + current.length)}`;
153
+ }
145
154
  function createTemplateBuilder() {
146
155
  const platform = new WxPlatform({
147
156
  helper: {
148
157
  recursiveMerge
149
158
  },
159
+ // c8 cannot observe callbacks Taro accepts but intentionally never invokes.
160
+ /* c8 ignore next */
150
161
  modifyWebpackChain() { },
162
+ /* c8 ignore next */
151
163
  registerPlatform() { }
152
164
  }, {}, {});
153
165
  platform.modifyTemplate({});
154
166
  const taroTemplateBuilder = platform.template;
155
- /**
156
- * Replaces one pinned Taro fragment so an upstream template change cannot silently break the coordinated
157
- * framework/runtime/WXML boundary or partially apply the feature.
158
- */
159
- function replaceExactlyOnce(source, current, replacement, description) {
160
- const firstIndex = source.indexOf(current);
161
- const duplicateIndex = firstIndex === -1 ? -1 : source.indexOf(current, firstIndex + current.length);
162
- if (firstIndex === -1 || duplicateIndex !== -1) {
163
- throw new Error(`Expected one ${description}, found ${firstIndex === -1 ? 0 : 'multiple'}`);
164
- }
165
- return `${source.slice(0, firstIndex)}${replacement}${source.slice(firstIndex + current.length)}`;
166
- }
167
167
  return {
168
168
  buildBaseTemplate: (componentConfig) => {
169
169
  const source = taroTemplateBuilder.buildTemplate(componentConfig);
@@ -395,7 +395,7 @@ function importedReferenceContext(node, parent, ancestors) {
395
395
  while (semanticParent?.type === 'ParenthesizedExpression' && semanticParent.expression === expression) {
396
396
  expression = semanticParent;
397
397
  ancestorIndex -= 1;
398
- semanticParent = ancestors[ancestorIndex] ?? null;
398
+ semanticParent = ancestors[ancestorIndex];
399
399
  }
400
400
  if (semanticParent?.type === 'CallExpression' && semanticParent.callee === expression)
401
401
  return 'unbound';
@@ -492,18 +492,13 @@ function takeGeneratedName(base, used) {
492
492
  function moduleExportName(name) {
493
493
  return name.type === 'Literal' ? String(name.value) : name.name;
494
494
  }
495
- function declarationBindingNames(declaration, filename) {
496
- switch (declaration.type) {
497
- case 'VariableDeclaration':
498
- return declaration.declarations.flatMap(({ id }) => bindingNames(id));
499
- case 'FunctionDeclaration':
500
- case 'ClassDeclaration':
501
- if (declaration.id)
502
- return [declaration.id.name];
503
- throw unsupported(filename, 'anonymous declaration export');
504
- default:
505
- throw unsupported(filename, `declaration export ${declaration.type}`);
495
+ function declarationBindingNames(declaration, _filename) {
496
+ if (declaration.type === 'VariableDeclaration') {
497
+ return declaration.declarations.flatMap(({ id }) => bindingNames(id));
506
498
  }
499
+ // Final JavaScript named-declaration exports can otherwise contain only named functions or classes.
500
+ const namedDeclaration = declaration;
501
+ return [namedDeclaration.id.name];
507
502
  }
508
503
  function bindingNames(pattern) {
509
504
  switch (pattern.type) {
@@ -543,7 +538,6 @@ function createSourceMap(editor, filename) {
543
538
  sources: generated.sources,
544
539
  sourcesContent: generated.sourcesContent,
545
540
  names: generated.names,
546
- mappings: generated.mappings,
547
- ...(generated.x_google_ignoreList ? { x_google_ignoreList: generated.x_google_ignoreList } : {})
541
+ mappings: generated.mappings
548
542
  };
549
543
  }
@@ -87,7 +87,8 @@ export class StringEditor {
87
87
  return output;
88
88
  }
89
89
  #renderInsertions(position) {
90
+ // Callers enumerate this map's keys, so the matching insertion journal is guaranteed to exist.
90
91
  const insertions = this.#insertions.get(position);
91
- return insertions ? `${insertions.prepend.join('')}${insertions.append.join('')}` : '';
92
+ return `${insertions.prepend.join('')}${insertions.append.join('')}`;
92
93
  }
93
94
  }
@@ -91,18 +91,19 @@ function analyzeModule(program, filename) {
91
91
  outerBindings.add(name);
92
92
  });
93
93
  break;
94
- case 'FunctionDeclaration':
95
- if (!node.id || !node.body || node.declare) {
96
- throw unsupported(filename, 'anonymous or ambient function declaration');
97
- }
94
+ case 'FunctionDeclaration': {
95
+ // Anonymous and ambient functions cannot occur as direct declarations in final JavaScript chunks.
96
+ const identifier = node.id;
98
97
  functions.push(node);
99
- outerBindings.add(node.id.name);
98
+ outerBindings.add(identifier.name);
100
99
  break;
101
- case 'ClassDeclaration':
102
- if (!node.id || node.declare)
103
- throw unsupported(filename, 'anonymous or ambient class declaration');
104
- outerBindings.add(node.id.name);
100
+ }
101
+ case 'ClassDeclaration': {
102
+ // Anonymous and ambient classes can occur only in source forms rejected before final chunk rendering.
103
+ const identifier = node.id;
104
+ outerBindings.add(identifier.name);
105
105
  break;
106
+ }
106
107
  }
107
108
  }
108
109
  requireNoDirectEval(hasDirectEval, filename);
@@ -143,8 +144,6 @@ function collectImport(declaration, dependencyBySource, importBindings, outerBin
143
144
  dependency.imports.push({ imported: null, local: specifier.local.name });
144
145
  break;
145
146
  case 'ImportSpecifier':
146
- if (specifier.importKind === 'type')
147
- throw unsupported(filename, 'type-only import specifier');
148
147
  dependency.imports.push({ imported: moduleExportName(specifier.imported), local: specifier.local.name });
149
148
  break;
150
149
  }
@@ -159,8 +158,6 @@ function collectExports(declaration, exportNamesByLocal, filename) {
159
158
  throw unsupported(filename, 'declaration exports, re-exports, export attributes, or type-only exports');
160
159
  }
161
160
  for (const specifier of declaration.specifiers) {
162
- if (specifier.exportKind === 'type')
163
- throw unsupported(filename, 'type-only export specifier');
164
161
  const local = moduleExportName(specifier.local);
165
162
  const exported = moduleExportName(specifier.exported);
166
163
  // Aliases are accumulated in declaration order because nested live-binding calls must publish in the same order as Babel.
@@ -205,11 +202,8 @@ function applyProgramEdits(editor, model) {
205
202
  }
206
203
  /** Changes module variables into assignments to declaration-scope cells shared with setters and hoisted functions. */
207
204
  function transformTopLevelVariables(editor, declaration, exportBinding, exportNamesByLocal) {
208
- const [first] = declaration.declarations;
209
- if (!first) {
210
- editor.remove(declaration.start, declaration.end);
211
- return;
212
- }
205
+ // ESTree variable declarations always contain at least one declarator.
206
+ const first = declaration.declarations[0];
213
207
  // `const value = init` becomes `(value = init)`: the declaration cell itself is emitted once in the registration scope.
214
208
  editor.overwrite(declaration.start, first.start, '(');
215
209
  editor.appendLeft(statementTerminatorStart(editor.original, declaration.end), ')');
@@ -217,11 +211,8 @@ function transformTopLevelVariables(editor, declaration, exportBinding, exportNa
217
211
  }
218
212
  /** Hoists module-scoped var declarations nested in statements without changing their control-flow position. */
219
213
  function transformNestedHoistedVariables(editor, declaration, isForIterationBinding, exportBinding, exportNamesByLocal) {
220
- const [first] = declaration.declarations;
221
- if (!first) {
222
- editor.remove(declaration.start, declaration.end);
223
- return;
224
- }
214
+ // ESTree variable declarations always contain at least one declarator.
215
+ const first = declaration.declarations[0];
225
216
  editor.overwrite(declaration.start, first.start, isForIterationBinding ? '' : '(');
226
217
  if (!isForIterationBinding)
227
218
  editor.appendLeft(statementTerminatorStart(editor.original, declaration.end), ')');
@@ -237,7 +228,8 @@ function transformVariableInitializers(editor, declaration, exportBinding, expor
237
228
  continue;
238
229
  if (declarator.id.type === 'Identifier') {
239
230
  // Nested calls publish every alias while preserving the initializer's completion value.
240
- const names = exportNamesByLocal.get(declarator.id.name) ?? [];
231
+ // exportedBindings above proves this identifier owns at least one public alias.
232
+ const names = exportNamesByLocal.get(declarator.id.name);
241
233
  editor.prependLeft(declarator.start, exportExpressionPrefix(exportBinding, names, ''));
242
234
  editor.appendRight(declarator.end, exportExpressionSuffix(names));
243
235
  continue;
@@ -248,10 +240,10 @@ function transformVariableInitializers(editor, declaration, exportBinding, expor
248
240
  }
249
241
  /** Turns a class declaration into the execute-time assignment required by cyclic SystemJS linking. */
250
242
  function transformTopLevelClass(editor, declaration, exportBinding, exportNamesByLocal) {
251
- if (!declaration.id)
252
- return;
253
- const names = exportNamesByLocal.get(declaration.id.name) ?? [];
254
- editor.prependLeft(declaration.start, exportExpressionPrefix(exportBinding, names, `${declaration.id.name}=`));
243
+ // Anonymous classes can occur only in default exports, which analysis rejects before this pass.
244
+ const identifier = declaration.id;
245
+ const names = exportNamesByLocal.get(identifier.name) ?? [];
246
+ editor.prependLeft(declaration.start, exportExpressionPrefix(exportBinding, names, `${identifier.name}=`));
255
247
  editor.appendRight(declaration.end, exportExpressionSuffix(names));
256
248
  }
257
249
  /** Rewrites expression-level module semantics in one scope-aware O(n) traversal. */
@@ -383,7 +375,7 @@ function createRegistrationShell(model, options) {
383
375
  }
384
376
  /** Produces declaration-time exports for functions and uninitialized variables, matching cyclic ESM availability. */
385
377
  function renderEarlyExports(model) {
386
- const functionNames = new Set(model.functions.flatMap((declaration) => (declaration.id ? [declaration.id.name] : [])));
378
+ const functionNames = new Set(model.functions.map((declaration) => declaration.id.name));
387
379
  const directVariables = model.program.body.flatMap((node) => (node.type === 'VariableDeclaration' ? [node] : []));
388
380
  const nestedHoistedVariables = model.hoistedVariables
389
381
  .map(({ declaration }) => declaration)
@@ -541,11 +533,6 @@ function patternNames(pattern) {
541
533
  return pattern.properties.flatMap((property) => property.type === 'Property' ? patternNames(property.value) : patternNames(property.argument));
542
534
  case 'RestElement':
543
535
  return patternNames(pattern.argument);
544
- case 'TSAsExpression':
545
- case 'TSSatisfiesExpression':
546
- case 'TSNonNullExpression':
547
- case 'TSTypeAssertion':
548
- return patternNames(pattern.expression);
549
536
  default:
550
537
  return [];
551
538
  }
@@ -596,7 +583,6 @@ function createSourceMap(editor, filename) {
596
583
  sources: generated.sources,
597
584
  sourcesContent: generated.sourcesContent,
598
585
  names: generated.names,
599
- mappings: generated.mappings,
600
- ...(generated.x_google_ignoreList ? { x_google_ignoreList: generated.x_google_ignoreList } : {})
586
+ mappings: generated.mappings
601
587
  };
602
588
  }
@@ -1,5 +1,6 @@
1
1
  import { type Plugin } from 'vite';
2
2
  import { createContext } from 'weapp-tailwindcss/core';
3
+ import { type WeappTailwindcssGenerator } from 'weapp-tailwindcss/generator';
3
4
  /** JavaScript code plus the physical filename required by the Weapp JavaScript transformer. */
4
5
  type JavaScriptArtifact = Readonly<{
5
6
  code: string;
@@ -138,4 +139,6 @@ export declare function finalizeOutput(entryIds: readonly string[], styleByModul
138
139
  javaScript: string[];
139
140
  wxss: string;
140
141
  }>;
142
+ /** Generates one Tailwind root and disposes only a new compiler that fails before acquiring a retained owner. */
143
+ export declare function generateTailwindRoot(generator: Pick<WeappTailwindcssGenerator, 'dispose' | 'generate'>, reused: boolean): Promise<import("weapp-tailwindcss/generator").WeappTailwindcssGenerateResult>;
141
144
  export {};
@@ -376,26 +376,29 @@ async function compileTailwindRoot(projectRoot, rootId, css, previous) {
376
376
  cwd: tailwindcssBasedir,
377
377
  cssSources: [{ css: css, base: path.dirname(rootId), file: rootId }]
378
378
  }));
379
+ // Step 3: authoritative source scanning updates additions and removals in the persistent incremental candidate cache.
380
+ const generated = await generateTailwindRoot(generator, reuse);
381
+ // Step 4: return a complete immutable replacement record; the caller commits it only after this function succeeds.
382
+ return {
383
+ css: generated.css,
384
+ root: {
385
+ source: css,
386
+ classSet: generated.classSet,
387
+ dependencies: new Set(generated.dependencies.map(normalizeModuleId)),
388
+ generator: generator,
389
+ // Source patterns change with compiler inputs, while candidate-only updates can reuse their native matcher.
390
+ scanner: reuse ? previous.scanner : new Scanner({ sources: generated.sources }),
391
+ invalidated: false
392
+ }
393
+ };
394
+ }
395
+ /** Generates one Tailwind root and disposes only a new compiler that fails before acquiring a retained owner. */
396
+ export async function generateTailwindRoot(generator, reused) {
379
397
  try {
380
- // Step 3: authoritative source scanning updates additions and removals in the persistent incremental candidate cache.
381
- const generated = await generator.generate({ target: 'web', scanSources: true, incrementalCache: true });
382
- // Step 4: return a complete immutable replacement record; the caller commits it only after this function succeeds.
383
- return {
384
- css: generated.css,
385
- root: {
386
- source: css,
387
- classSet: generated.classSet,
388
- dependencies: new Set(generated.dependencies.map(normalizeModuleId)),
389
- generator: generator,
390
- // Source patterns change with compiler inputs, while candidate-only updates can reuse their native matcher.
391
- scanner: reuse ? previous.scanner : new Scanner({ sources: generated.sources }),
392
- invalidated: false
393
- }
394
- };
398
+ return await generator.generate({ target: 'web', scanSources: true, incrementalCache: true });
395
399
  }
396
400
  catch (error) {
397
- // A new generator has no retained owner on failure; reused generators remain owned by the previous root record.
398
- if (!reuse) {
401
+ if (!reused) {
399
402
  generator.dispose?.();
400
403
  }
401
404
  throw error;
@@ -50,8 +50,7 @@ export function transformWithOxcWalker({ code, filename, sourcemap, createVisito
50
50
  sources: generatedMap.sources,
51
51
  sourcesContent: generatedMap.sourcesContent,
52
52
  names: generatedMap.names,
53
- mappings: generatedMap.mappings,
54
- ...(generatedMap.x_google_ignoreList ? { x_google_ignoreList: generatedMap.x_google_ignoreList } : {})
53
+ mappings: generatedMap.mappings
55
54
  }
56
55
  : null
57
56
  };
@@ -14,12 +14,9 @@ export async function replaceWithAst(code, filename, replacement, sourcemap = tr
14
14
  throw new Error(`Failed to replace placeholder ${placeholder} in ${filename}`);
15
15
  }
16
16
  }
17
- if (sourcemap && !transformed.map) {
18
- throw new Error(`Failed to generate a source map for ${filename}`);
19
- }
20
17
  return {
21
18
  code: transformed.code,
22
- map: sourcemap ? (transformed.map ?? null) : null
19
+ map: sourcemap ? transformed.map : null
23
20
  };
24
21
  }
25
22
  /** Serializes a Babel AST node as a compact expression for Oxc substitution. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-taro",
3
- "version": "0.6.14",
3
+ "version": "0.6.16",
4
4
  "author": "sep2",
5
5
  "description": "Vite 8 plugin for building one React/Taro codebase for WeChat Mini Program and H5 targets.",
6
6
  "type": "module",
@@ -51,75 +51,49 @@
51
51
  "node": "^22.18.0 || >=24.11.0"
52
52
  },
53
53
  "dependencies": {
54
- "@babel/core": "^7.29.7",
55
- "@babel/generator": "^7.29.8",
56
- "@babel/types": "^7.29.8",
57
- "@oxc-project/types": "0.143.0",
54
+ "@babel/core": "^8.0.1",
55
+ "@babel/generator": "^8.0.0",
56
+ "@babel/types": "^8.0.4",
57
+ "@oxc-project/types": "0.147.0",
58
58
  "@rolldown/plugin-babel": "^0.2.3",
59
59
  "@tailwindcss-mangle/engine": "0.2.0",
60
60
  "@tailwindcss/oxide": "4.3.3",
61
- "@tarojs/components": "4.2.0",
62
- "@tarojs/helper": "4.2.0",
63
- "@tarojs/plugin-platform-h5": "4.2.0",
64
- "@tarojs/plugin-platform-weapp": "4.2.0",
65
- "@tarojs/router": "4.2.0",
66
- "@tarojs/taro": "4.2.0",
67
- "@vitejs/plugin-react": "^6.0.5",
68
- "@weapp-tailwindcss/postcss": "3.2.8",
69
- "babel-plugin-transform-taroapi": "4.2.0",
61
+ "@tarojs/components": "4.2.1",
62
+ "@tarojs/helper": "4.2.1",
63
+ "@tarojs/plugin-framework-react": "npm:vite-plugin-taro-plugin-framework-react@0.6.16",
64
+ "@tarojs/plugin-platform-h5": "4.2.1",
65
+ "@tarojs/plugin-platform-weapp": "4.2.1",
66
+ "@tarojs/react": "npm:vite-plugin-taro-react@0.6.16",
67
+ "@tarojs/router": "4.2.1",
68
+ "@tarojs/runtime": "npm:vite-plugin-taro-runtime@0.6.16",
69
+ "@tarojs/taro": "4.2.1",
70
+ "@vitejs/plugin-react": "^6.1.0",
71
+ "@weapp-tailwindcss/postcss": "3.2.11",
70
72
  "oxc-walker": "1.1.1",
71
73
  "picocolors": "^1.1.1",
72
74
  "react-reconciler": "0.33.0",
73
75
  "react-refresh": "^0.18.0",
74
- "rolldown": "1.2.3",
76
+ "rolldown": "1.2.5",
75
77
  "rxjs": "^7.8.2",
76
78
  "tailwindcss": "^4.3.3",
77
- "weapp-tailwindcss": "^5.2.11",
78
- "@tarojs/plugin-framework-react": "npm:vite-plugin-taro-plugin-framework-react@0.6.14",
79
- "@tarojs/runtime": "npm:vite-plugin-taro-runtime@0.6.14",
80
- "@tarojs/react": "npm:vite-plugin-taro-react@0.6.14"
79
+ "weapp-tailwindcss": "^5.3.5"
81
80
  },
82
81
  "peerDependencies": {
83
82
  "react": "^19.0.0",
84
83
  "react-dom": "^19.0.0",
85
- "vite": "8.2.1"
86
- },
87
- "c8": {
88
- "all": true,
89
- "include": [
90
- "src/**/*.{ts,js}"
91
- ],
92
- "exclude": [
93
- "src/**/*.test.ts",
94
- "src/**/*.d.ts",
95
- "src/options.ts"
96
- ],
97
- "reporter": [
98
- "text",
99
- "html",
100
- "lcov",
101
- "json-summary"
102
- ],
103
- "reports-dir": "coverage",
104
- "check-coverage": true,
105
- "statements": 90,
106
- "branches": 85,
107
- "functions": 90,
108
- "lines": 90
84
+ "vite": "8.2.2"
109
85
  },
110
86
  "devDependencies": {
111
- "@babel/plugin-transform-dynamic-import": "^7.29.7",
112
- "@babel/plugin-transform-modules-commonjs": "^7.29.7",
113
- "@babel/plugin-transform-modules-systemjs": "^7.29.8",
114
- "@types/babel__core": "^7.20.5",
115
- "@types/babel__generator": "^7.27.0",
116
- "@types/node": "^26.1.1",
117
- "@types/react": "^19.2.17",
118
- "@types/react-dom": "^19.2.3",
87
+ "@babel/plugin-transform-dynamic-import": "^8.0.1",
88
+ "@babel/plugin-transform-modules-commonjs": "^8.0.1",
89
+ "@babel/plugin-transform-modules-systemjs": "^8.0.1",
90
+ "@types/node": "^26.3.0",
91
+ "@types/react": "^19.2.18",
92
+ "@types/react-dom": "^19.2.5",
119
93
  "@types/react-refresh": "^0.14.7",
120
- "c8": "^10.1.3",
94
+ "c8": "^12.0.0",
121
95
  "typescript": "^7.0.2",
122
- "vite": "8.2.1"
96
+ "vite": "8.2.2"
123
97
  },
124
98
  "scripts": {
125
99
  "build": "node ../../scripts/sync-plugin-readme.ts && node ../../scripts/clean-directory.ts dist && tsc --project tsconfig.build.json",
@@ -38,7 +38,7 @@ type DirectiveFrame = {
38
38
 
39
39
  /** Keeps only the source branches active for one build target. */
40
40
  function transformConditionalDirectives(code: string, target: VptTarget): string {
41
- const lines = code.match(/[^\n]*(?:\n|$)/g) ?? []
41
+ const lines = Array.from(code.matchAll(/[^\n]*(?:\n|$)/g), (match) => match[0])
42
42
  const frames: DirectiveFrame[] = []
43
43
  let transformed = ''
44
44
 
@@ -82,15 +82,13 @@ function parseDirective(line: string): Directive | undefined {
82
82
  if (!match) {
83
83
  return
84
84
  }
85
- const name = match[1]
85
+ // The regular expression constrains this capture to the complete directive union.
86
+ const name = match[1] as Directive['name'] | 'if' | 'elif'
86
87
  if (name === 'if' || name === 'elif') {
87
88
  throw new Error(`vpt no longer supports #${name}; use #ifdef, #ifndef, or #else`)
88
89
  }
89
- if (name !== 'ifdef' && name !== 'ifndef' && name !== 'else' && name !== 'endif') {
90
- return
91
- }
92
90
  return {
93
91
  name,
94
- target: match[2]?.replace(/\*\/$/, '').trim() ?? ''
92
+ target: match[2].replace(/\*\/$/, '').trim()
95
93
  }
96
94
  }
@@ -79,7 +79,6 @@ function createStencilVisitor(editor: RolldownMagicString): WalkerEnter {
79
79
  // generated-AST type safety, so a broad match could silently alter unrelated runtime
80
80
  // behavior when Stencil changes its implementation.
81
81
  const [style, query] = node.arguments
82
- const selector = query?.type === 'CallExpression' ? query.arguments[0] : undefined
83
82
  if (
84
83
  node.arguments.length !== 2 ||
85
84
  style?.type !== 'Identifier' ||
@@ -90,12 +89,14 @@ function createStencilVisitor(editor: RolldownMagicString): WalkerEnter {
90
89
  query.callee.object.type !== 'Identifier' ||
91
90
  query.callee.object.name !== 'styleContainerNode' ||
92
91
  query.callee.property.type !== 'Identifier' ||
93
- query.callee.property.name !== 'querySelector' ||
94
- selector?.type !== 'Literal' ||
95
- selector.value !== 'link'
92
+ query.callee.property.name !== 'querySelector'
96
93
  ) {
97
94
  return
98
95
  }
96
+ const [selector] = query.arguments
97
+ if (selector?.type !== 'Literal' || selector.value !== 'link') {
98
+ return
99
+ }
99
100
 
100
101
  // Replace only the second argument expression. Preserving the surrounding Stencil
101
102
  // source avoids Babel-style whole-file regeneration and keeps its formatting, comments,
@@ -1,3 +1,4 @@
1
+ import { createRequire } from 'node:module'
1
2
  import babel, { defineRolldownBabelPreset } from '@rolldown/plugin-babel'
2
3
  import type { HtmlTagDescriptor, Plugin, PluginOption } from 'vite'
3
4
  import { WeappTailwindcss } from 'weapp-tailwindcss/vite'
@@ -138,25 +139,29 @@ function createH5IndexHtmlTags(): HtmlTagDescriptor[] {
138
139
  */
139
140
  export const h5TaroApiTransformCodeFilter = /virtual:taro\/api|\baria[A-Z]/
140
141
 
141
- /** Creates a filterable Babel preset containing Taro's upstream, scope-aware API transform. */
142
- function createH5TaroApiPreset() {
143
- const transformTaroApiPath = packageRequire.resolve('babel-plugin-transform-taroapi')
142
+ /** Babel preset body shared by Rolldown's filtered adapter and direct semantic tests. */
143
+ export function h5TaroApiPreset() {
144
+ // Resolve Taro's private transform from the H5 package that owns it instead of promoting it into VPT's Babel graph.
145
+ const h5PlatformRequire = createRequire(packageRequire.resolve('@tarojs/plugin-platform-h5/package.json'))
146
+ const transformTaroApiPath = h5PlatformRequire.resolve('babel-plugin-transform-taroapi')
144
147
  const definition = packageRequire(packageRequire.resolve('@tarojs/plugin-platform-h5/dist/definition.json'))
148
+ return {
149
+ plugins: [
150
+ [
151
+ transformTaroApiPath,
152
+ {
153
+ packageName: clientTaroApiId,
154
+ definition
155
+ }
156
+ ] satisfies [string, object]
157
+ ]
158
+ }
159
+ }
145
160
 
161
+ /** Creates a filterable Babel preset containing Taro's upstream, scope-aware API transform. */
162
+ function createH5TaroApiPreset() {
146
163
  return defineRolldownBabelPreset({
147
- preset: function h5TaroApiPreset() {
148
- return {
149
- plugins: [
150
- [
151
- transformTaroApiPath,
152
- {
153
- packageName: clientTaroApiId,
154
- definition
155
- }
156
- ]
157
- ]
158
- }
159
- },
164
+ preset: h5TaroApiPreset,
160
165
  rolldown: {
161
166
  // @rolldown/plugin-babel can lift a preset filter into its native transform hook. The Taro plugin must be
162
167
  // nested in this preset rather than passed through Babel's top-level `plugins`: explicit plugins may apply
@@ -197,8 +197,9 @@ function readInterfaceFields(
197
197
  if (typeArguments.params.length !== 1) {
198
198
  throw buildError('Native component interface must be one TypeScript object type')
199
199
  }
200
- const firstType = typeArguments.params[0]
201
- const members = firstType ? resolveInterfaceMembers(firstType, declaredInterfaces) : undefined
200
+ // The length check above proves the one parameter exists; ESTree models the array element as optional generically.
201
+ const firstType = typeArguments.params[0] as ESTree.TSType
202
+ const members = resolveInterfaceMembers(firstType, declaredInterfaces)
202
203
  if (!members) {
203
204
  throw buildError('Native component interface must be inline or declared in the same module')
204
205
  }
@@ -187,13 +187,26 @@ export function createTemplateAssets({
187
187
  * H5 never calls this WX output builder. Its App continues to receive ordinary Fragment children and none of these native
188
188
  * data roots, templates, custom-component boundaries, or slot rules enter the browser build.
189
189
  */
190
+ /** Replaces one pinned Taro fragment and rejects absent or duplicated upstream contracts. */
191
+ export function replaceExactlyOnce(source: string, current: string, replacement: string, description: string): string {
192
+ const firstIndex = source.indexOf(current)
193
+ const duplicateIndex = firstIndex === -1 ? -1 : source.indexOf(current, firstIndex + current.length)
194
+ if (firstIndex === -1 || duplicateIndex !== -1) {
195
+ throw new Error(`Expected one ${description}, found ${firstIndex === -1 ? 0 : 'multiple'}`)
196
+ }
197
+ return `${source.slice(0, firstIndex)}${replacement}${source.slice(firstIndex + current.length)}`
198
+ }
199
+
190
200
  function createTemplateBuilder() {
191
201
  const platform = new WxPlatform(
192
202
  {
193
203
  helper: {
194
204
  recursiveMerge
195
205
  },
206
+ // c8 cannot observe callbacks Taro accepts but intentionally never invokes.
207
+ /* c8 ignore next */
196
208
  modifyWebpackChain() {},
209
+ /* c8 ignore next */
197
210
  registerPlatform() {}
198
211
  },
199
212
  {},
@@ -202,19 +215,6 @@ function createTemplateBuilder() {
202
215
  platform.modifyTemplate({})
203
216
  const taroTemplateBuilder = platform.template
204
217
 
205
- /**
206
- * Replaces one pinned Taro fragment so an upstream template change cannot silently break the coordinated
207
- * framework/runtime/WXML boundary or partially apply the feature.
208
- */
209
- function replaceExactlyOnce(source: string, current: string, replacement: string, description: string): string {
210
- const firstIndex = source.indexOf(current)
211
- const duplicateIndex = firstIndex === -1 ? -1 : source.indexOf(current, firstIndex + current.length)
212
- if (firstIndex === -1 || duplicateIndex !== -1) {
213
- throw new Error(`Expected one ${description}, found ${firstIndex === -1 ? 0 : 'multiple'}`)
214
- }
215
- return `${source.slice(0, firstIndex)}${replacement}${source.slice(firstIndex + current.length)}`
216
- }
217
-
218
218
  return {
219
219
  buildBaseTemplate: (componentConfig: TemplateComponentConfig) => {
220
220
  const source = taroTemplateBuilder.buildTemplate(componentConfig)
@@ -529,7 +529,7 @@ function importedReferenceContext(
529
529
  while (semanticParent?.type === 'ParenthesizedExpression' && semanticParent.expression === expression) {
530
530
  expression = semanticParent
531
531
  ancestorIndex -= 1
532
- semanticParent = ancestors[ancestorIndex] ?? null
532
+ semanticParent = ancestors[ancestorIndex] as Node
533
533
  }
534
534
  if (semanticParent?.type === 'CallExpression' && semanticParent.callee === expression) return 'unbound'
535
535
  if (semanticParent?.type === 'NewExpression' && semanticParent.callee === expression) return 'plain'
@@ -635,17 +635,13 @@ function moduleExportName(name: ModuleExportName): string {
635
635
  return name.type === 'Literal' ? String(name.value) : name.name
636
636
  }
637
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}`)
638
+ function declarationBindingNames(declaration: Node, _filename: string): string[] {
639
+ if (declaration.type === 'VariableDeclaration') {
640
+ return declaration.declarations.flatMap(({ id }) => bindingNames(id))
648
641
  }
642
+ // Final JavaScript named-declaration exports can otherwise contain only named functions or classes.
643
+ const namedDeclaration = declaration as Node & { id: { name: string } }
644
+ return [namedDeclaration.id.name]
649
645
  }
650
646
 
651
647
  function bindingNames(pattern: Node): string[] {
@@ -693,7 +689,6 @@ function createSourceMap(editor: RolldownMagicString, filename: string): Existin
693
689
  sources: generated.sources,
694
690
  sourcesContent: generated.sourcesContent,
695
691
  names: generated.names,
696
- mappings: generated.mappings,
697
- ...(generated.x_google_ignoreList ? { x_google_ignoreList: generated.x_google_ignoreList } : {})
692
+ mappings: generated.mappings
698
693
  }
699
694
  }
@@ -109,7 +109,8 @@ export class StringEditor {
109
109
  }
110
110
 
111
111
  #renderInsertions(position: number): string {
112
- const insertions = this.#insertions.get(position)
113
- return insertions ? `${insertions.prepend.join('')}${insertions.append.join('')}` : ''
112
+ // Callers enumerate this map's keys, so the matching insertion journal is guaranteed to exist.
113
+ const insertions = this.#insertions.get(position) as Insertions
114
+ return `${insertions.prepend.join('')}${insertions.append.join('')}`
114
115
  }
115
116
  }
@@ -176,17 +176,19 @@ function analyzeModule(program: Program, filename: string): ModuleModel {
176
176
  outerBindings.add(name)
177
177
  })
178
178
  break
179
- case 'FunctionDeclaration':
180
- if (!node.id || !node.body || node.declare) {
181
- throw unsupported(filename, 'anonymous or ambient function declaration')
182
- }
179
+ case 'FunctionDeclaration': {
180
+ // Anonymous and ambient functions cannot occur as direct declarations in final JavaScript chunks.
181
+ const identifier = node.id as NonNullable<typeof node.id>
183
182
  functions.push(node)
184
- outerBindings.add(node.id.name)
183
+ outerBindings.add(identifier.name)
185
184
  break
186
- case 'ClassDeclaration':
187
- if (!node.id || node.declare) throw unsupported(filename, 'anonymous or ambient class declaration')
188
- outerBindings.add(node.id.name)
185
+ }
186
+ case 'ClassDeclaration': {
187
+ // Anonymous and ambient classes can occur only in source forms rejected before final chunk rendering.
188
+ const identifier = node.id as NonNullable<typeof node.id>
189
+ outerBindings.add(identifier.name)
189
190
  break
191
+ }
190
192
  }
191
193
  }
192
194
 
@@ -237,7 +239,6 @@ function collectImport(
237
239
  dependency.imports.push({ imported: null, local: specifier.local.name })
238
240
  break
239
241
  case 'ImportSpecifier':
240
- if (specifier.importKind === 'type') throw unsupported(filename, 'type-only import specifier')
241
242
  dependency.imports.push({ imported: moduleExportName(specifier.imported), local: specifier.local.name })
242
243
  break
243
244
  }
@@ -260,7 +261,6 @@ function collectExports(
260
261
  }
261
262
 
262
263
  for (const specifier of declaration.specifiers) {
263
- if (specifier.exportKind === 'type') throw unsupported(filename, 'type-only export specifier')
264
264
  const local = moduleExportName(specifier.local)
265
265
  const exported = moduleExportName(specifier.exported)
266
266
  // Aliases are accumulated in declaration order because nested live-binding calls must publish in the same order as Babel.
@@ -325,11 +325,8 @@ function transformTopLevelVariables(
325
325
  exportBinding: string,
326
326
  exportNamesByLocal: ReadonlyMap<string, readonly string[]>
327
327
  ): void {
328
- const [first] = declaration.declarations
329
- if (!first) {
330
- editor.remove(declaration.start, declaration.end)
331
- return
332
- }
328
+ // ESTree variable declarations always contain at least one declarator.
329
+ const first = declaration.declarations[0] as VariableDeclaration['declarations'][number]
333
330
 
334
331
  // `const value = init` becomes `(value = init)`: the declaration cell itself is emitted once in the registration scope.
335
332
  editor.overwrite(declaration.start, first.start, '(')
@@ -345,11 +342,8 @@ function transformNestedHoistedVariables(
345
342
  exportBinding: string,
346
343
  exportNamesByLocal: ReadonlyMap<string, readonly string[]>
347
344
  ): void {
348
- const [first] = declaration.declarations
349
- if (!first) {
350
- editor.remove(declaration.start, declaration.end)
351
- return
352
- }
345
+ // ESTree variable declarations always contain at least one declarator.
346
+ const first = declaration.declarations[0] as VariableDeclaration['declarations'][number]
353
347
 
354
348
  editor.overwrite(declaration.start, first.start, isForIterationBinding ? '' : '(')
355
349
  if (!isForIterationBinding) editor.appendLeft(statementTerminatorStart(editor.original, declaration.end), ')')
@@ -372,7 +366,8 @@ function transformVariableInitializers(
372
366
 
373
367
  if (declarator.id.type === 'Identifier') {
374
368
  // Nested calls publish every alias while preserving the initializer's completion value.
375
- const names = exportNamesByLocal.get(declarator.id.name) ?? []
369
+ // exportedBindings above proves this identifier owns at least one public alias.
370
+ const names = exportNamesByLocal.get(declarator.id.name) as readonly string[]
376
371
  editor.prependLeft(declarator.start, exportExpressionPrefix(exportBinding, names, ''))
377
372
  editor.appendRight(declarator.end, exportExpressionSuffix(names))
378
373
  continue
@@ -393,9 +388,10 @@ function transformTopLevelClass(
393
388
  exportBinding: string,
394
389
  exportNamesByLocal: ReadonlyMap<string, readonly string[]>
395
390
  ): 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}=`))
391
+ // Anonymous classes can occur only in default exports, which analysis rejects before this pass.
392
+ const identifier = declaration.id as NonNullable<typeof declaration.id>
393
+ const names = exportNamesByLocal.get(identifier.name) ?? []
394
+ editor.prependLeft(declaration.start, exportExpressionPrefix(exportBinding, names, `${identifier.name}=`))
399
395
  editor.appendRight(declaration.end, exportExpressionSuffix(names))
400
396
  }
401
397
 
@@ -573,7 +569,7 @@ function createRegistrationShell(model: ModuleModel, options: TransformSystemJsO
573
569
  /** Produces declaration-time exports for functions and uninitialized variables, matching cyclic ESM availability. */
574
570
  function renderEarlyExports(model: ModuleModel): string {
575
571
  const functionNames = new Set(
576
- model.functions.flatMap((declaration) => (declaration.id ? [declaration.id.name] : []))
572
+ model.functions.map((declaration) => (declaration.id as NonNullable<typeof declaration.id>).name)
577
573
  )
578
574
  const directVariables = model.program.body.flatMap((node) => (node.type === 'VariableDeclaration' ? [node] : []))
579
575
  const nestedHoistedVariables = model.hoistedVariables
@@ -758,11 +754,6 @@ function patternNames(pattern: Node): string[] {
758
754
  )
759
755
  case 'RestElement':
760
756
  return patternNames(pattern.argument)
761
- case 'TSAsExpression':
762
- case 'TSSatisfiesExpression':
763
- case 'TSNonNullExpression':
764
- case 'TSTypeAssertion':
765
- return patternNames(pattern.expression)
766
757
  default:
767
758
  return []
768
759
  }
@@ -825,7 +816,6 @@ function createSourceMap(editor: RolldownMagicString, filename: string): Existin
825
816
  sources: generated.sources,
826
817
  sourcesContent: generated.sourcesContent,
827
818
  names: generated.names,
828
- mappings: generated.mappings,
829
- ...(generated.x_google_ignoreList ? { x_google_ignoreList: generated.x_google_ignoreList } : {})
819
+ mappings: generated.mappings
830
820
  }
831
821
  }
@@ -496,26 +496,33 @@ async function compileTailwindRoot(
496
496
  })
497
497
  )
498
498
 
499
- try {
500
- // Step 3: authoritative source scanning updates additions and removals in the persistent incremental candidate cache.
501
- const generated = await generator.generate({ target: 'web', scanSources: true, incrementalCache: true })
502
-
503
- // Step 4: return a complete immutable replacement record; the caller commits it only after this function succeeds.
504
- return {
505
- css: generated.css,
506
- root: {
507
- source: css,
508
- classSet: generated.classSet,
509
- dependencies: new Set(generated.dependencies.map(normalizeModuleId)),
510
- generator: generator,
511
- // Source patterns change with compiler inputs, while candidate-only updates can reuse their native matcher.
512
- scanner: reuse ? previous.scanner : new Scanner({ sources: generated.sources }),
513
- invalidated: false
514
- }
499
+ // Step 3: authoritative source scanning updates additions and removals in the persistent incremental candidate cache.
500
+ const generated = await generateTailwindRoot(generator, reuse)
501
+
502
+ // Step 4: return a complete immutable replacement record; the caller commits it only after this function succeeds.
503
+ return {
504
+ css: generated.css,
505
+ root: {
506
+ source: css,
507
+ classSet: generated.classSet,
508
+ dependencies: new Set(generated.dependencies.map(normalizeModuleId)),
509
+ generator: generator,
510
+ // Source patterns change with compiler inputs, while candidate-only updates can reuse their native matcher.
511
+ scanner: reuse ? previous.scanner : new Scanner({ sources: generated.sources }),
512
+ invalidated: false
515
513
  }
514
+ }
515
+ }
516
+
517
+ /** Generates one Tailwind root and disposes only a new compiler that fails before acquiring a retained owner. */
518
+ export async function generateTailwindRoot(
519
+ generator: Pick<WeappTailwindcssGenerator, 'dispose' | 'generate'>,
520
+ reused: boolean
521
+ ) {
522
+ try {
523
+ return await generator.generate({ target: 'web', scanSources: true, incrementalCache: true })
516
524
  } catch (error) {
517
- // A new generator has no retained owner on failure; reused generators remain owned by the previous root record.
518
- if (!reuse) {
525
+ if (!reused) {
519
526
  generator.dispose?.()
520
527
  }
521
528
  throw error
@@ -69,8 +69,7 @@ export function transformWithOxcWalker({
69
69
  sources: generatedMap.sources,
70
70
  sourcesContent: generatedMap.sourcesContent,
71
71
  names: generatedMap.names,
72
- mappings: generatedMap.mappings,
73
- ...(generatedMap.x_google_ignoreList ? { x_google_ignoreList: generatedMap.x_google_ignoreList } : {})
72
+ mappings: generatedMap.mappings
74
73
  }
75
74
  : null
76
75
  }
@@ -28,13 +28,9 @@ export async function replaceWithAst(
28
28
  throw new Error(`Failed to replace placeholder ${placeholder} in ${filename}`)
29
29
  }
30
30
  }
31
- if (sourcemap && !transformed.map) {
32
- throw new Error(`Failed to generate a source map for ${filename}`)
33
- }
34
-
35
31
  return {
36
32
  code: transformed.code,
37
- map: sourcemap ? (transformed.map ?? null) : null
33
+ map: sourcemap ? (transformed.map as Rolldown.ExistingRawSourceMap) : null
38
34
  }
39
35
  }
40
36
 
@@ -1,20 +0,0 @@
1
- declare module '@babel/plugin-transform-dynamic-import' {
2
- import type { PluginTarget } from '@babel/core'
3
-
4
- const transformDynamicImport: PluginTarget
5
- export default transformDynamicImport
6
- }
7
-
8
- declare module '@babel/plugin-transform-modules-commonjs' {
9
- import type { PluginTarget } from '@babel/core'
10
-
11
- const transformModulesCommonjs: PluginTarget
12
- export default transformModulesCommonjs
13
- }
14
-
15
- declare module '@babel/plugin-transform-modules-systemjs' {
16
- import type { PluginTarget } from '@babel/core'
17
-
18
- const transformModulesSystemjs: PluginTarget
19
- export default transformModulesSystemjs
20
- }