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,14 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import colors from 'picocolors';
3
- import { build } from 'rolldown';
4
- import { dev, viteReporterPlugin } from 'rolldown/experimental';
5
- import { once } from '../../../utils/once.js';
6
- import { resolvePackageFile } from '../../../utils/packages.js';
7
- import { appShellFileName } from '../module.js';
8
- import { createWxDevMode } from './create-wx-dev-mode.js';
9
- import { emptyOutputDirectory } from './empty-output-directory.js';
3
+ import { dev } from 'rolldown/experimental';
4
+ import { SerializedTaskQueue } from '../../../utils/serialized-task-queue.js';
10
5
  import { hmrControlPath, hmrInfoFileName, hmrPatchesFileName, renderHmrInfo, renderInitialHmrPatches, writeHmrFile } from './hmr-files.js';
11
6
  import { PatchPublisher } from './patch-publisher.js';
7
+ import { installWxDevOptions, requireSingleOutput } from './wx-dev-options.js';
12
8
  /**
13
9
  * Creates the wx dev host: the adapter that owns the physical Rolldown DevEngine (created
14
10
  * with dev(...)) and the patch publisher, and replaces Vite's bundledDev.listen so the
@@ -17,20 +13,13 @@ import { PatchPublisher } from './patch-publisher.js';
17
13
  */
18
14
  export async function createWxDevHost({ server, options }) {
19
15
  const bundledDev = getBundledDev(server);
20
- // DevEngine does not reject run() after an initial plugin failure, so settle startup from its first buildEnd result.
21
- // Later build errors belong to the running server and continue through onOutput/onHmrUpdates.
22
- const initialBuild = Promise.withResolvers();
23
- const settleInitialBuild = once((error) => {
24
- if (error) {
25
- initialBuild.reject(error);
26
- }
27
- else {
28
- initialBuild.resolve();
29
- }
30
- });
16
+ // Rolldown invokes output callbacks without awaiting their promises. This queue is the single owner of mutable HMR host
17
+ // state and physical metadata writes, preventing a later patch or build identity from being overwritten by older work.
18
+ const hostTasks = new SerializedTaskQueue((operation, error) => logWxError(server.config.logger, operation, error));
31
19
  const publisher = new PatchPublisher((content) => writeHmrFile(server.config.build.outDir, hmrPatchesFileName, content));
32
- // Must install rolldown options before create engine
33
- installRolldownOptions();
20
+ // DevEngine does not reject run() after an initial plugin failure. The options layer owns a first-build buildEnd barrier
21
+ // and exposes only its result; later build errors continue independently through onOutput and onHmrUpdates.
22
+ const initialBuild = installWxDevOptions({ bundledDev, server, options });
34
23
  const engine = await createEngine();
35
24
  // The wx dev host owns the only DevEngine. Vite's default listen() would create a second
36
25
  // skip-write engine that renders into memory for browser HMR; instead the physical
@@ -39,30 +28,27 @@ export async function createWxDevHost({ server, options }) {
39
28
  bundledDev._devEngine = engine;
40
29
  bundledDev.triggerBundleRegenerationIfStale = async () => false;
41
30
  bundledDev.listen = async () => {
42
- await Promise.all([engine.run(), initialBuild.promise]);
31
+ await Promise.all([engine.run(), initialBuild]);
43
32
  await engine.ensureCurrentBuildFinish();
44
33
  };
45
34
  // Vite binds the port only after initServer (and therefore the initial build) completes, so
46
35
  // the actual port is not observable while onOutput runs for the first build. The App metadata
47
36
  // is written once the port is real; later full builds rewrite it from onOutput.
48
- server.httpServer?.once('listening', startFreshBuild);
37
+ server.httpServer?.once('listening', () => {
38
+ hostTasks.enqueue('wx HMR initialization failed', rotateBuildSession);
39
+ });
49
40
  // The runtime's metadata-only reports land on the control path; the buildId in each report
50
41
  // IS the Rolldown client ID.
51
42
  server.middlewares.use(hmrControlPath, (req, res) => void handleReport(req, res));
52
- // Append the DevTools project directory line after Vite's own startup banner: DevTools
53
- // opens the output directory directly, so the printed path is the one to select.
54
- const originalPrintUrls = server.printUrls.bind(server);
55
- server.printUrls = () => {
56
- originalPrintUrls();
57
- server.config.logger.info(` ${colors.green('➜')} ${colors.bold('WeChat DevTools')}: ${colors.cyan(relativeToViteConfig(server.config.build.outDir, server.config.configFile, server.config.root))}`);
58
- };
43
+ installDevToolsPrinter(server);
59
44
  return {
60
45
  close: async () => {
46
+ await hostTasks.waitForIdle();
61
47
  await engine.close();
62
48
  }
63
49
  };
64
50
  /** Rotates the build identity and materializes the App metadata for it. */
65
- async function startFreshBuild() {
51
+ async function rotateBuildSession() {
66
52
  const port = boundPort(server);
67
53
  if (port === undefined) {
68
54
  // The initial output finishes before Vite binds its port. The listening listener
@@ -74,10 +60,7 @@ export async function createWxDevHost({ server, options }) {
74
60
  await engine.removeClient(previousBuildId);
75
61
  }
76
62
  await engine.registerClient(buildId);
77
- // Publish info last: a heap that observes the new identity is guaranteed to see the
78
- // matching reset patch file.
79
- await writeHmrFile(server.config.build.outDir, hmrPatchesFileName, renderInitialHmrPatches());
80
- await writeHmrInfo(server, buildId, port);
63
+ await publishBuildMetadata(server, buildId, port);
81
64
  }
82
65
  async function handleReport(req, res) {
83
66
  if (req.method !== 'POST') {
@@ -87,21 +70,7 @@ export async function createWxDevHost({ server, options }) {
87
70
  }
88
71
  try {
89
72
  const report = JSON.parse(await readBody(req));
90
- // Only the current build's reports can advance physical delivery;
91
- // delayed reports from older builds are ignored so they can never influence the
92
- // live build.
93
- if (!publisher.isCurrentBuild(report.buildId)) {
94
- res.end();
95
- return;
96
- }
97
- if (report.kind === 'rebuild') {
98
- engine.triggerFullBuild();
99
- res.end();
100
- return;
101
- }
102
- // Commit every newly acknowledged Rolldown payload to this client's ship map.
103
- const deliveredFiles = publisher.acknowledge(report.seq);
104
- await Promise.all(deliveredFiles.map((fileName) => engine.notifyPayloadDelivered(fileName)));
73
+ await hostTasks.run(() => processReport(report));
105
74
  res.end();
106
75
  }
107
76
  catch (e) {
@@ -110,105 +79,89 @@ export async function createWxDevHost({ server, options }) {
110
79
  res.end();
111
80
  }
112
81
  }
82
+ /** Applies one runtime receipt to the active physical patch history. */
83
+ function processReport(report) {
84
+ // Delayed reports from older builds must never prune the live build's cumulative patch history.
85
+ if (!publisher.isCurrentBuild(report.buildId)) {
86
+ return;
87
+ }
88
+ switch (report.kind) {
89
+ case 'rebuild': {
90
+ server.config.logger.info(`[vpt] wx runtime requested a full rebuild: ${report.reason}`);
91
+ engine.triggerFullBuild();
92
+ return;
93
+ }
94
+ case 'applied': {
95
+ publisher.acknowledge(report.seq);
96
+ return;
97
+ }
98
+ }
99
+ }
113
100
  /** Creates the physical DevEngine with the wx dev host hooks. */
114
101
  async function createEngine() {
115
102
  const rolldownOptions = await bundledDev.getRolldownOptions();
116
- if (!rolldownOptions.output || Array.isArray(rolldownOptions.output)) {
117
- throw new Error('wx development requires exactly one Rolldown output.');
118
- }
119
- return dev(rolldownOptions, rolldownOptions.output, {
120
- onHmrUpdates: async (result) => {
121
- if (result instanceof Error) {
122
- logWxError(server.config.logger, 'wx HMR update failed', result);
123
- return;
124
- }
125
- // Collect the current client's batch first: one HMR event can carry several
126
- // client envelopes, while only the active build may enter its patch history.
127
- const batch = [];
128
- for (const { clientId, update } of result.updates) {
129
- // Removed and delayed client sessions can still appear in a completed
130
- // engine batch; they must never enter the current build's patch history.
131
- if (!publisher.isCurrentBuild(clientId) || update.type === 'Noop') {
132
- continue;
133
- }
134
- if (update.type === 'Patch') {
135
- batch.push(update);
136
- continue;
137
- }
138
- server.config.logger.info(`[vpt] wx full rebuild required${update.reason ? `: ${update.reason}` : ''}`);
139
- engine.triggerFullBuild();
140
- return;
141
- }
142
- if (batch.length > 0) {
143
- await publisher.produce(batch);
144
- // server.config.logger.info('[vpt] wx patch produced')
145
- }
146
- },
147
- onOutput: async (result) => {
148
- if (result instanceof Error) {
149
- logWxError(server.config.logger, 'wx dev build failed', result);
150
- return;
151
- }
152
- // A fresh build identity per complete physical build; the App runtime reads it
153
- // from hmr/info.js before any module registers.
154
- await startFreshBuild();
155
- },
103
+ const output = requireSingleOutput(rolldownOptions);
104
+ return dev(rolldownOptions, output, {
105
+ onHmrUpdates: handleHmrUpdates,
106
+ onOutput: handleDevOutput,
156
107
  rebuildStrategy: 'never',
157
108
  watch: { skipWrite: false }
158
109
  });
159
110
  }
160
- /** Restores physical Mini Program output conventions after Vite applies browser bundled-dev defaults. */
161
- function installRolldownOptions() {
162
- const original = bundledDev.getRolldownOptions.bind(bundledDev);
163
- bundledDev.getRolldownOptions = async () => {
164
- const rolldownOptions = await original();
165
- if (Array.isArray(rolldownOptions.output)) {
166
- throw new Error('wx development requires one configured Rolldown output.');
111
+ /** Converts Rolldown's non-awaited callback into one ordered host publication task. */
112
+ function handleHmrUpdates(result) {
113
+ if (result instanceof Error) {
114
+ logWxError(server.config.logger, 'wx HMR update failed', result);
115
+ return;
116
+ }
117
+ hostTasks.enqueue('wx HMR publication failed', () => publishUpdates(result));
118
+ }
119
+ /** Publishes only the active client's patches or requests the complete build required by Rolldown. */
120
+ async function publishUpdates(result) {
121
+ const batch = [];
122
+ for (const { clientId, update } of result.updates) {
123
+ if (!publisher.isCurrentBuild(clientId) || update.type === 'Noop') {
124
+ continue;
167
125
  }
168
- rolldownOptions.output ??= {};
169
- const output = rolldownOptions.output;
170
- const configuredOutput = server.config.build.rolldownOptions.output;
171
- if (Array.isArray(configuredOutput)) {
172
- throw new Error('wx development supports one configured Rolldown output.');
126
+ if (update.type === 'Patch') {
127
+ batch.push(update);
128
+ continue;
173
129
  }
174
- // Every page entry must depend on hmr/patches.js: DevTools classifies a changed Page
175
- // dependency as Page JavaScript hot reload and re-executes live Pages, which is the only
176
- // trigger that delivers physical patches while keeping the App heap alive.
177
- const pageFiles = new Set(options.pages.map((page) => `${page.path}.js`));
178
- const configured = (configuredOutput ?? {});
179
- Object.assign(output, configured, {
180
- assetFileNames: createStableFileNames(configured.assetFileNames, 'assets/[name][extname]'),
181
- banner: createEntryBanner(pageFiles),
182
- chunkFileNames: createStableFileNames(configured.chunkFileNames, 'assets/[name].js'),
183
- entryFileNames: createStableFileNames(configured.entryFileNames, '[name]'),
184
- format: 'es',
185
- minify: true,
186
- sourcemap: false
187
- });
188
- rolldownOptions.experimental ??= {};
189
- rolldownOptions.experimental.devMode = createWxDevMode(rolldownOptions.experimental.devMode, await bundleRuntimeSource());
190
- const emptyOutputDirectoryPlugin = {
191
- name: 'vpt:wx-empty-output-directory',
192
- renderStart: {
193
- order: 'pre',
194
- // DevEngine bypasses Vite's build-only output preparation. Clear stale files before every complete
195
- // physical render while retaining the directory watched by WeChat DevTools.
196
- handler: () => emptyOutputDirectory(server.config.build.outDir)
197
- }
198
- };
199
- const reportInitialBuildPlugin = {
200
- name: 'vpt:wx-report-initial-build',
201
- buildEnd: settleInitialBuild
202
- };
203
- rolldownOptions.plugins = [
204
- emptyOutputDirectoryPlugin,
205
- rolldownOptions.plugins,
206
- reportInitialBuildPlugin,
207
- createViteReporter(server)
208
- ];
209
- disableViteOxcSourcemap(rolldownOptions.plugins);
210
- return rolldownOptions;
211
- };
130
+ server.config.logger.info(`[vpt] wx full rebuild required${update.reason ? `: ${update.reason}` : ''}`);
131
+ engine.triggerFullBuild();
132
+ return;
133
+ }
134
+ if (batch.length === 0) {
135
+ return;
136
+ }
137
+ // The physical file must exist before Rolldown advances: once committed, later patches may be generated relative to
138
+ // this batch even if DevTools has not observed its file event yet. PatchPublisher keeps the unapplied range cumulative,
139
+ // so any later file generation still carries every factory needed to bridge the runtime's older application frontier.
140
+ await publisher.produce(batch);
141
+ await commitPublishedBatch(batch);
142
+ }
143
+ /**
144
+ * Advances Rolldown's published frontier in the same sequence order materialized in the cumulative physical file.
145
+ *
146
+ * Given a batch [5, 6], publisher.produce has already made factories [5, 6] visible in hmr/patches.js. This method then
147
+ * commits payload 5 followed by payload 6. If DevTools observes neither event before sequence 7 is published, the next
148
+ * physical file contains [5, 6, 7], while Rolldown is free to generate 7 relative to its already-published sequence 6.
149
+ */
150
+ async function commitPublishedBatch(batch) {
151
+ // Do not use Promise.all or deduplicate filenames. Multiple sequential payloads may target the same output filename,
152
+ // and each notification commits one distinct Rolldown payload. Awaiting in order preserves the exact frontier encoded
153
+ // by PatchUpdate.seq and prevents a later payload from becoming visible to the engine before its predecessor.
154
+ for (const patch of batch) {
155
+ await engine.notifyPayloadDelivered(patch.filename);
156
+ }
157
+ }
158
+ /** Rotates metadata after each successful complete output. */
159
+ function handleDevOutput(result) {
160
+ if (result instanceof Error) {
161
+ logWxError(server.config.logger, 'wx dev build failed', result);
162
+ return;
163
+ }
164
+ hostTasks.enqueue('wx dev build finalization failed', rotateBuildSession);
212
165
  }
213
166
  }
214
167
  /** The bound HTTP port, or undefined before Vite's server is listening. */
@@ -223,14 +176,23 @@ function boundPort(server) {
223
176
  }
224
177
  return address.port;
225
178
  }
226
- /** Writes the immutable App metadata every full build starts from. */
227
- async function writeHmrInfo(server, buildId, port) {
179
+ /** Resets physical patches before exposing the matching immutable build identity to a new App heap. */
180
+ async function publishBuildMetadata(server, buildId, port) {
228
181
  const info = {
229
182
  buildId,
230
183
  endpoint: `${server.config.server.https ? 'https' : 'http'}://${resolveEndpointHost(server)}:${port}${hmrControlPath}`
231
184
  };
185
+ await writeHmrFile(server.config.build.outDir, hmrPatchesFileName, renderInitialHmrPatches());
232
186
  await writeHmrFile(server.config.build.outDir, hmrInfoFileName, renderHmrInfo(info));
233
187
  }
188
+ /** Appends the physical project directory after Vite's normal server URLs. */
189
+ function installDevToolsPrinter(server) {
190
+ const originalPrintUrls = server.printUrls.bind(server);
191
+ server.printUrls = () => {
192
+ originalPrintUrls();
193
+ server.config.logger.info(` ${colors.green('➜')} ${colors.bold('WeChat DevTools')}: ${colors.cyan(relativeToViteConfig(server.config.build.outDir, server.config.configFile, server.config.root))}`);
194
+ };
195
+ }
234
196
  /**
235
197
  * The project directory shown in the DevTools banner, relative to the Vite config:
236
198
  * `dist/wx` instead of the absolute output path, so it can be pasted into DevTools.
@@ -251,20 +213,6 @@ function logWxError(logger, prefix, error) {
251
213
  logger.error(`[vpt] ${prefix} with unknown error: ${error}`);
252
214
  }
253
215
  }
254
- // The runtime source is immutable for the host's lifetime (it changes only when the
255
- // plugin is rebuilt), so the nested bundle runs once and every build reuses it.
256
- const bundleRuntimeSource = once(
257
- /** Bundles the runtime host and state machine into one plain script for injection. */
258
- async function bundleRuntimeSource() {
259
- // write: false — only the code is consumed; without it rolldown drops the bundle into
260
- // the default dist/ of the running project.
261
- const result = await build({
262
- input: resolvePackageFile('dist/runtime/wx/dev/dev-runtime.js'),
263
- output: { format: 'iife', minify: true, sourcemap: false },
264
- write: false
265
- });
266
- return result.output[0].code;
267
- });
268
216
  const maximumBodyBytes = 64 * 1024;
269
217
  function readBody(req) {
270
218
  return new Promise((resolve, reject) => {
@@ -292,67 +240,6 @@ function resolveEndpointHost(server) {
292
240
  }
293
241
  return address.address.includes(':') ? `[${address.address}]` : address.address;
294
242
  }
295
- /**
296
- * Prepends entry banners. Banners are plain text appended after Rolldown's analysis, so the
297
- * requires never become chunk dependencies (a bare require inside the injected runtime source
298
- * would stall the build), and the wx render pipeline keeps this text after the hoisted chunk
299
- * requires — so the runtime chunk exists before these run:
300
- * - the app entry loads hmr/info.js and initializes the runtime before any module registers;
301
- * - every page requires hmr/patches.js, the changed dependency that makes DevTools re-execute
302
- * live Pages and thereby load physical updates.
303
- */
304
- function createEntryBanner(pageFiles) {
305
- return (chunk) => {
306
- if (chunk.name === appShellFileName) {
307
- return "__rolldown_runtime__.initialize(require('./hmr/info.js'));\n";
308
- }
309
- if (pageFiles.has(chunk.name)) {
310
- // Page files live at `pages/<route>/index.js`, so the dependency path must be
311
- // computed relative to each page's own directory.
312
- const patchesPath = path.posix.relative(path.posix.dirname(chunk.fileName), 'hmr/patches.js');
313
- return `require('${patchesPath}');\n`;
314
- }
315
- return '';
316
- };
317
- }
318
- function createStableFileNames(addon, fallback) {
319
- if (typeof addon === 'function') {
320
- return (value) => toStableFileName(String(addon(value)));
321
- }
322
- return toStableFileName(typeof addon === 'string' ? addon : fallback);
323
- }
324
- function toStableFileName(fileName) {
325
- return fileName
326
- .replace(/(^|\/)\[hash(?::\d+)?\](?=\.|$)/g, '$1[name]')
327
- .replace(/[-_.]\[hash(?::\d+)?\]/g, '')
328
- .replace(/\[hash(?::\d+)?\]/g, '[name]');
329
- }
330
- function disableViteOxcSourcemap(pluginOption) {
331
- if (Array.isArray(pluginOption)) {
332
- pluginOption.forEach(disableViteOxcSourcemap);
333
- return;
334
- }
335
- if (!pluginOption || typeof pluginOption !== 'object') {
336
- return;
337
- }
338
- const plugin = pluginOption;
339
- if (plugin.name === 'builtin:vite-transform' && plugin._options?.transformOptions) {
340
- plugin._options.transformOptions.sourcemap = false;
341
- }
342
- }
343
- function createViteReporter(server) {
344
- const { build, logger, root } = server.config;
345
- return viteReporterPlugin({
346
- assetsDir: path.join(build.assetsDir, '/'),
347
- chunkLimit: 2000,
348
- isLib: Boolean(build.lib),
349
- isTty: Boolean(process.stdout.isTTY && !process.env.CI),
350
- logInfo: (message) => logger.info(message),
351
- reportCompressedSize: false,
352
- root,
353
- warnLargeChunks: false
354
- });
355
- }
356
243
  function getBundledDev(server) {
357
244
  const bundledDev = server.environments.client.bundledDev;
358
245
  if (!bundledDev) {
@@ -17,12 +17,11 @@ export declare function renderHmrInfo(info: HmrInfo): string;
17
17
  /** Provides a valid dependency before the host has a patch range to publish. */
18
18
  export declare function renderInitialHmrPatches(): string;
19
19
  /**
20
- * Renders the missing patch suffix as a passive physical delivery module.
20
+ * Renders the cumulative patch suffix as inert CommonJS data.
21
21
  *
22
- * DevTools re-executes the Page because this file changed. The module only stores the literal
23
- * Rolldown factories in the persistent App runtime; storePatches applies them synchronously,
24
- * so the Page's imports below the require resolve against the freshly registered modules.
22
+ * DevTools re-executes the Page because this dependency changed. The Page entry passes the exported payload to the persistent
23
+ * App runtime synchronously before importing its capsule, keeping delivery explicit and leaving this file free of side effects.
25
24
  */
26
25
  export declare function renderHmrPatches(buildId: string, patches: readonly PatchUpdate[]): string;
27
- /** Publishes one physical HMR file (direct write; atomic rename is reserved for later analysis). */
26
+ /** Atomically publishes one physical HMR module so DevTools can observe only complete JavaScript generations. */
28
27
  export declare function writeHmrFile(outDir: string, fileName: string, source: string): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  export const hmrInfoFileName = 'hmr/info.js';
@@ -13,22 +14,31 @@ export function renderInitialHmrPatches() {
13
14
  return 'module.exports = undefined;\n';
14
15
  }
15
16
  /**
16
- * Renders the missing patch suffix as a passive physical delivery module.
17
+ * Renders the cumulative patch suffix as inert CommonJS data.
17
18
  *
18
- * DevTools re-executes the Page because this file changed. The module only stores the literal
19
- * Rolldown factories in the persistent App runtime; storePatches applies them synchronously,
20
- * so the Page's imports below the require resolve against the freshly registered modules.
19
+ * DevTools re-executes the Page because this dependency changed. The Page entry passes the exported payload to the persistent
20
+ * App runtime synchronously before importing its capsule, keeping delivery explicit and leaving this file free of side effects.
21
21
  */
22
22
  export function renderHmrPatches(buildId, patches) {
23
23
  if (patches.length === 0) {
24
24
  throw new Error('Cannot render an empty WX patch range.');
25
25
  }
26
26
  const rendered = patches.map((patch) => `{seq: ${patch.seq}, changedIds: ${JSON.stringify(patch.changedIds)}, factory: () => {\n${patch.code}\n}}`);
27
- return `__rolldown_runtime__.storePatches({buildId: ${JSON.stringify(buildId)}, patches: [${rendered.join(',')}]});\n`;
27
+ return `module.exports = {buildId: ${JSON.stringify(buildId)}, patches: [${rendered.join(',')}]};\n`;
28
28
  }
29
- /** Publishes one physical HMR file (direct write; atomic rename is reserved for later analysis). */
29
+ /** Atomically publishes one physical HMR module so DevTools can observe only complete JavaScript generations. */
30
30
  export async function writeHmrFile(outDir, fileName, source) {
31
31
  const filePath = path.join(outDir, fileName);
32
- await fs.mkdir(path.dirname(filePath), { recursive: true });
33
- await fs.writeFile(filePath, source);
32
+ const directory = path.dirname(filePath);
33
+ await fs.mkdir(directory, { recursive: true });
34
+ const temporaryPath = path.join(directory, `.${path.basename(filePath)}.${randomUUID()}.txt`);
35
+ try {
36
+ // DevTools ignores the temporary .txt file; keeping it beside the destination guarantees a same-filesystem rename.
37
+ await fs.writeFile(temporaryPath, source);
38
+ await fs.rename(temporaryPath, filePath);
39
+ }
40
+ finally {
41
+ // rename removes the source path on success; force cleanup covers interrupted writes and failed replacements.
42
+ await fs.rm(temporaryPath, { force: true });
43
+ }
34
44
  }
@@ -1,11 +1,23 @@
1
1
  import { type PatchUpdate } from './hmr-files.ts';
2
2
  /** Abstracts the physical patches write; the engine owns the file destination. */
3
3
  export type WritePatches = (content: string) => Promise<void>;
4
- /** Owns one build's unacknowledged patches and physical publish decision. */
4
+ /**
5
+ * Owns the cumulative sequence range between the host's published frontier and the runtime's applied frontier.
6
+ *
7
+ * For example, if the runtime has applied sequence 4:
8
+ *
9
+ * 1. produce([5, 6]) writes [5, 6];
10
+ * 2. produce([7]) before an application report writes [5, 6, 7], so DevTools may miss the first file event safely;
11
+ * 3. acknowledge(6) retains [7];
12
+ * 4. produce([8]) then writes [7, 8], exactly the suffix the runtime still needs.
13
+ *
14
+ * The host may advance Rolldown's published frontier through 8 immediately after those writes, but this history is pruned only
15
+ * through 6 until the runtime reports successful application. Published sequence and applied sequence are deliberately distinct.
16
+ */
5
17
  export declare class PatchPublisher {
6
18
  private readonly writePatches;
7
19
  private buildId;
8
- /** Executable updates retained only until the runtime acknowledges physical delivery. */
20
+ /** Executable updates retained until the runtime confirms successful application. */
9
21
  private readonly pendingPatches;
10
22
  constructor(writePatches: WritePatches);
11
23
  /** True when the buildId belongs to the current full build. */
@@ -15,8 +27,16 @@ export declare class PatchPublisher {
15
27
  buildId: string;
16
28
  previousBuildId: string | undefined;
17
29
  }>;
18
- /** Appends a Rolldown batch and publishes every unacknowledged patch. */
30
+ /**
31
+ * Appends a Rolldown batch and publishes the complete range not yet applied by the runtime.
32
+ *
33
+ * Resolution means the cumulative JavaScript generation is physically visible. The host may then advance Rolldown's
34
+ * published frontier immediately; pendingPatches remains intact until an application report proves the runtime caught up.
35
+ */
19
36
  produce(patches: readonly PatchUpdate[]): Promise<void>;
20
- /** Removes the pending prefix covered by the runtime's Rolldown sequence receipt. */
21
- acknowledge(seq: number): string[];
37
+ /**
38
+ * Removes only the prefix covered by the runtime's successful application frontier. Publication alone never calls this:
39
+ * retaining the gap between published and applied frontiers is what lets DevTools safely miss intermediate file events.
40
+ */
41
+ acknowledge(seq: number): void;
22
42
  }
@@ -1,10 +1,22 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { renderHmrPatches } from './hmr-files.js';
3
- /** Owns one build's unacknowledged patches and physical publish decision. */
3
+ /**
4
+ * Owns the cumulative sequence range between the host's published frontier and the runtime's applied frontier.
5
+ *
6
+ * For example, if the runtime has applied sequence 4:
7
+ *
8
+ * 1. produce([5, 6]) writes [5, 6];
9
+ * 2. produce([7]) before an application report writes [5, 6, 7], so DevTools may miss the first file event safely;
10
+ * 3. acknowledge(6) retains [7];
11
+ * 4. produce([8]) then writes [7, 8], exactly the suffix the runtime still needs.
12
+ *
13
+ * The host may advance Rolldown's published frontier through 8 immediately after those writes, but this history is pruned only
14
+ * through 6 until the runtime reports successful application. Published sequence and applied sequence are deliberately distinct.
15
+ */
4
16
  export class PatchPublisher {
5
17
  writePatches;
6
18
  buildId;
7
- /** Executable updates retained only until the runtime acknowledges physical delivery. */
19
+ /** Executable updates retained until the runtime confirms successful application. */
8
20
  pendingPatches = [];
9
21
  // Explicit field assignment: node --test strips types and does not support parameter properties.
10
22
  constructor(writePatches) {
@@ -22,19 +34,28 @@ export class PatchPublisher {
22
34
  this.pendingPatches.length = 0;
23
35
  return { buildId, previousBuildId };
24
36
  }
25
- /** Appends a Rolldown batch and publishes every unacknowledged patch. */
37
+ /**
38
+ * Appends a Rolldown batch and publishes the complete range not yet applied by the runtime.
39
+ *
40
+ * Resolution means the cumulative JavaScript generation is physically visible. The host may then advance Rolldown's
41
+ * published frontier immediately; pendingPatches remains intact until an application report proves the runtime caught up.
42
+ */
26
43
  async produce(patches) {
27
- if (this.buildId === undefined || patches.length === 0)
44
+ if (this.buildId === undefined || patches.length === 0) {
28
45
  return;
46
+ }
29
47
  this.pendingPatches.push(...patches);
30
48
  await this.writePatches(renderHmrPatches(this.buildId, this.pendingPatches));
31
49
  }
32
- /** Removes the pending prefix covered by the runtime's Rolldown sequence receipt. */
50
+ /**
51
+ * Removes only the prefix covered by the runtime's successful application frontier. Publication alone never calls this:
52
+ * retaining the gap between published and applied frontiers is what lets DevTools safely miss intermediate file events.
53
+ */
33
54
  acknowledge(seq) {
34
- let deliveredCount = 0;
35
- while (deliveredCount < this.pendingPatches.length && this.pendingPatches[deliveredCount].seq <= seq) {
36
- deliveredCount++;
55
+ let appliedCount = 0;
56
+ while (appliedCount < this.pendingPatches.length && this.pendingPatches[appliedCount].seq <= seq) {
57
+ appliedCount++;
37
58
  }
38
- return this.pendingPatches.splice(0, deliveredCount).map((patch) => patch.filename);
59
+ this.pendingPatches.splice(0, appliedCount);
39
60
  }
40
61
  }
@@ -18,3 +18,15 @@ import type { Plugin } from 'vite';
18
18
  * transformed by at most one of them, and modules outside all three never reach a handler.
19
19
  */
20
20
  export declare function createWxReactRefreshTransforms(): Plugin[];
21
+ export declare function transformRefreshRuntime({ code, id }: {
22
+ code: string;
23
+ id: string;
24
+ }): import("../../../utils/transform.ts").AstTransformResult;
25
+ export declare function transformReactDevtoolsHook({ code, id }: {
26
+ code: string;
27
+ id: string;
28
+ }): import("../../../utils/transform.ts").AstTransformResult;
29
+ export declare function removeRefreshPreambleGuard({ code, id }: {
30
+ code: string;
31
+ id: string;
32
+ }): import("../../../utils/transform.ts").AstTransformResult;