vite-plugin-taro 0.5.4 → 0.5.6

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 (33) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/node/plugins/h5/create-stencil-client-adapter.d.ts +10 -13
  4. package/dist/node/plugins/h5/create-stencil-client-adapter.js +63 -51
  5. package/dist/node/plugins/h5/plugins.d.ts +10 -0
  6. package/dist/node/plugins/h5/plugins.js +39 -11
  7. package/dist/node/plugins/wx/dev/dev-host.js +103 -216
  8. package/dist/node/plugins/wx/dev/hmr-files.d.ts +4 -5
  9. package/dist/node/plugins/wx/dev/hmr-files.js +18 -8
  10. package/dist/node/plugins/wx/dev/patch-publisher.d.ts +25 -5
  11. package/dist/node/plugins/wx/dev/patch-publisher.js +30 -9
  12. package/dist/node/plugins/wx/dev/react-refresh.d.ts +12 -0
  13. package/dist/node/plugins/wx/dev/react-refresh.js +111 -108
  14. package/dist/node/plugins/wx/dev/wx-dev-options.d.ts +25 -0
  15. package/dist/node/plugins/wx/dev/wx-dev-options.js +147 -0
  16. package/dist/node/utils/oxc-transform.d.ts +21 -0
  17. package/dist/node/utils/oxc-transform.js +58 -0
  18. package/dist/node/utils/serialized-task-queue.d.ts +6 -2
  19. package/dist/node/utils/serialized-task-queue.js +10 -2
  20. package/dist/runtime/wx/capsule/page.js +14 -5
  21. package/dist/runtime/wx/dev/dev-runtime.js +70 -26
  22. package/package.json +4 -3
  23. package/src/node/plugins/h5/create-stencil-client-adapter.ts +74 -73
  24. package/src/node/plugins/h5/plugins.ts +42 -13
  25. package/src/node/plugins/wx/dev/dev-host.ts +121 -256
  26. package/src/node/plugins/wx/dev/hmr-files.ts +20 -8
  27. package/src/node/plugins/wx/dev/patch-publisher.ts +36 -10
  28. package/src/node/plugins/wx/dev/react-refresh.ts +125 -129
  29. package/src/node/plugins/wx/dev/wx-dev-options.ts +200 -0
  30. package/src/node/utils/oxc-transform.ts +77 -0
  31. package/src/node/utils/serialized-task-queue.ts +15 -3
  32. package/src/runtime/wx/capsule/page.ts +23 -9
  33. package/src/runtime/wx/dev/dev-runtime.ts +80 -28
@@ -1,8 +1,19 @@
1
- import { types } from '@babel/core';
1
+ import { isReferenceIdentifier } from 'oxc-walker';
2
2
  import { memoize } from '../../../utils/memoize.js';
3
- import { transformWithBabel } from '../../../utils/transform.js';
3
+ import { transformWithOxcWalker } from '../../../utils/oxc-transform.js';
4
4
  /** The React DevTools hook protocol name; free references must target `global` in wx. */
5
5
  const reactDevtoolsHookProtocol = '__REACT_DEVTOOLS_GLOBAL_HOOK__';
6
+ /**
7
+ * Refresh protocol globals that must live on the WeChat `global` object:
8
+ * - `__registerBeforePerformReactRefresh` is assigned at module evaluation so the HMR client can register work that
9
+ * must finish before a refresh. Leaving it on `window` throws before the refresh runtime can initialize.
10
+ * - `__getReactRefreshIgnoredExports` is an optional extension point read while validating a refresh boundary.
11
+ * Leaving that read on the nonexistent `window` crashes every update validation pass.
12
+ *
13
+ * Keeping this list explicit prevents the adapter from rewriting unrelated browser accesses if the vendored runtime
14
+ * gains new code. Any future React Refresh protocol addition therefore requires a deliberate compatibility decision.
15
+ */
16
+ const refreshRuntimeWindowGlobals = ['__registerBeforePerformReactRefresh', '__getReactRefreshIgnoredExports'];
6
17
  /**
7
18
  * Creates the serve-only React Refresh adaptation transforms for the wx target.
8
19
  *
@@ -58,7 +69,7 @@ export function createWxReactRefreshTransforms() {
58
69
  // occurrence, in boundary modules.
59
70
  filter: { code: /window\.\$RefreshReg\$/ },
60
71
  handler(code, id) {
61
- return transformWithBabel(code, id, [removeRefreshPreambleGuard], false);
72
+ return removeRefreshPreambleGuard({ code, id });
62
73
  }
63
74
  }
64
75
  }
@@ -70,129 +81,121 @@ export function createWxReactRefreshTransforms() {
70
81
  * The renderer checks the hook with `typeof __REACT_DEVTOOLS_GLOBAL_HOOK__` and injects via
71
82
  * `hook.inject(...)` — but in the WeChat runtime, free-variable reads never resolve against
72
83
  * `global`'s properties (verified: the free lookup is undefined while
73
- * `global.__REACT_DEVTOOLS_GLOBAL_HOOK__` exists), so the renderer would silently skip
74
- * injection and React Refresh would have no renderer to schedule re-renders on.
84
+ * `global.__REACT_DEVTOOLS_GLOBAL_HOOK__` exists). The renderer would therefore silently
85
+ * skip injection, leaving React Refresh with no renderer on which to schedule re-renders.
75
86
  *
76
- * The name is unique to the React DevTools protocol, so rewriting every free reference to an
77
- * explicit member access is precise. The hook itself is created on `global` by the dev
78
- * runtime chunk (see dev-runtime.ts).
87
+ * The protocol name is unique, but only reference identifiers are rewritten. Declaration
88
+ * keys and explicit members such as `global.__REACT_DEVTOOLS_GLOBAL_HOOK__` must remain
89
+ * untouched; rewriting those would either produce invalid syntax or double-prefix the hook.
90
+ * The hook itself is created on `global` by the dev runtime chunk in `dev-runtime.ts`.
79
91
  */
80
- function rewriteReactDevtoolsHookGlobal() {
81
- return {
82
- name: 'vpt:wx-react-devtools-hook-global',
83
- visitor: {
84
- Identifier(identifierPath) {
85
- const node = identifierPath.node;
86
- if (!types.isIdentifier(node, { name: reactDevtoolsHookProtocol })) {
87
- return;
88
- }
89
- const parent = identifierPath.parentPath;
90
- // A member expression property (e.g. `global.__REACT_DEVTOOLS_GLOBAL_HOOK__`)
91
- // is already explicit and must not be rewritten again.
92
- if (parent.isMemberExpression() && parent.node.property === node) {
93
- return;
94
- }
95
- identifierPath.replaceWith(types.memberExpression(types.identifier('global'), types.identifier(node.name)));
96
- }
92
+ function createReactDevtoolsHookVisitor(editor) {
93
+ return function enter(node, parent) {
94
+ if (node.type !== 'Identifier' ||
95
+ node.name !== reactDevtoolsHookProtocol ||
96
+ !isReferenceIdentifier(node, parent)) {
97
+ return;
97
98
  }
99
+ // An explicit member access is required because WeChat does not expose properties of
100
+ // its global object as free lexical bindings. Removing this edit disables renderer
101
+ // registration even though the hook object itself still exists.
102
+ editor.overwrite(node.start, node.end, `global.${reactDevtoolsHookProtocol}`);
98
103
  };
99
104
  }
100
105
  /**
101
- * Refresh runtime module: self-inject at evaluation — the wx App heap has no HTML preamble.
106
+ * Refresh runtime module: self-inject at evaluation and rewrite its browser protocol globals.
102
107
  *
103
- * In web Vite, the HTML preamble calls `injectIntoGlobalHook(window)` before any module
104
- * loads; nothing does that in wx. The call must live in this module itself:
105
- * - its closure owns the refresh state (helpersByRendererID, mountedRoots), so the same
106
- * instance that the generated boundary code imports must bootstrap itself;
107
- * - the dev runtime chunk cannot reach it: the module is in a lazily-loaded chunk that
108
- * requires the runtime chunk first, so it does not exist when the runtime evaluates.
108
+ * In web Vite, an HTML preamble calls `injectIntoGlobalHook(window)` before application
109
+ * modules load. wx has no HTML document or preamble, so nothing performs that bootstrap.
110
+ * The call must live in the refresh runtime module itself:
111
+ * - this module's closure owns `helpersByRendererID`, mounted roots, and the update helpers;
112
+ * - the wx dev-runtime chunk cannot call into it because the refresh module is in a later,
113
+ * lazily loaded chunk and does not exist when the dev-runtime chunk evaluates.
109
114
  *
110
- * Unlike the preamble's `$RefreshReg$` globals (removed by removeRefreshPreambleGuard
111
- * because boundary modules define local wrappers), the hook machinery has no local
112
- * equivalent — without it the refresh runtime never learns the renderer or the mounted
113
- * roots, so the injection is the one preamble responsibility that must be replicated.
115
+ * Unlike the preamble's `$RefreshReg$` globals, which boundary modules replace with local
116
+ * wrappers, the renderer-hook machinery has no local equivalent. Without this injected call,
117
+ * the runtime never learns about the renderer or mounted roots and updates cannot refresh UI.
114
118
  *
115
- * Timing is safe because injectIntoGlobalHook replays hook.renderers: the renderer already
116
- * injected into the hook (created by the dev runtime chunk) when the App mounted, and the
117
- * replay captures it; the patched commit hooks then track every later mount, including the
118
- * remounts on re-execution.
119
+ * Appending the call is safe even when React has already registered its renderer: the refresh
120
+ * runtime replays `hook.renderers` during injection, then its patched commit hooks observe all
121
+ * later mounts and remounts. The same module also contains two browser-only `window` protocol
122
+ * accesses; those must point at `global` or evaluation/update validation throws in WeChat.
119
123
  */
120
- function injectRefreshGlobalHook() {
121
- return {
122
- name: 'vpt:wx-refresh-global-hook-injection',
123
- visitor: {
124
- Program(programPath) {
125
- // The renderer already injected into the hook (created by the dev runtime
126
- // chunk) when the App mounted; injectIntoGlobalHook replays its renderers.
127
- programPath.pushContainer('body', types.expressionStatement(types.callExpression(types.identifier('injectIntoGlobalHook'), [types.identifier('global')])));
128
- }
124
+ function createRefreshRuntimeVisitor(editor) {
125
+ // The declarations must execute before self-injection, so the call is appended instead of
126
+ // prepended. Removing it would leave the web preamble's only essential responsibility
127
+ // unimplemented in wx.
128
+ editor.append('\ninjectIntoGlobalHook(global);');
129
+ return function enter(node) {
130
+ if (node.type !== 'MemberExpression' ||
131
+ node.computed ||
132
+ node.object.type !== 'Identifier' ||
133
+ node.object.name !== 'window' ||
134
+ node.property.type !== 'Identifier' ||
135
+ !refreshRuntimeWindowGlobals.some((globalName) => globalName === node.property.name)) {
136
+ return;
129
137
  }
138
+ // `global` is the shared wx App heap used by the dev runtime and hook injection. Only
139
+ // replacing the object range preserves the vendored runtime byte-for-byte otherwise
140
+ // and prevents unrelated `window` expressions from being silently adapted.
141
+ editor.overwrite(node.object.start, node.object.end, 'global');
130
142
  };
131
143
  }
132
144
  /**
133
- * Refresh runtime module: rewrites the generated `window.<protocol>` accesses to `global`.
134
- *
135
- * The vendored refresh runtime is written for browsers and uses `window` for its protocol
136
- * globals, but the WeChat runtime has no `window` — the free identifier is undefined — so the
137
- * top-level assignment would throw at module evaluation, and the ignored-exports read
138
- * inside the refresh validator would crash every update pass.
145
+ * Removes the web-only `if (!window.$RefreshReg$) throw Error(...)` assertion from boundary modules.
139
146
  *
140
- * Only the exact known protocol names are rewritten, one by one, so unrelated `window`
141
- * accesses are never touched and future protocol additions are deliberate.
147
+ * The assertion verifies that Vite's HTML preamble installed global registration helpers.
148
+ * wx has no preamble and evaluating `window` itself fails. The assertion is unnecessary here:
149
+ * @vitejs/plugin-react generates local `$RefreshReg$` and `$RefreshSig$` wrappers that delegate
150
+ * directly to the imported refresh runtime. Removing the whole statement therefore removes
151
+ * only an invalid platform check; component registration continues through those local wrappers.
152
+ * If this edit is removed, every transformed refresh boundary crashes before its module body runs.
142
153
  */
143
- function rewriteRefreshRuntimeWindowAccess() {
144
- /**
145
- * Refresh protocol globals that must land on the WeChat `global`:
146
- * - `__registerBeforePerformReactRefresh`: assigned at module scope; in web, the HMR
147
- * client registers pre-refresh callbacks through it — the assignment throws on
148
- * undefined `window`;
149
- * - `__getReactRefreshIgnoredExports`: read in validateRefreshBoundaryAndEnqueueUpdate
150
- * as an optional extension point — a read on undefined `window` crashes.
151
- */
152
- const refreshRuntimeWindowGlobals = ['__registerBeforePerformReactRefresh', '__getReactRefreshIgnoredExports'];
153
- return {
154
- name: 'vpt:wx-refresh-runtime-window-access',
155
- visitor: {
156
- MemberExpression(memberPath) {
157
- const member = memberPath.node;
158
- if (!types.isIdentifier(member.object, { name: 'window' }) ||
159
- !types.isIdentifier(member.property) ||
160
- !refreshRuntimeWindowGlobals.includes(member.property.name)) {
161
- return;
162
- }
163
- // `global` is the WeChat global object — the same one the hook injection
164
- // and the rest of the wx glue speak — so the protocol globals land on the
165
- // App heap like any other global hook.
166
- member.object = types.identifier('global');
167
- }
154
+ function createRefreshPreambleGuardVisitor(editor) {
155
+ return function enter(node) {
156
+ if (node.type !== 'IfStatement' ||
157
+ node.test.type !== 'UnaryExpression' ||
158
+ node.test.operator !== '!' ||
159
+ node.test.argument.type !== 'MemberExpression' ||
160
+ node.test.argument.computed ||
161
+ node.test.argument.object.type !== 'Identifier' ||
162
+ node.test.argument.object.name !== 'window' ||
163
+ node.test.argument.property.type !== 'Identifier' ||
164
+ node.test.argument.property.name !== '$RefreshReg$') {
165
+ return;
168
166
  }
167
+ // Matching the complete AST shape prevents an unrelated `$RefreshReg$` use from being
168
+ // removed. Skipping descendants is required because their ranges disappear with the
169
+ // parent and must not receive overlapping MagicString edits.
170
+ editor.remove(node.start, node.end);
171
+ this.skip();
169
172
  };
170
173
  }
171
- /** Boundary modules: remove the generated `if (!window.$RefreshReg$) throw Error(...)` guard. */
172
- function removeRefreshPreambleGuard() {
173
- return {
174
- name: 'vpt:wx-refresh-preamble-guard',
175
- visitor: {
176
- IfStatement(ifPath) {
177
- // The guard is a web-only sanity check that the HTML preamble installed the
178
- // `$RefreshReg$` global. wx has no preamble and no such global — but the
179
- // boundary module does not need it: the plugin transform generates local
180
- // `$RefreshReg$`/`$RefreshSig$` wrappers that delegate to the imported
181
- // refresh runtime, so registration works without the global. The guard
182
- // itself only crashes on the undefined `window`, so it is removed.
183
- const test = ifPath.node.test;
184
- if (types.isUnaryExpression(test, { operator: '!' }) &&
185
- types.isMemberExpression(test.argument) &&
186
- types.isIdentifier(test.argument.object, { name: 'window' }) &&
187
- types.isIdentifier(test.argument.property, { name: '$RefreshReg$' })) {
188
- ifPath.remove();
189
- }
190
- }
191
- }
192
- };
174
+ export function transformRefreshRuntime({ code, id }) {
175
+ return transformWithOxcWalker({
176
+ code,
177
+ filename: id,
178
+ sourcemap: false,
179
+ createVisitor: createRefreshRuntimeVisitor
180
+ });
181
+ }
182
+ export function transformReactDevtoolsHook({ code, id }) {
183
+ return transformWithOxcWalker({
184
+ code,
185
+ filename: id,
186
+ sourcemap: false,
187
+ createVisitor: createReactDevtoolsHookVisitor
188
+ });
189
+ }
190
+ export function removeRefreshPreambleGuard({ code, id }) {
191
+ return transformWithOxcWalker({
192
+ code,
193
+ filename: id,
194
+ sourcemap: false,
195
+ createVisitor: createRefreshPreambleGuardVisitor
196
+ });
193
197
  }
194
198
  // The refresh runtime module and the react-family modules are immutable for the server's
195
- // lifetime (they only change when the plugin is rebuilt), so their Babel transforms run once
196
- // per module and every build reuses the output.
197
- const fixRefreshRuntime = memoize(({ code, id }) => transformWithBabel(code, id, [rewriteRefreshRuntimeWindowAccess, injectRefreshGlobalHook], false), { getCacheKey: ({ code }) => code });
198
- const fixReactDevtoolsHook = memoize(({ code, id }) => transformWithBabel(code, id, [rewriteReactDevtoolsHookGlobal], false), { getCacheKey: ({ code }) => code });
199
+ // lifetime, so their Oxc parses run once per module and every build reuses the output.
200
+ const fixRefreshRuntime = memoize(transformRefreshRuntime, { getCacheKey: ({ code }) => code });
201
+ const fixReactDevtoolsHook = memoize(transformReactDevtoolsHook, { getCacheKey: ({ code }) => code });
@@ -0,0 +1,25 @@
1
+ import type { InputOptions, OutputOptions } from 'rolldown';
2
+ import { type DevEngine } from 'rolldown/experimental';
3
+ import type { ViteDevServer } from 'vite';
4
+ import type { VitePluginTaroOptions } from '../../../../options.ts';
5
+ export type BundledDevRolldownOptions = InputOptions & {
6
+ experimental?: {
7
+ [key: string]: unknown;
8
+ devMode?: boolean | Record<string, unknown>;
9
+ };
10
+ output?: OutputOptions | OutputOptions[];
11
+ };
12
+ export type BundledDev = {
13
+ _devEngine?: DevEngine;
14
+ getRolldownOptions(): Promise<BundledDevRolldownOptions>;
15
+ listen(): Promise<void>;
16
+ triggerBundleRegenerationIfStale(): Promise<boolean>;
17
+ };
18
+ /** Installs the physical WX output and runtime conventions over Vite's browser-oriented bundled-development options. */
19
+ export declare function installWxDevOptions({ bundledDev, server, options }: {
20
+ bundledDev: BundledDev;
21
+ server: ViteDevServer;
22
+ options: VitePluginTaroOptions;
23
+ }): Promise<void>;
24
+ /** Returns the configured output after rejecting states unsupported by the physical WX engine. */
25
+ export declare function requireSingleOutput(rolldownOptions: BundledDevRolldownOptions): OutputOptions;
@@ -0,0 +1,147 @@
1
+ import path from 'node:path';
2
+ import { build } from 'rolldown';
3
+ import { viteReporterPlugin } from 'rolldown/experimental';
4
+ import { once } from '../../../utils/once.js';
5
+ import { resolvePackageFile } from '../../../utils/packages.js';
6
+ import { appShellFileName } from '../module.js';
7
+ import { createWxDevMode } from './create-wx-dev-mode.js';
8
+ import { emptyOutputDirectory } from './empty-output-directory.js';
9
+ /** Installs the physical WX output and runtime conventions over Vite's browser-oriented bundled-development options. */
10
+ export function installWxDevOptions({ bundledDev, server, options }) {
11
+ // The buildEnd hook and its one-shot result belong to the same abstraction. Consumers can await startup without receiving
12
+ // a resolver capable of settling it externally.
13
+ const initialBuild = Promise.withResolvers();
14
+ const settleInitialBuild = once((error) => {
15
+ if (error) {
16
+ initialBuild.reject(error);
17
+ }
18
+ else {
19
+ initialBuild.resolve();
20
+ }
21
+ });
22
+ const original = bundledDev.getRolldownOptions.bind(bundledDev);
23
+ bundledDev.getRolldownOptions = async () => {
24
+ const rolldownOptions = await original();
25
+ const output = ensureSingleOutput(rolldownOptions);
26
+ const configuredOutput = server.config.build.rolldownOptions.output;
27
+ if (Array.isArray(configuredOutput)) {
28
+ throw new Error('wx development supports one configured Rolldown output.');
29
+ }
30
+ const configured = configuredOutput ?? {};
31
+ // Every page entry must depend on hmr/patches.js: DevTools classifies a changed Page dependency as Page JavaScript
32
+ // hot reload and re-executes live Pages, which is the only physical patch trigger that preserves the App heap.
33
+ const pageFiles = new Set(options.pages.map((page) => `${page.path}.js`));
34
+ Object.assign(output, configured, {
35
+ assetFileNames: createStableFileNames(configured.assetFileNames, 'assets/[name][extname]'),
36
+ banner: createEntryBanner(pageFiles),
37
+ chunkFileNames: createStableFileNames(configured.chunkFileNames, 'assets/[name].js'),
38
+ entryFileNames: createStableFileNames(configured.entryFileNames, '[name]'),
39
+ format: 'es',
40
+ minify: true,
41
+ sourcemap: false
42
+ });
43
+ rolldownOptions.experimental ??= {};
44
+ rolldownOptions.experimental.devMode = createWxDevMode(rolldownOptions.experimental.devMode, await bundleRuntimeSource());
45
+ const emptyOutputDirectoryPlugin = {
46
+ name: 'vpt:wx-empty-output-directory',
47
+ renderStart: {
48
+ order: 'pre',
49
+ // DevEngine bypasses Vite's build-only output preparation. Preserve the watched directory itself while
50
+ // removing stale contents before each complete physical render.
51
+ handler: () => emptyOutputDirectory(server.config.build.outDir)
52
+ }
53
+ };
54
+ const reportInitialBuildPlugin = {
55
+ name: 'vpt:wx-report-initial-build',
56
+ buildEnd: settleInitialBuild
57
+ };
58
+ rolldownOptions.plugins = [
59
+ emptyOutputDirectoryPlugin,
60
+ rolldownOptions.plugins,
61
+ reportInitialBuildPlugin,
62
+ createViteReporter(server)
63
+ ];
64
+ disableViteOxcSourcemap(rolldownOptions.plugins);
65
+ return rolldownOptions;
66
+ };
67
+ return initialBuild.promise;
68
+ }
69
+ /** Returns the configured output after rejecting states unsupported by the physical WX engine. */
70
+ export function requireSingleOutput(rolldownOptions) {
71
+ if (!rolldownOptions.output || Array.isArray(rolldownOptions.output)) {
72
+ throw new Error('wx development requires exactly one Rolldown output.');
73
+ }
74
+ return rolldownOptions.output;
75
+ }
76
+ /** Creates the one missing output object while rejecting a configured output array. */
77
+ function ensureSingleOutput(rolldownOptions) {
78
+ if (Array.isArray(rolldownOptions.output)) {
79
+ throw new Error('wx development requires one configured Rolldown output.');
80
+ }
81
+ rolldownOptions.output ??= {};
82
+ return rolldownOptions.output;
83
+ }
84
+ /**
85
+ * Prepends entry banners after Rolldown's analysis so their native requires remain physical dependencies rather than chunk
86
+ * graph edges. The App initializes the runtime identity, while every Page explicitly applies the watched patch data before its
87
+ * capsule import continues.
88
+ */
89
+ function createEntryBanner(pageFiles) {
90
+ return (chunk) => {
91
+ if (chunk.name === appShellFileName) {
92
+ return "__rolldown_runtime__.initialize(require('./hmr/info.js'));\n";
93
+ }
94
+ if (pageFiles.has(chunk.name)) {
95
+ const patchesPath = path.posix.relative(path.posix.dirname(chunk.fileName), 'hmr/patches.js');
96
+ return `__rolldown_runtime__.applyPatches(require('${patchesPath}'));\n`;
97
+ }
98
+ return '';
99
+ };
100
+ }
101
+ function createStableFileNames(addon, fallback) {
102
+ if (typeof addon === 'function') {
103
+ return (value) => toStableFileName(String(addon(value)));
104
+ }
105
+ return toStableFileName(typeof addon === 'string' ? addon : fallback);
106
+ }
107
+ function toStableFileName(fileName) {
108
+ return fileName
109
+ .replace(/(^|\/)\[hash(?::\d+)?\](?=\.|$)/g, '$1[name]')
110
+ .replace(/[-_.]\[hash(?::\d+)?\]/g, '')
111
+ .replace(/\[hash(?::\d+)?\]/g, '[name]');
112
+ }
113
+ function disableViteOxcSourcemap(pluginOption) {
114
+ if (Array.isArray(pluginOption)) {
115
+ pluginOption.forEach(disableViteOxcSourcemap);
116
+ return;
117
+ }
118
+ if (!pluginOption || typeof pluginOption !== 'object')
119
+ return;
120
+ const plugin = pluginOption;
121
+ if (plugin.name === 'builtin:vite-transform' && plugin._options?.transformOptions) {
122
+ plugin._options.transformOptions.sourcemap = false;
123
+ }
124
+ }
125
+ function createViteReporter(server) {
126
+ const { build, logger, root } = server.config;
127
+ return viteReporterPlugin({
128
+ assetsDir: path.join(build.assetsDir, '/'),
129
+ chunkLimit: 2000,
130
+ isLib: Boolean(build.lib),
131
+ isTty: Boolean(process.stdout.isTTY && !process.env.CI),
132
+ logInfo: (message) => logger.info(message),
133
+ reportCompressedSize: false,
134
+ root,
135
+ warnLargeChunks: false
136
+ });
137
+ }
138
+ // The runtime source is immutable for the host's lifetime, so the nested bundle is shared by every complete build.
139
+ const bundleRuntimeSource = once(async function bundleRuntimeSource() {
140
+ // write: false keeps this nested helper build from creating a second dist directory in the application project.
141
+ const result = await build({
142
+ input: resolvePackageFile('dist/runtime/wx/dev/dev-runtime.js'),
143
+ output: { format: 'iife', minify: true, sourcemap: false },
144
+ write: false
145
+ });
146
+ return result.output[0].code;
147
+ });
@@ -0,0 +1,21 @@
1
+ import { type WalkerEnter } from 'oxc-walker';
2
+ import { RolldownMagicString } from 'rolldown';
3
+ import type { AstTransformResult } from './transform.ts';
4
+ type OxcTransformOptions = {
5
+ code: string;
6
+ filename: string;
7
+ sourcemap: boolean;
8
+ createVisitor(editor: RolldownMagicString): WalkerEnter;
9
+ };
10
+ /**
11
+ * Parses once with Rolldown's Oxc parser and applies precise edits through its native Rust MagicString.
12
+ *
13
+ * These adapters previously used Babel plugins for one or two local range changes. Babel then
14
+ * cloned and regenerated the complete module, which was unnecessary work for large dependency
15
+ * sources and changed formatting outside the intended edit. Sharing Rolldown's parser and editor
16
+ * keeps one AST implementation across Vite's build engine and these transforms while preserving
17
+ * untouched source exactly. Callers provide only a visitor, so parsing, diagnostics, source-map
18
+ * policy, and result normalization cannot drift between adapters.
19
+ */
20
+ export declare function transformWithOxcWalker({ code, filename, sourcemap, createVisitor }: OxcTransformOptions): AstTransformResult;
21
+ export {};
@@ -0,0 +1,58 @@
1
+ import { walk } from 'oxc-walker';
2
+ import { RolldownMagicString } from 'rolldown';
3
+ import { parseSync } from 'rolldown/utils';
4
+ /**
5
+ * Parses once with Rolldown's Oxc parser and applies precise edits through its native Rust MagicString.
6
+ *
7
+ * These adapters previously used Babel plugins for one or two local range changes. Babel then
8
+ * cloned and regenerated the complete module, which was unnecessary work for large dependency
9
+ * sources and changed formatting outside the intended edit. Sharing Rolldown's parser and editor
10
+ * keeps one AST implementation across Vite's build engine and these transforms while preserving
11
+ * untouched source exactly. Callers provide only a visitor, so parsing, diagnostics, source-map
12
+ * policy, and result normalization cannot drift between adapters.
13
+ */
14
+ export function transformWithOxcWalker({ code, filename, sourcemap, createVisitor }) {
15
+ // `RolldownMagicString` stores edits in Rolldown's native layer. A separate `magic-string`
16
+ // instance would duplicate a dependency already supplied by the build engine and would make
17
+ // source-map generation cross the JavaScript boundary for every recorded segment.
18
+ const editor = new RolldownMagicString(code, { filename });
19
+ const result = parseSync(filename, code);
20
+ // Oxc can return a recoverable AST together with diagnostics. Walking that partial tree could
21
+ // let a visitor edit malformed input and hide the original syntax failure, so diagnostics are
22
+ // rejected before any caller-owned edits run.
23
+ if (result.errors.length > 0) {
24
+ const diagnostics = result.errors.map((error) => error.message).join('; ');
25
+ throw new Error(`Failed to parse ${filename} with Oxc: ${diagnostics}`);
26
+ }
27
+ // All visitor work is O(n) in AST size; range edits remain local and do not trigger a second
28
+ // parse or whole-file code-generation pass.
29
+ walk(result.program, { enter: createVisitor(editor) });
30
+ // Boundary-resolution maps retain exact mappings around edits without the much larger cost
31
+ // of mapping every character. Original content is required for Vite to compose this map with
32
+ // subsequent optimizer or application transforms.
33
+ const generatedMap = sourcemap
34
+ ? editor.generateMap({
35
+ file: filename,
36
+ hires: 'boundary',
37
+ includeContent: true,
38
+ source: filename
39
+ })
40
+ : null;
41
+ return {
42
+ code: editor.toString(),
43
+ map: generatedMap
44
+ ? {
45
+ // Return a plain source-map value instead of leaking Rolldown's native wrapper
46
+ // through Vite's plugin API. This also keeps the result identical in the normal
47
+ // application pipeline and the optimizer's independent Rolldown build.
48
+ version: generatedMap.version,
49
+ file: generatedMap.file,
50
+ sources: generatedMap.sources,
51
+ sourcesContent: generatedMap.sourcesContent,
52
+ names: generatedMap.names,
53
+ mappings: generatedMap.mappings,
54
+ ...(generatedMap.x_google_ignoreList ? { x_google_ignoreList: generatedMap.x_google_ignoreList } : {})
55
+ }
56
+ : null
57
+ };
58
+ }
@@ -1,9 +1,13 @@
1
- /** Runs recoverable background tasks in insertion order and reports failures without blocking later work. */
1
+ /** Runs asynchronous tasks in insertion order without letting one failure block later work. */
2
2
  export declare class SerializedTaskQueue {
3
+ /** Mutable promise tail confines ordering state to this queue. */
3
4
  private tail;
4
5
  private readonly reportError;
5
6
  constructor(reportError: (operation: string, error: unknown) => void);
6
- enqueue(operation: string, task: () => Promise<void>): void;
7
+ /** Schedules a background task and reports its failure. */
8
+ enqueue(operation: string, task: () => void | PromiseLike<void>): void;
9
+ /** Schedules a task whose result and failure belong to the caller. */
10
+ run<Result>(task: () => Result | PromiseLike<Result>): Promise<Result>;
7
11
  /** Waits for every task that was queued when this method was called. */
8
12
  waitForIdle(): Promise<void>;
9
13
  }
@@ -1,15 +1,23 @@
1
- /** Runs recoverable background tasks in insertion order and reports failures without blocking later work. */
1
+ /** Runs asynchronous tasks in insertion order without letting one failure block later work. */
2
2
  export class SerializedTaskQueue {
3
+ /** Mutable promise tail confines ordering state to this queue. */
3
4
  tail = Promise.resolve();
4
5
  reportError;
5
6
  constructor(reportError) {
6
7
  this.reportError = reportError;
7
8
  }
9
+ /** Schedules a background task and reports its failure. */
8
10
  enqueue(operation, task) {
9
- this.tail = this.tail.then(task).catch((error) => {
11
+ void this.run(task).catch((error) => {
10
12
  this.reportError(operation, error);
11
13
  });
12
14
  }
15
+ /** Schedules a task whose result and failure belong to the caller. */
16
+ run(task) {
17
+ const result = this.tail.then(task);
18
+ this.tail = result.then(() => undefined, () => undefined);
19
+ return result;
20
+ }
13
21
  /** Waits for every task that was queued when this method was called. */
14
22
  async waitForIdle() {
15
23
  await this.tail;
@@ -13,22 +13,27 @@ function forward(handler, receiver, args) {
13
13
  // DevTools re-executes the page and replays the replacement lifecycle on every edit. Taro's
14
14
  // onUnload unmounts the React tree and onLoad mounts a fresh one, destroying state. While
15
15
  // the runtime says a patch was just delivered, the pair is intercepted instead: onUnload
16
- // captures the surviving page identity, and onLoad restores it onto the replacement native
17
- // instance and rebinds the retained tree to it (pageElement.ctx + a full re-render). The
18
- // capsule module is cached across re-executions, so this closure holds the capture.
16
+ // captures the surviving page identity and its current native render data, and onLoad
17
+ // immediately paints that snapshot before rebinding and fully synchronizing the retained
18
+ // tree. The capsule module is cached across re-executions, so this closure holds the capture.
19
19
  // Ordinary navigation (no patch) passes through unchanged. The wrappers are regular
20
20
  // functions so the native page instance (`this`) reaches the original handlers.
21
21
  if (typeof __rolldown_runtime__ !== 'undefined') {
22
22
  const originalOnUnload = config.onUnload;
23
23
  const originalOnLoad = config.onLoad;
24
24
  const originalOnShow = config.onShow;
25
+ // This is the sole cross-instance handoff. It is mutable because DevTools destroys the
26
+ // old native Page before creating its replacement; ordinary navigation never reads it.
25
27
  let captured;
26
28
  config.onUnload = function (...args) {
27
29
  if (__rolldown_runtime__.isHotReloading()) {
28
30
  const instance = this;
29
31
  captured = {
30
32
  $taroPath: instance.$taroPath,
31
- $taroParams: instance.$taroParams
33
+ $taroParams: instance.$taroParams,
34
+ // WeChat owns this serializable view-model snapshot. Retaining it is cheaper
35
+ // than deep-cloning the complete recursive Taro node tree before every edit.
36
+ data: instance.data
32
37
  };
33
38
  return;
34
39
  }
@@ -36,10 +41,14 @@ if (typeof __rolldown_runtime__ !== 'undefined') {
36
41
  };
37
42
  config.onLoad = function (...args) {
38
43
  if (__rolldown_runtime__.isHotReloading() && captured) {
44
+ const instance = this;
45
+ // Paint the last native projection as the first bridge operation. Taro's full
46
+ // performUpdate below runs in a timer, so this direct setData removes most of the
47
+ // empty-page interval without delaying synchronization to the current tree.
48
+ instance.setData(captured.data);
39
49
  // Restore the surviving page identity onto the replacement native instance: the
40
50
  // re-loaded $taroPath embeds a fresh timestamp, so without this the tree and the
41
51
  // native side would disagree on the page key.
42
- const instance = this;
43
52
  instance.$taroPath = captured.$taroPath;
44
53
  instance.$taroParams = captured.$taroParams;
45
54
  // Rebind the retained tree to the replacement receiver and resync the native