@vitarx/plugin-vite 1.0.5 → 1.0.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.
@@ -1,12 +1,3 @@
1
- /**
2
- * Vitarx 运行时 API 名称
3
- * 这些 API 需要从 vitarx 包导入
4
- */
5
- /**
6
- * UI 相关的 API 名称列表
7
- * 这些 API 用于创建视图,在 HMR 时需要识别
8
- */
9
- export declare const UI_API_NAMES: readonly ["createView", "branch", "dynamic", "expr", "accessor", "withDirectives"];
10
1
  /**
11
2
  * Ref 相关 API 名称
12
3
  * 用于识别 ref 变量
@@ -1,12 +1,3 @@
1
- /**
2
- * Vitarx 运行时 API 名称
3
- * 这些 API 需要从 vitarx 包导入
4
- */
5
- /**
6
- * UI 相关的 API 名称列表
7
- * 这些 API 用于创建视图,在 HMR 时需要识别
8
- */
9
- export const UI_API_NAMES = ['createView', 'branch', 'dynamic', 'expr', 'accessor', 'withDirectives'];
10
1
  /**
11
2
  * Ref 相关 API 名称
12
3
  * 用于识别 ref 变量
@@ -46,6 +46,17 @@ export default class HMRManager {
46
46
  * const state = __$VITARX_HMR$__.instance.memo(__$VITARX_HMR_VIEW_NODE$__, 'state') ?? ref(0)
47
47
  */
48
48
  memo(view: ComponentView, name: string): any;
49
+ /**
50
+ * 无条件恢复状态(不做 Ref/reactive 门禁)
51
+ *
52
+ * 供解构声明的隐藏变量使用:composable 返回的控制器是普通对象
53
+ * (内含 Ref 与函数),memo 的响应式门禁会将其拒收导致恢复永远
54
+ * 落空。仅限转换器为解构声明生成的代码调用。
55
+ *
56
+ * @param {ComponentView} view - 组件视图
57
+ * @param {string} name - 变量名称
58
+ */
59
+ memoRaw(view: ComponentView, name: string): any;
49
60
  /**
50
61
  * 注册节点
51
62
  *
@@ -59,10 +70,25 @@ export default class HMRManager {
59
70
  * @param component - 组件构造函数
60
71
  */
61
72
  getId(component: Component): string;
73
+ /**
74
+ * 登记视图状态 getter
75
+ *
76
+ * 由转换器注入的代码在组件函数每次执行时调用,登记当前作用域的
77
+ * 惰性状态读取器(同一视图重复登记覆盖旧的)。读取动作延迟到
78
+ * 热更新快照时发生,正常挂载/重挂载不会产生任何状态残留。
79
+ *
80
+ * @param {ComponentView} view - 组件视图
81
+ * @param {Record<string, () => any>} getters - 变量名到读取器的映射
82
+ */
83
+ trackState(view: ComponentView, getters: Record<string, () => any>): void;
62
84
  /**
63
85
  * 模块更新
64
86
  *
65
87
  * @param newModule - 新模块对象
88
+ * @returns 模块导出中是否包含至少一个已绑定 HMR id 的组件。
89
+ * 供导入转发模块的 accept 回调判断兜底:转发目标若不含
90
+ * 可识别组件(如换成未转换模块的组件),调用方应
91
+ * invalidate 触发整页刷新,避免界面静默 stale。
66
92
  */
67
- update(newModule: ModuleNamespace): void;
93
+ update(newModule: ModuleNamespace): boolean;
68
94
  }
@@ -3,10 +3,11 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
3
3
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
4
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
5
  };
6
- var _HMRManager_idMapToView, _HMRManager_idMapToComponent;
6
+ var _HMRManager_instances, _HMRManager_idMapToView, _HMRManager_idMapToComponent, _HMRManager_viewStateGetters, _HMRManager_snapshotState;
7
7
  import { isComponent, isReactive, isRef } from 'vitarx';
8
8
  import { HMR } from '../constants/index.js';
9
9
  import { processUpdate } from './update.js';
10
+ import { collectLiveViews, registerViewRef } from './weak-refs.js';
10
11
  /**
11
12
  * HMR管理器
12
13
  *
@@ -14,10 +15,18 @@ import { processUpdate } from './update.js';
14
15
  */
15
16
  class HMRManager {
16
17
  constructor() {
18
+ _HMRManager_instances.add(this);
17
19
  /**
18
20
  * id模块映射到组件虚拟节点集合
19
21
  *
20
- * 模块id -> 组件虚拟节点
22
+ * 模块id -> 组件虚拟节点(WeakRef 弱引用)
23
+ *
24
+ * 为什么用 WeakRef:register 在组件每次执行时都会调用,若用强引用,
25
+ * 已销毁的视图(弹窗关闭、路由切换)会被本 Map 永久钉住造成内存泄漏。
26
+ * WeakRef 借助 GC 可达性天然区分「Freeze 停用」与「真销毁」——
27
+ * Freeze 缓存持有视图强引用,停用中的视图不会被回收,热更新照常处理;
28
+ * 真销毁的视图无引用可回收,deref() 返回空,遍历时剪枝。
29
+ * 剪枝时机为 register/update 遍历(dev 下高频),泄漏上限极小。
21
30
  */
22
31
  _HMRManager_idMapToView.set(this, new Map()
23
32
  /**
@@ -28,6 +37,20 @@ class HMRManager {
28
37
  * id映射到组件构造函数
29
38
  */
30
39
  _HMRManager_idMapToComponent.set(this, new Map()
40
+ /**
41
+ * 视图状态 getter 登记表
42
+ *
43
+ * 组件函数每次执行时通过 trackState 登记当前作用域状态的惰性读取器,
44
+ * 热更新时读取一次生成快照;视图销毁/重挂载不会保留,避免旧状态泄漏。
45
+ */
46
+ );
47
+ /**
48
+ * 视图状态 getter 登记表
49
+ *
50
+ * 组件函数每次执行时通过 trackState 登记当前作用域状态的惰性读取器,
51
+ * 热更新时读取一次生成快照;视图销毁/重挂载不会保留,避免旧状态泄漏。
52
+ */
53
+ _HMRManager_viewStateGetters.set(this, new WeakMap()
31
54
  /**
32
55
  * 获取单实例
33
56
  */
@@ -81,6 +104,21 @@ class HMRManager {
81
104
  return undefined;
82
105
  return state;
83
106
  }
107
+ /**
108
+ * 无条件恢复状态(不做 Ref/reactive 门禁)
109
+ *
110
+ * 供解构声明的隐藏变量使用:composable 返回的控制器是普通对象
111
+ * (内含 Ref 与函数),memo 的响应式门禁会将其拒收导致恢复永远
112
+ * 落空。仅限转换器为解构声明生成的代码调用。
113
+ *
114
+ * @param {ComponentView} view - 组件视图
115
+ * @param {string} name - 变量名称
116
+ */
117
+ memoRaw(view, name) {
118
+ if (!view)
119
+ return undefined;
120
+ return view[HMR.state]?.[name];
121
+ }
84
122
  /**
85
123
  * 注册节点
86
124
  *
@@ -92,11 +130,12 @@ class HMRManager {
92
130
  return;
93
131
  component ?? (component = view.component);
94
132
  const id = this.getId(component);
95
- if (__classPrivateFieldGet(this, _HMRManager_idMapToView, "f").has(id)) {
96
- __classPrivateFieldGet(this, _HMRManager_idMapToView, "f").get(id).add(view);
133
+ const refs = __classPrivateFieldGet(this, _HMRManager_idMapToView, "f").get(id);
134
+ if (refs) {
135
+ registerViewRef(refs, view);
97
136
  }
98
137
  else {
99
- __classPrivateFieldGet(this, _HMRManager_idMapToView, "f").set(id, new Set([view]));
138
+ __classPrivateFieldGet(this, _HMRManager_idMapToView, "f").set(id, new Set([new WeakRef(view)]));
100
139
  }
101
140
  }
102
141
  /**
@@ -107,14 +146,33 @@ class HMRManager {
107
146
  getId(component) {
108
147
  return Reflect.get(component, HMR.id);
109
148
  }
149
+ /**
150
+ * 登记视图状态 getter
151
+ *
152
+ * 由转换器注入的代码在组件函数每次执行时调用,登记当前作用域的
153
+ * 惰性状态读取器(同一视图重复登记覆盖旧的)。读取动作延迟到
154
+ * 热更新快照时发生,正常挂载/重挂载不会产生任何状态残留。
155
+ *
156
+ * @param {ComponentView} view - 组件视图
157
+ * @param {Record<string, () => any>} getters - 变量名到读取器的映射
158
+ */
159
+ trackState(view, getters) {
160
+ if (!view)
161
+ return;
162
+ __classPrivateFieldGet(this, _HMRManager_viewStateGetters, "f").set(view, getters);
163
+ }
110
164
  /**
111
165
  * 模块更新
112
166
  *
113
167
  * @param newModule - 新模块对象
168
+ * @returns 模块导出中是否包含至少一个已绑定 HMR id 的组件。
169
+ * 供导入转发模块的 accept 回调判断兜底:转发目标若不含
170
+ * 可识别组件(如换成未转换模块的组件),调用方应
171
+ * invalidate 触发整页刷新,避免界面静默 stale。
114
172
  */
115
173
  update(newModule) {
116
174
  if (!newModule)
117
- return;
175
+ return false;
118
176
  try {
119
177
  const components = [];
120
178
  const updatedView = new Set();
@@ -135,23 +193,39 @@ class HMRManager {
135
193
  for (const component of components) {
136
194
  const id = this.getId(component);
137
195
  // 模块活跃的虚拟节点集合
138
- const views = __classPrivateFieldGet(this, _HMRManager_idMapToView, "f").get(id);
139
- if (!views)
196
+ const refs = __classPrivateFieldGet(this, _HMRManager_idMapToView, "f").get(id);
197
+ if (!refs)
198
+ continue;
199
+ // 收集存活视图(死引用已剪枝),遍历使其更新
200
+ const views = collectLiveViews(refs);
201
+ // 集合清空则移除 Map 条目,避免空壳残留长期积累
202
+ if (refs.size === 0) {
203
+ __classPrivateFieldGet(this, _HMRManager_idMapToView, "f").delete(id);
140
204
  continue;
141
- // 遍历关联的所有视图,使其更新
205
+ }
142
206
  for (const view of views) {
143
207
  // 跳过已更新过的视图,避免同一次更新中同一个视图被更新多次
144
208
  if (updatedView.has(view))
145
209
  continue;
146
210
  // 仅在视图是活跃状态,且已挂载时才更新
147
211
  if (view.isActive && view.isMounted) {
148
- // 处理视图更新
149
- processUpdate(view, this.resolveComponent(view.component));
212
+ // 重执行前快照当前状态,使组件函数重执行时 memo 命中、保留状态
213
+ __classPrivateFieldGet(this, _HMRManager_instances, "m", _HMRManager_snapshotState).call(this, view);
214
+ try {
215
+ // 处理视图更新
216
+ processUpdate(view, this.resolveComponent(view.component));
217
+ }
218
+ finally {
219
+ // 快照仅服务于本次热更新,用完即清——
220
+ // 保证后续正常的「卸载→重挂载」拿到全新状态,与生产行为一致
221
+ delete view[HMR.state];
222
+ }
150
223
  // 标记视图已更新
151
224
  updatedView.add(view);
152
225
  }
153
226
  }
154
227
  }
228
+ return components.length > 0;
155
229
  }
156
230
  catch (e) {
157
231
  if (import.meta.hot) {
@@ -161,7 +235,29 @@ class HMRManager {
161
235
  throw e;
162
236
  }
163
237
  }
238
+ return false;
164
239
  }
165
240
  }
166
- _HMRManager_idMapToView = new WeakMap(), _HMRManager_idMapToComponent = new WeakMap();
241
+ _HMRManager_idMapToView = new WeakMap(), _HMRManager_idMapToComponent = new WeakMap(), _HMRManager_viewStateGetters = new WeakMap(), _HMRManager_instances = new WeakSet(), _HMRManager_snapshotState = function _HMRManager_snapshotState(view) {
242
+ const getters = __classPrivateFieldGet(this, _HMRManager_viewStateGetters, "f").get(view);
243
+ if (!getters)
244
+ return;
245
+ const snapshot = {};
246
+ for (const name in getters) {
247
+ try {
248
+ // 注意:getters 是 getter 属性对象——读取属性即执行 getter 并返回值,
249
+ // 不能再加调用括号(否则会把返回值当函数调用,全部抛 TypeError)
250
+ const value = getters[name];
251
+ // 防御:函数值(旧编译产物仍可能登记函数变量)不进入快照,
252
+ // 绝不调用组件内的业务函数
253
+ if (typeof value === 'function')
254
+ continue;
255
+ snapshot[name] = value;
256
+ }
257
+ catch {
258
+ // 单个变量读取失败(如暂时性死区)不影响其余状态恢复
259
+ }
260
+ }
261
+ view[HMR.state] = snapshot;
262
+ };
167
263
  export default HMRManager;
@@ -1,7 +1,16 @@
1
+ /**
2
+ * HMR 组件更新:统一「卸载 → 重建 → 挂载」语义
3
+ *
4
+ * 任何代码变更(含纯 UI/注释)都走完整重挂载:dispose 触发旧实例的
5
+ * onDispose/onScopeDispose 清理副作用,init 重执行组件函数(状态经
6
+ * memo/快照协议保留),mount 触发 onMounted——dev 与生产的生命周期
7
+ * 语义完全一致,不区分 logic/非 logic 分叉,不存在钩子滞留或孤儿监听。
8
+ * @module hmr-client/update
9
+ */
1
10
  import { Component, ComponentView } from 'vitarx';
2
11
  /**
3
- * 处理组件更新函数
12
+ * 处理组件更新:卸载旧实例 → 重建(重执行组件函数,状态经快照恢复)→ 挂载
4
13
  * @param view - 组件视图对象,包含当前组件的实例和节点信息
5
- * @param newComponent - 新的组件定义,用于替换现有组件
14
+ * @param newComponent - 新的组件定义
6
15
  */
7
16
  export declare function processUpdate(view: ComponentView, newComponent: Component): void;
@@ -1,72 +1,38 @@
1
- import { createCommentView, EffectScope, getRenderer, runComponent } from 'vitarx';
2
- import { diffComponentChange } from './utils.js';
3
1
  /**
4
- * 处理组件更新函数
2
+ * HMR 组件更新:统一「卸载 → 重建 → 挂载」语义
3
+ *
4
+ * 任何代码变更(含纯 UI/注释)都走完整重挂载:dispose 触发旧实例的
5
+ * onDispose/onScopeDispose 清理副作用,init 重执行组件函数(状态经
6
+ * memo/快照协议保留),mount 触发 onMounted——dev 与生产的生命周期
7
+ * 语义完全一致,不区分 logic/非 logic 分叉,不存在钩子滞留或孤儿监听。
8
+ * @module hmr-client/update
9
+ */
10
+ import { getRenderer } from 'vitarx';
11
+ /**
12
+ * 处理组件更新:卸载旧实例 → 重建(重执行组件函数,状态经快照恢复)→ 挂载
5
13
  * @param view - 组件视图对象,包含当前组件的实例和节点信息
6
- * @param newComponent - 新的组件定义,用于替换现有组件
14
+ * @param newComponent - 新的组件定义
7
15
  */
8
16
  export function processUpdate(view, newComponent) {
9
- // 获取渲染器实例
17
+ // 获取渲染器实例,创建占位注释节点并插入到旧元素位置,
18
+ // 作为新旧子树替换的锚点
10
19
  const renderer = getRenderer();
11
- // 创建一个占位符注释节点
12
20
  const placeholder = renderer.createComment('');
13
- // 获取旧组件的DOM元素
14
- const oldElement = view.node;
15
- // 将占位符插入到旧元素的位置
16
- renderer.insert(placeholder, oldElement);
17
- // 获取组件实例
18
- const instance = view.instance;
19
- // 比较新旧组件的差异,检查逻辑是否有变化
20
- const { logic } = diffComponentChange(view.component.toString(), newComponent.toString());
21
- // 更新视图中的组件引用
21
+ renderer.insert(placeholder, view.node);
22
+ // 更新视图中的组件引用为最新模块导出
22
23
  Object.defineProperty(view, 'component', {
23
24
  value: newComponent,
24
25
  writable: true,
25
26
  enumerable: true,
26
27
  configurable: true
27
28
  });
28
- // 如果逻辑有变化,则完全重新挂载组件
29
- if (logic) {
30
- const ctx = view.ctx;
31
- view.dispose();
32
- view.init(ctx);
33
- view.mount(placeholder, 'replace');
34
- return;
35
- }
36
- // 销毁旧作用域
37
- instance.scope.dispose();
38
- // 重新创建新的作用域
39
- const scope = new EffectScope({
40
- name: view.name,
41
- errorHandler: (error, source) => {
42
- instance.reportError(error, `effect:${source}`);
43
- }
44
- });
45
- Object.defineProperty(instance, 'scope', {
46
- value: scope,
47
- writable: true,
48
- enumerable: true,
49
- configurable: true
50
- });
51
- // 销毁旧的子树
52
- instance.subView.dispose();
53
- // 创建新的子树
54
- let subView;
55
- try {
56
- subView = runComponent(instance, () => newComponent(view.props));
57
- }
58
- catch (e) {
59
- instance.reportError(e, 'component:run');
60
- subView = createCommentView(`Component<${view.name}>:failed`);
61
- }
62
- Object.defineProperty(instance, 'subView', {
63
- value: instance['normalizeView'](subView),
64
- writable: true,
65
- enumerable: true,
66
- configurable: true
67
- });
68
- // 初始化新的子树
69
- instance.subView.init(instance.subViewContext);
70
- // 挂载新的子树
71
- instance.subView.mount(placeholder, 'replace');
29
+ // 完整重挂载:dispose(清理旧副作用,触发 onDispose/onScopeDispose)
30
+ // → init(重执行组件函数,memo 命中快照保留状态)
31
+ // → mount(触发 onMounted,重新注册副作用)
32
+ // 注意:ctx 必须在 dispose 之前捕获——dispose 会拆除 provide 链,
33
+ // 之后读取 ctx 会导致重执行时 inject(如 useRouter)解析失败
34
+ const ctx = view.ctx;
35
+ view.dispose();
36
+ view.init(ctx);
37
+ view.mount(placeholder, 'replace');
72
38
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * HMR 视图弱引用集合工具
3
+ *
4
+ * #idMapToView 借助 WeakRef 让已销毁视图可被 GC 回收,避免被管理器
5
+ * 永久强引用造成内存泄漏。GC 可达性天然区分「Freeze 停用」与
6
+ * 「真销毁」——Freeze 缓存持有视图强引用,停用中的视图不会被回收,
7
+ * 热更新照常处理;真销毁的视图无引用可回收,deref() 返回空,遍历时剪枝。
8
+ *
9
+ * 剪枝/去重逻辑抽为纯函数并接受 WeakRefLike 结构类型,便于以伪引用
10
+ * 做确定性单元测试(真实 WeakRef 的 deref 时点依赖 GC,无法确定性断言)。
11
+ * @module hmr-client/weak-refs
12
+ */
13
+ import type { ComponentView } from 'vitarx';
14
+ /**
15
+ * WeakRef 结构类型
16
+ *
17
+ * 真实 WeakRef 天然满足此结构;测试可注入自定义伪引用。
18
+ */
19
+ export interface WeakRefLike<T extends object> {
20
+ deref(): T | undefined;
21
+ }
22
+ /**
23
+ * 向引用集合登记视图:剪枝死引用 + 按 deref 比对去重
24
+ *
25
+ * WeakRef 身份 ≠ 视图身份,同一视图多次登记(组件每次执行都会
26
+ * register)必须按解引用结果比对,否则集合会被同视图的多个
27
+ * WeakRef 无限撑大。
28
+ * @param refs - 视图弱引用集合(原地修改)
29
+ * @param view - 待登记的视图
30
+ */
31
+ export declare function registerViewRef(refs: Set<WeakRefLike<ComponentView>>, view: ComponentView): void;
32
+ /**
33
+ * 收集集合中仍存活(未被 GC)的视图,并原地剪枝死引用
34
+ * @param refs - 视图弱引用集合(原地修改)
35
+ * @returns 存活视图数组
36
+ */
37
+ export declare function collectLiveViews(refs: Set<WeakRefLike<ComponentView>>): ComponentView[];
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 向引用集合登记视图:剪枝死引用 + 按 deref 比对去重
3
+ *
4
+ * WeakRef 身份 ≠ 视图身份,同一视图多次登记(组件每次执行都会
5
+ * register)必须按解引用结果比对,否则集合会被同视图的多个
6
+ * WeakRef 无限撑大。
7
+ * @param refs - 视图弱引用集合(原地修改)
8
+ * @param view - 待登记的视图
9
+ */
10
+ export function registerViewRef(refs, view) {
11
+ let exists = false;
12
+ for (const ref of refs) {
13
+ const cached = ref.deref();
14
+ if (!cached) {
15
+ refs.delete(ref);
16
+ }
17
+ else if (cached === view) {
18
+ exists = true;
19
+ }
20
+ }
21
+ if (!exists)
22
+ refs.add(new WeakRef(view));
23
+ }
24
+ /**
25
+ * 收集集合中仍存活(未被 GC)的视图,并原地剪枝死引用
26
+ * @param refs - 视图弱引用集合(原地修改)
27
+ * @returns 存活视图数组
28
+ */
29
+ export function collectLiveViews(refs) {
30
+ const live = [];
31
+ for (const ref of refs) {
32
+ const view = ref.deref();
33
+ if (view) {
34
+ live.push(view);
35
+ }
36
+ else {
37
+ refs.delete(ref);
38
+ }
39
+ }
40
+ return live;
41
+ }
@@ -2,4 +2,4 @@
2
2
  * HMR 注入模块入口
3
3
  * @module passes/hmr
4
4
  */
5
- export { injectHMRSupport } from './inject.js';
5
+ export { injectHMRSupport, injectHMRAcceptOnly } from './inject.js';
@@ -2,4 +2,4 @@
2
2
  * HMR 注入模块入口
3
3
  * @module passes/hmr
4
4
  */
5
- export { injectHMRSupport } from './inject.js';
5
+ export { injectHMRSupport, injectHMRAcceptOnly } from './inject.js';
@@ -20,15 +20,21 @@ export declare function createHMRRegistrationStatements(variableNames: string[])
20
20
  */
21
21
  export declare function createWrappedComponentHMRStatements(componentName: string): t.Statement[];
22
22
  /**
23
- * 从函数体中收集局部变量名
24
- * 递归遍历函数体中的所有变量声明,用于状态恢复
23
+ * 为函数体注入状态恢复代码并收集需要登记(trackState)的变量名
24
+ *
25
+ * 处理规则:
26
+ * - 标识符声明(非函数初始值):初始值包装为 memo(view, '变量名') ?? 原始初始值
27
+ * - useModel 调用:豁免(桥接状态每次重建,见 isUseModelCall)
28
+ * - 解构声明(如 `const { a } = useFoo()`):拆分为隐藏变量整包 memo +
29
+ * 原声明解构隐藏变量——composable 返回的状态对象得以跨重执行保留,
30
+ * 其内部创建的响应式状态与已挂载的副作用保持一致
31
+ * - 函数类型初始值 / 无初始值:不注入
32
+ * - for 循环头部声明:循环局部绑定,不注入(仅递归循环体)
33
+ *
34
+ * 单趟完成注入与收集,返回按执行顺序排列的登记名列表(标识符名 +
35
+ * 隐藏变量名),供 trackState 生成 getter。隐藏变量采用用户标识符不可能
36
+ * 命中的 `__$VITARX_D<N>$__` 命名,按组件函数内确定性计数。
25
37
  * @param functionBody - 函数体语句块
26
- * @returns 变量名数组
38
+ * @returns 需要状态登记的变量名数组
27
39
  */
28
- export declare function collectLocalVariableNames(functionBody: t.BlockStatement): string[];
29
- /**
30
- * 为函数体内的变量声明注入状态恢复代码
31
- * 递归遍历所有变量声明(包括 if/for/while/switch/try 嵌套块)
32
- * @param functionBody - 函数体语句块
33
- */
34
- export declare function injectStatePreservation(functionBody: t.BlockStatement): void;
40
+ export declare function injectStatePreservation(functionBody: t.BlockStatement): string[];
@@ -5,7 +5,6 @@
5
5
  */
6
6
  import * as t from '@babel/types';
7
7
  import { HMR } from '../../constants/index.js';
8
- import { collectPatternBindings } from '../../utils/index.js';
9
8
  import { GET_COMPONENT_VIEW_ALIAS } from './inject-imports.js';
10
9
  /**
11
10
  * 创建组件函数体内的 HMR 注册代码
@@ -25,15 +24,13 @@ export function createHMRRegistrationStatements(variableNames) {
25
24
  statements.push(t.expressionStatement(t.callExpression(t.memberExpression(t.memberExpression(t.identifier(HMR.manager), t.identifier('instance')), t.identifier('register')), [t.identifier(HMR.view)])));
26
25
  // 构建状态对象的 getter 属性,延迟求值避免不必要的计算
27
26
  const stateProperties = variableNames.map(name => t.objectMethod('get', t.identifier(name), [], t.blockStatement([t.returnStatement(t.identifier(name))])));
28
- // 语句3: __$VITARX_HMR_VIEW_NODE$__ && Promise.resolve().then(() => {
29
- // __$VITARX_HMR_VIEW_NODE$__.__$vitarx_hmr_state$__ = { get 变量名() { return 变量名 } }
30
- // })
31
- // 异步保存状态到视图节点,确保 DOM 更新完成后再保存
32
- statements.push(t.expressionStatement(t.logicalExpression('&&', t.identifier(HMR.view), t.callExpression(t.memberExpression(t.callExpression(t.memberExpression(t.identifier('Promise'), t.identifier('resolve')), []), t.identifier('then')), [
33
- t.arrowFunctionExpression([], t.blockStatement([
34
- t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.identifier(HMR.view), t.identifier(HMR.state)), t.objectExpression(stateProperties)))
35
- ]))
36
- ]))));
27
+ // 语句3: __$VITARX_HMR_VIEW_NODE$__ && __$VITARX_HMR$__.instance.trackState(
28
+ // __$VITARX_HMR_VIEW_NODE$__,
29
+ // { get 变量名() { return 变量名 } }
30
+ // )
31
+ // 惰性登记状态 getter:只在热更新时由管理器读取快照写入视图,
32
+ // 正常「卸载→重挂载」不会写入状态,memo 永远不命中,避免旧状态泄漏
33
+ statements.push(t.expressionStatement(t.logicalExpression('&&', t.identifier(HMR.view), t.callExpression(t.memberExpression(t.memberExpression(t.identifier(HMR.manager), t.identifier('instance')), t.identifier('trackState')), [t.identifier(HMR.view), t.objectExpression(stateProperties)]))));
37
34
  return statements;
38
35
  }
39
36
  /**
@@ -54,90 +51,10 @@ export function createWrappedComponentHMRStatements(componentName) {
54
51
  statements.push(t.expressionStatement(t.callExpression(t.memberExpression(t.memberExpression(t.identifier(HMR.manager), t.identifier('instance')), t.identifier('register')), [t.identifier(HMR.view), t.identifier(componentName)])));
55
52
  return statements;
56
53
  }
57
- /**
58
- * 从函数体中收集局部变量名
59
- * 递归遍历函数体中的所有变量声明,用于状态恢复
60
- * @param functionBody - 函数体语句块
61
- * @returns 变量名数组
62
- */
63
- export function collectLocalVariableNames(functionBody) {
64
- const variableNames = new Set();
65
- forEachVariableDeclarator(functionBody, decl => {
66
- if (decl.id.type !== 'VoidPattern') {
67
- collectPatternBindings(decl.id, variableNames);
68
- }
69
- });
70
- return Array.from(variableNames);
71
- }
72
- /**
73
- * 遍历语句块中的所有变量声明,对每个 VariableDeclarator 执行回调
74
- * 递归处理嵌套的控制流语句(if/for/while/switch/try 等)
75
- * @param block - 语句块节点
76
- * @param visitor - 变量声明访问者回调
77
- */
78
- function forEachVariableDeclarator(block, visitor) {
79
- for (const stmt of block.body) {
80
- traverseStatementForDeclarators(stmt, visitor);
81
- }
82
- }
83
- /**
84
- * 从单个语句中递归查找变量声明并执行回调
85
- * 支持多种语句类型:变量声明、条件语句、循环语句、switch、try-catch 等
86
- * @param stmt - 语句节点
87
- * @param visitor - 变量声明访问者回调
88
- */
89
- function traverseStatementForDeclarators(stmt, visitor) {
90
- if (stmt.type === 'VariableDeclaration') {
91
- for (const decl of stmt.declarations) {
92
- visitor(decl);
93
- }
94
- }
95
- else if (stmt.type === 'IfStatement') {
96
- traverseBodyForDeclarators(stmt.consequent, visitor);
97
- if (stmt.alternate) {
98
- traverseBodyForDeclarators(stmt.alternate, visitor);
99
- }
100
- }
101
- else if (stmt.type === 'ForStatement' || stmt.type === 'WhileStatement' || stmt.type === 'DoWhileStatement') {
102
- traverseBodyForDeclarators(stmt.body, visitor);
103
- }
104
- else if (stmt.type === 'ForInStatement' || stmt.type === 'ForOfStatement') {
105
- traverseBodyForDeclarators(stmt.body, visitor);
106
- }
107
- else if (stmt.type === 'BlockStatement') {
108
- forEachVariableDeclarator(stmt, visitor);
109
- }
110
- else if (stmt.type === 'SwitchStatement') {
111
- for (const c of stmt.cases) {
112
- for (const s of c.consequent) {
113
- traverseStatementForDeclarators(s, visitor);
114
- }
115
- }
116
- }
117
- else if (stmt.type === 'TryStatement') {
118
- forEachVariableDeclarator(stmt.block, visitor);
119
- if (stmt.handler) {
120
- forEachVariableDeclarator(stmt.handler.body, visitor);
121
- }
122
- if (stmt.finalizer) {
123
- forEachVariableDeclarator(stmt.finalizer, visitor);
124
- }
125
- }
126
- }
127
- /**
128
- * 遍历语句体(可能是 BlockStatement 或单条语句)中的变量声明
129
- */
130
- function traverseBodyForDeclarators(body, visitor) {
131
- if (body.type === 'BlockStatement') {
132
- forEachVariableDeclarator(body, visitor);
133
- }
134
- else {
135
- traverseStatementForDeclarators(body, visitor);
136
- }
137
- }
138
54
  /**
139
55
  * 判断表达式是否为函数类型
140
- * 函数类型不需要状态恢复(函数定义本身不会改变)
56
+ * 函数类型不需要状态恢复(函数定义本身不会改变);且快照读取 getter 会
57
+ * 拿到函数值,一旦被误调用将执行组件内的业务函数
141
58
  * @param expr - 表达式节点
142
59
  * @returns 是否为函数类型
143
60
  */
@@ -149,44 +66,152 @@ function isFunctionExpression(expr) {
149
66
  expr.type === 'ClassExpression' // 类表达式
150
67
  );
151
68
  }
69
+ /**
70
+ * 判断是否为 useModel 调用
71
+ *
72
+ * useModel 返回的 ModelRef 是父组件状态的双向桥接(派生视图)而非独立
73
+ * 状态:其内部以 flush:'sync' 的 watch 同步 props→镜像,该 watch 绑定在
74
+ * 执行时的作用域上。若被 memo 复用,旧 watch 会随旧作用域销毁,表现为
75
+ * props→镜像同步断裂(v-model 显示残留)。因此每次执行都重建 ModelRef
76
+ * ——镜像从当前 props 重新初始化,语义正确。
77
+ * @param expr - 表达式节点
78
+ * @returns 是否为 useModel 调用
79
+ */
80
+ function isUseModelCall(expr) {
81
+ return (!!expr &&
82
+ expr.type === 'CallExpression' &&
83
+ expr.callee.type === 'Identifier' &&
84
+ expr.callee.name === 'useModel');
85
+ }
152
86
  /**
153
87
  * 创建状态恢复表达式
154
- * 格式:__$VITARX_HMR$__.instance.memo(__$VITARX_HMR_VIEW_NODE$__, '变量名') ?? 原始初始值
155
- * 如果 memo 方法返回保存的状态则使用,否则使用原始初始值
88
+ * 格式:__$VITARX_HMR$__.instance.memo(view, '变量名') ?? 原始初始值
156
89
  * @param variableName - 变量名
157
- * @param originalInit - 原始初始化表达式
90
+ * @param originalInit - 原始初始值表达式
91
+ * @param raw - 是否绕过响应式门禁(memoRaw):解构声明的隐藏变量保存的是
92
+ * composable 返回的普通对象(内含 Ref 与函数),memo 的门禁会拒收,
93
+ * 必须走无条件恢复
158
94
  * @returns 状态恢复表达式
159
95
  */
160
- function createMemoExpression(variableName, originalInit) {
96
+ function createMemoExpression(variableName, originalInit, raw = false) {
161
97
  return t.logicalExpression('??', // 使用空值合并运算符
162
- t.callExpression(t.memberExpression(t.memberExpression(t.identifier(HMR.manager), t.identifier('instance')), t.identifier('memo')), [t.identifier(HMR.view), t.stringLiteral(variableName)]), originalInit // 原始初始值作为 fallback
98
+ t.callExpression(t.memberExpression(t.memberExpression(t.identifier(HMR.manager), t.identifier('instance')), t.identifier(raw ? 'memoRaw' : 'memo')), [t.identifier(HMR.view), t.stringLiteral(variableName)]), originalInit // 原始初始值作为 fallback
163
99
  );
164
100
  }
165
101
  /**
166
- * 为单个变量声明注入状态恢复代码
167
- * 将 const x = value 转换为 const x = memo(view, 'x') ?? value
168
- * @param decl - 变量声明节点
169
- */
170
- function injectStatePreservationForDeclaration(decl) {
171
- // 只处理标识符形式的变量名(跳过解构赋值)
172
- if (decl.id.type !== 'Identifier')
173
- return;
174
- // 跳过没有初始值的声明
175
- if (!decl.init)
176
- return;
177
- // 跳过函数类型的初始值(不需要状态恢复)
178
- if (isFunctionExpression(decl.init))
179
- return;
180
- // 替换初始值为状态恢复表达式
181
- decl.init = createMemoExpression(decl.id.name, decl.init);
182
- }
183
- /**
184
- * 为函数体内的变量声明注入状态恢复代码
185
- * 递归遍历所有变量声明(包括 if/for/while/switch/try 嵌套块)
102
+ * 为函数体注入状态恢复代码并收集需要登记(trackState)的变量名
103
+ *
104
+ * 处理规则:
105
+ * - 标识符声明(非函数初始值):初始值包装为 memo(view, '变量名') ?? 原始初始值
106
+ * - useModel 调用:豁免(桥接状态每次重建,见 isUseModelCall)
107
+ * - 解构声明(如 `const { a } = useFoo()`):拆分为隐藏变量整包 memo +
108
+ * 原声明解构隐藏变量——composable 返回的状态对象得以跨重执行保留,
109
+ * 其内部创建的响应式状态与已挂载的副作用保持一致
110
+ * - 函数类型初始值 / 无初始值:不注入
111
+ * - for 循环头部声明:循环局部绑定,不注入(仅递归循环体)
112
+ *
113
+ * 单趟完成注入与收集,返回按执行顺序排列的登记名列表(标识符名 +
114
+ * 隐藏变量名),供 trackState 生成 getter。隐藏变量采用用户标识符不可能
115
+ * 命中的 `__$VITARX_D<N>$__` 命名,按组件函数内确定性计数。
186
116
  * @param functionBody - 函数体语句块
117
+ * @returns 需要状态登记的变量名数组
187
118
  */
188
119
  export function injectStatePreservation(functionBody) {
189
- forEachVariableDeclarator(functionBody, decl => {
190
- injectStatePreservationForDeclaration(decl);
191
- });
120
+ const trackedNames = [];
121
+ let destructureCounter = 0;
122
+ processBlock(functionBody);
123
+ return trackedNames;
124
+ /** 处理语句块:注入可能把一条声明拆成多条,需重建语句数组 */
125
+ function processBlock(block) {
126
+ const newBody = [];
127
+ for (const stmt of block.body) {
128
+ newBody.push(...processStatement(stmt));
129
+ }
130
+ block.body = newBody;
131
+ }
132
+ /** 处理单条语句,返回替换它的语句列表(1~n 条) */
133
+ function processStatement(stmt) {
134
+ if (stmt.type === 'VariableDeclaration') {
135
+ return processVariableDeclaration(stmt);
136
+ }
137
+ if (stmt.type === 'IfStatement') {
138
+ stmt.consequent = wrapBody(stmt.consequent);
139
+ if (stmt.alternate)
140
+ stmt.alternate = wrapBody(stmt.alternate);
141
+ }
142
+ else if (stmt.type === 'ForStatement' ||
143
+ stmt.type === 'WhileStatement' ||
144
+ stmt.type === 'DoWhileStatement') {
145
+ // for 头部声明是循环局部绑定不注入;仅递归循环体
146
+ stmt.body = wrapBody(stmt.body);
147
+ }
148
+ else if (stmt.type === 'ForInStatement' || stmt.type === 'ForOfStatement') {
149
+ // 同上:for-of/for-in 的 left 绑定按迭代取值,不做状态保留
150
+ stmt.body = wrapBody(stmt.body);
151
+ }
152
+ else if (stmt.type === 'BlockStatement') {
153
+ processBlock(stmt);
154
+ }
155
+ else if (stmt.type === 'SwitchStatement') {
156
+ for (const switchCase of stmt.cases) {
157
+ switchCase.consequent = processStatementList(switchCase.consequent);
158
+ }
159
+ }
160
+ else if (stmt.type === 'TryStatement') {
161
+ processBlock(stmt.block);
162
+ if (stmt.handler)
163
+ processBlock(stmt.handler.body);
164
+ if (stmt.finalizer)
165
+ processBlock(stmt.finalizer);
166
+ }
167
+ return [stmt];
168
+ }
169
+ function processStatementList(list) {
170
+ const results = [];
171
+ for (const stmt of list) {
172
+ results.push(...processStatement(stmt));
173
+ }
174
+ return results;
175
+ }
176
+ /** 语句体若因注入产生多条语句,需要块化包装保持语法合法 */
177
+ function wrapBody(body) {
178
+ if (body.type === 'BlockStatement') {
179
+ processBlock(body);
180
+ return body;
181
+ }
182
+ const results = processStatement(body);
183
+ return results.length === 1 ? results[0] : t.blockStatement(results);
184
+ }
185
+ /** 逐 declarator 处理变量声明(多 declarator 时拆分为多条声明) */
186
+ function processVariableDeclaration(stmt) {
187
+ const results = [];
188
+ for (const decl of stmt.declarations) {
189
+ const init = decl.init ?? null;
190
+ // 无初始值 / VoidPattern / 函数初始值:原样保留
191
+ if (decl.id.type === 'VoidPattern' || !init || isFunctionExpression(init)) {
192
+ results.push(t.variableDeclaration(stmt.kind, [decl]));
193
+ continue;
194
+ }
195
+ if (decl.id.type === 'Identifier') {
196
+ if (isUseModelCall(init)) {
197
+ // 桥接状态豁免:不 memo、不登记(见 isUseModelCall 注释)
198
+ results.push(t.variableDeclaration(stmt.kind, [decl]));
199
+ continue;
200
+ }
201
+ trackedNames.push(decl.id.name);
202
+ decl.init = createMemoExpression(decl.id.name, init);
203
+ results.push(t.variableDeclaration(stmt.kind, [decl]));
204
+ continue;
205
+ }
206
+ // 解构声明:隐藏变量整包 memo(raw 恢复)+ 原声明解构隐藏变量
207
+ const hiddenName = `__$VITARX_D${destructureCounter++}$__`;
208
+ trackedNames.push(hiddenName);
209
+ results.push(t.variableDeclaration('const', [
210
+ t.variableDeclarator(t.identifier(hiddenName), createMemoExpression(hiddenName, init, true))
211
+ ]), t.variableDeclaration(stmt.kind, [
212
+ t.variableDeclarator(decl.id, t.identifier(hiddenName))
213
+ ]));
214
+ }
215
+ return results;
216
+ }
192
217
  }
@@ -13,3 +13,13 @@ import type { ComponentInfo } from '../../utils/index.js';
13
13
  * @param filename - 文件名(用于生成组件唯一 ID)
14
14
  */
15
15
  export declare function injectHMRSupport(program: t.Program, components: ComponentInfo[], filename: string): void;
16
+ /**
17
+ * 为「导入转发组件默认导出」的模块注入 accept-only HMR 支持
18
+ *
19
+ * 典型场景:文件路由的 _layout 转发文件(export default AdminLayout)。
20
+ * 模块本身不含组件函数定义,无需 bindId 与函数体注入(函数体属于
21
+ * 源模块,其自身已有完整 HMR);仅需自接受模块更新并把新导出交给
22
+ * HMRManager.update 按组件 id 热替换。
23
+ * @param program - AST Program 节点
24
+ */
25
+ export declare function injectHMRAcceptOnly(program: t.Program): void;
@@ -6,15 +6,14 @@
6
6
  import * as t from '@babel/types';
7
7
  import { HMR } from '../../constants/index.js';
8
8
  import { injectHMRImport, injectGetComponentViewImport } from './inject-imports.js';
9
- import { createHMRRegistrationStatements, createWrappedComponentHMRStatements, collectLocalVariableNames, injectStatePreservation } from './inject-state.js';
9
+ import { createHMRRegistrationStatements, createWrappedComponentHMRStatements, injectStatePreservation } from './inject-state.js';
10
10
  /**
11
11
  * 为组件函数注入 HMR 注册代码
12
12
  * @param func - 函数声明/表达式/箭头函数
13
- * @param variableNames - 需要追踪的变量名列表
14
13
  * @param isWrapped - 是否为包装组件(defineComponent/builder)
15
14
  * @param componentName - 组件名称(包装组件需要)
16
15
  */
17
- function injectHMRIntoFunction(func, variableNames, isWrapped = false, componentName) {
16
+ function injectHMRIntoFunction(func, isWrapped = false, componentName) {
18
17
  // 如果是箭头函数且函数体不是块语句(单行箭头函数),转换为块语句
19
18
  if (func.type === 'ArrowFunctionExpression' && func.body.type !== 'BlockStatement') {
20
19
  const returnStmt = t.returnStatement(func.body);
@@ -23,10 +22,8 @@ function injectHMRIntoFunction(func, variableNames, isWrapped = false, component
23
22
  // 确保函数体是块语句
24
23
  if (!func.body || func.body.type !== 'BlockStatement')
25
24
  return;
26
- // 非包装组件需要注入状态恢复代码
27
- if (!isWrapped) {
28
- injectStatePreservation(func.body);
29
- }
25
+ // 非包装组件:注入状态恢复代码并收集登记名(单趟完成)
26
+ const variableNames = isWrapped ? [] : injectStatePreservation(func.body);
30
27
  // 根据组件类型创建对应的 HMR 注册语句
31
28
  const statements = isWrapped
32
29
  ? createWrappedComponentHMRStatements(componentName)
@@ -58,6 +55,35 @@ function createHotAcceptStatement() {
58
55
  ]))
59
56
  ]));
60
57
  }
58
+ /**
59
+ * 创建导入转发模块的 hot.accept 语句(带 invalidate 兜底)
60
+ *
61
+ * 生成:import.meta.hot.accept(mod => {
62
+ * if (!__$VITARX_HMR$__.instance.update(mod)) {
63
+ * import.meta.hot.invalidate('[VitarxHMR]: ...')
64
+ * }
65
+ * })
66
+ *
67
+ * 转发模块本身不含组件定义,update 的返回值表示新模块导出中是否
68
+ * 含已绑定 HMR id 的组件——为 false 说明转发目标不可识别(如换成
69
+ * 未转换模块的组件),必须 invalidate 整页刷新,避免界面静默 stale。
70
+ *
71
+ * 已知边界:转发目标在两个均有 HMR id 的组件间切换(export default A
72
+ * 改为 export default B)时,update 只登记 B 的 id 并返回 true,不会
73
+ * invalidate,挂在旧 id A 名下的视图不会被更新——文件路由场景由
74
+ * vitarx-router 虚拟路由模块自身的 hot accept 重建路由树兜底;非路由
75
+ * 场景需手动刷新。此为接受的降级路径,不做运行时检测。
76
+ * @returns hot.accept 调用语句
77
+ */
78
+ function createForwardingHotAcceptStatement() {
79
+ return t.expressionStatement(t.callExpression(t.memberExpression(t.memberExpression(t.memberExpression(t.identifier('import'), t.identifier('meta')), t.identifier('hot')), t.identifier('accept')), [
80
+ t.arrowFunctionExpression([t.identifier('mod')], t.blockStatement([
81
+ t.ifStatement(t.unaryExpression('!', t.callExpression(t.memberExpression(t.memberExpression(t.identifier(HMR.manager), t.identifier('instance')), t.identifier('update')), [t.identifier('mod')])), t.blockStatement([
82
+ t.expressionStatement(t.callExpression(t.memberExpression(t.memberExpression(t.memberExpression(t.identifier('import'), t.identifier('meta')), t.identifier('hot')), t.identifier('invalidate')), [t.stringLiteral('[VitarxHMR]: 转发的默认导出不是可热更新的组件,回退整页刷新')]))
83
+ ]))
84
+ ]))
85
+ ]));
86
+ }
61
87
  /**
62
88
  * 生成组件唯一 ID(文件路径 + 组件名称)
63
89
  * 使用 FNV-1a 哈希算法生成短哈希值
@@ -92,9 +118,7 @@ export function injectHMRSupport(program, components, filename) {
92
118
  injectGetComponentViewImport(program);
93
119
  // 步骤2: 为每个组件函数注入 HMR 注册代码和状态恢复
94
120
  for (const { node, isWrapped, name } of components) {
95
- // 收集函数体内的局部变量名(用于状态保存)
96
- const variableNames = node.body?.type === 'BlockStatement' ? collectLocalVariableNames(node.body) : [];
97
- injectHMRIntoFunction(node, variableNames, isWrapped, name);
121
+ injectHMRIntoFunction(node, isWrapped, name);
98
122
  }
99
123
  // 步骤3: 为每个组件创建 bindId 语句(用于热更新时识别)
100
124
  for (const { name } of components) {
@@ -103,3 +127,16 @@ export function injectHMRSupport(program, components, filename) {
103
127
  // 步骤4: 创建 import.meta.hot.accept 语句(监听热更新事件)
104
128
  program.body.push(createHotAcceptStatement());
105
129
  }
130
+ /**
131
+ * 为「导入转发组件默认导出」的模块注入 accept-only HMR 支持
132
+ *
133
+ * 典型场景:文件路由的 _layout 转发文件(export default AdminLayout)。
134
+ * 模块本身不含组件函数定义,无需 bindId 与函数体注入(函数体属于
135
+ * 源模块,其自身已有完整 HMR);仅需自接受模块更新并把新导出交给
136
+ * HMRManager.update 按组件 id 热替换。
137
+ * @param program - AST Program 节点
138
+ */
139
+ export function injectHMRAcceptOnly(program) {
140
+ injectHMRImport(program);
141
+ program.body.push(createForwardingHotAcceptStatement());
142
+ }
@@ -7,4 +7,4 @@ export { processPureCompileComponent, processSwitch, processIfBlock } from './co
7
7
  export { processVIfChain, processDirectives } from './directives/index.js';
8
8
  export { processChildren, processJSXElement, transformJSXElement, processJSXFragment } from './jsx/index.js';
9
9
  export { processProps, type PropsResult } from './props/index.js';
10
- export { injectHMRSupport } from './hmr/index.js';
10
+ export { injectHMRSupport, injectHMRAcceptOnly } from './hmr/index.js';
@@ -13,4 +13,4 @@ export { processChildren, processJSXElement, transformJSXElement, processJSXFrag
13
13
  // Props 处理
14
14
  export { processProps } from './props/index.js';
15
15
  // HMR 注入
16
- export { injectHMRSupport } from './hmr/index.js';
16
+ export { injectHMRSupport, injectHMRAcceptOnly } from './hmr/index.js';
package/dist/transform.js CHANGED
@@ -7,8 +7,8 @@ import generate from '@babel/generator';
7
7
  import { parse } from '@babel/parser';
8
8
  import traverse from '@babel/traverse';
9
9
  import { createContext } from './context.js';
10
- import { collectExistingImports, collectLocalBindings, collectNonRefVariables, collectRefApiAliases, collectRefVariables, injectHMRSupport, injectImports, processJSXElement, processJSXFragment, processPureCompileComponent, processVIfChain, transformJSXElement } from './passes/index.js';
11
- import { collectComponentFunctions, generateUniqueAlias, getJSXElementName, isPureCompileComponent } from './utils/index.js';
10
+ import { collectExistingImports, collectLocalBindings, collectNonRefVariables, collectRefApiAliases, collectRefVariables, injectHMRSupport, injectHMRAcceptOnly, injectImports, processJSXElement, processJSXFragment, processPureCompileComponent, processVIfChain, transformJSXElement } from './passes/index.js';
11
+ import { collectComponentFunctions, generateUniqueAlias, getJSXElementName, hasForwardedComponentDefaultExport, isPureCompileComponent } from './utils/index.js';
12
12
  const entryPatterns = [/\/src\/main\.(tsx|jsx)$/, /\\src\\main\.(tsx|jsx)$/];
13
13
  /**
14
14
  * 创建解析器选项
@@ -152,8 +152,15 @@ export async function transform(code, id, options) {
152
152
  ctx.nonRefVariables = collectNonRefVariables(ast.program);
153
153
  transformAST(ast, ctx);
154
154
  injectImports(ast.program, ctx);
155
- if (options.hmr && components.length > 0 && !isEntryFile(id)) {
156
- injectHMRSupport(ast.program, components, id);
155
+ if (options.hmr && !isEntryFile(id)) {
156
+ if (components.length > 0) {
157
+ injectHMRSupport(ast.program, components, id);
158
+ }
159
+ else if (hasForwardedComponentDefaultExport(ast.program)) {
160
+ // 导入转发组件默认导出的模块(如 _layout 转发文件):无本地组件定义,
161
+ // 注入 accept-only 自接受,避免编辑时回落整页刷新
162
+ injectHMRAcceptOnly(ast.program);
163
+ }
157
164
  }
158
165
  return generateCode(ast, code, id, options.sourceMap);
159
166
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 导入转发默认导出检测
3
+ *
4
+ * 识别「纯转发组件默认导出」的模块——典型场景是文件路由的 _layout 布局
5
+ * 转发文件(`export default AdminLayout` / `export { AdminLayout as default }`,
6
+ * AdminLayout 为 import 绑定)。此类模块本身不含组件函数定义,组件收集
7
+ * 结果为空,此前不会注入任何 HMR 代码,编辑该文件时 Vite 找不到自接受
8
+ * 边界而回落整页刷新。
9
+ *
10
+ * 注意只匹配组件形态(大写开头)的导入绑定:转发普通值(工具函数、常量)
11
+ * 的模块不具备组件热更新能力,保持整页刷新的默认行为。
12
+ * @module utils/forward-export
13
+ */
14
+ import * as t from '@babel/types';
15
+ /**
16
+ * 检测模块是否存在「导入转发形式的组件默认导出」
17
+ *
18
+ * 覆盖两种转发形态(转发目标均为本地 import 绑定且大写开头):
19
+ * 1. `export default AdminLayout`(含 `as T` 断言包装)
20
+ * 2. `export { AdminLayout as default }`
21
+ *
22
+ * 带 source 的 re-export(`export { X as default } from './x'`)没有本地
23
+ * 绑定,不在导入绑定集合中,天然被排除。
24
+ *
25
+ * @param program - AST Program 节点
26
+ * @returns 是否存在转发形式的组件默认导出
27
+ */
28
+ export declare function hasForwardedComponentDefaultExport(program: t.Program): boolean;
@@ -0,0 +1,76 @@
1
+ import { isValidComponentName } from './component-collect.js';
2
+ /**
3
+ * 收集模块内所有本地 import 绑定名称
4
+ * @param program - AST Program 节点
5
+ * @returns 导入绑定名称集合
6
+ */
7
+ function collectImportedBindingNames(program) {
8
+ const importedBindings = new Set();
9
+ for (const node of program.body) {
10
+ if (node.type !== 'ImportDeclaration')
11
+ continue;
12
+ for (const spec of node.specifiers) {
13
+ importedBindings.add(spec.local.name);
14
+ }
15
+ }
16
+ return importedBindings;
17
+ }
18
+ /**
19
+ * 解包 TS 类型包装表达式
20
+ *
21
+ * `export default X as T` 在 babel TS 插件下解析为 TSAsExpression
22
+ * (还有 satisfies / 非空断言变体),解包到真实表达式后再判定。
23
+ * @param node - 默认导出声明表达式
24
+ * @returns 解包后的表达式
25
+ */
26
+ function unwrapTsWrappers(node) {
27
+ while (node.type === 'TSAsExpression' ||
28
+ node.type === 'TSSatisfiesExpression' ||
29
+ node.type === 'TSNonNullExpression') {
30
+ node = node.expression;
31
+ }
32
+ return node;
33
+ }
34
+ /**
35
+ * 检测模块是否存在「导入转发形式的组件默认导出」
36
+ *
37
+ * 覆盖两种转发形态(转发目标均为本地 import 绑定且大写开头):
38
+ * 1. `export default AdminLayout`(含 `as T` 断言包装)
39
+ * 2. `export { AdminLayout as default }`
40
+ *
41
+ * 带 source 的 re-export(`export { X as default } from './x'`)没有本地
42
+ * 绑定,不在导入绑定集合中,天然被排除。
43
+ *
44
+ * @param program - AST Program 节点
45
+ * @returns 是否存在转发形式的组件默认导出
46
+ */
47
+ export function hasForwardedComponentDefaultExport(program) {
48
+ const importedBindings = collectImportedBindingNames(program);
49
+ if (importedBindings.size === 0)
50
+ return false;
51
+ for (const node of program.body) {
52
+ // 形态1:export default X
53
+ if (node.type === 'ExportDefaultDeclaration') {
54
+ const decl = unwrapTsWrappers(node.declaration);
55
+ if (decl.type === 'Identifier') {
56
+ const name = decl.name;
57
+ if (importedBindings.has(name) && isValidComponentName(name))
58
+ return true;
59
+ }
60
+ }
61
+ // 形态2:export { X as default }
62
+ if (node.type === 'ExportNamedDeclaration') {
63
+ for (const spec of node.specifiers) {
64
+ if (spec.type === 'ExportSpecifier' &&
65
+ spec.exported.type === 'Identifier' &&
66
+ spec.exported.name === 'default' &&
67
+ spec.local.type === 'Identifier' &&
68
+ importedBindings.has(spec.local.name) &&
69
+ isValidComponentName(spec.local.name)) {
70
+ return true;
71
+ }
72
+ }
73
+ }
74
+ }
75
+ return false;
76
+ }
@@ -5,4 +5,5 @@ export { collectPatternBindings, collectObjectPatternBindings } from './pattern-
5
5
  export { createBranch, createBinaryBranch, buildNestedCondition, type BranchConfig } from './branch-factory.js';
6
6
  export { validateVIfChain, collectVIfChainInfo, collectFragmentVIfChains, type VIfChainInfo } from './vif-helpers.js';
7
7
  export { collectComponentFunctions, type ComponentInfo } from './component-collect.js';
8
+ export { hasForwardedComponentDefaultExport } from './forward-export.js';
8
9
  export { generateUniqueAlias, generateUniqueDefaultName } from './generate.js';
@@ -12,5 +12,7 @@ export { createBranch, createBinaryBranch, buildNestedCondition } from './branch
12
12
  export { validateVIfChain, collectVIfChainInfo, collectFragmentVIfChains } from './vif-helpers.js';
13
13
  // 组件收集
14
14
  export { collectComponentFunctions } from './component-collect.js';
15
+ // 导入转发默认导出检测
16
+ export { hasForwardedComponentDefaultExport } from './forward-export.js';
15
17
  // 生成器
16
18
  export { generateUniqueAlias, generateUniqueDefaultName } from './generate.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitarx/plugin-vite",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "The official plugin for Vitarx support in Vite.",
5
5
  "author": "ZhuChongLin <8210856@qq.com>",
6
6
  "license": "MIT",
@@ -47,7 +47,6 @@
47
47
  "acorn-walk": "^8.3.5"
48
48
  },
49
49
  "devDependencies": {
50
- "typescript": "^6.0.3",
51
50
  "@commitlint/cli": "^20.4.2",
52
51
  "@commitlint/config-conventional": "^20.4.2",
53
52
  "@commitlint/types": "^20.4.0",
@@ -55,13 +54,15 @@
55
54
  "@types/babel__generator": "^7.27.0",
56
55
  "@types/babel__traverse": "^7.28.0",
57
56
  "@types/node": "^25.3.2",
57
+ "happy-dom": "^20.14.5",
58
58
  "husky": "^9.1.7",
59
59
  "node": "^25.7.0",
60
60
  "prettier": "^3.8.1",
61
61
  "rimraf": "^6.1.3",
62
- "vitest": "^4.0.18",
62
+ "typescript": "^6.0.3",
63
+ "vitarx": "^4.0.6",
63
64
  "vite": "^8.0.5",
64
- "vitarx": "^4.0.6"
65
+ "vitest": "^4.0.18"
65
66
  },
66
67
  "scripts": {
67
68
  "build": "vitest run && rimraf dist && tsc -p tsconfig.build.json",
@@ -1,23 +0,0 @@
1
- /**
2
- * 代码变更类型
3
- */
4
- export interface ChangeCode {
5
- /** UI 描述代码是否变化(需要重新构建视图) */
6
- build: boolean;
7
- /** 非 UI 代码是否变化(需要完全重新挂载) */
8
- logic: boolean;
9
- }
10
- /**
11
- * 判断两个函数组件的代码差异
12
- *
13
- * 分析策略:
14
- * 1. 解析函数代码,提取所有 createView/branch/expr 等 UI API 调用
15
- * 2. 将这些调用识别为 UI 描述代码
16
- * 3. 其余代码识别为非 UI 代码(逻辑代码)
17
- * 4. 分别比较 UI 代码和非 UI 代码是否变化
18
- *
19
- * @param newCode 新函数代码
20
- * @param oldCode 旧函数代码
21
- * @returns {ChangeCode} 变更检测结果
22
- */
23
- export declare function diffComponentChange(newCode: string, oldCode: string): ChangeCode;
@@ -1,137 +0,0 @@
1
- /**
2
- * HMR 代码变更检测工具
3
- * 用于判断组件代码中 UI 描述部分和非 UI 部分的变更
4
- * @module hmr-client/utils
5
- */
6
- import { parse } from 'acorn';
7
- import { simple as walkSimple } from 'acorn-walk';
8
- import { UI_API_NAMES } from '../constants/index.js';
9
- /**
10
- * UI 相关的运行时 API 名称
11
- * 这些 API 调用代表 UI 描述代码
12
- */
13
- const UI_APIS = new Set([...UI_API_NAMES, 'jsxDEV', 'h']);
14
- /**
15
- * 判断标识符名称是否为 UI API
16
- * 支持原始名称和带后缀的名称(如 jsxDEV$1、branch$2、expr$1)
17
- */
18
- function isUIApi(name) {
19
- if (UI_APIS.has(name))
20
- return true;
21
- for (const api of UI_APIS) {
22
- if (name.startsWith(api + '$'))
23
- return true;
24
- }
25
- return false;
26
- }
27
- /**
28
- * 从函数代码中分离 UI 代码和非 UI 代码
29
- * @param functionCode 完整函数代码
30
- * @returns 包含逻辑代码和渲染代码的对象
31
- */
32
- function separateLogicAndRender(functionCode) {
33
- const uiNodes = [];
34
- // 包装代码以便解析
35
- const wrappedCode = `(${functionCode})`;
36
- try {
37
- const ast = parse(wrappedCode, {
38
- ecmaVersion: 'latest',
39
- sourceType: 'module',
40
- allowReturnOutsideFunction: true,
41
- allowAwaitOutsideFunction: true,
42
- allowImportExportEverywhere: true,
43
- allowHashBang: true,
44
- allowReserved: true
45
- });
46
- // 遍历 AST 收集所有 UI API 调用节点
47
- walkSimple(ast, {
48
- CallExpression(node) {
49
- const callee = node.callee;
50
- // 直接调用:createView(...)
51
- if (callee.type === 'Identifier' && isUIApi(callee.name)) {
52
- uiNodes.push({ start: node.start, end: node.end });
53
- return;
54
- }
55
- // 成员调用:exports.createView(...)
56
- if (callee.type === 'MemberExpression' &&
57
- callee.property.type === 'Identifier' &&
58
- isUIApi(callee.property.name)) {
59
- uiNodes.push({ start: node.start, end: node.end });
60
- }
61
- }
62
- });
63
- }
64
- catch {
65
- // 解析失败时,返回原始代码作为逻辑代码
66
- return {
67
- logicCode: functionCode.trim(),
68
- renderCode: ''
69
- };
70
- }
71
- // 按位置排序,从后向前替换以保持索引正确
72
- uiNodes.sort((a, b) => a.start - b.start);
73
- const filteredNodes = [];
74
- for (const node of uiNodes) {
75
- const isContained = filteredNodes.some(parent => node.start >= parent.start && node.end <= parent.end);
76
- if (!isContained) {
77
- filteredNodes.push(node);
78
- }
79
- }
80
- filteredNodes.sort((a, b) => b.start - a.start);
81
- // 提取 UI 代码
82
- const renderCode = filteredNodes
83
- .map(n => wrappedCode.slice(n.start, n.end))
84
- .reverse()
85
- .join('\n');
86
- // 移除 UI 代码后的逻辑代码
87
- let logicCode = wrappedCode;
88
- for (const node of filteredNodes) {
89
- logicCode = logicCode.slice(0, node.start) + logicCode.slice(node.end);
90
- }
91
- return {
92
- logicCode: logicCode.trim(),
93
- renderCode
94
- };
95
- }
96
- /**
97
- * 规范化代码字符串
98
- * 移除多余空白、注释等,用于比较
99
- */
100
- function normalizeCode(code) {
101
- // 移除多行注释
102
- let result = code.replace(/\/\*[\s\S]*?\*\//g, '');
103
- // 移除单行注释
104
- result = result.replace(/\/\/.*$/gm, '');
105
- // 移除多余空白
106
- result = result.replace(/\s+/g, ' ');
107
- // 移除首尾空白
108
- return result.trim();
109
- }
110
- /**
111
- * 判断两个函数组件的代码差异
112
- *
113
- * 分析策略:
114
- * 1. 解析函数代码,提取所有 createView/branch/expr 等 UI API 调用
115
- * 2. 将这些调用识别为 UI 描述代码
116
- * 3. 其余代码识别为非 UI 代码(逻辑代码)
117
- * 4. 分别比较 UI 代码和非 UI 代码是否变化
118
- *
119
- * @param newCode 新函数代码
120
- * @param oldCode 旧函数代码
121
- * @returns {ChangeCode} 变更检测结果
122
- */
123
- export function diffComponentChange(newCode, oldCode) {
124
- const { renderCode: newRenderCode, logicCode: newLogicCode } = separateLogicAndRender(newCode);
125
- const { renderCode: oldRenderCode, logicCode: oldLogicCode } = separateLogicAndRender(oldCode);
126
- // 规范化后比较,避免格式差异导致误判
127
- const normalizedNewLogic = normalizeCode(newLogicCode);
128
- const normalizedOldLogic = normalizeCode(oldLogicCode);
129
- const normalizedNewRender = normalizeCode(newRenderCode);
130
- const normalizedOldRender = normalizeCode(oldRenderCode);
131
- return {
132
- // UI 代码变化:需要重新构建视图
133
- build: normalizedNewRender !== normalizedOldRender,
134
- // 非 UI 代码变化:需要完全重新挂载组件
135
- logic: normalizedNewLogic !== normalizedOldLogic
136
- };
137
- }