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
@@ -26,18 +26,23 @@ function forward(handler: unknown, receiver: unknown, args: unknown[]): void {
26
26
  ;(handler as LifecycleHandler | undefined)?.apply(receiver, args)
27
27
  }
28
28
 
29
- /** The surviving page identity: its unique path and params. */
30
- type PageIdentity = {
29
+ /** The old native projection retained while DevTools replaces its Page instance. */
30
+ type PageSnapshot = {
31
31
  $taroPath: string
32
32
  $taroParams: Record<string, unknown>
33
+ data: Record<string, unknown>
34
+ }
35
+
36
+ type NativePage = PageSnapshot & {
37
+ setData(data: Record<string, unknown>): void
33
38
  }
34
39
 
35
40
  // DevTools re-executes the page and replays the replacement lifecycle on every edit. Taro's
36
41
  // onUnload unmounts the React tree and onLoad mounts a fresh one, destroying state. While
37
42
  // the runtime says a patch was just delivered, the pair is intercepted instead: onUnload
38
- // captures the surviving page identity, and onLoad restores it onto the replacement native
39
- // instance and rebinds the retained tree to it (pageElement.ctx + a full re-render). The
40
- // capsule module is cached across re-executions, so this closure holds the capture.
43
+ // captures the surviving page identity and its current native render data, and onLoad
44
+ // immediately paints that snapshot before rebinding and fully synchronizing the retained
45
+ // tree. The capsule module is cached across re-executions, so this closure holds the capture.
41
46
  // Ordinary navigation (no patch) passes through unchanged. The wrappers are regular
42
47
  // functions so the native page instance (`this`) reaches the original handlers.
43
48
  if (typeof __rolldown_runtime__ !== 'undefined') {
@@ -45,14 +50,19 @@ if (typeof __rolldown_runtime__ !== 'undefined') {
45
50
  const originalOnLoad = config.onLoad
46
51
  const originalOnShow = config.onShow
47
52
 
48
- let captured: PageIdentity | undefined
53
+ // This is the sole cross-instance handoff. It is mutable because DevTools destroys the
54
+ // old native Page before creating its replacement; ordinary navigation never reads it.
55
+ let captured: PageSnapshot | undefined
49
56
 
50
57
  config.onUnload = function (this: unknown, ...args: unknown[]) {
51
58
  if (__rolldown_runtime__.isHotReloading()) {
52
- const instance = this as unknown as PageIdentity
59
+ const instance = this as unknown as NativePage
53
60
  captured = {
54
61
  $taroPath: instance.$taroPath,
55
- $taroParams: instance.$taroParams
62
+ $taroParams: instance.$taroParams,
63
+ // WeChat owns this serializable view-model snapshot. Retaining it is cheaper
64
+ // than deep-cloning the complete recursive Taro node tree before every edit.
65
+ data: instance.data
56
66
  }
57
67
  return
58
68
  }
@@ -60,10 +70,14 @@ if (typeof __rolldown_runtime__ !== 'undefined') {
60
70
  }
61
71
  config.onLoad = function (this: unknown, ...args: unknown[]) {
62
72
  if (__rolldown_runtime__.isHotReloading() && captured) {
73
+ const instance = this as unknown as NativePage
74
+ // Paint the last native projection as the first bridge operation. Taro's full
75
+ // performUpdate below runs in a timer, so this direct setData removes most of the
76
+ // empty-page interval without delaying synchronization to the current tree.
77
+ instance.setData(captured.data)
63
78
  // Restore the surviving page identity onto the replacement native instance: the
64
79
  // re-loaded $taroPath embeds a fresh timestamp, so without this the tree and the
65
80
  // native side would disagree on the page key.
66
- const instance = this as unknown as PageIdentity
67
81
  instance.$taroPath = captured.$taroPath
68
82
  instance.$taroParams = captured.$taroParams
69
83
  // Rebind the retained tree to the replacement receiver and resync the native
@@ -5,9 +5,8 @@
5
5
  // The `DevRuntime` base class is injected into the chunk by Rolldown's dev-mode
6
6
  // transform, so the WX host only extends it.
7
7
  //
8
- // Delivery is passive: every valid hmr/patches.js suffix is acknowledged by Rolldown
9
- // sequence, then applied synchronously before the Page continues evaluating. Already-applied
10
- // sequences are ignored when another Page requires the same physical file.
8
+ // Every Page explicitly passes the inert hmr/patches.js export here before importing its capsule. The runtime applies that
9
+ // cumulative suffix synchronously, reports its successful application frontier, and ignores sequences replayed by other Pages.
11
10
 
12
11
  import type { DevRuntime as RolldownDevRuntime } from 'rolldown/experimental/runtime-types'
13
12
 
@@ -110,7 +109,7 @@ class WxDevRuntime extends DevRuntime {
110
109
  }
111
110
 
112
111
  /** Computes accepting boundaries and every executed module that must be re-armed. */
113
- private computeHmrUpdate(changedIds: readonly string[]): HmrUpdate | undefined {
112
+ private computeHmrUpdate(changedIds: Iterable<string>): HmrUpdate | undefined {
114
113
  // Mutable traversal accumulators are confined to one synchronous update plan.
115
114
  const boundaries: string[] = []
116
115
  const updateSet = new Set<string>()
@@ -166,7 +165,7 @@ class WxDevRuntime extends DevRuntime {
166
165
  * Applies one patch in three phases: capture old contexts, evict the whole update set,
167
166
  * then re-run accepting modules and pass their fresh exports to the old contexts.
168
167
  */
169
- private applyHmrUpdate(changedIds: readonly string[]): void {
168
+ private applyHmrUpdate(changedIds: Iterable<string>): void {
170
169
  const update = this.computeHmrUpdate(changedIds)
171
170
  if (!update) return
172
171
 
@@ -195,14 +194,12 @@ class WxDevRuntime extends DevRuntime {
195
194
  }
196
195
  }
197
196
 
198
- /** Consumed once per App heap from hmr/info.js; the host buildId is the delivery identity. */
197
+ /** Consumed once per App heap from hmr/info.js; the host buildId identifies its cumulative patch history. */
199
198
  initialize(info: HmrInfo): void {
200
199
  if (this.hmrInfo) {
201
200
  return
202
201
  }
203
202
  this.hmrInfo = info
204
- // Anchor: the host publishes only after the runtime reports its delivery position.
205
- void this.sendReport({ kind: 'delivery', seq: this.appliedSeq })
206
203
  }
207
204
 
208
205
  /** True between a patch delivery and the next page show: a hot reload is in progress. */
@@ -218,17 +215,17 @@ class WxDevRuntime extends DevRuntime {
218
215
  this.hotReloading = false
219
216
  }
220
217
 
221
- /** The only direct effect of hmr/patches.js: validate, store, acknowledge, and apply. */
222
- storePatches(payload: PatchPayload): void {
218
+ /** Applies one Page-delivered payload before that Page imports its capsule. */
219
+ applyPatches(payload: PatchPayload | undefined): void {
220
+ // The initial physical dependency exports undefined until the host has a patch range.
221
+ if (!payload) return
222
+
223
223
  const info = this.hmrInfo
224
224
  if (!info || payload.buildId !== info.buildId) {
225
225
  console.warn('[vpt] patches for a stale build')
226
226
  return
227
227
  }
228
228
 
229
- const deliveredSeq = Math.max(this.appliedSeq, payload.patches.at(-1)?.seq ?? 0)
230
- void this.sendReport({ kind: 'delivery', seq: deliveredSeq })
231
-
232
229
  // A delivered patch means DevTools is about to replay the page lifecycle on the
233
230
  // re-executing Pages; the capsule wrapper suppresses the synthetic unmount/mount so
234
231
  // the React tree survives and Refresh swaps the code in place.
@@ -236,27 +233,82 @@ class WxDevRuntime extends DevRuntime {
236
233
 
237
234
  // Apply synchronously: the page's imports below the require resolve against the
238
235
  // freshly registered modules, so the re-executed Page evaluates with the new code.
239
- this.applyPatches(payload.patches)
236
+ if (this.applyPatchBatch(payload.patches)) {
237
+ // The host may publish later generations while this synchronous apply runs. Reporting only afterward makes this
238
+ // the application frontier: publisher history is never pruned merely because its JavaScript file was observed.
239
+ void this.sendReport({ kind: 'applied', seq: this.appliedSeq })
240
+ }
240
241
  }
241
242
 
242
- /** Applies one physical suffix in Rolldown sequence order. */
243
- private applyPatches(patches: readonly PatchProgram[]): void {
244
- for (const patch of patches) {
245
- if (patch.seq <= this.appliedSeq) continue
243
+ /**
244
+ * Folds one cumulative physical patch file into a single logical HMR update.
245
+ *
246
+ * `hmr/patches.js` can contain several unacknowledged Rolldown patches and can be required by several Page shells. The
247
+ * runtime must therefore ignore replayed sequences, reject a missing sequence, and move directly from the currently live
248
+ * module generation to the latest published generation without rendering intermediate generations.
249
+ *
250
+ * For an appliedSeq of 4:
251
+ *
252
+ * - [5, 6, 7] registers every factory, performs one update using their unioned changedIds, commits appliedSeq 7, and reports 7;
253
+ * - a second Page evaluating [5, 6, 7] skips the complete replay and leaves the latest factories untouched;
254
+ * - [5, 7] fails at the expected sequence 6, keeps appliedSeq 4, and requests a full rebuild because factory 6 is unrecoverable.
255
+ */
256
+ private applyPatchBatch(patches: readonly PatchProgram[]): boolean {
257
+ // Keep the initial watermark immutable throughout the fold. Comparing replays against a moving watermark would allow
258
+ // a duplicate new sequence in the same payload to masquerade as an already-applied patch.
259
+ const previousSeq = this.appliedSeq
260
+
261
+ // Mutable only during this synchronous pass. `nextSeq` validates continuity while `changedIds` unions every incremental
262
+ // patch's roots for the one final graph traversal; neither value escapes into persistent runtime state.
263
+ let nextSeq = previousSeq + 1
264
+ const changedIds = new Set<string>()
265
+
266
+ try {
267
+ for (const patch of patches) {
268
+ // The same physical file is evaluated once per affected Page. Those later evaluations replay its prefix and
269
+ // must not let an old factory overwrite the newer factory already stored in the Rolldown runtime.
270
+ if (patch.seq <= previousSeq) {
271
+ continue
272
+ }
246
273
 
247
- try {
248
- const expectedSeq = this.appliedSeq + 1
249
- if (patch.seq !== expectedSeq) {
250
- throw new Error(`missing patch sequence ${expectedSeq}`)
274
+ // Every patch is incremental: if one physical write was missed, later factories cannot reconstruct modules
275
+ // changed only by that missing patch. Rebuilding is safer than silently running a mixed module generation.
276
+ if (patch.seq !== nextSeq) {
277
+ throw new Error(`missing patch sequence ${nextSeq}`)
251
278
  }
279
+
280
+ // A patch factory only registers module graphs and module factories; it does not execute application modules.
281
+ // Running all of them first leaves the registry at the latest generation while avoiding intermediate renders.
252
282
  patch.factory()
253
- this.applyHmrUpdate(patch.changedIds)
254
- this.appliedSeq = patch.seq
255
- } catch (error) {
256
- console.warn(`[vpt] patch sequence ${patch.seq} failed; apply stopped`, error)
257
- void this.sendReport({ kind: 'rebuild' })
258
- return
283
+
284
+ for (const changedId of patch.changedIds) {
285
+ changedIds.add(changedId)
286
+ }
287
+
288
+ nextSeq++
259
289
  }
290
+
291
+ // A payload containing only replayed sequences requires no graph work and must not alter the application watermark.
292
+ if (nextSeq === previousSeq + 1) {
293
+ return true
294
+ }
295
+
296
+ // Apply once after every factory is registered. React Refresh arms a newly executed module's hot.accept callback
297
+ // in a microtask; applying consecutive patches separately would inspect that intermediate context too early and
298
+ // incorrectly bubble through the capsule to the native Page shell as though no HMR boundary existed.
299
+ this.applyHmrUpdate(changedIds)
300
+
301
+ // Commit application only after graph propagation and boundary callbacks succeed. A failure leaves the old watermark
302
+ // intact and requests a full rebuild below, so a partially applied batch is never acknowledged as healthy.
303
+ this.appliedSeq = nextSeq - 1
304
+ return true
305
+ } catch (error) {
306
+ console.warn('[vpt] patch batch failed; apply stopped', error)
307
+ void this.sendReport({
308
+ kind: 'rebuild',
309
+ reason: Error.isError(error) ? error.message : String(error)
310
+ })
311
+ return false
260
312
  }
261
313
  }
262
314