vite-plugin-taro 0.6.1 → 0.6.3

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 (63) hide show
  1. package/README.en.md +6 -1
  2. package/README.md +6 -1
  3. package/dist/node/plugins/h5/transform-app.js +1 -1
  4. package/dist/node/plugins/wx/dev/plugins.d.ts +2 -12
  5. package/dist/node/plugins/wx/dev/plugins.js +12 -66
  6. package/dist/node/plugins/wx/dev/react-refresh.d.ts +9 -3
  7. package/dist/node/plugins/wx/dev/react-refresh.js +35 -7
  8. package/dist/node/plugins/wx/dev/wx-dev-options.js +3 -4
  9. package/dist/node/plugins/wx/{module.js → module/module.js} +2 -2
  10. package/dist/node/plugins/wx/placer/placement.d.ts +1 -0
  11. package/dist/node/plugins/wx/placer/placement.js +1 -1
  12. package/dist/node/plugins/wx/placer/placer.d.ts +17 -1
  13. package/dist/node/plugins/wx/placer/placer.js +25 -1
  14. package/dist/node/plugins/wx/plugins.js +1 -1
  15. package/dist/node/plugins/wx/render/capsule.d.ts +35 -3
  16. package/dist/node/plugins/wx/render/capsule.js +53 -12
  17. package/dist/node/plugins/wx/render/native.d.ts +20 -2
  18. package/dist/node/plugins/wx/render/native.js +539 -35
  19. package/dist/node/plugins/wx/render/system-js/string-editor.d.ts +24 -0
  20. package/dist/node/plugins/wx/render/system-js/string-editor.js +93 -0
  21. package/dist/node/plugins/wx/render/system-js/system-js.d.ts +23 -0
  22. package/dist/node/plugins/wx/render/system-js/system-js.js +602 -0
  23. package/dist/node/plugins/wx/render/transport.js +3 -3
  24. package/dist/node/plugins/wx/resolve/resolver.js +5 -5
  25. package/dist/node/plugins/wx/resolve/{specialize-bootstrap.d.ts → specialize-app-capsule.d.ts} +2 -2
  26. package/dist/node/plugins/wx/resolve/{specialize-bootstrap.js → specialize-app-capsule.js} +3 -3
  27. package/dist/node/plugins/wx/resolve/specialize-page-capsule.js +1 -1
  28. package/dist/node/utils/transform.d.ts +0 -3
  29. package/dist/node/utils/transform.js +0 -23
  30. package/dist/runtime/wx/amphibious/bootstrap.d.ts +0 -2
  31. package/dist/runtime/wx/amphibious/bootstrap.js +0 -2
  32. package/dist/runtime/wx/capsule/app.d.ts +2 -2
  33. package/dist/runtime/wx/capsule/app.js +2 -2
  34. package/dist/runtime/wx/dev/dev-runtime.js +55 -79
  35. package/package.json +8 -6
  36. package/src/node/plugins/h5/transform-app.ts +1 -1
  37. package/src/node/plugins/wx/dev/plugins.ts +12 -69
  38. package/src/node/plugins/wx/dev/react-refresh.ts +36 -7
  39. package/src/node/plugins/wx/dev/wx-dev-options.ts +3 -4
  40. package/src/node/plugins/wx/{module.ts → module/module.ts} +2 -2
  41. package/src/node/plugins/wx/placer/placement.ts +1 -1
  42. package/src/node/plugins/wx/placer/placer.ts +27 -2
  43. package/src/node/plugins/wx/plugins.ts +1 -1
  44. package/src/node/plugins/wx/render/capsule.ts +53 -17
  45. package/src/node/plugins/wx/render/native.ts +670 -55
  46. package/src/node/plugins/wx/render/system-js/string-editor.ts +115 -0
  47. package/src/node/plugins/wx/render/system-js/system-js.ts +831 -0
  48. package/src/node/plugins/wx/render/transport.ts +3 -3
  49. package/src/node/plugins/wx/resolve/resolver.ts +5 -5
  50. package/src/node/plugins/wx/resolve/{specialize-bootstrap.ts → specialize-app-capsule.ts} +3 -3
  51. package/src/node/plugins/wx/resolve/specialize-page-capsule.ts +1 -1
  52. package/src/node/utils/transform.ts +0 -30
  53. package/src/runtime/wx/amphibious/bootstrap.ts +0 -5
  54. package/src/runtime/wx/capsule/app.ts +5 -2
  55. package/src/runtime/wx/dev/dev-runtime.ts +71 -120
  56. package/src/runtime/wx/wechat.d.ts +1 -2
  57. package/dist/node/plugins/wx/render/capsule-wrapper.d.ts +0 -3
  58. package/dist/node/plugins/wx/render/capsule-wrapper.js +0 -64
  59. package/src/node/plugins/wx/render/capsule-wrapper.ts +0 -86
  60. /package/dist/node/plugins/wx/{chunk-path.d.ts → module/chunk-path.d.ts} +0 -0
  61. /package/dist/node/plugins/wx/{chunk-path.js → module/chunk-path.js} +0 -0
  62. /package/dist/node/plugins/wx/{module.d.ts → module/module.d.ts} +0 -0
  63. /package/src/node/plugins/wx/{chunk-path.ts → module/chunk-path.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { types } from '@babel/core';
1
+ import * as types from '@babel/types';
2
2
  import { replaceWithAst } from '../../../utils/transform.js';
3
3
  const pagePathPlaceholder = '__VPT_PAGE_PATH__';
4
4
  const pageConfigPlaceholder = '__VPT_PAGE_CONFIG__';
@@ -1,4 +1,3 @@
1
- import { type PluginItem } from '@babel/core';
2
1
  import { generate } from '@babel/generator';
3
2
  import { type Rolldown } from 'vite';
4
3
  export type AstTransformResult = {
@@ -7,5 +6,3 @@ export type AstTransformResult = {
7
6
  };
8
7
  /** Replaces each placeholder with a Babel AST expression while transforming the module through Oxc. */
9
8
  export declare function replaceWithAst(code: string, filename: string, replacement: Readonly<Record<string, Parameters<typeof generate>[0]>>, sourcemap?: boolean): Promise<AstTransformResult>;
10
- /** Transforms one module with Babel's shared parser and source-map configuration. */
11
- export declare function transformWithBabel(code: string, filename: string, plugins: PluginItem[], sourcemap?: boolean): AstTransformResult;
@@ -1,4 +1,3 @@
1
- import { transformSync } from '@babel/core';
2
1
  import { generate } from '@babel/generator';
3
2
  import { transformWithOxc } from 'vite';
4
3
  import { esTarget } from './constant.js';
@@ -34,25 +33,3 @@ function requireOnePlaceholder(code, placeholder) {
34
33
  throw new Error(`Expected one placeholder ${placeholder}, found ${replacementCount}`);
35
34
  }
36
35
  }
37
- /** Transforms one module with Babel's shared parser and source-map configuration. */
38
- export function transformWithBabel(code, filename, plugins, sourcemap = true) {
39
- const transformed = transformSync(code, {
40
- babelrc: false,
41
- compact: true,
42
- minified: true,
43
- comments: false,
44
- configFile: false,
45
- filename,
46
- plugins,
47
- sourceFileName: filename,
48
- sourceMaps: sourcemap,
49
- sourceType: 'module'
50
- });
51
- if (!transformed?.code || (sourcemap && !transformed.map)) {
52
- throw new Error(`Failed to transform ${filename} with Babel`);
53
- }
54
- return {
55
- code: transformed.code,
56
- map: sourcemap ? transformed.map : null
57
- };
58
- }
@@ -1,4 +1,2 @@
1
1
  import '../systemjs/system-core.js';
2
- /** Shares one App configuration object between the specialized bootstrap and App capsule. */
3
- export declare const appConfig: Record<string, unknown>;
4
2
  export declare const __vitePreload: <Value>(load: () => Value) => Value;
@@ -1,8 +1,6 @@
1
1
  // Install the minimal SystemJS loader and its synchronous-import extension before any native entry requests a capsule.
2
2
  import '../systemjs/system-core.js';
3
3
  import { transport } from './transport.js';
4
- /** Shares one App configuration object between the specialized bootstrap and App capsule. */
5
- export const appConfig = __VPT_APP_CONFIG__;
6
4
  // WX has no modulepreload transport. Genuine application import() boundaries retain System.import() and may load
7
5
  // asynchronous subpackage or top-level-await graphs through this identity wrapper.
8
6
  export const __vitePreload = (load) => load();
@@ -1,2 +1,2 @@
1
- declare const _default: any;
2
- export default _default;
1
+ declare const config: any;
2
+ export default config;
@@ -1,7 +1,7 @@
1
1
  // biome-ignore assist/source/organizeImports: Taro must initialize before the App component.
2
2
  import { createReactApp, ReactDOM } from './taro-runtime.js';
3
3
  import React from 'react';
4
- import { appConfig } from '../amphibious/bootstrap.js';
5
4
  // @ts-expect-error: The wx build resolves this private App component.
6
5
  import AppComponent from '\0vpt:app-component';
7
- export default createReactApp(AppComponent, React, ReactDOM, appConfig);
6
+ const config = createReactApp(AppComponent, React, ReactDOM, __VPT_APP_CONFIG__);
7
+ export default config;
@@ -5,16 +5,13 @@
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
- // Every Page explicitly passes the inert hmr/patches.js export here before importing its capsule. The runtime applies that
8
+ // Every Page explicitly passes the inert hmr/patches.js export here before native Page registration. 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) {
10
+ const pageHmrStateKey = Symbol('vpt.pageHmrState');
11
+ /** Calls a native lifecycle with the same Page bound to `this` and the same arguments. */
12
+ function forward(handler, page, args) {
12
13
  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;
14
+ handler.apply(page, args);
18
15
  }
19
16
  /** Shared no-op CSS contract because physical rebuilds replace styles wholesale. */
20
17
  const hotContextInternals = Object.freeze({
@@ -168,92 +165,76 @@ class WxDevRuntime extends DevRuntime {
168
165
  return;
169
166
  this.session = { ...info, appliedSeq: 0 };
170
167
  }
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) {
180
- return;
168
+ /** Tracks the mounted native Page and prepares its static config for HMR re-registration. */
169
+ injectPageHmr(config) {
170
+ const existingState = config[pageHmrStateKey];
171
+ if (existingState) {
172
+ const mountedPage = existingState.mountedPage;
173
+ /*
174
+ * The static config can outlive a native Page instance. Before its first ordinary onLoad, or after a real onUnload,
175
+ * there is no mounted Page or current view-model to carry into another registration. Leave the lifecycle gate
176
+ * unarmed and preserve the config's existing initial data so a future real onLoad still enters Taro normally.
177
+ */
178
+ if (!mountedPage) {
179
+ return config;
180
+ }
181
+ /*
182
+ * Arm the lifecycle wrappers on this exact static config before it is passed back to `Page(config)`. DevTools then
183
+ * triggers an unload/load/show sequence for that native re-registration: unload and load observe `true` and return
184
+ * before entering Taro, preserving the mounted React tree and its original Page connection; show consumes the
185
+ * one-shot gate by restoring `false`. Ordinary navigation never enters this branch, and every Page config owns an
186
+ * independent state object, so no route map, global phase, or Page identity comparison participates in the decision.
187
+ */
188
+ existingState.isReregistering = true;
189
+ /*
190
+ * `Page(config)` reads `config.data` as the initial native view-model for this registration. Supplying the mounted
191
+ * Page's latest data before that call prevents the temporary Page used for re-registration callbacks from starting
192
+ * empty. This is an O(1) reference assignment in vpt: it does not clone the recursive data tree, call `setData`, move
193
+ * React state, or rebind Taro. The ordinary Taro lifecycle remains suppressed until re-registration onShow, so its
194
+ * React tree and output connection stay attached to `mountedPage`; every later registration reads its latest data.
195
+ */
196
+ config.data = mountedPage.data;
197
+ return config;
181
198
  }
182
- this.taro = {
183
- current,
184
- document,
185
- injectPageInstance,
186
- pageReplacements: new Map()
187
- };
188
- }
189
- /** Injects snapshot-preserving behavior into one route-specific Taro Page configuration. */
190
- injectPageHmr(config, route) {
191
199
  const originalOnUnload = config.onUnload;
192
200
  const originalOnLoad = config.onLoad;
193
201
  const originalOnShow = config.onShow;
194
- const runtime = this;
202
+ const state = {
203
+ isReregistering: false,
204
+ mountedPage: undefined
205
+ };
206
+ /*
207
+ * Attach the one mutable HMR state object to this exact static config. The lifecycle wrappers below close over the same
208
+ * object, while a later `injectPageHmr(config)` call finds it through the symbol and knows wrapping is already complete.
209
+ * A symbol cannot collide with WeChat or Taro's string-named Page options, and config-local ownership avoids route maps,
210
+ * a runtime-wide WeakMap, or state shared by two Page configs. The state becomes unreachable together with the config.
211
+ */
212
+ config[pageHmrStateKey] = state;
195
213
  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
- });
214
+ if (state.isReregistering) {
205
215
  return;
206
216
  }
207
217
  forward(originalOnUnload, this, args);
218
+ state.mountedPage = undefined;
208
219
  };
209
220
  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
+ if (state.isReregistering) {
221
222
  return;
222
223
  }
223
224
  forward(originalOnLoad, this, args);
225
+ state.mountedPage = this;
224
226
  };
225
227
  config.onShow = function (...args) {
226
- if (runtime.requireTaro().pageReplacements.delete(route)) {
227
- // Synthetic shows must not repeat requests or reset application state.
228
+ if (state.isReregistering) {
229
+ state.isReregistering = false;
228
230
  return;
229
231
  }
230
232
  forward(originalOnShow, this, args);
231
233
  };
234
+ return config;
232
235
  }
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;
243
- }
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;
254
- }
255
- /** Applies one Page-delivered payload and arms its route for native replacement. */
256
- applyPatches(payload, route) {
236
+ /** Applies one Page-delivered payload before its native shell registers the static route configuration. */
237
+ applyPatches(payload) {
257
238
  // The initial physical dependency exports undefined until the host has a patch range.
258
239
  if (!payload)
259
240
  return;
@@ -262,11 +243,6 @@ class WxDevRuntime extends DevRuntime {
262
243
  console.warn('[vpt] patches for a stale build');
263
244
  return;
264
245
  }
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
- }
270
246
  // Apply synchronously: the page's imports below the require resolve against the
271
247
  // freshly registered modules, so the re-executed Page evaluates with the new code.
272
248
  if (this.applyPatchBatch(session, payload.patches)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-taro",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
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",
@@ -53,9 +53,8 @@
53
53
  "dependencies": {
54
54
  "@babel/core": "^7.29.7",
55
55
  "@babel/generator": "^7.29.8",
56
- "@babel/plugin-transform-dynamic-import": "^7.29.7",
57
- "@babel/plugin-transform-modules-commonjs": "^7.29.7",
58
- "@babel/plugin-transform-modules-systemjs": "^7.29.8",
56
+ "@babel/types": "^7.29.8",
57
+ "@oxc-project/types": "0.143.0",
59
58
  "@rolldown/plugin-babel": "^0.2.3",
60
59
  "@tailwindcss-mangle/engine": "0.2.0",
61
60
  "@tarojs/components": "4.2.0",
@@ -76,8 +75,8 @@
76
75
  "rxjs": "^7.8.2",
77
76
  "tailwindcss": "^4.3.3",
78
77
  "weapp-tailwindcss": "^5.2.11",
79
- "@tarojs/plugin-framework-react": "npm:vite-plugin-taro-plugin-framework-react@0.6.1",
80
- "@tarojs/react": "npm:vite-plugin-taro-react@0.6.1"
78
+ "@tarojs/plugin-framework-react": "npm:vite-plugin-taro-plugin-framework-react@0.6.3",
79
+ "@tarojs/react": "npm:vite-plugin-taro-react@0.6.3"
81
80
  },
82
81
  "peerDependencies": {
83
82
  "react": "^19.0.0",
@@ -85,6 +84,9 @@
85
84
  "vite": "8.2.1"
86
85
  },
87
86
  "devDependencies": {
87
+ "@babel/plugin-transform-dynamic-import": "^7.29.7",
88
+ "@babel/plugin-transform-modules-commonjs": "^7.29.7",
89
+ "@babel/plugin-transform-modules-systemjs": "^7.29.8",
88
90
  "@types/babel__core": "^7.20.5",
89
91
  "@types/babel__generator": "^7.27.0",
90
92
  "@types/node": "^26.1.1",
@@ -1,4 +1,4 @@
1
- import { types } from '@babel/core'
1
+ import * as types from '@babel/types'
2
2
  import type { VptOptions, VptPageOption } from '../../../options.ts'
3
3
  import { createPageComponentImportPath } from '../../utils/modules.ts'
4
4
  import { createAppConfig } from '../../utils/project-config.ts'
@@ -3,13 +3,11 @@ import type { VptOptions } from '../../../../options.ts'
3
3
  import { esTarget } from '../../../utils/constant.ts'
4
4
  import { memoize } from '../../../utils/memoize.ts'
5
5
  import { normalizeModuleId } from '../../../utils/modules.ts'
6
- import { appCapsulePath, pageCapsulePath, rolldownRuntimeId, taroRuntimePath } from '../module.ts'
6
+ import { pageShellPath, rolldownRuntimeId } from '../module/module.ts'
7
7
  import { createWxDevHost, type WxDevHost } from './dev-host.ts'
8
8
  import { developmentAppWxssFileName } from './hmr-files.ts'
9
9
  import { createWxReactRefreshTransforms } from './react-refresh.ts'
10
10
 
11
- const taroRuntimeId = '@tarojs/runtime'
12
-
13
11
  /** Selects the sole Vite environment that owns the physical Mini Program development project. */
14
12
  export function isWxClientEnvironment(environment: Readonly<{ name: string }>): boolean {
15
13
  return environment.name === 'client'
@@ -32,9 +30,7 @@ export function createWxDevelopmentPlugin(options: VptOptions, applicationEntryI
32
30
  */
33
31
  let host: WxDevHost | null = null
34
32
  // Portable hook filters stay broad; these exact identities exclude similarly named user modules.
35
- const normalizedAppCapsulePath = normalizePath(appCapsulePath)
36
- const normalizedPageCapsulePath = normalizePath(pageCapsulePath)
37
- const normalizedTaroRuntimePath = normalizePath(taroRuntimePath)
33
+ const normalizedPageShellPath = normalizePath(pageShellPath)
38
34
 
39
35
  return [
40
36
  {
@@ -116,38 +112,14 @@ export function createWxDevelopmentPlugin(options: VptOptions, applicationEntryI
116
112
  }
117
113
  },
118
114
  {
119
- name: 'vpt:wx-react-refresh-bootstrap',
120
- apply: 'serve',
121
- transform: {
122
- order: 'post',
123
- filter: { id: /\/runtime\/wx\/capsule\/app\.js(?:\?|$)/ },
124
- handler(code, id) {
125
- if (normalizeModuleId(id) !== normalizedAppCapsulePath) return
126
- return injectReactRefreshBootstrap(code)
127
- }
128
- }
129
- },
130
- {
131
- name: 'vpt:wx-page-hmr',
132
- apply: 'serve',
133
- transform: {
134
- order: 'post',
135
- filter: { id: /\/runtime\/wx\/capsule\/page\.js(?:\?|$)/ },
136
- handler(code, id) {
137
- if (normalizeModuleId(id) !== normalizedPageCapsulePath) return
138
- return injectPageHmr(code, getPageRoute(id))
139
- }
140
- }
141
- },
142
- {
143
- name: 'vpt:wx-taro-hmr',
115
+ name: 'vpt:wx-page-shell-hmr',
144
116
  apply: 'serve',
145
117
  transform: {
146
118
  order: 'post',
147
- filter: { id: /\/runtime\/wx\/capsule\/taro-runtime\.js(?:\?|$)/ },
119
+ filter: { id: /\/runtime\/wx\/native\/page\.js(?:\?|$)/ },
148
120
  handler(code, id) {
149
- if (normalizeModuleId(id) !== normalizedTaroRuntimePath) return
150
- return injectTaroConnection(code)
121
+ if (normalizeModuleId(id) !== normalizedPageShellPath) return
122
+ return injectPageShellHmr(code)
151
123
  }
152
124
  }
153
125
  },
@@ -174,48 +146,19 @@ export function removeDevelopmentAppWxss(bundle: Record<string, unknown>): void
174
146
  delete bundle[developmentAppWxssFileName]
175
147
  }
176
148
 
177
- /** Ensures the Refresh hook exists before React's renderer evaluates and injects itself. */
178
- export function injectReactRefreshBootstrap(code: string): { code: string; map: null } {
179
- return {
180
- code: `import ${JSON.stringify('/@react-refresh')};\n${code}`,
181
- map: null
182
- }
183
- }
184
-
185
- /** Connects the shared WX dev runtime to the application graph's Taro runtime instance. */
186
- export function injectTaroConnection(code: string): { code: string; map: null } {
187
- if (!/\bCurrent\b/.test(code) || !/\bdocument\b/.test(code) || !/\binjectPageInstance\b/.test(code)) {
188
- throw new Error('WX Taro runtime must expose Current, document, and injectPageInstance for HMR')
189
- }
190
-
191
- const taroImport = `import { Current as __vptCurrent, document as __vptDocument, injectPageInstance as __vptInjectPageInstance } from ${JSON.stringify(taroRuntimeId)};`
192
-
193
- return {
194
- code: `${code}\n${taroImport}\n__rolldown_runtime__.connectTaro(__vptCurrent, __vptDocument, __vptInjectPageInstance);`,
195
- map: null
196
- }
197
- }
198
-
199
- /** Activates development-only lifecycle handling for one plugin-owned Page capsule. */
200
- export function injectPageHmr(code: string, route: string): { code: string; map: null } {
201
- if (!/\bconst\s+config\s*=/.test(code) || !/\bexport\s+default\s+config\b/.test(code)) {
202
- throw new Error('WX Page capsule must declare and default-export config before HMR injection')
149
+ /** Injects the retained snapshot at the exact native Page registration edge. */
150
+ export function injectPageShellHmr(code: string): { code: string; map: null } {
151
+ const registration = 'Page(pageConfig)'
152
+ if (!code.includes(registration)) {
153
+ throw new Error('WX native Page shell must register pageConfig')
203
154
  }
204
155
 
205
156
  return {
206
- code: `${code}\n__rolldown_runtime__.injectPageHmr(config, ${JSON.stringify(route)});`,
157
+ code: code.replace(registration, 'Page(__rolldown_runtime__.injectPageHmr(pageConfig))'),
207
158
  map: null
208
159
  }
209
160
  }
210
161
 
211
- /** Reads the stable route carried by every specialized Page capsule ID. */
212
- function getPageRoute(id: string): string {
213
- const queryIndex = id.indexOf('?')
214
- const route = queryIndex < 0 ? null : new URLSearchParams(id.slice(queryIndex + 1)).get('route')
215
- if (!route) throw new Error(`WX Page capsule is missing its route: ${id}`)
216
- return route
217
- }
218
-
219
162
  /*
220
163
  * memoize owns a mutable one-entry-by-input cache. The assembled runtime source is byte-identical across complete generations
221
164
  * because both the Rolldown base and injected implementation are immutable for the server lifetime. Reusing its lowered result
@@ -2,7 +2,9 @@ import { isReferenceIdentifier, type WalkerEnter } from 'oxc-walker'
2
2
  import type { RolldownMagicString } from 'rolldown'
3
3
  import type { Plugin } from 'vite'
4
4
  import { memoize } from '../../../utils/memoize.ts'
5
+ import { normalizeModuleId } from '../../../utils/modules.ts'
5
6
  import { transformWithOxcWalker } from '../../../utils/oxc-transform.ts'
7
+ import { reactReconcilerRoot } from '../module/module.ts'
6
8
 
7
9
  /** The React DevTools hook protocol name; free references must target `global` in wx. */
8
10
  const reactDevtoolsHookProtocol = '__REACT_DEVTOOLS_GLOBAL_HOOK__'
@@ -14,7 +16,7 @@ const reactDevtoolsHookProtocol = '__REACT_DEVTOOLS_GLOBAL_HOOK__'
14
16
  * - `__getReactRefreshIgnoredExports` is an optional extension point read while validating a refresh boundary.
15
17
  * Leaving that read on the nonexistent `window` crashes every update validation pass.
16
18
  *
17
- * Keeping this list explicit prevents the adapter from rewriting unrelated browser accesses if the vendored runtime
19
+ * Keeping this list explicit prevents the adapter from rewriting unrelated browser accesses if the refresh runtime
18
20
  * gains new code. Any future React Refresh protocol addition therefore requires a deliberate compatibility decision.
19
21
  */
20
22
  const refreshRuntimeWindowGlobals = ['__registerBeforePerformReactRefresh', '__getReactRefreshIgnoredExports'] as const
@@ -24,8 +26,9 @@ const refreshRuntimeWindowGlobals = ['__registerBeforePerformReactRefresh', '__g
24
26
  *
25
27
  * @vitejs/plugin-react's generated refresh code assumes the web HTML preamble and a browser
26
28
  * global scope; wx has neither. Each transform adapts one piece of that contract:
27
- * - the refresh runtime module (id-filtered): the vendored runtime reads and assigns
29
+ * - the refresh runtime module (id-filtered): the runtime reads and assigns
28
30
  * `window` protocol globals (rewritten to `global`) and must inject itself at evaluation;
31
+ * - React Reconciler: its renderer injection statically depends on the refresh runtime, fixing cold-start order;
29
32
  * - react-family modules (filtered on free references): the DevTools hook is read as a free
30
33
  * variable, which the WeChat runtime scope never resolves against `global` — every free
31
34
  * reference becomes an explicit member access;
@@ -33,8 +36,8 @@ const refreshRuntimeWindowGlobals = ['__registerBeforePerformReactRefresh', '__g
33
36
  * needed because the transform generates local wrappers over the imported refresh
34
37
  * runtime, so the guard that checks for the global is removed.
35
38
  *
36
- * Each transform's filter is its routing: the three domains are disjoint, so a module is
37
- * transformed by at most one of them, and modules outside all three never reach a handler.
39
+ * Filters route every adaptation directly. The Reconciler intentionally receives the renderer dependency first and the
40
+ * global-hook rewrite second; modules outside these explicit domains never reach a handler.
38
41
  */
39
42
  export function createWxReactRefreshTransforms(): Plugin[] {
40
43
  return [
@@ -43,13 +46,26 @@ export function createWxReactRefreshTransforms(): Plugin[] {
43
46
  apply: 'serve',
44
47
  transform: {
45
48
  order: 'post',
46
- // The vendored refresh runtime module; id-filtered, so no code scan.
49
+ // The refresh runtime module is id-filtered, so no code scan is needed.
47
50
  filter: { id: /^\/@react-refresh(?:\?|$)/ },
48
51
  handler(code, id) {
49
52
  return fixRefreshRuntime({ code, id })
50
53
  }
51
54
  }
52
55
  },
56
+ {
57
+ name: 'vpt:wx-react-refresh-renderer-dependency',
58
+ apply: 'serve',
59
+ transform: {
60
+ order: 'post',
61
+ filter: { id: /\/react-reconciler\/cjs\/react-reconciler\.development\.js(?:\?|$)/ },
62
+ handler(code, id) {
63
+ const rendererId = `${reactReconcilerRoot}/cjs/react-reconciler.development.js`
64
+ if (normalizeModuleId(id) !== rendererId) return
65
+ return injectReactRefreshRendererDependency(code)
66
+ }
67
+ }
68
+ },
53
69
  {
54
70
  name: 'vpt:wx-react-devtools-hook',
55
71
  apply: 'serve',
@@ -92,7 +108,8 @@ export function createWxReactRefreshTransforms(): Plugin[] {
92
108
  * The protocol name is unique, but only reference identifiers are rewritten. Declaration
93
109
  * keys and explicit members such as `global.__REACT_DEVTOOLS_GLOBAL_HOOK__` must remain
94
110
  * untouched; rewriting those would either produce invalid syntax or double-prefix the hook.
95
- * The eagerly evaluated refresh runtime creates the hook on `global` before the renderer loads.
111
+ * The Reconciler's injected static dependency orders hook injection before renderer initialization independently of Rolldown's
112
+ * final chunking decision.
96
113
  */
97
114
  function createReactDevtoolsHookVisitor(editor: RolldownMagicString): WalkerEnter {
98
115
  return function enter(node, parent) {
@@ -146,7 +163,7 @@ function createRefreshRuntimeVisitor(editor: RolldownMagicString): WalkerEnter {
146
163
  refreshRuntimeWindowGlobals.some((globalName) => globalName === node.property.name)
147
164
  ) {
148
165
  // `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
166
+ // replacing the object range preserves the upstream runtime byte-for-byte otherwise
150
167
  // and prevents unrelated `window` expressions from being silently adapted.
151
168
  editor.overwrite(node.object.start, node.object.end, 'global')
152
169
  }
@@ -187,6 +204,18 @@ function createRefreshPreambleGuardVisitor(editor: RolldownMagicString): WalkerE
187
204
  }
188
205
  }
189
206
 
207
+ /** Makes renderer hook injection statically depend on the refresh runtime. */
208
+ export function injectReactRefreshRendererDependency(code: string): { code: string; map: null } {
209
+ if (!/\bhook\.inject\(internals\)/.test(code)) {
210
+ throw new Error('React Reconciler must inject its renderer into the DevTools hook')
211
+ }
212
+
213
+ return {
214
+ code: `import '/@react-refresh'\n${code}`,
215
+ map: null
216
+ }
217
+ }
218
+
190
219
  export function transformRefreshRuntime({ code, id }: { code: string; id: string }) {
191
220
  return transformWithOxcWalker({
192
221
  code,
@@ -6,7 +6,7 @@ import type { ViteDevServer } from 'vite'
6
6
  import type { VptOptions } from '../../../../options.ts'
7
7
  import { once } from '../../../utils/once.ts'
8
8
  import { resolvePackageFile } from '../../../utils/packages.ts'
9
- import { appShellFileName } from '../module.ts'
9
+ import { appShellFileName } from '../module/module.ts'
10
10
 
11
11
  type BundledDevRolldownOptions = InputOptions & {
12
12
  experimental?: {
@@ -145,7 +145,7 @@ function ensureSingleOutput(rolldownOptions: BundledDevRolldownOptions): OutputO
145
145
  /**
146
146
  * Prepends entry banners after Rolldown's analysis so their native requires remain physical dependencies rather than chunk
147
147
  * graph edges. The App initializes the runtime identity, while every Page explicitly applies the watched patch data before its
148
- * capsule import continues.
148
+ * native Page registration.
149
149
  */
150
150
  function createEntryBanner(pageFiles: ReadonlySet<string>): (chunk: { name: string; fileName: string }) => string {
151
151
  return (chunk) => {
@@ -154,8 +154,7 @@ function createEntryBanner(pageFiles: ReadonlySet<string>): (chunk: { name: stri
154
154
  }
155
155
  if (pageFiles.has(chunk.name)) {
156
156
  const patchesPath = path.posix.relative(path.posix.dirname(chunk.fileName), 'hmr/patches.js')
157
- const route = chunk.name.slice(0, -'.js'.length)
158
- return `__rolldown_runtime__.applyPatches(require('${patchesPath}'), ${JSON.stringify(route)});\n`
157
+ return `__rolldown_runtime__.applyPatches(require('${patchesPath}'));\n`
159
158
  }
160
159
  return ''
161
160
  }
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path'
2
2
  import { normalizePath, type Rolldown } from 'vite'
3
- import { normalizeModuleId } from '../../utils/modules.ts'
4
- import { packageRequire, resolvePackageFile } from '../../utils/packages.ts'
3
+ import { normalizeModuleId } from '../../../utils/modules.ts'
4
+ import { packageRequire, resolvePackageFile } from '../../../utils/packages.ts'
5
5
 
6
6
  /** Identifies Rolldown's generated helper module independently of its unstable output filename. */
7
7
  export const rolldownRuntimeId = '\0rolldown/runtime.js'
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto'
2
2
  import type { Rolldown } from 'vite'
3
3
 
4
4
  // Leave headroom below WeChat's 2M subpackage limit for rendered wrappers and native assets.
5
- const subpackagePlanningBudget = 1_900_000
5
+ export const subpackagePlanningBudget = 1_900_000
6
6
  const generatedSubpackageRootPrefix = 'sub/p_'
7
7
 
8
8
  /** Identifies one generated code-only subpackage by its physical output root. */
@@ -1,10 +1,19 @@
1
- import type { Plugin, Rolldown } from 'vite'
2
- import { getWxExecutionKind, isTransportModule } from '../module.ts'
1
+ import { normalizePath, type Plugin, type Rolldown } from 'vite'
2
+ import { getWxExecutionKind, isTransportModule } from '../module/module.ts'
3
3
  import { getNativeComponentAssetBytes } from '../native/native-component-assets.ts'
4
4
  import { createPlacement, type GeneratedSubpackage, type PackageLocation, type Placement } from './placement.ts'
5
5
 
6
6
  export type { GeneratedSubpackage, Placement } from './placement.ts'
7
7
 
8
+ const pnpmFrameworkPackagePattern = /\/node_modules\/\.pnpm\/(?:@tarojs\+|react(?:-dom|-reconciler)?@|scheduler@)/
9
+ const workspaceFrameworkPackagePattern = /\/packages\/(?:taro-react|taro-plugin-framework-react)\//
10
+
11
+ /** Selects the explicit React/Taro roots whose complete dependency closure forms the framework vendor chunk. */
12
+ export function isWxFrameworkVendorModule(moduleId: string): boolean {
13
+ const normalizedId = normalizePath(moduleId)
14
+ return pnpmFrameworkPackagePattern.test(normalizedId) || workspaceFrameworkPackagePattern.test(normalizedId)
15
+ }
16
+
8
17
  type PlacementState =
9
18
  | { phase: 'idle' }
10
19
  | { phase: 'awaiting-chunks' }
@@ -30,6 +39,22 @@ export const placementRolldownOptions = {
30
39
  * participation only; LTHP mutates the resulting OutputChunk filenames later without replacing the chunks.
31
40
  */
32
41
  output: {
42
+ /**
43
+ * React and Taro form one stable framework boundary shared by the App and every Page capsule. Keeping their complete
44
+ * dependency closure together prevents application edits from invalidating framework chunk identity and makes later
45
+ * development generations eligible to reuse the unchanged vendor. All remaining modules use Rolldown's automatic
46
+ * chunking; physical WX package placement operates on those final chunks without imposing another split strategy.
47
+ */
48
+ codeSplitting: {
49
+ groups: [
50
+ {
51
+ name: 'vendor',
52
+ test: isWxFrameworkVendorModule,
53
+ priority: 100,
54
+ includeDependenciesRecursively: true
55
+ }
56
+ ]
57
+ },
33
58
  /**
34
59
  * Native App/Page/Component shells are files addressed directly by WeChat and must retain the exact names configured
35
60
  * in `input`, such as `app.js` and `pages/home/index.js`. Transport is excluded even though it is CommonJS:
@@ -4,7 +4,7 @@ import { esTarget } from '../../utils/constant.ts'
4
4
  import { packageRequire } from '../../utils/packages.ts'
5
5
  import { clientTaroNativeId } from '../client/constant.ts'
6
6
  import { createWxDevelopmentPlugin } from './dev/plugins.ts'
7
- import { getWxExecutionKind, isTransportModule } from './module.ts'
7
+ import { getWxExecutionKind, isTransportModule } from './module/module.ts'
8
8
  import { compileNativeComponentInterface } from './native/compile-native-component-interface.ts'
9
9
  import { createOutputFiles } from './output/files.ts'
10
10
  import { createWxPlacementPlugin, type WxPlacementPlugin } from './placer/placer.ts'