@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.
- package/dist/constants/index.d.ts +0 -9
- package/dist/constants/index.js +0 -9
- package/dist/hmr-client/index.d.ts +27 -1
- package/dist/hmr-client/index.js +108 -12
- package/dist/hmr-client/update.d.ts +11 -2
- package/dist/hmr-client/update.js +25 -59
- package/dist/hmr-client/weak-refs.d.ts +37 -0
- package/dist/hmr-client/weak-refs.js +41 -0
- package/dist/passes/hmr/index.d.ts +1 -1
- package/dist/passes/hmr/index.js +1 -1
- package/dist/passes/hmr/inject-state.d.ts +16 -10
- package/dist/passes/hmr/inject-state.js +145 -120
- package/dist/passes/hmr/inject.d.ts +10 -0
- package/dist/passes/hmr/inject.js +47 -10
- package/dist/passes/index.d.ts +1 -1
- package/dist/passes/index.js +1 -1
- package/dist/transform.js +11 -4
- package/dist/utils/forward-export.d.ts +28 -0
- package/dist/utils/forward-export.js +76 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +2 -0
- package/package.json +5 -4
- package/dist/hmr-client/utils.d.ts +0 -23
- package/dist/hmr-client/utils.js +0 -137
|
@@ -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 变量
|
package/dist/constants/index.js
CHANGED
|
@@ -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):
|
|
93
|
+
update(newModule: ModuleNamespace): boolean;
|
|
68
94
|
}
|
package/dist/hmr-client/index.js
CHANGED
|
@@ -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
|
-
|
|
96
|
-
|
|
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
|
|
139
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
}
|
package/dist/passes/hmr/index.js
CHANGED
|
@@ -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
|
|
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$__ &&
|
|
29
|
-
// __$VITARX_HMR_VIEW_NODE$__
|
|
30
|
-
// }
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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(
|
|
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
|
-
*
|
|
168
|
-
*
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/passes/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/passes/index.js
CHANGED
|
@@ -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 &&
|
|
156
|
-
|
|
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
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/utils/index.js
CHANGED
|
@@ -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.
|
|
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
|
-
"
|
|
62
|
+
"typescript": "^6.0.3",
|
|
63
|
+
"vitarx": "^4.0.6",
|
|
63
64
|
"vite": "^8.0.5",
|
|
64
|
-
"
|
|
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;
|
package/dist/hmr-client/utils.js
DELETED
|
@@ -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
|
-
}
|