@vitarx/plugin-vite 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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';
@@ -9,11 +9,18 @@ import { markImport } from '../../context.js';
9
9
  import { addPureComment, createAccessorCall, createBinaryBranch, createExprCall, getAlias } from '../../utils/index.js';
10
10
  /**
11
11
  * 处理成员表达式
12
- * object 为简单标识符时使用 accessor(细粒度追踪)
13
- * object 为其他表达式(嵌套成员访问等)时降级为 expr(粗粒度追踪)
12
+ * 静态成员(obj.prop,computed=false)时使用 accessor(细粒度追踪),
13
+ * property 标识符即属性名,字符串化后作为 key;
14
+ * 计算成员(obj[keyExpr],computed=true)时降级为 expr(粗粒度追踪)——
15
+ * accessor(obj, key) 的 key 在构建期一次求值,无法追踪 key 本身的变化,
16
+ * 且 key 为标识符时若误字符串化会读成 obj.key 改变语义(读取不存在的属性
17
+ * 渲染为空),必须保留动态求值语义。
14
18
  */
15
19
  function handleMemberExpression(expr, ctx) {
16
- if (isIdentifier(expr.object)) {
20
+ // key 静态(obj.prop 或 obj['prop'])才可 accessor:字符串字面量 key 原样
21
+ // 透传即为正确的静态 key;标识符(非计算成员)即属性名,由 accessor 构建器字符串化
22
+ const isStaticKey = !expr.computed || t.isStringLiteral(expr.property);
23
+ if (isStaticKey && isIdentifier(expr.object)) {
17
24
  markImport(ctx, 'accessor');
18
25
  const accessorAlias = getAlias(ctx.vitarxAliases, 'accessor');
19
26
  return createAccessorCall(expr.object, expr.property, accessorAlias);
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.7",
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",