@vobs/compiler 1.2.1 → 1.3.0

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.
@@ -170,6 +170,80 @@ describe('compiler', () => {
170
170
  expect(result).not.toContain('setAttribute(_el2, "key"')
171
171
  })
172
172
 
173
+ it('编译嵌套三元的全部分支(不再只保留第一个分支)', () => {
174
+ const result = compile(`const el = <div>{flag ? <A /> : other ? <B /> : <C />}</div>`)
175
+
176
+ expect(result).toContain('insertDynamic')
177
+ // 三个分支全部编译为组件调用
178
+ expect(result.match(/createComponent\(resolveComponent/gu)).toHaveLength(3)
179
+ expect(result).not.toContain('React')
180
+ })
181
+
182
+ it('编译 && 与嵌套动态节点组合', () => {
183
+ const result = compile(`const el = <div>{flag && (other ? <A /> : <B />)}</div>`)
184
+
185
+ expect(result).toContain('insertDynamic')
186
+ expect(result.match(/createComponent\(resolveComponent/gu)).toHaveLength(2)
187
+ })
188
+
189
+ it('编译 if 块内的 JSX 早返回(不再泄漏到 React 降级路径)', () => {
190
+ const code = `
191
+ function App() {
192
+ if (items.value.length === 0) return <Empty />
193
+ return <div><Footer /></div>
194
+ }
195
+ `
196
+ const result = compile(code)
197
+
198
+ // 早返回分支与主 return 分支都编译为 vobs 组件调用,源码中不残留 JSX
199
+ expect(result.match(/createComponent\(resolveComponent/gu)).toHaveLength(2)
200
+ expect(result).not.toContain('<Empty')
201
+ expect(result).not.toContain('<Footer')
202
+ expect(result).not.toContain('React')
203
+ })
204
+
205
+ it('编译函数体内部初始化器与嵌套函数中的 JSX', () => {
206
+ const code = `
207
+ function App() {
208
+ const render = () => <Inner />
209
+ if (cond.value) { slot = <Aside /> }
210
+ return <div>{render()}</div>
211
+ }
212
+ `
213
+ const result = compile(code)
214
+
215
+ expect(result.match(/createComponent\(resolveComponent/gu)).toHaveLength(2)
216
+ expect(result).not.toContain('<Inner')
217
+ expect(result).not.toContain('<Aside')
218
+ expect(result).not.toContain('React')
219
+ })
220
+
221
+ it('hmrModuleId 将模块顶层 state 包装为 HMR 保鲜引用', () => {
222
+ const code = `import { state } from '@vobs/reactivity'
223
+ export const count = state(0)
224
+ function helper() {
225
+ const local = state(1)
226
+ return local.value
227
+ }
228
+ `
229
+ const result = compile(code, { hmrModuleId: 'src/stores/counter.ts' })
230
+
231
+ // 顶层声明被包装,函数内的局部声明不受影响
232
+ expect(result).toContain('hmrStateRef("src/stores/counter.ts#count"')
233
+ expect(result).toContain('() => state(0, "count")')
234
+ expect(result).not.toContain('hmrStateRef("src/stores/counter.ts#local"')
235
+ })
236
+
237
+ it('hmrModuleId 保留显式 debugName 且兼容别名导入', () => {
238
+ const code = `import { state as st } from '@vobs/reactivity'
239
+ export const width = st(50, 'doc.width')
240
+ `
241
+ const result = compile(code, { hmrModuleId: 'src/stores/doc.ts' })
242
+
243
+ expect(result).toContain('hmrStateRef("src/stores/doc.ts#width"')
244
+ expect(result).toContain("() => st(50, 'doc.width')")
245
+ })
246
+
173
247
  it('在 JSX 转换前执行编译器插件的分析、程序与节点钩子', () => {
174
248
  const filenames: string[] = []
175
249
  const plugin: CompilerPlugin = {
package/src/compile.ts CHANGED
@@ -39,6 +39,8 @@ interface CompileState {
39
39
  localBindings: ReadonlySet<string>
40
40
  /** 编译器自身产出的诊断(如不支持的 JSX 形态),与 TypeScript 解析诊断合并返回。 */
41
41
  diagnostics: CompilerDiagnostic[]
42
+ /** HMR 模块标识:提供后模块顶层 state() 声明包装为 hmrStateRef,跨热更新保活信号。 */
43
+ hmrModuleId: string | null
42
44
  }
43
45
 
44
46
  interface SourcePosition {
@@ -101,7 +103,8 @@ export function compileWithSourceMap(code: string, options: CompileOptions = {})
101
103
  sourceLocation: options.sourceLocation ?? true,
102
104
  stateAliases: collectStateAliases(sourceFile),
103
105
  localBindings: collectLocallyDeclaredNames(sourceFile),
104
- diagnostics: []
106
+ diagnostics: [],
107
+ hmrModuleId: options.hmrModuleId ?? null
105
108
  }
106
109
  const cleanFilename = filename.split(/[?#]/u, 1)[0] || filename
107
110
  const diagnostics = ts.transpileModule(code, {
@@ -130,16 +133,17 @@ export function compileWithSourceMap(code: string, options: CompileOptions = {})
130
133
  for (const plugin of plugins) sourceFile = transformPluginNodes(sourceFile, plugin, context)
131
134
 
132
135
  const statements = sourceFile.statements.map(statement =>
133
- ts.isImportDeclaration(statement) ? rebuildImport(state, statement) : transformStatement(state, statement)
136
+ ts.isImportDeclaration(statement) ? rebuildImport(state, statement) : transformStatement(state, statement, true)
134
137
  )
135
138
  // 模板声明必须先于 runtime import 生成:声明里的 createTemplate 依赖
136
139
  // helperRef 注册别名,import 需要在别名全部就绪后再构建。
137
140
  const templateDeclarations = createTemplateDeclarations(state)
138
- const resultFile = ts.factory.updateSourceFile(sourceFile, [
141
+ let resultFile = ts.factory.updateSourceFile(sourceFile, [
139
142
  ...createRuntimeImports(state),
140
143
  ...templateDeclarations,
141
144
  ...statements
142
145
  ])
146
+ resultFile = transformResidualJsx(state, resultFile)
143
147
 
144
148
  const generated = ts.createPrinter().printFile(resultFile)
145
149
  return {
@@ -490,7 +494,7 @@ function rebuildImport(state: CompileState, node: ts.ImportDeclaration): ts.Impo
490
494
  ), node)
491
495
  }
492
496
 
493
- function transformStatement(state: CompileState, node: ts.Statement): ts.Statement {
497
+ function transformStatement(state: CompileState, node: ts.Statement, moduleScope = false): ts.Statement {
494
498
  if (ts.isFunctionDeclaration(node) && node.body) {
495
499
  return tagStatement(state, ts.factory.updateFunctionDeclaration(
496
500
  node,
@@ -504,7 +508,7 @@ function transformStatement(state: CompileState, node: ts.Statement): ts.Stateme
504
508
  ), node)
505
509
  }
506
510
 
507
- if (ts.isVariableStatement(node)) return transformVariableStatement(state, node)
511
+ if (ts.isVariableStatement(node)) return transformVariableStatement(state, node, moduleScope)
508
512
  if (ts.isExportAssignment(node) && containsJsx(node.expression)) {
509
513
  return tagStatement(state, ts.factory.updateExportAssignment(node, node.modifiers, transformEmbeddedExpression(state, node.expression)), node)
510
514
  }
@@ -517,7 +521,7 @@ function transformStatement(state: CompileState, node: ts.Statement): ts.Stateme
517
521
  return node
518
522
  }
519
523
 
520
- function transformVariableStatement(state: CompileState, node: ts.VariableStatement): ts.VariableStatement {
524
+ function transformVariableStatement(state: CompileState, node: ts.VariableStatement, moduleScope = false): ts.VariableStatement {
521
525
  let changed = false
522
526
  const declarations = node.declarationList.declarations.map(declaration => {
523
527
  const initializer = declaration.initializer
@@ -526,6 +530,9 @@ function transformVariableStatement(state: CompileState, node: ts.VariableStatem
526
530
  let nextInitializer = inferStateDebugName(state, declaration, initializer) ?? initializer
527
531
  if (containsJsx(nextInitializer)) {
528
532
  nextInitializer = transformEmbeddedExpression(state, nextInitializer)
533
+ } else if (moduleScope && state.hmrModuleId !== null) {
534
+ // HMR 状态保鲜仅限模块顶层:函数内局部 state 每次调用都应创建新信号
535
+ nextInitializer = wrapStateWithHmrRef(state, declaration, nextInitializer) ?? nextInitializer
529
536
  }
530
537
  if (nextInitializer === initializer) return declaration
531
538
 
@@ -578,7 +585,41 @@ function inferStateDebugName(
578
585
  ])
579
586
  }
580
587
 
581
- function containsJsx(expression: ts.Expression): boolean {
588
+ /**
589
+ * HMR 状态保鲜:模块热更新重执行时,模块级 state() 会创建全新信号实例,与未重执行的
590
+ * 导入方持有旧实例并存,形成"两份状态"(症状:编辑不生效、页面半边失灵,全量刷新也无法
591
+ * 消除)。开启 hmrModuleId 后,模块顶层的 state 声明改经运行时注册表取值:
592
+ * `const x = state(init)` → `const x = hmrStateRef(moduleId, 'x', () => state(init, 'x'))`。
593
+ * 首次执行照常创建;模块重执行时直接复用既有信号,模块逻辑(副作用、导出绑定)照常重跑。
594
+ */
595
+ function wrapStateWithHmrRef(
596
+ state: CompileState,
597
+ declaration: ts.VariableDeclaration,
598
+ initializer: ts.Expression
599
+ ): ts.Expression | undefined {
600
+ let call = initializer
601
+ while (ts.isParenthesizedExpression(call) || ts.isAsExpression(call) || ts.isTypeAssertionExpression(call) || ts.isSatisfiesExpression(call)) {
602
+ call = call.expression
603
+ }
604
+ if (!ts.isCallExpression(call)) return undefined
605
+ const callee = call.expression
606
+ if (!ts.isIdentifier(callee) || !state.stateAliases.has(callee.text)) return undefined
607
+ if (state.localBindings.has(callee.text)) return undefined
608
+ if (!ts.isIdentifier(declaration.name)) return undefined
609
+ return ts.factory.createCallExpression(helperRef(state, 'hmrStateRef'), undefined, [
610
+ ts.factory.createStringLiteral(`${state.hmrModuleId}#${declaration.name.text}`),
611
+ ts.factory.createArrowFunction(
612
+ undefined,
613
+ undefined,
614
+ [],
615
+ undefined,
616
+ ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
617
+ initializer
618
+ )
619
+ ])
620
+ }
621
+
622
+ function containsJsx(expression: ts.Expression | ts.SourceFile): boolean {
582
623
  let found = false
583
624
  const visit = (node: ts.Node): void => {
584
625
  if (isJsxExpression(node as ts.Expression)) {
@@ -895,6 +936,28 @@ function transformEmbeddedExpression(state: CompileState, expression: ts.Express
895
936
  }
896
937
  }
897
938
 
939
+
940
+ function transformResidualJsx(state: CompileState, sourceFile: ts.SourceFile): ts.SourceFile {
941
+ if (!containsJsx(sourceFile)) return sourceFile
942
+ const result = ts.transform(sourceFile, [context => root => {
943
+ const visit: ts.Visitor = node => {
944
+ if (ts.isReturnStatement(node) && node.expression && containsJsx(node.expression)) {
945
+ return ts.factory.updateReturnStatement(node, transformEmbeddedExpression(state, node.expression))
946
+ }
947
+ if (isJsxExpression(node as ts.Expression)) {
948
+ return transformJsxExpression(state, node as ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment)
949
+ }
950
+ return ts.visitEachChild(node, visit, context)
951
+ }
952
+ return ts.visitNode(root, visit) as ts.SourceFile
953
+ }])
954
+ try {
955
+ return result.transformed[0]
956
+ } finally {
957
+ result.dispose()
958
+ }
959
+ }
960
+
898
961
  /**
899
962
  * 静态元素判定:DOM 标签 + 全部属性为字符串字面量或无值 + 全部子节点为文本或递归静态元素。
900
963
  * 保守排除项(语义或序列化等价性无把握,走原路径):
@@ -1194,38 +1257,71 @@ function createSourceLocation(node: ts.Node): ts.ObjectLiteralExpression {
1194
1257
  }
1195
1258
 
1196
1259
  function transformDynamicExpression(state: CompileState, expression: ts.Expression): ts.ArrowFunction | null {
1260
+ const converted = convertDynamicNodeExpression(state, expression)
1261
+ return converted ? createGetter(converted) : null
1262
+ }
1263
+
1264
+ /**
1265
+ * 把产出节点的动态表达式(`cond ? <A/> : <B/>`、`cond && <A/>`,含任意嵌套组合)
1266
+ * 转换为条件表达式树;各分支中的 JSX 递归编译为节点工厂,由 insertDynamic 挂载/卸载。
1267
+ * 返回 null 表示没有任何分支产出节点(纯文本/数值场景走 insertDynamicValue 文本绑定)。
1268
+ */
1269
+ function convertDynamicNodeExpression(state: CompileState, expression: ts.Expression): ts.ConditionalExpression | null {
1197
1270
  if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
1198
1271
  const right = unwrapExpression(expression.right)
1199
- if (!isJsxExpression(right)) return null
1200
- return createGetter(ts.factory.createConditionalExpression(
1201
- expression.left,
1202
- ts.factory.createToken(ts.SyntaxKind.QuestionToken),
1203
- transformJsxExpression(state, right),
1204
- ts.factory.createToken(ts.SyntaxKind.ColonToken),
1205
- ts.factory.createNull()
1206
- ))
1272
+ if (isJsxExpression(right)) {
1273
+ return createNodeConditional(expression.left, transformJsxExpression(state, right), null)
1274
+ }
1275
+ // 右侧是嵌套的动态节点表达式(如 cond && (sub ? <A/> : <B/>))时递归转换,
1276
+ // 转换失败(纯文本分支)则整体回落为动态值绑定,保持语义可静态判定。
1277
+ const convertedRight = convertDynamicNodeExpression(state, right)
1278
+ if (convertedRight) return createNodeConditional(expression.left, convertedRight, null)
1279
+ return null
1207
1280
  }
1208
1281
 
1209
1282
  if (ts.isConditionalExpression(expression)) {
1210
1283
  const whenTrue = transformDynamicBranch(state, expression.whenTrue)
1211
1284
  const whenFalse = transformDynamicBranch(state, expression.whenFalse)
1212
1285
  if (!whenTrue && !whenFalse) return null
1213
- return createGetter(ts.factory.createConditionalExpression(
1286
+ return ts.factory.createConditionalExpression(
1214
1287
  expression.condition,
1215
1288
  ts.factory.createToken(ts.SyntaxKind.QuestionToken),
1216
1289
  whenTrue ?? ts.factory.createNull(),
1217
1290
  ts.factory.createToken(ts.SyntaxKind.ColonToken),
1218
1291
  whenFalse ?? ts.factory.createNull()
1219
- ))
1292
+ )
1220
1293
  }
1221
1294
 
1222
1295
  return null
1223
1296
  }
1224
1297
 
1298
+ function createNodeConditional(
1299
+ condition: ts.Expression,
1300
+ whenTrue: ts.Expression,
1301
+ whenFalse: ts.Expression | null
1302
+ ): ts.ConditionalExpression {
1303
+ return ts.factory.createConditionalExpression(
1304
+ condition,
1305
+ ts.factory.createToken(ts.SyntaxKind.QuestionToken),
1306
+ whenTrue,
1307
+ ts.factory.createToken(ts.SyntaxKind.ColonToken),
1308
+ whenFalse ?? ts.factory.createNull()
1309
+ )
1310
+ }
1311
+
1312
+ /**
1313
+ * 转换单个分支:JSX → 节点工厂;null/false 原样保留;嵌套的三元与 `&&`
1314
+ * 动态节点表达式递归转换(此前嵌套三元只编译第一个分支,其余分支被静默丢弃)。
1315
+ * 其余表达式(字符串、数值等)返回 null,由调用方回落为 null 分支。
1316
+ */
1225
1317
  function transformDynamicBranch(state: CompileState, expression: ts.Expression): ts.Expression | null {
1226
1318
  const branch = unwrapExpression(expression)
1227
1319
  if (isJsxExpression(branch)) return transformJsxExpression(state, branch)
1228
1320
  if (branch.kind === ts.SyntaxKind.NullKeyword || branch.kind === ts.SyntaxKind.FalseKeyword) return branch
1321
+ if (ts.isConditionalExpression(branch)
1322
+ || (ts.isBinaryExpression(branch) && branch.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)) {
1323
+ return convertDynamicNodeExpression(state, branch)
1324
+ }
1229
1325
  return null
1230
1326
  }
1231
1327
 
package/src/plugin.ts CHANGED
@@ -41,6 +41,12 @@ export interface CompileOptions extends CompilerOptions {
41
41
  * 默认 true。生产构建应传 false 以减小产物体积,省略后错误仍带组件名,定位走 source map。
42
42
  */
43
43
  sourceLocation?: boolean
44
+ /**
45
+ * HMR 模块标识(dev 由 Vite 插件注入,通常为模块绝对路径)。提供后,模块顶层的
46
+ * state() 声明会包装为 hmrStateRef(...):热更新重执行模块时复用既有信号实例,
47
+ * 避免"新旧两份模块实例、两份状态"导致的页面半边失灵。
48
+ */
49
+ hmrModuleId?: string
44
50
  }
45
51
 
46
52
  export interface VobsSourceMap {