@vobs/compiler 1.0.0 → 1.2.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.
package/src/compile.ts CHANGED
@@ -10,16 +10,36 @@ import type {
10
10
  VobsCompiler
11
11
  } from './plugin'
12
12
 
13
- let generatedId = 0
14
- let currentFilename = 'component.tsx'
15
- let currentSourceFile: ts.SourceFile | null = null
16
- let statementSources = new WeakMap<ts.Statement, SourcePosition>()
17
- /** 源文件中已声明的绑定名(含嵌套作用域):注入运行时 import 与生成临时变量时避开命名冲突。 */
18
- let takenNames = new Set<string>()
19
- /** 运行时 helper 的规范名 → 产物中的引用名(无冲突时与规范名相同)。 */
20
- let helperAliases = new Map<string, string>()
21
- /** 编译器自身产出的诊断(如不支持的 JSX 形态),与 TypeScript 解析诊断合并返回。 */
22
- let compileDiagnostics: CompilerDiagnostic[] = []
13
+ /**
14
+ * 单次编译的全部可变状态。
15
+ *
16
+ * 此前这些字段是模块级变量,编译器因此不可重入:嵌套调用 compile()
17
+ * (如插件内部再次编译片段)会互相污染状态。现在每次 compile 调用
18
+ * 创建独立的 CompileState 并显式穿参,编译器对并发与嵌套完全安全。
19
+ */
20
+ interface CompileState {
21
+ /** 临时标识符计数器(_el0、_el1…)。 */
22
+ generatedId: number
23
+ filename: string
24
+ /** 最初解析出的源文件。插件 program 变换后的树节点无法回溯原始位置时,用它兜底取行列。 */
25
+ sourceFile: ts.SourceFile
26
+ /** 生成语句 → 原始源码位置,用于 source map 生成。 */
27
+ statementSources: WeakMap<ts.Statement, SourcePosition>
28
+ /** 源文件中已声明的绑定名(含嵌套作用域):注入运行时 import 与生成临时变量时避开命名冲突。 */
29
+ takenNames: Set<string>
30
+ /** 运行时 helper 的规范名 → 产物中的引用名(无冲突时与规范名相同)。 */
31
+ helperAliases: Map<string, string>
32
+ /** 静态模板声明:HTML → 模块级模板变量,按内容去重,输出在 import 之后。 */
33
+ templates: Map<string, ts.Identifier>
34
+ /** 是否为组件调用生成源码位置(生产构建传 false 剔除,减小产物体积)。 */
35
+ sourceLocation: boolean
36
+ /** 从 @vobs/reactivity / @vobs/vobs 导入的 `state` 别名(含 as 别名),用于 debugName 自动推断。 */
37
+ stateAliases: ReadonlySet<string>
38
+ /** 非 import 的本地声明绑定名:`state` 被本地声明遮蔽时禁用 debugName 推断。 */
39
+ localBindings: ReadonlySet<string>
40
+ /** 编译器自身产出的诊断(如不支持的 JSX 形态),与 TypeScript 解析诊断合并返回。 */
41
+ diagnostics: CompilerDiagnostic[]
42
+ }
23
43
 
24
44
  interface SourcePosition {
25
45
  readonly line: number
@@ -62,10 +82,7 @@ export function compile(code: string, options: CompileOptions = {}): string {
62
82
  }
63
83
 
64
84
  export function compileWithSourceMap(code: string, options: CompileOptions = {}): CompileResult {
65
- generatedId = 0
66
85
  const filename = options.filename ?? 'component.tsx'
67
- currentFilename = filename
68
- statementSources = new WeakMap()
69
86
  let sourceFile = ts.createSourceFile(
70
87
  filename,
71
88
  code,
@@ -73,10 +90,19 @@ export function compileWithSourceMap(code: string, options: CompileOptions = {})
73
90
  true,
74
91
  ts.ScriptKind.TSX
75
92
  )
76
- currentSourceFile = sourceFile
77
- takenNames = collectDeclaredNames(sourceFile)
78
- helperAliases = new Map()
79
- compileDiagnostics = []
93
+ const state: CompileState = {
94
+ generatedId: 0,
95
+ filename,
96
+ sourceFile,
97
+ statementSources: new WeakMap(),
98
+ takenNames: collectDeclaredNames(sourceFile),
99
+ helperAliases: new Map(),
100
+ templates: new Map(),
101
+ sourceLocation: options.sourceLocation ?? true,
102
+ stateAliases: collectStateAliases(sourceFile),
103
+ localBindings: collectLocallyDeclaredNames(sourceFile),
104
+ diagnostics: []
105
+ }
80
106
  const cleanFilename = filename.split(/[?#]/u, 1)[0] || filename
81
107
  const diagnostics = ts.transpileModule(code, {
82
108
  // Vite appends query strings (for example `?direct`) to module IDs;
@@ -92,9 +118,9 @@ export function compileWithSourceMap(code: string, options: CompileOptions = {})
92
118
  filename,
93
119
  factory: ts.factory,
94
120
  addRuntimeImport(name: string): void {
95
- resolveHelperName(name)
121
+ resolveHelperName(state, name)
96
122
  },
97
- helperRef
123
+ helperRef: (name: string) => helperRef(state, name)
98
124
  }
99
125
 
100
126
  for (const plugin of plugins) plugin.analyze?.(sourceFile, context)
@@ -104,15 +130,22 @@ export function compileWithSourceMap(code: string, options: CompileOptions = {})
104
130
  for (const plugin of plugins) sourceFile = transformPluginNodes(sourceFile, plugin, context)
105
131
 
106
132
  const statements = sourceFile.statements.map(statement =>
107
- ts.isImportDeclaration(statement) ? rebuildImport(statement) : transformStatement(statement)
133
+ ts.isImportDeclaration(statement) ? rebuildImport(state, statement) : transformStatement(state, statement)
108
134
  )
109
- const resultFile = ts.factory.updateSourceFile(sourceFile, [...createRuntimeImports(), ...statements])
135
+ // 模板声明必须先于 runtime import 生成:声明里的 createTemplate 依赖
136
+ // helperRef 注册别名,import 需要在别名全部就绪后再构建。
137
+ const templateDeclarations = createTemplateDeclarations(state)
138
+ const resultFile = ts.factory.updateSourceFile(sourceFile, [
139
+ ...createRuntimeImports(state),
140
+ ...templateDeclarations,
141
+ ...statements
142
+ ])
110
143
 
111
144
  const generated = ts.createPrinter().printFile(resultFile)
112
145
  return {
113
146
  code: generated,
114
- map: buildSourceMap(filename, code, generated, resultFile),
115
- diagnostics: [...diagnostics, ...compileDiagnostics]
147
+ map: buildSourceMap(state, filename, code, generated, resultFile),
148
+ diagnostics: [...diagnostics, ...state.diagnostics]
116
149
  }
117
150
  }
118
151
 
@@ -153,17 +186,17 @@ function buildCodeFrame(
153
186
  * 不支持的 JSX 标签形态(成员表达式 `<Foo.Bar>`、命名空间 `<svg:rect>` 等)。
154
187
  * 诊断以 error 级返回,compile() 与 Vite 插件会直接失败,不再静默产出无效 DOM 标签。
155
188
  */
156
- function reportUnsupportedTag(tagName: ts.JsxTagNameExpression): void {
157
- const sourceFile = tagName.getSourceFile() ?? currentSourceFile
189
+ function reportUnsupportedTag(state: CompileState, tagName: ts.JsxTagNameExpression): void {
190
+ const sourceFile = tagName.getSourceFile() ?? state.sourceFile
158
191
  if (!sourceFile) return
159
192
  const label = tagName.getText()
160
193
  const kindNote = tagName.kind === ts.SyntaxKind.JsxNamespacedName ? '(JSX 命名空间标签)' : ''
161
194
  const { line, column, codeFrame } = buildCodeFrame(sourceFile, tagName.getStart(sourceFile), tagName.getWidth(sourceFile))
162
- compileDiagnostics.push({
195
+ state.diagnostics.push({
163
196
  code: 'VOBS_C101',
164
197
  severity: 'error',
165
198
  message: `不支持的 JSX 标签形态:<${label}>${kindNote}。组件必须是大写开头的标识符,DOM 元素必须是小写标签名。`,
166
- location: { file: currentFilename, line, column },
199
+ location: { file: state.filename, line, column },
167
200
  codeFrame,
168
201
  fix: `把 <${label}> 改为 <Component /> 形式的组件或小写 DOM 标签;Fragment 请使用 <Fragment> 或 <>...</>。`
169
202
  })
@@ -183,6 +216,7 @@ interface MappingSegment {
183
216
  * statement it was produced from.
184
217
  */
185
218
  function buildSourceMap(
219
+ state: CompileState,
186
220
  filename: string,
187
221
  source: string,
188
222
  generated: string,
@@ -190,7 +224,7 @@ function buildSourceMap(
190
224
  ): import('./plugin').VobsSourceMap {
191
225
  const reparsed = ts.createSourceFile(filename, generated, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
192
226
  const segments: MappingSegment[] = []
193
- walkPairedTrees(resultFile, reparsed, reparsed, segments)
227
+ walkPairedTrees(state, resultFile, reparsed, reparsed, segments)
194
228
  segments.sort((a, b) => a.genLine - b.genLine || a.genCol - b.genCol)
195
229
  return {
196
230
  version: 3,
@@ -215,6 +249,7 @@ function statementLists(node: ts.Node): readonly ts.Statement[] | null {
215
249
  * any divergence (length mismatch) simply abandons that subtree.
216
250
  */
217
251
  function walkPairedTrees(
252
+ state: CompileState,
218
253
  original: ts.Node,
219
254
  generated: ts.Node,
220
255
  reparsed: ts.SourceFile,
@@ -227,8 +262,8 @@ function walkPairedTrees(
227
262
  for (let index = 0; index < originalStatements.length; index++) {
228
263
  const originalStatement = originalStatements[index]
229
264
  const generatedStatement = generatedStatements[index]
230
- recordSegment(originalStatement, generatedStatement, reparsed, segments)
231
- walkPairedTrees(originalStatement, generatedStatement, reparsed, segments)
265
+ recordSegment(state, originalStatement, generatedStatement, reparsed, segments)
266
+ walkPairedTrees(state, originalStatement, generatedStatement, reparsed, segments)
232
267
  }
233
268
  return
234
269
  }
@@ -239,17 +274,18 @@ function walkPairedTrees(
239
274
  ts.forEachChild(generated, node => { generatedChildren.push(node) })
240
275
  if (originalChildren.length !== generatedChildren.length) return
241
276
  for (let index = 0; index < originalChildren.length; index++) {
242
- walkPairedTrees(originalChildren[index], generatedChildren[index], reparsed, segments)
277
+ walkPairedTrees(state, originalChildren[index], generatedChildren[index], reparsed, segments)
243
278
  }
244
279
  }
245
280
 
246
281
  function recordSegment(
282
+ state: CompileState,
247
283
  originalStatement: ts.Statement,
248
284
  generatedStatement: ts.Statement,
249
285
  reparsed: ts.SourceFile,
250
286
  segments: MappingSegment[]
251
287
  ): void {
252
- const source = statementSources.get(originalStatement) ?? positionOfOriginalStatement(originalStatement)
288
+ const source = state.statementSources.get(originalStatement) ?? positionOfOriginalStatement(state, originalStatement)
253
289
  if (!source) return
254
290
  const position = reparsed.getLineAndCharacterOfPosition(generatedStatement.getStart(reparsed))
255
291
  segments.push({
@@ -260,10 +296,10 @@ function recordSegment(
260
296
  })
261
297
  }
262
298
 
263
- function positionOfOriginalStatement(statement: ts.Statement): SourcePosition | null {
264
- if (statement.pos < 0 || !currentSourceFile) return null
265
- const { line, character } = currentSourceFile.getLineAndCharacterOfPosition(
266
- statement.getStart(currentSourceFile)
299
+ function positionOfOriginalStatement(state: CompileState, statement: ts.Statement): SourcePosition | null {
300
+ if (statement.pos < 0 || !state.sourceFile) return null
301
+ const { line, character } = state.sourceFile.getLineAndCharacterOfPosition(
302
+ statement.getStart(state.sourceFile)
267
303
  )
268
304
  return { line, column: character }
269
305
  }
@@ -357,6 +393,36 @@ function collectDeclaredNames(sourceFile: ts.SourceFile): Set<string> {
357
393
  return names
358
394
  }
359
395
 
396
+ /** 收集从 @vobs/reactivity / @vobs/vobs 导入的 `state` 绑定名(含 `as` 别名)。 */
397
+ function collectStateAliases(sourceFile: ts.SourceFile): Set<string> {
398
+ const aliases = new Set<string>()
399
+ for (const statement of sourceFile.statements) {
400
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue
401
+ const module = statement.moduleSpecifier.text
402
+ if (module !== '@vobs/reactivity' && module !== '@vobs/vobs') continue
403
+ const clause = statement.importClause
404
+ if (!clause?.namedBindings || !ts.isNamedImports(clause.namedBindings)) continue
405
+ for (const element of clause.namedBindings.elements) {
406
+ if (element.propertyName ? element.propertyName.text === 'state' : element.name.text === 'state') {
407
+ aliases.add(element.name.text)
408
+ }
409
+ }
410
+ }
411
+ return aliases
412
+ }
413
+
414
+ /** 与 collectDeclaredNames 相同,但跳过 import 声明:用于判断 helper 名是否被本地声明遮蔽。 */
415
+ function collectLocallyDeclaredNames(sourceFile: ts.SourceFile): Set<string> {
416
+ const names = new Set<string>()
417
+ const visit = (node: ts.Node): void => {
418
+ if (ts.isImportDeclaration(node)) return
419
+ if (ts.isIdentifier(node) && isBindingName(node)) names.add(node.text)
420
+ ts.forEachChild(node, visit)
421
+ }
422
+ visit(sourceFile)
423
+ return names
424
+ }
425
+
360
426
  /** 标识符是否为某个声明的绑定名(import、变量、函数、参数、类成员等)。 */
361
427
  function isBindingName(node: ts.Identifier): boolean {
362
428
  const parent = node.parent
@@ -373,27 +439,27 @@ function isBindingName(node: ts.Identifier): boolean {
373
439
  * the source never binds it; otherwise a collision-free alias is allocated and
374
440
  * the injected import uses the same alias (`import { createElement as _vobs_createElement }`).
375
441
  */
376
- function resolveHelperName(name: string): string {
377
- const existing = helperAliases.get(name)
442
+ function resolveHelperName(state: CompileState, name: string): string {
443
+ const existing = state.helperAliases.get(name)
378
444
  if (existing) return existing
379
445
  let alias = name
380
- if (takenNames.has(alias)) {
446
+ if (state.takenNames.has(alias)) {
381
447
  alias = `_vobs_${name}`
382
448
  let suffix = 1
383
- while (takenNames.has(alias)) alias = `_vobs_${name}_${suffix++}`
449
+ while (state.takenNames.has(alias)) alias = `_vobs_${name}_${suffix++}`
384
450
  }
385
- helperAliases.set(name, alias)
451
+ state.helperAliases.set(name, alias)
386
452
  return alias
387
453
  }
388
454
 
389
455
  /** Create a reference to a runtime helper in generated code, matching the injected import. */
390
- function helperRef(name: string): ts.Identifier {
391
- return ts.factory.createIdentifier(resolveHelperName(name))
456
+ function helperRef(state: CompileState, name: string): ts.Identifier {
457
+ return ts.factory.createIdentifier(resolveHelperName(state, name))
392
458
  }
393
459
 
394
- function createRuntimeImports(): ts.ImportDeclaration[] {
460
+ function createRuntimeImports(state: CompileState): ts.ImportDeclaration[] {
395
461
  const modules = new Map<string, ts.ImportSpecifier[]>()
396
- for (const [name, alias] of helperAliases) {
462
+ for (const [name, alias] of state.helperAliases) {
397
463
  const module = name === 'insertResourceBoundary' ? '@vobs/resource' : '@vobs/vobs'
398
464
  const imported = modules.get(module) ?? []
399
465
  imported.push(ts.factory.createImportSpecifier(
@@ -414,9 +480,9 @@ function createRuntimeImports(): ts.ImportDeclaration[] {
414
480
  ))
415
481
  }
416
482
 
417
- function rebuildImport(node: ts.ImportDeclaration): ts.ImportDeclaration {
483
+ function rebuildImport(state: CompileState, node: ts.ImportDeclaration): ts.ImportDeclaration {
418
484
  if (!ts.isStringLiteral(node.moduleSpecifier)) return node
419
- return tagStatement(ts.factory.createImportDeclaration(
485
+ return tagStatement(state, ts.factory.createImportDeclaration(
420
486
  node.modifiers,
421
487
  node.importClause,
422
488
  ts.factory.createStringLiteral(node.moduleSpecifier.text),
@@ -424,9 +490,9 @@ function rebuildImport(node: ts.ImportDeclaration): ts.ImportDeclaration {
424
490
  ), node)
425
491
  }
426
492
 
427
- function transformStatement(node: ts.Statement): ts.Statement {
493
+ function transformStatement(state: CompileState, node: ts.Statement): ts.Statement {
428
494
  if (ts.isFunctionDeclaration(node) && node.body) {
429
- return tagStatement(ts.factory.updateFunctionDeclaration(
495
+ return tagStatement(state, ts.factory.updateFunctionDeclaration(
430
496
  node,
431
497
  node.modifiers,
432
498
  node.asteriskToken,
@@ -434,48 +500,84 @@ function transformStatement(node: ts.Statement): ts.Statement {
434
500
  node.typeParameters,
435
501
  node.parameters,
436
502
  node.type,
437
- transformBlock(node.body)
503
+ transformBlock(state, node.body)
438
504
  ), node)
439
505
  }
440
506
 
441
- if (ts.isVariableStatement(node)) return transformVariableStatement(node)
507
+ if (ts.isVariableStatement(node)) return transformVariableStatement(state, node)
442
508
  if (ts.isExportAssignment(node) && containsJsx(node.expression)) {
443
- return tagStatement(ts.factory.updateExportAssignment(node, node.modifiers, transformEmbeddedExpression(node.expression)), node)
509
+ return tagStatement(state, ts.factory.updateExportAssignment(node, node.modifiers, transformEmbeddedExpression(state, node.expression)), node)
444
510
  }
445
511
  if (ts.isExpressionStatement(node) && containsJsx(node.expression)) {
446
- return tagStatement(ts.factory.updateExpressionStatement(node, transformEmbeddedExpression(node.expression)), node)
512
+ return tagStatement(state, ts.factory.updateExpressionStatement(node, transformEmbeddedExpression(state, node.expression)), node)
447
513
  }
448
514
  if (ts.isReturnStatement(node) && node.expression && containsJsx(node.expression)) {
449
- return tagStatement(ts.factory.updateReturnStatement(node, transformEmbeddedExpression(node.expression)), node)
515
+ return tagStatement(state, ts.factory.updateReturnStatement(node, transformEmbeddedExpression(state, node.expression)), node)
450
516
  }
451
517
  return node
452
518
  }
453
519
 
454
- function transformVariableStatement(node: ts.VariableStatement): ts.VariableStatement {
520
+ function transformVariableStatement(state: CompileState, node: ts.VariableStatement): ts.VariableStatement {
521
+ let changed = false
455
522
  const declarations = node.declarationList.declarations.map(declaration => {
456
523
  const initializer = declaration.initializer
457
- if (!initializer || !containsJsx(initializer)) return declaration
524
+ if (!initializer) return declaration
525
+
526
+ let nextInitializer = inferStateDebugName(state, declaration, initializer) ?? initializer
527
+ if (containsJsx(nextInitializer)) {
528
+ nextInitializer = transformEmbeddedExpression(state, nextInitializer)
529
+ }
530
+ if (nextInitializer === initializer) return declaration
458
531
 
532
+ changed = true
459
533
  return ts.factory.updateVariableDeclaration(
460
534
  declaration,
461
535
  declaration.name,
462
536
  declaration.exclamationToken,
463
537
  declaration.type,
464
- transformEmbeddedExpression(initializer)
538
+ nextInitializer
465
539
  )
466
540
  })
467
541
 
468
- if (declarations.every((declaration, index) => declaration === node.declarationList.declarations[index])) {
469
- return node
470
- }
542
+ if (!changed) return node
471
543
 
472
- return tagStatement(ts.factory.updateVariableStatement(
544
+ return tagStatement(state, ts.factory.updateVariableStatement(
473
545
  node,
474
546
  node.modifiers,
475
547
  ts.factory.updateVariableDeclarationList(node.declarationList, declarations)
476
548
  ), node)
477
549
  }
478
550
 
551
+ /**
552
+ * `const name = state(initial)` 在未显式传入 debugName 时从变量名推断:
553
+ * `const username = state('')` → `state('', 'username')`,使 DevTools 信号名称与源码命名一致。
554
+ * 仅当 `state` 确认来自 @vobs/reactivity / @vobs/vobs、未被本地声明遮蔽、
555
+ * 且调用只带一个参数时启用;其余形态保持原样。
556
+ */
557
+ function inferStateDebugName(
558
+ state: CompileState,
559
+ declaration: ts.VariableDeclaration,
560
+ initializer: ts.Expression
561
+ ): ts.Expression | undefined {
562
+ if (state.stateAliases.size === 0) return undefined
563
+ if (!ts.isIdentifier(declaration.name)) return undefined
564
+
565
+ let call = initializer
566
+ while (ts.isParenthesizedExpression(call) || ts.isAsExpression(call) || ts.isTypeAssertionExpression(call) || ts.isSatisfiesExpression(call)) {
567
+ call = call.expression
568
+ }
569
+ if (!ts.isCallExpression(call)) return undefined
570
+ const callee = call.expression
571
+ if (!ts.isIdentifier(callee) || !state.stateAliases.has(callee.text)) return undefined
572
+ if (state.localBindings.has(callee.text)) return undefined
573
+ if (call.arguments.length !== 1) return undefined
574
+
575
+ return ts.factory.createCallExpression(callee, call.typeArguments, [
576
+ ...call.arguments,
577
+ ts.factory.createStringLiteral(declaration.name.text)
578
+ ])
579
+ }
580
+
479
581
  function containsJsx(expression: ts.Expression): boolean {
480
582
  let found = false
481
583
  const visit = (node: ts.Node): void => {
@@ -489,14 +591,14 @@ function containsJsx(expression: ts.Expression): boolean {
489
591
  return found
490
592
  }
491
593
 
492
- function transformBlock(block: ts.Block): ts.Block {
594
+ function transformBlock(state: CompileState, block: ts.Block): ts.Block {
493
595
  const statements = block.statements.map(statement => {
494
- if (!ts.isReturnStatement(statement) || !statement.expression) return transformStatement(statement)
596
+ if (!ts.isReturnStatement(statement) || !statement.expression) return transformStatement(state, statement)
495
597
  const expression = ts.isParenthesizedExpression(statement.expression)
496
598
  ? statement.expression.expression
497
599
  : statement.expression
498
600
  return isJsxExpression(expression)
499
- ? tagStatement(ts.factory.updateReturnStatement(statement, transformJsxExpression(expression)), statement)
601
+ ? tagStatement(state, ts.factory.updateReturnStatement(statement, transformJsxExpression(state, expression)), statement)
500
602
  : statement
501
603
  })
502
604
  return ts.factory.updateBlock(block, statements)
@@ -506,61 +608,71 @@ function isJsxExpression(node: ts.Expression): node is ts.JsxElement | ts.JsxSel
506
608
  return ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)
507
609
  }
508
610
 
509
- function transformJsxExpression(node: ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment): ts.Expression {
510
- if (ts.isJsxFragment(node)) return transformFragment(node.children)
611
+ function transformJsxExpression(state: CompileState, node: ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment): ts.Expression {
612
+ if (ts.isJsxFragment(node)) return transformFragment(state, node.children)
511
613
  if (ts.isJsxElement(node)) {
512
- return transformElement(node, node.openingElement.tagName, node.openingElement.attributes, node.children)
614
+ return transformElement(state, node, node.openingElement.tagName, node.openingElement.attributes, node.children)
513
615
  }
514
- return transformElement(node, node.tagName, node.attributes, [])
616
+ return transformElement(state, node, node.tagName, node.attributes, [])
515
617
  }
516
618
 
517
619
  function transformElement(
620
+ state: CompileState,
518
621
  node: ts.JsxElement | ts.JsxSelfClosingElement,
519
622
  tagName: ts.JsxTagNameExpression,
520
623
  attributes: ts.JsxAttributes,
521
624
  children: readonly ts.JsxChild[]
522
625
  ): ts.Expression {
523
- if (isFragmentTag(tagName)) return transformFragment(children)
626
+ if (isFragmentTag(tagName)) return transformFragment(state, children)
524
627
  if (!ts.isIdentifier(tagName)) {
525
628
  // <Foo.Bar>、<svg:rect> 等形态此前会静默编译成无效 DOM 标签(createElement("Foo.Bar"))。
526
629
  // 报结构化诊断后按原路径继续,保证产物结构稳定;compile()/Vite 插件会因 error 诊断直接失败。
527
- reportUnsupportedTag(tagName)
630
+ reportUnsupportedTag(state, tagName)
528
631
  }
529
632
  if (ts.isIdentifier(tagName) && tagName.text === 'ResourceBoundary') {
530
- return transformResourceBoundary(node, attributes, children)
633
+ return transformResourceBoundary(state, node, attributes, children)
531
634
  }
532
635
  if (ts.isIdentifier(tagName) && tagName.text === 'AsyncBoundary') {
533
- return transformAsyncBoundary(node, attributes, children)
636
+ return transformAsyncBoundary(state, node, attributes, children)
534
637
  }
535
638
  if (ts.isIdentifier(tagName) && tagName.text === 'ErrorBoundary') {
536
- return transformErrorBoundary(node, attributes, children)
639
+ return transformErrorBoundary(state, node, attributes, children)
537
640
  }
538
641
  if (ts.isIdentifier(tagName) && tagName.text === 'Profiler') {
539
- return transformProfiler(node, attributes, children)
642
+ return transformProfiler(state, node, attributes, children)
540
643
  }
541
644
  if (ts.isIdentifier(tagName) && /^[A-Z]/.test(tagName.text)) {
645
+ const args: ts.Expression[] = [
646
+ ts.factory.createCallExpression(helperRef(state, 'resolveComponent'), undefined, [
647
+ tagName,
648
+ ts.factory.createStringLiteral(state.filename),
649
+ ts.factory.createStringLiteral(tagName.text)
650
+ ]),
651
+ createComponentProps(state, attributes, children)
652
+ ]
653
+ // 源码位置仅用于错误定位与 DevTools;生产构建可整体剔除(错误仍带组件名,定位走 source map)。
654
+ if (state.sourceLocation) args.push(createSourceLocation(tagName))
655
+ return ts.factory.createCallExpression(helperRef(state, 'createComponent'), undefined, args)
656
+ }
657
+
658
+ // 静态模板提升:完全静态的 DOM 子树(无事件/动态绑定/spread/property 属性)序列化为
659
+ // 模块级模板,运行时一次 cloneNode 替代 createElement + setStaticProps + 逐子插入。
660
+ if (isStaticElement(tagName, attributes, children)) {
542
661
  return ts.factory.createCallExpression(
543
- helperRef('createComponent'),
662
+ helperRef(state, 'cloneTemplate'),
544
663
  undefined,
545
- [
546
- ts.factory.createCallExpression(helperRef('resolveComponent'), undefined, [
547
- tagName,
548
- ts.factory.createStringLiteral(currentFilename),
549
- ts.factory.createStringLiteral(tagName.text)
550
- ]),
551
- createComponentProps(attributes, children),
552
- createSourceLocation(tagName)
553
- ]
664
+ [registerTemplate(state, serializeStaticHtml(node))]
554
665
  )
555
666
  }
556
667
 
557
668
  const elementName = tagName.getText()
558
- const elementId = nextIdentifier('_el')
669
+ const elementId = nextIdentifier(state, '_el')
559
670
  const statements: ts.Statement[] = [
560
671
  createConstStatement(
672
+ state,
561
673
  elementId,
562
674
  ts.factory.createCallExpression(
563
- helperRef('createElement'),
675
+ helperRef(state, 'createElement'),
564
676
  undefined,
565
677
  [ts.factory.createStringLiteral(elementName)]
566
678
  ),
@@ -568,9 +680,9 @@ function transformElement(
568
680
  )
569
681
  ]
570
682
 
571
- appendAttributes(statements, elementId, attributes)
572
- appendChildren(statements, elementId, children)
573
- statements.push(tagStatement(ts.factory.createReturnStatement(elementId), node))
683
+ appendAttributes(state, statements, elementId, attributes)
684
+ appendChildren(state, statements, elementId, children)
685
+ statements.push(tagStatement(state, ts.factory.createReturnStatement(elementId), node))
574
686
 
575
687
  return ts.factory.createCallExpression(
576
688
  ts.factory.createArrowFunction(
@@ -590,13 +702,13 @@ function isFragmentTag(tagName: ts.JsxTagNameExpression): boolean {
590
702
  return tagName.getText() === 'Fragment' || tagName.getText() === 'Vobs.Fragment'
591
703
  }
592
704
 
593
- function transformFragment(children: readonly ts.JsxChild[]): ts.Expression {
594
- const parent = nextIdentifier('_fragmentParent')
595
- const anchor = nextIdentifier('_fragmentAnchor')
705
+ function transformFragment(state: CompileState, children: readonly ts.JsxChild[]): ts.Expression {
706
+ const parent = nextIdentifier(state, '_fragmentParent')
707
+ const anchor = nextIdentifier(state, '_fragmentAnchor')
596
708
  const statements: ts.Statement[] = []
597
- appendChildren(statements, parent, children, anchor)
709
+ appendChildren(state, statements, parent, children, anchor)
598
710
  return ts.factory.createCallExpression(
599
- helperRef('createFragment'),
711
+ helperRef(state, 'createFragment'),
600
712
  undefined,
601
713
  [ts.factory.createArrowFunction(
602
714
  undefined,
@@ -613,6 +725,7 @@ function transformFragment(children: readonly ts.JsxChild[]): ts.Expression {
613
725
  }
614
726
 
615
727
  function transformResourceBoundary(
728
+ state: CompileState,
616
729
  node: ts.JsxElement | ts.JsxSelfClosingElement,
617
730
  attributes: ts.JsxAttributes,
618
731
  children: readonly ts.JsxChild[]
@@ -620,29 +733,31 @@ function transformResourceBoundary(
620
733
  const resource = getAttributeExpression(attributes, 'resource')
621
734
  if (!resource) throw new VobsError({ code: 'VOBS_C002', layer: 'compiler', message: 'ResourceBoundary 必须提供 resource 属性', fix: '为 ResourceBoundary 添加 resource={resource}。' })
622
735
  const options: ts.ObjectLiteralElementLike[] = [
623
- ts.factory.createPropertyAssignment('resource', transformEmbeddedExpression(resource)),
624
- ts.factory.createPropertyAssignment('children', createBoundaryFactory(children))
736
+ ts.factory.createPropertyAssignment('resource', transformEmbeddedExpression(state, resource)),
737
+ ts.factory.createPropertyAssignment('children', createBoundaryFactory(state, children))
625
738
  ]
626
- appendBoundaryOptionalProperty(options, attributes, 'loading')
627
- appendBoundaryOptionalProperty(options, attributes, 'empty')
628
- appendBoundaryOptionalProperty(options, attributes, 'fallback')
629
- return createBoundaryFragment(node, 'insertResourceBoundary', options)
739
+ appendBoundaryOptionalProperty(state, options, attributes, 'loading')
740
+ appendBoundaryOptionalProperty(state, options, attributes, 'empty')
741
+ appendBoundaryOptionalProperty(state, options, attributes, 'fallback')
742
+ return createBoundaryFragment(state, node, 'insertResourceBoundary', options)
630
743
  }
631
744
 
632
745
  function transformErrorBoundary(
746
+ state: CompileState,
633
747
  node: ts.JsxElement | ts.JsxSelfClosingElement,
634
748
  attributes: ts.JsxAttributes,
635
749
  children: readonly ts.JsxChild[]
636
750
  ): ts.Expression {
637
751
  const fallback = getAttributeExpression(attributes, 'fallback')
638
752
  if (!fallback) throw new VobsError({ code: 'VOBS_C002', layer: 'compiler', message: 'ErrorBoundary 必须提供 fallback 属性', fix: '为 ErrorBoundary 添加 fallback={(error, retry) => ...}。' })
639
- return createBoundaryFragment(node, 'insertErrorBoundary', [
640
- ts.factory.createPropertyAssignment('children', createBoundaryFactory(children)),
641
- ts.factory.createPropertyAssignment('fallback', transformEmbeddedExpression(fallback))
753
+ return createBoundaryFragment(state, node, 'insertErrorBoundary', [
754
+ ts.factory.createPropertyAssignment('children', createBoundaryFactory(state, children)),
755
+ ts.factory.createPropertyAssignment('fallback', transformEmbeddedExpression(state, fallback))
642
756
  ])
643
757
  }
644
758
 
645
759
  function transformAsyncBoundary(
760
+ state: CompileState,
646
761
  node: ts.JsxElement | ts.JsxSelfClosingElement,
647
762
  attributes: ts.JsxAttributes,
648
763
  children: readonly ts.JsxChild[]
@@ -650,17 +765,18 @@ function transformAsyncBoundary(
650
765
  const promise = getAttributeExpression(attributes, 'promise')
651
766
  if (!promise) throw new VobsError({ code: 'VOBS_C002', layer: 'compiler', message: 'AsyncBoundary 必须提供 promise 属性', fix: '为 AsyncBoundary 添加 promise={promise}。' })
652
767
  const options: ts.ObjectLiteralElementLike[] = [
653
- ts.factory.createPropertyAssignment('promise', transformEmbeddedExpression(promise)),
654
- ts.factory.createPropertyAssignment('children', createAsyncFactory(children))
768
+ ts.factory.createPropertyAssignment('promise', transformEmbeddedExpression(state, promise)),
769
+ ts.factory.createPropertyAssignment('children', createAsyncFactory(state, children))
655
770
  ]
656
- appendBoundaryOptionalProperty(options, attributes, 'loading')
657
- appendBoundaryOptionalProperty(options, attributes, 'fallback')
771
+ appendBoundaryOptionalProperty(state, options, attributes, 'loading')
772
+ appendBoundaryOptionalProperty(state, options, attributes, 'fallback')
658
773
  const resetKey = getAttributeExpression(attributes, 'resetKey')
659
774
  if (resetKey) options.push(ts.factory.createPropertyAssignment('resetKey', createGetter(resetKey)))
660
- return createBoundaryFragment(node, 'insertAsyncBoundary', options)
775
+ return createBoundaryFragment(state, node, 'insertAsyncBoundary', options)
661
776
  }
662
777
 
663
778
  function transformProfiler(
779
+ state: CompileState,
664
780
  node: ts.JsxElement | ts.JsxSelfClosingElement,
665
781
  attributes: ts.JsxAttributes,
666
782
  children: readonly ts.JsxChild[]
@@ -673,22 +789,23 @@ function transformProfiler(
673
789
  : null
674
790
  if (!id) throw new VobsError({ code: 'VOBS_C002', layer: 'compiler', message: 'Profiler 必须提供 id 属性', fix: '为 Profiler 添加 id="ComponentName"。' })
675
791
  const options: ts.ObjectLiteralElementLike[] = [
676
- ts.factory.createPropertyAssignment('id', transformEmbeddedExpression(id)),
677
- ts.factory.createPropertyAssignment('children', createBoundaryFactory(children))
792
+ ts.factory.createPropertyAssignment('id', transformEmbeddedExpression(state, id)),
793
+ ts.factory.createPropertyAssignment('children', createBoundaryFactory(state, children))
678
794
  ]
679
- appendBoundaryOptionalProperty(options, attributes, 'onRender')
680
- return createBoundaryFragment(node, 'insertProfiler', options)
795
+ appendBoundaryOptionalProperty(state, options, attributes, 'onRender')
796
+ return createBoundaryFragment(state, node, 'insertProfiler', options)
681
797
  }
682
798
 
683
799
  function createBoundaryFragment(
800
+ state: CompileState,
684
801
  node: ts.JsxElement | ts.JsxSelfClosingElement,
685
802
  helper: 'insertResourceBoundary' | 'insertErrorBoundary' | 'insertAsyncBoundary' | 'insertProfiler',
686
803
  options: readonly ts.ObjectLiteralElementLike[]
687
804
  ): ts.Expression {
688
- const parent = nextIdentifier('_boundaryParent')
689
- const anchor = nextIdentifier('_boundaryAnchor')
805
+ const parent = nextIdentifier(state, '_boundaryParent')
806
+ const anchor = nextIdentifier(state, '_boundaryAnchor')
690
807
  return ts.factory.createCallExpression(
691
- helperRef('createFragment'),
808
+ helperRef(state, 'createFragment'),
692
809
  undefined,
693
810
  [ts.factory.createArrowFunction(
694
811
  undefined,
@@ -699,7 +816,7 @@ function createBoundaryFragment(
699
816
  ],
700
817
  undefined,
701
818
  ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
702
- ts.factory.createBlock([callStatement(helper, [
819
+ ts.factory.createBlock([callStatement(state, helper, [
703
820
  parent,
704
821
  anchor,
705
822
  ts.factory.createObjectLiteralExpression(options, true)
@@ -708,8 +825,8 @@ function createBoundaryFragment(
708
825
  )
709
826
  }
710
827
 
711
- function createBoundaryFactory(children: readonly ts.JsxChild[]): ts.ArrowFunction {
712
- const content = transformFragment(children)
828
+ function createBoundaryFactory(state: CompileState, children: readonly ts.JsxChild[]): ts.ArrowFunction {
829
+ const content = transformFragment(state, children)
713
830
  return ts.factory.createArrowFunction(
714
831
  undefined,
715
832
  undefined,
@@ -720,29 +837,30 @@ function createBoundaryFactory(children: readonly ts.JsxChild[]): ts.ArrowFuncti
720
837
  )
721
838
  }
722
839
 
723
- function createAsyncFactory(children: readonly ts.JsxChild[]): ts.ArrowFunction {
840
+ function createAsyncFactory(state: CompileState, children: readonly ts.JsxChild[]): ts.ArrowFunction {
724
841
  const value = ts.factory.createIdentifier('value')
725
842
  const expressionChild = children.length === 1 && children[0].kind === ts.SyntaxKind.JsxExpression
726
843
  ? (children[0] as ts.JsxExpression).expression
727
844
  : undefined
728
845
  if (expressionChild && ts.isArrowFunction(expressionChild)) {
729
- const transformed = transformEmbeddedExpression(expressionChild)
846
+ const transformed = transformEmbeddedExpression(state, expressionChild)
730
847
  return transformed as ts.ArrowFunction
731
848
  }
732
- const content = transformFragment(children)
849
+ const content = transformFragment(state, children)
733
850
  return ts.factory.createArrowFunction(undefined, undefined, [
734
851
  ts.factory.createParameterDeclaration(undefined, undefined, value)
735
852
  ], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), content)
736
853
  }
737
854
 
738
855
  function appendBoundaryOptionalProperty(
856
+ state: CompileState,
739
857
  properties: ts.ObjectLiteralElementLike[],
740
858
  attributes: ts.JsxAttributes,
741
859
  name: string
742
860
  ): void {
743
861
  const expression = getAttributeExpression(attributes, name)
744
862
  if (expression) {
745
- const transformed = transformEmbeddedExpression(expression)
863
+ const transformed = transformEmbeddedExpression(state, expression)
746
864
  const value = isJsxExpression(unwrapExpression(expression))
747
865
  ? createGetter(transformed)
748
866
  : transformed
@@ -760,11 +878,11 @@ function getAttributeExpression(attributes: ts.JsxAttributes, name: string): ts.
760
878
  return null
761
879
  }
762
880
 
763
- function transformEmbeddedExpression(expression: ts.Expression): ts.Expression {
881
+ function transformEmbeddedExpression(state: CompileState, expression: ts.Expression): ts.Expression {
764
882
  const result = ts.transform(expression, [context => root => {
765
883
  const visit: ts.Visitor = node => {
766
884
  if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) {
767
- return transformJsxExpression(node)
885
+ return transformJsxExpression(state, node)
768
886
  }
769
887
  return ts.visitEachChild(node, visit, context)
770
888
  }
@@ -777,7 +895,125 @@ function transformEmbeddedExpression(expression: ts.Expression): ts.Expression {
777
895
  }
778
896
  }
779
897
 
898
+ /**
899
+ * 静态元素判定:DOM 标签 + 全部属性为字符串字面量或无值 + 全部子节点为文本或递归静态元素。
900
+ * 保守排除项(语义或序列化等价性无把握,走原路径):
901
+ * - property 属性(value/checked/disabled 等):HTML attribute 与 setProperty 初始语义存在差异;
902
+ * - 事件(on*)、ref、spread、key:本身是动态行为;
903
+ * - 嵌套组件/Fragment/Boundary:不是纯 DOM 子树。
904
+ */
905
+ function isStaticElement(
906
+ tagName: ts.JsxTagNameExpression,
907
+ attributes: ts.JsxAttributes,
908
+ children: readonly ts.JsxChild[]
909
+ ): boolean {
910
+ if (!ts.isIdentifier(tagName) || !/^[a-z]/.test(tagName.text)) return false
911
+ for (const attribute of attributes.properties) {
912
+ if (ts.isJsxSpreadAttribute(attribute)) return false
913
+ if (!ts.isJsxAttribute(attribute)) return false
914
+ const name = attribute.name.getText()
915
+ if (name === 'key' || name === 'ref' || name.startsWith('on')) return false
916
+ if (isPropertyAttribute(name)) return false
917
+ const initializer = attribute.initializer
918
+ if (initializer && !ts.isStringLiteral(initializer)) return false
919
+ }
920
+ for (const child of children) {
921
+ if (ts.isJsxText(child)) continue
922
+ if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) {
923
+ const nested = ts.isJsxElement(child)
924
+ ? { tagName: child.openingElement.tagName, attributes: child.openingElement.attributes, children: child.children }
925
+ : { tagName: child.tagName, attributes: child.attributes, children: [] as readonly ts.JsxChild[] }
926
+ if (!isStaticElement(nested.tagName, nested.attributes, nested.children)) return false
927
+ continue
928
+ }
929
+ return false
930
+ }
931
+ return true
932
+ }
933
+
934
+ /** Serialize a fully-static JSX element to HTML, preserving the compiler's text normalization. */
935
+ function serializeStaticHtml(node: ts.JsxElement | ts.JsxSelfClosingElement): string {
936
+ const { tagName, attributes, children } = ts.isJsxElement(node)
937
+ ? { tagName: node.openingElement.tagName, attributes: node.openingElement.attributes, children: node.children }
938
+ : { tagName: node.tagName, attributes: node.attributes, children: [] as readonly ts.JsxChild[] }
939
+ return serializeStaticElement(tagName, attributes, children)
940
+ }
941
+
942
+ function serializeStaticElement(
943
+ tagName: ts.JsxTagNameExpression,
944
+ attributes: ts.JsxAttributes,
945
+ children: readonly ts.JsxChild[]
946
+ ): string {
947
+ const name = tagName.getText()
948
+ let html = `<${name}`
949
+ for (const attribute of attributes.properties) {
950
+ if (!ts.isJsxAttribute(attribute)) continue
951
+ const attributeName = attribute.name.getText() === 'className' ? 'class' : attribute.name.getText()
952
+ const initializer = attribute.initializer
953
+ if (!initializer) {
954
+ html += ` ${attributeName}=""`
955
+ continue
956
+ }
957
+ if (ts.isStringLiteral(initializer)) {
958
+ html += ` ${attributeName}="${escapeHtmlAttribute(initializer.text)}"`
959
+ }
960
+ }
961
+ html += '>'
962
+
963
+ for (const child of children) {
964
+ if (ts.isJsxText(child)) {
965
+ // 与 appendChildren 的文本规范化保持一致,保证提升前后 DOM 文本逐字相同。
966
+ const text = child.text.replace(/\s+/g, ' ').trimStart()
967
+ if (text.trim()) html += escapeHtmlText(text)
968
+ continue
969
+ }
970
+ if (ts.isJsxElement(child)) {
971
+ html += serializeStaticElement(
972
+ child.openingElement.tagName,
973
+ child.openingElement.attributes,
974
+ child.children
975
+ )
976
+ continue
977
+ }
978
+ if (ts.isJsxSelfClosingElement(child)) {
979
+ html += serializeStaticElement(child.tagName, child.attributes, [])
980
+ }
981
+ }
982
+ html += `</${name}>`
983
+ return html
984
+ }
985
+
986
+ function escapeHtmlAttribute(value: string): string {
987
+ return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;')
988
+ }
989
+
990
+ function escapeHtmlText(value: string): string {
991
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
992
+ }
993
+
994
+ /** Register a hoisted template declaration; identical HTML shares one declaration. */
995
+ function registerTemplate(state: CompileState, html: string): ts.Expression {
996
+ const existing = state.templates.get(html)
997
+ if (existing) return existing
998
+ const identifier = nextIdentifier(state, '_tpl')
999
+ state.templates.set(html, identifier)
1000
+ return identifier
1001
+ }
1002
+
1003
+ function createTemplateDeclarations(state: CompileState): ts.Statement[] {
1004
+ return [...state.templates.entries()].map(([html, identifier]) =>
1005
+ ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
1006
+ ts.factory.createVariableDeclaration(identifier, undefined, undefined, ts.factory.createCallExpression(
1007
+ helperRef(state, 'createTemplate'),
1008
+ undefined,
1009
+ [ts.factory.createStringLiteral(html)]
1010
+ ))
1011
+ ], ts.NodeFlags.Const))
1012
+ )
1013
+ }
1014
+
780
1015
  function appendAttributes(
1016
+ state: CompileState,
781
1017
  statements: ts.Statement[],
782
1018
  element: ts.Identifier,
783
1019
  attributes: ts.JsxAttributes
@@ -786,7 +1022,7 @@ function appendAttributes(
786
1022
  const hasSpread = attributes.properties.some(attribute => ts.isJsxSpreadAttribute(attribute))
787
1023
  for (const attribute of attributes.properties) {
788
1024
  if (ts.isJsxSpreadAttribute(attribute)) {
789
- statements.push(callStatement('spreadProps', [element, transformEmbeddedExpression(attribute.expression)], attribute))
1025
+ statements.push(callStatement(state, 'spreadProps', [element, transformEmbeddedExpression(state, attribute.expression)], attribute))
790
1026
  continue
791
1027
  }
792
1028
  if (!ts.isJsxAttribute(attribute)) continue
@@ -795,14 +1031,14 @@ function appendAttributes(
795
1031
  if (name === 'ref') {
796
1032
  const initializer = attribute.initializer
797
1033
  if (initializer && ts.isJsxExpression(initializer) && initializer.expression) {
798
- statements.push(callStatement('setRef', [element, transformEmbeddedExpression(initializer.expression)], attribute))
1034
+ statements.push(callStatement(state, 'setRef', [element, transformEmbeddedExpression(state, initializer.expression)], attribute))
799
1035
  }
800
1036
  continue
801
1037
  }
802
1038
  const initializer = attribute.initializer
803
1039
 
804
1040
  if (name.startsWith('on') && initializer && ts.isJsxExpression(initializer) && initializer.expression) {
805
- statements.push(callStatement('addEventListener', [
1041
+ statements.push(callStatement(state, 'addEventListener', [
806
1042
  element,
807
1043
  ts.factory.createStringLiteral(name.slice(2).toLowerCase()),
808
1044
  initializer.expression
@@ -812,7 +1048,7 @@ function appendAttributes(
812
1048
 
813
1049
  if (!initializer) {
814
1050
  if (hasSpread) {
815
- statements.push(callStatement(isPropertyAttribute(name) ? 'setProperty' : 'setAttribute', [element, ts.factory.createStringLiteral(isPropertyAttribute(name) ? name : name === 'className' ? 'class' : name), isPropertyAttribute(name) ? ts.factory.createTrue() : ts.factory.createStringLiteral('')], attribute))
1051
+ statements.push(callStatement(state, isPropertyAttribute(name) ? 'setProperty' : 'setAttribute', [element, ts.factory.createStringLiteral(isPropertyAttribute(name) ? name : name === 'className' ? 'class' : name), isPropertyAttribute(name) ? ts.factory.createTrue() : ts.factory.createStringLiteral('')], attribute))
816
1052
  continue
817
1053
  }
818
1054
  if (isPropertyAttribute(name)) staticProps.push(createStaticProperty(name, ts.factory.createTrue()))
@@ -821,7 +1057,7 @@ function appendAttributes(
821
1057
  }
822
1058
  if (ts.isStringLiteral(initializer)) {
823
1059
  if (hasSpread) {
824
- statements.push(callStatement(isPropertyAttribute(name) ? 'setProperty' : 'setAttribute', [element, ts.factory.createStringLiteral(isPropertyAttribute(name) ? name : name === 'className' ? 'class' : name), ts.factory.createStringLiteral(initializer.text)], attribute))
1060
+ statements.push(callStatement(state, isPropertyAttribute(name) ? 'setProperty' : 'setAttribute', [element, ts.factory.createStringLiteral(isPropertyAttribute(name) ? name : name === 'className' ? 'class' : name), ts.factory.createStringLiteral(initializer.text)], attribute))
825
1061
  continue
826
1062
  }
827
1063
  staticProps.push(createStaticProperty(isPropertyAttribute(name) ? name : name === 'className' ? 'class' : name,
@@ -832,14 +1068,14 @@ function appendAttributes(
832
1068
  const attributeName = name === 'className' ? 'class' : name
833
1069
  const propertyAttribute = isPropertyAttribute(name)
834
1070
  if (ts.isJsxExpression(initializer) && initializer.expression) {
835
- statements.push(callStatement(propertyAttribute ? 'bindProperty' : 'bindAttribute', [
1071
+ statements.push(callStatement(state, propertyAttribute ? 'bindProperty' : 'bindAttribute', [
836
1072
  element,
837
1073
  ts.factory.createStringLiteral(propertyAttribute ? name : attributeName),
838
1074
  createGetter(initializer.expression)
839
1075
  ], attribute))
840
1076
  }
841
1077
  }
842
- if (staticProps.length) statements.splice(1, 0, callStatement('setStaticProps', [
1078
+ if (staticProps.length) statements.splice(1, 0, callStatement(state, 'setStaticProps', [
843
1079
  element,
844
1080
  ts.factory.createObjectLiteralExpression(staticProps, true)
845
1081
  ], attributes))
@@ -856,6 +1092,7 @@ function isPropertyAttribute(name: string): boolean {
856
1092
  }
857
1093
 
858
1094
  function appendChildren(
1095
+ state: CompileState,
859
1096
  statements: ts.Statement[],
860
1097
  element: ts.Identifier,
861
1098
  children: readonly ts.JsxChild[],
@@ -865,9 +1102,9 @@ function appendChildren(
865
1102
  if (ts.isJsxText(child)) {
866
1103
  const text = child.text.replace(/\s+/g, ' ').trimStart()
867
1104
  if (text.trim()) {
868
- statements.push(callStatement('insertBefore', [
1105
+ statements.push(callStatement(state, 'insertBefore', [
869
1106
  element,
870
- ts.factory.createCallExpression(helperRef('createText'), undefined, [
1107
+ ts.factory.createCallExpression(helperRef(state, 'createText'), undefined, [
871
1108
  ts.factory.createStringLiteral(text)
872
1109
  ]),
873
1110
  anchor
@@ -877,9 +1114,9 @@ function appendChildren(
877
1114
  }
878
1115
 
879
1116
  if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {
880
- statements.push(callStatement('insertBefore', [
1117
+ statements.push(callStatement(state, 'insertBefore', [
881
1118
  element,
882
- transformJsxExpression(child),
1119
+ transformJsxExpression(state, child),
883
1120
  anchor
884
1121
  ], child))
885
1122
  continue
@@ -888,30 +1125,31 @@ function appendChildren(
888
1125
  if (child.kind === ts.SyntaxKind.JsxExpression) {
889
1126
  const expression = (child as ts.JsxExpression).expression
890
1127
  if (!expression) continue
891
- const list = transformListExpression(element, expression, anchor)
1128
+ const list = transformListExpression(state, element, expression, anchor)
892
1129
  if (list) {
893
- statements.push(callStatement('insertList', list, child))
1130
+ statements.push(callStatement(state, 'insertList', list, child))
894
1131
  continue
895
1132
  }
896
- const dynamic = transformDynamicExpression(expression)
1133
+ const dynamic = transformDynamicExpression(state, expression)
897
1134
  if (dynamic) {
898
- statements.push(callStatement('insertDynamic', [element, anchor, dynamic], child))
1135
+ statements.push(callStatement(state, 'insertDynamic', [element, anchor, dynamic], child))
899
1136
  continue
900
1137
  }
901
1138
  if (!containsJsx(expression) && !ts.isIdentifier(expression)) {
902
- const textId = nextIdentifier('_text')
903
- statements.push(createConstStatement(textId, ts.factory.createCallExpression(helperRef('createText'), undefined, [ts.factory.createStringLiteral('')]), child))
904
- statements.push(callStatement('insertBefore', [element, textId, anchor], child))
905
- statements.push(callStatement('bindText', [textId, createGetter(expression)], child))
1139
+ const textId = nextIdentifier(state, '_text')
1140
+ statements.push(createConstStatement(state, textId, ts.factory.createCallExpression(helperRef(state, 'createText'), undefined, [ts.factory.createStringLiteral('')]), child))
1141
+ statements.push(callStatement(state, 'insertBefore', [element, textId, anchor], child))
1142
+ statements.push(callStatement(state, 'bindText', [textId, createGetter(expression)], child))
906
1143
  continue
907
1144
  }
908
- const value = transformEmbeddedExpression(expression)
909
- statements.push(callStatement('insertDynamicValue', [element, anchor, createGetter(value)], child))
1145
+ const value = transformEmbeddedExpression(state, expression)
1146
+ statements.push(callStatement(state, 'insertDynamicValue', [element, anchor, createGetter(value)], child))
910
1147
  }
911
1148
  }
912
1149
  }
913
1150
 
914
1151
  function createComponentProps(
1152
+ state: CompileState,
915
1153
  attributes: ts.JsxAttributes,
916
1154
  children: readonly ts.JsxChild[]
917
1155
  ): ts.ObjectLiteralExpression {
@@ -931,11 +1169,11 @@ function createComponentProps(
931
1169
  } else if (ts.isStringLiteral(initializer)) {
932
1170
  properties.push(ts.factory.createPropertyAssignment(name, ts.factory.createStringLiteral(initializer.text)))
933
1171
  } else if (ts.isJsxExpression(initializer) && initializer.expression) {
934
- properties.push(createGetterProperty(name, transformEmbeddedExpression(initializer.expression)))
1172
+ properties.push(createGetterProperty(name, transformEmbeddedExpression(state, initializer.expression)))
935
1173
  }
936
1174
  }
937
1175
 
938
- const childExpressions = children.flatMap(childToComponentExpression)
1176
+ const childExpressions = children.flatMap(child => childToComponentExpression(state, child))
939
1177
  if (childExpressions.length === 1) {
940
1178
  properties.push(createGetterProperty('children', childExpressions[0]))
941
1179
  } else if (childExpressions.length > 1) {
@@ -955,22 +1193,22 @@ function createSourceLocation(node: ts.Node): ts.ObjectLiteralExpression {
955
1193
  ], true)
956
1194
  }
957
1195
 
958
- function transformDynamicExpression(expression: ts.Expression): ts.ArrowFunction | null {
1196
+ function transformDynamicExpression(state: CompileState, expression: ts.Expression): ts.ArrowFunction | null {
959
1197
  if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
960
1198
  const right = unwrapExpression(expression.right)
961
1199
  if (!isJsxExpression(right)) return null
962
1200
  return createGetter(ts.factory.createConditionalExpression(
963
1201
  expression.left,
964
1202
  ts.factory.createToken(ts.SyntaxKind.QuestionToken),
965
- transformJsxExpression(right),
1203
+ transformJsxExpression(state, right),
966
1204
  ts.factory.createToken(ts.SyntaxKind.ColonToken),
967
1205
  ts.factory.createNull()
968
1206
  ))
969
1207
  }
970
1208
 
971
1209
  if (ts.isConditionalExpression(expression)) {
972
- const whenTrue = transformDynamicBranch(expression.whenTrue)
973
- const whenFalse = transformDynamicBranch(expression.whenFalse)
1210
+ const whenTrue = transformDynamicBranch(state, expression.whenTrue)
1211
+ const whenFalse = transformDynamicBranch(state, expression.whenFalse)
974
1212
  if (!whenTrue && !whenFalse) return null
975
1213
  return createGetter(ts.factory.createConditionalExpression(
976
1214
  expression.condition,
@@ -984,14 +1222,15 @@ function transformDynamicExpression(expression: ts.Expression): ts.ArrowFunction
984
1222
  return null
985
1223
  }
986
1224
 
987
- function transformDynamicBranch(expression: ts.Expression): ts.Expression | null {
1225
+ function transformDynamicBranch(state: CompileState, expression: ts.Expression): ts.Expression | null {
988
1226
  const branch = unwrapExpression(expression)
989
- if (isJsxExpression(branch)) return transformJsxExpression(branch)
1227
+ if (isJsxExpression(branch)) return transformJsxExpression(state, branch)
990
1228
  if (branch.kind === ts.SyntaxKind.NullKeyword || branch.kind === ts.SyntaxKind.FalseKeyword) return branch
991
1229
  return null
992
1230
  }
993
1231
 
994
1232
  function transformListExpression(
1233
+ state: CompileState,
995
1234
  parent: ts.Identifier,
996
1235
  expression: ts.Expression,
997
1236
  anchor: ts.Expression
@@ -1005,7 +1244,7 @@ function transformListExpression(
1005
1244
  if (!isJsxExpression(body) || ts.isJsxFragment(body)) return null
1006
1245
 
1007
1246
  const key = findKeyExpression(body)
1008
- const renderItem = transformListCallback(callback, transformJsxExpression(body))
1247
+ const renderItem = transformListCallback(callback, transformJsxExpression(state, body))
1009
1248
  const args: ts.Expression[] = [
1010
1249
  parent,
1011
1250
  anchor,
@@ -1073,17 +1312,17 @@ function unwrapExpression(node: ts.Expression | ts.ConciseBody): ts.Expression {
1073
1312
  return ts.isParenthesizedExpression(node) ? node.expression : node as ts.Expression
1074
1313
  }
1075
1314
 
1076
- function childToComponentExpression(child: ts.JsxChild): ts.Expression[] {
1315
+ function childToComponentExpression(state: CompileState, child: ts.JsxChild): ts.Expression[] {
1077
1316
  if (ts.isJsxText(child)) {
1078
1317
  const text = child.text.replace(/\s+/g, ' ').trim()
1079
1318
  return text ? [ts.factory.createStringLiteral(text)] : []
1080
1319
  }
1081
1320
  if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {
1082
- return [transformJsxExpression(child)]
1321
+ return [transformJsxExpression(state, child)]
1083
1322
  }
1084
1323
  if (child.kind === ts.SyntaxKind.JsxExpression) {
1085
1324
  const expression = (child as ts.JsxExpression).expression
1086
- return expression ? [transformEmbeddedExpression(expression)] : []
1325
+ return expression ? [transformEmbeddedExpression(state, expression)] : []
1087
1326
  }
1088
1327
  return []
1089
1328
  }
@@ -1115,42 +1354,42 @@ function createGetterProperty(name: string | ts.PropertyName, expression: ts.Exp
1115
1354
  )
1116
1355
  }
1117
1356
 
1118
- function callStatement(name: string, args: ts.Expression[], source?: ts.Node): ts.ExpressionStatement {
1357
+ function callStatement(state: CompileState, name: string, args: ts.Expression[], source?: ts.Node): ts.ExpressionStatement {
1119
1358
  const statement = ts.factory.createExpressionStatement(
1120
- ts.factory.createCallExpression(helperRef(name), undefined, args)
1359
+ ts.factory.createCallExpression(helperRef(state, name), undefined, args)
1121
1360
  )
1122
- return source ? tagStatement(statement, source) : statement
1361
+ return source ? tagStatement(state, statement, source) : statement
1123
1362
  }
1124
1363
 
1125
- function createConstStatement(name: ts.Identifier, initializer: ts.Expression, source?: ts.Node): ts.VariableStatement {
1364
+ function createConstStatement(state: CompileState, name: ts.Identifier, initializer: ts.Expression, source?: ts.Node): ts.VariableStatement {
1126
1365
  const statement = ts.factory.createVariableStatement(
1127
1366
  undefined,
1128
1367
  ts.factory.createVariableDeclarationList([
1129
1368
  ts.factory.createVariableDeclaration(name, undefined, undefined, initializer)
1130
1369
  ], ts.NodeFlags.Const)
1131
1370
  )
1132
- return source ? tagStatement(statement, source) : statement
1371
+ return source ? tagStatement(state, statement, source) : statement
1133
1372
  }
1134
1373
 
1135
1374
  /** Record where an emitted statement originated from, for source map generation. */
1136
- function tagStatement<T extends ts.Statement>(statement: T, source: ts.Node): T {
1137
- const position = positionOfNode(source)
1138
- if (position) statementSources.set(statement, position)
1375
+ function tagStatement<T extends ts.Statement>(state: CompileState, statement: T, source: ts.Node): T {
1376
+ const position = positionOfNode(state, source)
1377
+ if (position) state.statementSources.set(statement, position)
1139
1378
  return statement
1140
1379
  }
1141
1380
 
1142
- function positionOfNode(node: ts.Node): SourcePosition | null {
1381
+ function positionOfNode(state: CompileState, node: ts.Node): SourcePosition | null {
1143
1382
  // ts.transform 产生的节点副本可能丢失 sourceFile 引用,回退到当前编译的源文件。
1144
- const sourceFile = node.getSourceFile() ?? currentSourceFile
1383
+ const sourceFile = node.getSourceFile() ?? state.sourceFile
1145
1384
  if (!sourceFile || node.pos < 0) return null
1146
1385
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
1147
1386
  return { line, column: character }
1148
1387
  }
1149
1388
 
1150
- function nextIdentifier(prefix: string): ts.Identifier {
1389
+ function nextIdentifier(state: CompileState, prefix: string): ts.Identifier {
1151
1390
  let name: string
1152
1391
  do {
1153
- name = `${prefix}${generatedId++}`
1154
- } while (takenNames.has(name))
1392
+ name = `${prefix}${state.generatedId++}`
1393
+ } while (state.takenNames.has(name))
1155
1394
  return ts.factory.createIdentifier(name)
1156
1395
  }