vite-plugin-taro 0.5.6 → 0.5.7

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.
@@ -7,34 +7,59 @@
7
7
  //
8
8
  // Every Page explicitly passes the inert hmr/patches.js export here before importing its capsule. The runtime applies that
9
9
  // cumulative suffix synchronously, reports its successful application frontier, and ignores sequences replayed by other Pages.
10
+ /** Forwards a native lifecycle with its original Page receiver. */
11
+ function forward(handler, receiver, args) {
12
+ if (typeof handler === 'function')
13
+ handler.apply(receiver, args);
14
+ }
15
+ /** Narrows the untyped element returned across the Taro connection boundary. */
16
+ function isTaroRoot(value) {
17
+ return typeof value === 'object' && value !== null && 'ctx' in value;
18
+ }
19
+ /** Shared no-op CSS contract because physical rebuilds replace styles wholesale. */
20
+ const hotContextInternals = Object.freeze({
21
+ updateStyle() { },
22
+ removeStyle() { }
23
+ });
10
24
  /**
11
25
  * Per-module hot state, mirroring Rolldown's web runtime: accept is a passive registration;
12
26
  * the propagation invokes the previous execution's callbacks with the fresh exports.
13
27
  */
14
28
  class WxHotContext {
15
- _internal = {
16
- updateStyle() { },
17
- removeStyle() { }
18
- };
19
- /** Registered self-accept callbacks; a bare accept is represented by a no-op callback. */
20
- acceptCallbacks = [];
21
- /** Set when a callback rejects a Refresh boundary; the current update then rebuilds. */
22
- invalidationReason;
23
- accept(callback) {
24
- this.acceptCallbacks.push(callback ?? (() => { }));
29
+ _internal = hotContextInternals;
30
+ /**
31
+ * Callbacks registered by this module execution. The array is allocated on first accept,
32
+ * remains attached to the old execution across cache eviction, and is invoked by the next
33
+ * boundary execution. Undefined means this context stays passive.
34
+ */
35
+ acceptCallbacks;
36
+ /** Stable module identity and shared sparse index used only when this context accepts. */
37
+ moduleId;
38
+ acceptingContexts;
39
+ constructor(moduleId, acceptingContexts) {
40
+ this.moduleId = moduleId;
41
+ this.acceptingContexts = acceptingContexts;
25
42
  }
26
- hasAccepts() {
27
- return this.acceptCallbacks.length > 0;
43
+ accept(callback) {
44
+ if (!this.acceptCallbacks) {
45
+ this.acceptCallbacks = [callback];
46
+ this.acceptingContexts.set(this.moduleId, this);
47
+ return;
48
+ }
49
+ this.acceptCallbacks.push(callback);
28
50
  }
29
- /** Invokes this old context's callbacks and returns any requested invalidation. */
51
+ /** Invokes this old context's callbacks; invalidation aborts the update synchronously. */
30
52
  runAccept(moduleExports) {
31
- for (const callback of this.acceptCallbacks) {
32
- callback(moduleExports);
53
+ const callbacks = this.acceptCallbacks;
54
+ if (!callbacks) {
55
+ throw new Error(`passive hot context reached as boundary: ${this.moduleId}`);
56
+ }
57
+ for (const callback of callbacks) {
58
+ callback?.(moduleExports);
33
59
  }
34
- return this.invalidationReason;
35
60
  }
36
61
  invalidate(reason) {
37
- this.invalidationReason = reason ?? 'the accepting module invalidated the update';
62
+ throw new Error(reason ?? 'the accepting module invalidated the update');
38
63
  }
39
64
  // Vite's generated CSS module calls hot.prune with its style teardown; the physical
40
65
  // rebuild replaces styles wholesale, so it is a no-op.
@@ -42,10 +67,17 @@ class WxHotContext {
42
67
  }
43
68
  /** The WX host: extends the Rolldown contract instead of reimplementing it. */
44
69
  class WxDevRuntime extends DevRuntime {
45
- hmrInfo;
46
- /** Highest Rolldown sequence successfully applied. */
47
- appliedSeq = 0;
48
- /** Active hot contexts per module id; the propagation invokes their callbacks. */
70
+ /**
71
+ * One session for this App heap. Undefined only before app.js consumes hmr/info.js; its
72
+ * identity and endpoint then stay fixed while appliedSeq records the committed frontier.
73
+ */
74
+ session;
75
+ /**
76
+ * Sparse current-generation accepting boundaries keyed by module id. Entries alone must
77
+ * outlive Rolldown module-cache eviction so old callbacks can receive fresh exports.
78
+ * createModuleHotContext removes the prior generation immediately; first accept inserts
79
+ * the new one. Passive contexts are never retained, so space is O(number of boundaries).
80
+ */
49
81
  moduleHotContexts = new Map();
50
82
  constructor() {
51
83
  // The base has no messenger: the engine tracks per-client shipped payloads instead
@@ -54,13 +86,13 @@ class WxDevRuntime extends DevRuntime {
54
86
  }
55
87
  /**
56
88
  * Generated code always calls this before registerModule and reads `_internal` from the
57
- * return value. Mirrors Rolldown's web runtime: each execution immediately replaces the
58
- * module's hot context; the apply plan has already captured the previous callbacks.
89
+ * return value. Each execution immediately retires its previous accepting boundary;
90
+ * calling accept registers the new context after the apply plan captured old callbacks.
59
91
  */
60
92
  createModuleHotContext(moduleId) {
61
- const hotContext = new WxHotContext();
62
- this.moduleHotContexts.set(moduleId, hotContext);
63
- return hotContext;
93
+ // A new execution supersedes the old boundary before it decides whether to accept.
94
+ this.moduleHotContexts.delete(moduleId);
95
+ return new WxHotContext(moduleId, this.moduleHotContexts);
64
96
  }
65
97
  /** Computes accepting boundaries and every executed module that must be re-armed. */
66
98
  computeHmrUpdate(changedIds) {
@@ -84,7 +116,7 @@ class WxDevRuntime extends DevRuntime {
84
116
  if (updateSet.has(moduleId))
85
117
  return undefined;
86
118
  updateSet.add(moduleId);
87
- if (this.moduleHotContexts.get(moduleId)?.hasAccepts()) {
119
+ if (this.moduleHotContexts.has(moduleId)) {
88
120
  boundaries.push(moduleId);
89
121
  return undefined;
90
122
  }
@@ -127,49 +159,120 @@ class WxDevRuntime extends DevRuntime {
127
159
  this.removeModuleCache(moduleId);
128
160
  }
129
161
  for (const { moduleId, hotContext } of applies) {
130
- const invalidationReason = hotContext?.runAccept(this.initModule(moduleId));
131
- if (invalidationReason) {
132
- throw new Error(`${moduleId}: ${invalidationReason}`);
133
- }
162
+ hotContext?.runAccept(this.initModule(moduleId));
134
163
  }
135
164
  }
136
165
  /** Consumed once per App heap from hmr/info.js; the host buildId identifies its cumulative patch history. */
137
166
  initialize(info) {
138
- if (this.hmrInfo) {
167
+ if (this.session)
168
+ return;
169
+ this.session = { ...info, appliedSeq: 0 };
170
+ }
171
+ /**
172
+ * One-shot bridge to the application's Taro singleton. It cannot be imported into this
173
+ * separately bundled global runtime without creating a second Taro identity. Undefined
174
+ * only before the serve-only facade connection; route transactions share its lifetime.
175
+ */
176
+ taro;
177
+ /** Connects HMR to the same Taro singleton used by the application module graph. */
178
+ connectTaro(current, document, injectPageInstance) {
179
+ if (this.taro) {
139
180
  return;
140
181
  }
141
- this.hmrInfo = info;
182
+ this.taro = {
183
+ current,
184
+ document,
185
+ injectPageInstance,
186
+ pageReplacements: new Map()
187
+ };
142
188
  }
143
- /** True between a patch delivery and the next page show: a hot reload is in progress. */
144
- hotReloading = false;
145
- /** The capsule wrapper asks this during the synthetic lifecycle of a hot reload. */
146
- isHotReloading() {
147
- return this.hotReloading;
189
+ /** Injects snapshot-preserving behavior into one route-specific Taro Page configuration. */
190
+ injectPageHmr(config, route) {
191
+ const originalOnUnload = config.onUnload;
192
+ const originalOnLoad = config.onLoad;
193
+ const originalOnShow = config.onShow;
194
+ const runtime = this;
195
+ config.onUnload = function (...args) {
196
+ const taro = runtime.requireTaro();
197
+ if (taro.pageReplacements.has(route)) {
198
+ taro.pageReplacements.set(route, {
199
+ $taroPath: this.$taroPath,
200
+ $taroParams: this.$taroParams,
201
+ // WeChat owns this serializable view-model. Keeping its reference is O(1),
202
+ // unlike cloning the complete recursive projection before every edit.
203
+ data: this.data
204
+ });
205
+ return;
206
+ }
207
+ forward(originalOnUnload, this, args);
208
+ };
209
+ config.onLoad = function (...args) {
210
+ const taro = runtime.requireTaro();
211
+ const snapshot = taro.pageReplacements.get(route);
212
+ if (snapshot) {
213
+ // Replace the transaction before native work so exceptions cannot retain the
214
+ // large data snapshot while the route waits for its synthetic onShow.
215
+ taro.pageReplacements.set(route, null);
216
+ // Snapshot paint is the first bridge operation and removes the empty-page gap.
217
+ this.setData(snapshot.data);
218
+ this.$taroPath = snapshot.$taroPath;
219
+ this.$taroParams = snapshot.$taroParams;
220
+ runtime.bindPage(this, snapshot.$taroPath);
221
+ return;
222
+ }
223
+ forward(originalOnLoad, this, args);
224
+ };
225
+ config.onShow = function (...args) {
226
+ if (runtime.requireTaro().pageReplacements.delete(route)) {
227
+ // Synthetic shows must not repeat requests or reset application state.
228
+ return;
229
+ }
230
+ forward(originalOnShow, this, args);
231
+ };
232
+ }
233
+ /** Returns the Taro connection or fails at the first incorrectly ordered use. */
234
+ requireTaro() {
235
+ if (!this.taro)
236
+ throw new Error('[vpt] WX HMR used before the Taro runtime was connected');
237
+ return this.taro;
238
+ }
239
+ /** Returns a retained Taro root after normal Page mount has created it. */
240
+ findRoot(path) {
241
+ const root = this.requireTaro().document.getElementById(path);
242
+ return isTaroRoot(root) ? root : undefined;
148
243
  }
149
- /** The wrapped onShow ends the hot reload after the replacement cycle. */
150
- clearHotReloading() {
151
- this.hotReloading = false;
244
+ /** Rebinds a retained Taro tree to one replacement native Page without repainting. */
245
+ bindPage(instance, path) {
246
+ const taro = this.requireTaro();
247
+ taro.injectPageInstance(instance, path);
248
+ taro.current.page = instance;
249
+ const pageElement = this.findRoot(path);
250
+ if (!pageElement) {
251
+ throw new Error(`[vpt] retained Taro page not found: ${path}`);
252
+ }
253
+ pageElement.ctx = instance;
152
254
  }
153
- /** Applies one Page-delivered payload before that Page imports its capsule. */
154
- applyPatches(payload) {
255
+ /** Applies one Page-delivered payload and arms its route for native replacement. */
256
+ applyPatches(payload, route) {
155
257
  // The initial physical dependency exports undefined until the host has a patch range.
156
258
  if (!payload)
157
259
  return;
158
- const info = this.hmrInfo;
159
- if (!info || payload.buildId !== info.buildId) {
260
+ const session = this.session;
261
+ if (!session || payload.buildId !== session.buildId) {
160
262
  console.warn('[vpt] patches for a stale build');
161
263
  return;
162
264
  }
163
- // A delivered patch means DevTools is about to replay the page lifecycle on the
164
- // re-executing Pages; the capsule wrapper suppresses the synthetic unmount/mount so
165
- // the React tree survives and Refresh swaps the code in place.
166
- this.hotReloading = true;
265
+ // A replayed payload still causes DevTools to replace this physical Page. Arm the
266
+ // route independently of whether this App heap already applied its patch sequence.
267
+ if (route) {
268
+ this.requireTaro().pageReplacements.set(route, null);
269
+ }
167
270
  // Apply synchronously: the page's imports below the require resolve against the
168
271
  // freshly registered modules, so the re-executed Page evaluates with the new code.
169
- if (this.applyPatchBatch(payload.patches)) {
272
+ if (this.applyPatchBatch(session, payload.patches)) {
170
273
  // The host may publish later generations while this synchronous apply runs. Reporting only afterward makes this
171
274
  // the application frontier: publisher history is never pruned merely because its JavaScript file was observed.
172
- void this.sendReport({ kind: 'applied', seq: this.appliedSeq });
275
+ void this.sendReport({ kind: 'applied', seq: session.appliedSeq });
173
276
  }
174
277
  }
175
278
  /**
@@ -185,10 +288,10 @@ class WxDevRuntime extends DevRuntime {
185
288
  * - a second Page evaluating [5, 6, 7] skips the complete replay and leaves the latest factories untouched;
186
289
  * - [5, 7] fails at the expected sequence 6, keeps appliedSeq 4, and requests a full rebuild because factory 6 is unrecoverable.
187
290
  */
188
- applyPatchBatch(patches) {
291
+ applyPatchBatch(session, patches) {
189
292
  // Keep the initial watermark immutable throughout the fold. Comparing replays against a moving watermark would allow
190
293
  // a duplicate new sequence in the same payload to masquerade as an already-applied patch.
191
- const previousSeq = this.appliedSeq;
294
+ const previousSeq = session.appliedSeq;
192
295
  // Mutable only during this synchronous pass. `nextSeq` validates continuity while `changedIds` unions every incremental
193
296
  // patch's roots for the one final graph traversal; neither value escapes into persistent runtime state.
194
297
  let nextSeq = previousSeq + 1;
@@ -223,7 +326,7 @@ class WxDevRuntime extends DevRuntime {
223
326
  this.applyHmrUpdate(changedIds);
224
327
  // Commit application only after graph propagation and boundary callbacks succeed. A failure leaves the old watermark
225
328
  // intact and requests a full rebuild below, so a partially applied batch is never acknowledged as healthy.
226
- this.appliedSeq = nextSeq - 1;
329
+ session.appliedSeq = nextSeq - 1;
227
330
  return true;
228
331
  }
229
332
  catch (error) {
@@ -237,17 +340,17 @@ class WxDevRuntime extends DevRuntime {
237
340
  }
238
341
  /** Sends one metadata-only report to the host; executable code never travels over HTTP. */
239
342
  sendReport(data) {
240
- const info = this.hmrInfo;
241
- if (!info) {
343
+ const session = this.session;
344
+ if (!session) {
242
345
  // A report without initialize is a programming error; fail loudly instead of
243
346
  // silently dropping the sync traffic.
244
347
  throw new Error('WX dev runtime is not initialized');
245
348
  }
246
349
  return new Promise((resolve, reject) => {
247
350
  wx.request({
248
- url: info.endpoint,
351
+ url: session.endpoint,
249
352
  method: 'POST',
250
- data: { buildId: info.buildId, ...data },
353
+ data: { buildId: session.buildId, ...data },
251
354
  header: { 'content-type': 'application/json' },
252
355
  success() {
253
356
  resolve();
@@ -261,30 +364,4 @@ class WxDevRuntime extends DevRuntime {
261
364
  }
262
365
  const runtime = new WxDevRuntime();
263
366
  globalThis.__rolldown_runtime__ = runtime;
264
- // Install the React DevTools hook before the Taro renderer injects itself: the runtime chunk
265
- // is the first module of the App heap, and the renderer checks the hook when it evaluates at
266
- // App mount. The refresh runtime's own injection (see react-refresh.ts) replays this hook's
267
- // renderers, so the hook must store what inject receives — a real DevTools hook keeps the
268
- // renderer in the renderers Map; without the stored renderer the replay captures nothing and
269
- // Refresh has no renderer helpers to schedule re-renders on.
270
- //
271
- // The hook lives on `global`, and every free `__REACT_DEVTOOLS_GLOBAL_HOOK__` reference in
272
- // react-family modules is rewritten to `global.__REACT_DEVTOOLS_GLOBAL_HOOK__`: the
273
- // The WeChat runtime scope does not resolve free variables against `global` (verified: the free
274
- // lookup is undefined while the member access exists), so the renderer would never inject
275
- // otherwise. `??=` keeps a pre-installed hook (e.g. a real DevTools integration) intact; the
276
- // runtime chunk's own lowering keeps the operator es2018-compatible.
277
- const reactDevtoolsHook = {
278
- renderers: new Map(),
279
- supportsFiber: true,
280
- inject: (injected) => {
281
- const id = reactDevtoolsHook.renderers.size;
282
- reactDevtoolsHook.renderers.set(id, injected);
283
- return id;
284
- },
285
- onScheduleFiberRoot: () => { },
286
- onCommitFiberRoot: () => { },
287
- onCommitFiberUnmount: () => { }
288
- };
289
- global.__REACT_DEVTOOLS_GLOBAL_HOOK__ ??= reactDevtoolsHook;
290
367
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-taro",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "author": "sep2",
5
5
  "description": "Vite 8 plugin for building one React/Taro codebase for WeChat Mini Program and H5 targets.",
6
6
  "type": "module",
@@ -75,8 +75,8 @@
75
75
  "rolldown": "1.2.3",
76
76
  "tailwindcss": "^4.3.3",
77
77
  "weapp-tailwindcss": "^5.2.11",
78
- "@tarojs/react": "npm:vite-plugin-taro-react@0.5.6",
79
- "@tarojs/plugin-framework-react": "npm:vite-plugin-taro-plugin-framework-react@0.5.6"
78
+ "@tarojs/plugin-framework-react": "npm:vite-plugin-taro-plugin-framework-react@0.5.7",
79
+ "@tarojs/react": "npm:vite-plugin-taro-react@0.5.7"
80
80
  },
81
81
  "peerDependencies": {
82
82
  "react": "^19.0.0",
@@ -0,0 +1,170 @@
1
+ import type { PluginOption } from 'vite'
2
+ import { transformWithOxc } from 'vite'
3
+ import type { VitePluginTaroOptions } from '../../../../options.ts'
4
+ import { esTarget } from '../../../utils/constant.ts'
5
+ import { memoize } from '../../../utils/memoize.ts'
6
+ import { normalizeModuleId } from '../../../utils/modules.ts'
7
+ import { appCapsulePath, pageCapsulePath, rolldownRuntimeId, taroRuntimePath } from '../module.ts'
8
+ import { createWxDevHost, type WxDevHost } from './dev-host.ts'
9
+ import { createWxReactRefreshTransforms } from './react-refresh.ts'
10
+
11
+ const taroRuntimeId = '@tarojs/runtime'
12
+
13
+ /**
14
+ * Adds the serve-only bundled-development plugin set for the wx target: the dev adapter,
15
+ * Page HMR activation, and React Refresh adaptation transforms.
16
+ */
17
+ export function createWxDevelopmentPlugin(options: VitePluginTaroOptions): PluginOption[] {
18
+ let host: WxDevHost | null = null
19
+ // Portable hook filters stay broad; these exact identities exclude similarly named user modules.
20
+ const normalizedAppCapsulePath = normalizeModuleId(appCapsulePath)
21
+ const normalizedPageCapsulePath = normalizeModuleId(pageCapsulePath)
22
+ const normalizedTaroRuntimePath = normalizeModuleId(taroRuntimePath)
23
+
24
+ return [
25
+ {
26
+ name: 'vpt:wx-dev',
27
+ apply: 'serve',
28
+
29
+ config() {
30
+ return {
31
+ build: {
32
+ // Disable maps in resolved environment config as well as final output so Oxc and Babel skip producing
33
+ // intermediate maps that Rolldown would discard.
34
+ sourcemap: false
35
+ },
36
+ experimental: {
37
+ // Ask Vite to resolve its bundled-development graph and expose the private adapter instance. The wx
38
+ // configureServer hook replaces only its startup method with the directly writing DevEngine.
39
+ bundledDev: true
40
+ }
41
+ }
42
+ },
43
+
44
+ configureServer: {
45
+ // Install after Vite and user plugins have finished configuring the environment, but before server.listen()
46
+ // asks bundledDev to create its hard-coded skip-write DevEngine.
47
+ order: 'post',
48
+ async handler(server) {
49
+ host = await createWxDevHost({ server, options })
50
+ }
51
+ },
52
+
53
+ closeBundle() {
54
+ return host?.close()
55
+ }
56
+ },
57
+ {
58
+ name: 'vpt:wx-runtime-lowering',
59
+ apply: 'serve',
60
+ transform: {
61
+ order: 'post',
62
+ // The dev-mode transform assembles the runtime chunk (Rolldown's base runtime
63
+ // plus our injected implement) as this module's transform output, which
64
+ // bypasses the build's es2018 lowering. Real-device engines and WeChat's
65
+ // upload parser predate class fields and nullish operators, so the assembled
66
+ // runtime is lowered here — the only module that needs it. The exact id
67
+ // filter needs no code scan; the id must stay in sync with rolldownRuntimeId
68
+ // in module.ts (kept as a regex for the Rolldown-side filter).
69
+ filter: { id: /^\0rolldown\/runtime\.js(?:\?|$)/ },
70
+ handler(code) {
71
+ // The `setPublicClassFields` assumption emits plain `this.x = ...`
72
+ // assignments instead of external helpers, whose references the later
73
+ // minifier would mangle.
74
+ return fixRolldownRuntime(code)
75
+ }
76
+ }
77
+ },
78
+ {
79
+ name: 'vpt:wx-react-refresh-bootstrap',
80
+ apply: 'serve',
81
+ transform: {
82
+ order: 'post',
83
+ filter: { id: /\/runtime\/wx\/capsule\/app\.js(?:\?|$)/ },
84
+ handler(code, id) {
85
+ if (normalizeModuleId(id) !== normalizedAppCapsulePath) return
86
+ return injectReactRefreshBootstrap(code)
87
+ }
88
+ }
89
+ },
90
+ {
91
+ name: 'vpt:wx-page-hmr',
92
+ apply: 'serve',
93
+ transform: {
94
+ order: 'post',
95
+ filter: { id: /\/runtime\/wx\/capsule\/page\.js(?:\?|$)/ },
96
+ handler(code, id) {
97
+ if (normalizeModuleId(id) !== normalizedPageCapsulePath) return
98
+ return injectPageHmr(code, getPageRoute(id))
99
+ }
100
+ }
101
+ },
102
+ {
103
+ name: 'vpt:wx-taro-hmr',
104
+ apply: 'serve',
105
+ transform: {
106
+ order: 'post',
107
+ filter: { id: /\/runtime\/wx\/capsule\/taro-runtime\.js(?:\?|$)/ },
108
+ handler(code, id) {
109
+ if (normalizeModuleId(id) !== normalizedTaroRuntimePath) return
110
+ return injectTaroConnection(code)
111
+ }
112
+ }
113
+ },
114
+ ...createWxReactRefreshTransforms()
115
+ ]
116
+ }
117
+
118
+ /** Ensures the Refresh hook exists before React's renderer evaluates and injects itself. */
119
+ export function injectReactRefreshBootstrap(code: string): { code: string; map: null } {
120
+ return {
121
+ code: `import ${JSON.stringify('/@react-refresh')};\n${code}`,
122
+ map: null
123
+ }
124
+ }
125
+
126
+ /** Connects the shared WX dev runtime to the application graph's Taro runtime instance. */
127
+ export function injectTaroConnection(code: string): { code: string; map: null } {
128
+ if (!/\bCurrent\b/.test(code) || !/\bdocument\b/.test(code) || !/\binjectPageInstance\b/.test(code)) {
129
+ throw new Error('WX Taro runtime must expose Current, document, and injectPageInstance for HMR')
130
+ }
131
+
132
+ const taroImport = `import { Current as __vptCurrent, document as __vptDocument, injectPageInstance as __vptInjectPageInstance } from ${JSON.stringify(taroRuntimeId)};`
133
+
134
+ return {
135
+ code: `${code}\n${taroImport}\n__rolldown_runtime__.connectTaro(__vptCurrent, __vptDocument, __vptInjectPageInstance);`,
136
+ map: null
137
+ }
138
+ }
139
+
140
+ /** Activates development-only lifecycle handling for one plugin-owned Page capsule. */
141
+ export function injectPageHmr(code: string, route: string): { code: string; map: null } {
142
+ if (!/\bconst\s+config\s*=/.test(code) || !/\bexport\s+default\s+config\b/.test(code)) {
143
+ throw new Error('WX Page capsule must declare and default-export config before HMR injection')
144
+ }
145
+
146
+ return {
147
+ code: `${code}\n__rolldown_runtime__.injectPageHmr(config, ${JSON.stringify(route)});`,
148
+ map: null
149
+ }
150
+ }
151
+
152
+ /** Reads the stable route carried by every specialized Page capsule ID. */
153
+ function getPageRoute(id: string): string {
154
+ const queryIndex = id.indexOf('?')
155
+ const route = queryIndex < 0 ? null : new URLSearchParams(id.slice(queryIndex + 1)).get('route')
156
+ if (!route) throw new Error(`WX Page capsule is missing its route: ${id}`)
157
+ return route
158
+ }
159
+
160
+ // The assembled runtime chunk is byte-identical on every build (the base runtime and the
161
+ // bundled implement are immutable for the server's lifetime), so the lowering runs once
162
+ // and every build reuses it.
163
+ const fixRolldownRuntime = memoize((code: string) => {
164
+ return transformWithOxc(code, rolldownRuntimeId, {
165
+ lang: 'js',
166
+ target: esTarget,
167
+ sourcemap: false,
168
+ assumptions: { setPublicClassFields: true }
169
+ })
170
+ })
@@ -25,8 +25,7 @@ const refreshRuntimeWindowGlobals = ['__registerBeforePerformReactRefresh', '__g
25
25
  * @vitejs/plugin-react's generated refresh code assumes the web HTML preamble and a browser
26
26
  * global scope; wx has neither. Each transform adapts one piece of that contract:
27
27
  * - the refresh runtime module (id-filtered): the vendored runtime reads and assigns
28
- * `window` protocol globals (rewritten to `global`) and must inject itself at evaluation
29
- * — the preamble's `injectIntoGlobalHook` call has no HTML home in wx;
28
+ * `window` protocol globals (rewritten to `global`) and must inject itself at evaluation;
30
29
  * - react-family modules (filtered on free references): the DevTools hook is read as a free
31
30
  * variable, which the WeChat runtime scope never resolves against `global` — every free
32
31
  * reference becomes an explicit member access;
@@ -93,7 +92,7 @@ export function createWxReactRefreshTransforms(): Plugin[] {
93
92
  * The protocol name is unique, but only reference identifiers are rewritten. Declaration
94
93
  * keys and explicit members such as `global.__REACT_DEVTOOLS_GLOBAL_HOOK__` must remain
95
94
  * 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`.
95
+ * The eagerly evaluated refresh runtime creates the hook on `global` before the renderer loads.
97
96
  */
98
97
  function createReactDevtoolsHookVisitor(editor: RolldownMagicString): WalkerEnter {
99
98
  return function enter(node, parent) {
@@ -139,20 +138,18 @@ function createRefreshRuntimeVisitor(editor: RolldownMagicString): WalkerEnter {
139
138
 
140
139
  return function enter(node) {
141
140
  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)
141
+ node.type === 'MemberExpression' &&
142
+ !node.computed &&
143
+ node.object.type === 'Identifier' &&
144
+ node.object.name === 'window' &&
145
+ node.property.type === 'Identifier' &&
146
+ refreshRuntimeWindowGlobals.some((globalName) => globalName === node.property.name)
148
147
  ) {
149
- return
148
+ // `global` is the shared wx App heap used by the dev runtime and hook injection. Only
149
+ // replacing the object range preserves the vendored runtime byte-for-byte otherwise
150
+ // and prevents unrelated `window` expressions from being silently adapted.
151
+ editor.overwrite(node.object.start, node.object.end, 'global')
150
152
  }
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')
156
153
  }
157
154
  }
158
155
 
@@ -136,7 +136,8 @@ function createEntryBanner(pageFiles: ReadonlySet<string>): (chunk: { name: stri
136
136
  }
137
137
  if (pageFiles.has(chunk.name)) {
138
138
  const patchesPath = path.posix.relative(path.posix.dirname(chunk.fileName), 'hmr/patches.js')
139
- return `__rolldown_runtime__.applyPatches(require('${patchesPath}'));\n`
139
+ const route = chunk.name.slice(0, -'.js'.length)
140
+ return `__rolldown_runtime__.applyPatches(require('${patchesPath}'), ${JSON.stringify(route)});\n`
140
141
  }
141
142
  return ''
142
143
  }
@@ -47,6 +47,9 @@ export const pageCapsuleId = '\0vpt:page-capsule'
47
47
  /** Provides the Page capsule source specialized through a stable route query. */
48
48
  export const pageCapsulePath = resolvePackageFile('dist/runtime/wx/capsule/page.js')
49
49
 
50
+ /** Identifies the Taro facade shared by the App, Page, and recursive Component capsules. */
51
+ export const taroRuntimePath = resolvePackageFile('dist/runtime/wx/capsule/taro-runtime.js')
52
+
50
53
  /** Identifies the reusable synchronous native Page shell source. */
51
54
  export const pageShellPath = resolvePackageFile('dist/runtime/wx/native/page.js')
52
55
 
@@ -3,7 +3,7 @@ import type { VitePluginTaroOptions } from '../../../options.ts'
3
3
  import { esTarget } from '../../utils/constant.ts'
4
4
  import { packageRequire } from '../../utils/packages.ts'
5
5
  import { clientTaroNativeId } from '../client/constant.ts'
6
- import { createWxDevelopmentPlugin } from './dev/plugin.ts'
6
+ import { createWxDevelopmentPlugin } from './dev/plugins.ts'
7
7
  import { getWxExecutionKind, isTransportModule } from './module.ts'
8
8
  import { compileNativeComponentInterface } from './native/compile-native-component-interface.ts'
9
9
  import { getNativeComponentAssetBytes } from './native/native-component-assets.ts'