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