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