@vobs/compiler 1.0.0 → 1.1.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/README.md CHANGED
@@ -45,6 +45,8 @@ Plugins can analyze or rewrite the compile tree; `context.helperRef` guarantees
45
45
 
46
46
  Unsupported JSX shapes fail with structured errors — code `VOBS_Cxxx`, source location, code frame, and a fix hint — instead of silently emitting broken output (e.g. member-expression tags like `<Foo.Bar>` used as a DOM tag).
47
47
 
48
+ Component calls carry a `{ file, line, column }` source location for error reporting and DevTools. Pass `sourceLocation: false` to omit it in production builds (the Vite plugin does this automatically for `vite build`); errors still carry component names and positions resolve via source maps.
49
+
48
50
  ## API
49
51
 
50
52
  | Signature | Description |
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "LICENSE"
7
7
  ],
8
8
  "name": "@vobs/compiler",
9
- "version": "1.0.0",
9
+ "version": "1.1.0",
10
10
  "type": "module",
11
11
  "main": "src/index.ts",
12
12
  "types": "src/index.ts",
@@ -16,6 +16,6 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "typescript": "^5.9.3",
19
- "@vobs/runtime": "1.0.0"
19
+ "@vobs/runtime": "1.1.0"
20
20
  }
21
21
  }
@@ -24,10 +24,10 @@ describe('compiler', () => {
24
24
  })
25
25
 
26
26
  it('编译 JSX 为 DOM 渲染调用', () => {
27
- const code = `const el = <div>hello</div>`
28
-
27
+ const code = `const el = <div>hello {name.value}</div>`
28
+
29
29
  const result = compile(code)
30
-
30
+
31
31
  expect(result).toContain('createElement')
32
32
  expect(result).toContain('createText')
33
33
  expect(result).toContain('insertBefore')
@@ -37,12 +37,12 @@ describe('compiler', () => {
37
37
  const code = `
38
38
  function Counter() {
39
39
  const count = state(0)
40
- return <div>hello</div>
40
+ return <div>hello {count.value}</div>
41
41
  }
42
42
  `
43
-
43
+
44
44
  const result = compile(code)
45
-
45
+
46
46
  expect(result).toContain('function Counter')
47
47
  expect(result).toContain('createElement')
48
48
  })
@@ -51,13 +51,50 @@ describe('compiler', () => {
51
51
  const code = `
52
52
  const el = <button onClick={() => { console.log('clicked') }}>点击</button>
53
53
  `
54
-
54
+
55
55
  const result = compile(code)
56
-
56
+
57
57
  expect(result).toContain('addEventListener')
58
58
  expect(result).toContain('click')
59
59
  })
60
60
 
61
+ it('重入安全:插件在编译过程中调用 compile() 不污染外层编译状态', () => {
62
+ // 外层源码的变量名会命中内层片段用到的 helper 名,验证 takenNames/helperAliases
63
+ // 等状态在外层编译全程保持独立(此前为模块级变量,嵌套编译会互相覆盖)。
64
+ const nestedPlugin: CompilerPlugin = {
65
+ name: 'nested-compile',
66
+ analyze() {
67
+ compile(`const el = <span>inner</span>`, { filename: 'inner.tsx' })
68
+ }
69
+ }
70
+ const code = `
71
+ const createElement = () => null
72
+ const el = <div>outer {name.value}</div>
73
+ `
74
+
75
+ const result = compileWithSourceMap(code, {
76
+ filename: 'outer.tsx',
77
+ plugins: [nestedPlugin]
78
+ })
79
+
80
+ // 外层的局部绑定 createElement 仍在,运行时 helper 注入为别名导入
81
+ expect(result.code).toContain('const createElement = () => null')
82
+ expect(result.code).toContain('_vobs_createElement')
83
+ expect(result.code).toContain('outer')
84
+ expect(result.diagnostics).toEqual([])
85
+ })
86
+
87
+ it('重入安全:连续多次编译各自独立,计数器与别名不跨编译泄漏', () => {
88
+ const first = compileWithSourceMap(`const a = <div className="x">{a.value}</div>`, { filename: 'a.tsx' })
89
+ const second = compileWithSourceMap(`const b = <div className="y">{b.value}</div>`, { filename: 'b.tsx' })
90
+
91
+ // 临时变量计数器每次编译从 0 开始
92
+ expect(first.code).toContain('_el0')
93
+ expect(second.code).toContain('_el0')
94
+ // 第二次编译不带第一次残留的诊断
95
+ expect(second.diagnostics).toEqual([])
96
+ })
97
+
61
98
  it('将静态属性合并为一次 setStaticProps 调用', () => {
62
99
  const result = compile(`const el = <input className="field" disabled id="name" />`)
63
100
  expect(result).toContain('setStaticProps')
@@ -168,9 +205,11 @@ describe('compiler', () => {
168
205
  it('只导入生成代码实际使用的运行时 helper', () => {
169
206
  const result = compile(`const el = <div>hello</div>`)
170
207
 
171
- expect(result).toContain('createElement')
172
- expect(result).toContain('createText')
173
- expect(result).toContain('insertBefore')
208
+ // 完全静态的子树提升为模板:只需 createTemplate + cloneTemplate
209
+ expect(result).toContain('createTemplate')
210
+ expect(result).toContain('cloneTemplate')
211
+ expect(result).not.toContain('createElement')
212
+ expect(result).not.toContain('insertBefore')
174
213
  expect(result).not.toContain('bindAttribute')
175
214
  expect(result).not.toContain('bindText')
176
215
  expect(result).not.toContain('insertDynamic')
@@ -240,7 +279,7 @@ describe('compiler', () => {
240
279
  const result = compile(`
241
280
  import { createElement } from './shim'
242
281
  export const tag = createElement('span')
243
- const el = <div>hello</div>
282
+ const el = <div>hello {name.value}</div>
244
283
  `)
245
284
 
246
285
  // 用户导入与调用保持不变,编译器 helper 走别名,不再产生重复声明
@@ -255,12 +294,12 @@ describe('compiler', () => {
255
294
  it('源文件本地声明与 helper 同名时注入别名 import', () => {
256
295
  const result = compile(`
257
296
  const createText = (value: string) => value
258
- const el = <div>hello</div>
297
+ const el = <div>hello {name.value}</div>
259
298
  `)
260
299
 
261
300
  expect(result).toContain('createText as _vobs_createText')
262
301
  expect(result).toContain('from "@vobs/vobs"')
263
- expect(result).toContain('insertBefore(_el0, _vobs_createText("hello")')
302
+ expect(result).toContain('insertBefore(_el0, _vobs_createText("hello ")')
264
303
  // 用户本地声明不受影响
265
304
  expect(result).toContain('const createText = (value: string) => value')
266
305
  })
@@ -283,7 +322,7 @@ describe('compiler', () => {
283
322
  it('同名绑定触发别名时保留别名一致性(同一 helper 只注入一次)', () => {
284
323
  const result = compile(`
285
324
  const createElement = String
286
- const el = <div><span>one</span></div>
325
+ const el = <div><span>one {name.value}</span></div>
287
326
  `)
288
327
 
289
328
  expect(result.match(/_vobs_createElement/g)?.length).toBeGreaterThanOrEqual(2)
@@ -294,7 +333,7 @@ describe('compiler', () => {
294
333
  it('生成的临时变量避开用户已声明的名称', () => {
295
334
  const result = compile(`
296
335
  const _el0 = 'reserved'
297
- const el = <div>hello</div>
336
+ const el = <div>hello {name.value}</div>
298
337
  `)
299
338
 
300
339
  // _el0 被用户占用,编译产物必须改用下一个可用名称
@@ -335,6 +374,17 @@ describe('compiler', () => {
335
374
  expect(result).toContain('line: 2')
336
375
  })
337
376
 
377
+ it('sourceLocation: false 剔除组件源码位置', () => {
378
+ const result = compile(`
379
+ const el = <Editor><input disabled={locked.value} value={text.value} /></Editor>
380
+ `, { filename: 'src/editor.tsx', sourceLocation: false })
381
+
382
+ expect(result).toContain('createComponent')
383
+ expect(result).not.toContain('file:')
384
+ expect(result).not.toContain('line:')
385
+ expect(result).not.toContain('column:')
386
+ })
387
+
338
388
  it('生成包含原始内容的 Source Map', () => {
339
389
  const result = compileWithSourceMap(`const el = <div>hello</div>`, { filename: 'src/App.tsx' })
340
390
 
package/src/compile.ts CHANGED
@@ -10,16 +10,32 @@ 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
+ /** 编译器自身产出的诊断(如不支持的 JSX 形态),与 TypeScript 解析诊断合并返回。 */
37
+ diagnostics: CompilerDiagnostic[]
38
+ }
23
39
 
24
40
  interface SourcePosition {
25
41
  readonly line: number
@@ -62,10 +78,7 @@ export function compile(code: string, options: CompileOptions = {}): string {
62
78
  }
63
79
 
64
80
  export function compileWithSourceMap(code: string, options: CompileOptions = {}): CompileResult {
65
- generatedId = 0
66
81
  const filename = options.filename ?? 'component.tsx'
67
- currentFilename = filename
68
- statementSources = new WeakMap()
69
82
  let sourceFile = ts.createSourceFile(
70
83
  filename,
71
84
  code,
@@ -73,10 +86,17 @@ export function compileWithSourceMap(code: string, options: CompileOptions = {})
73
86
  true,
74
87
  ts.ScriptKind.TSX
75
88
  )
76
- currentSourceFile = sourceFile
77
- takenNames = collectDeclaredNames(sourceFile)
78
- helperAliases = new Map()
79
- compileDiagnostics = []
89
+ const state: CompileState = {
90
+ generatedId: 0,
91
+ filename,
92
+ sourceFile,
93
+ statementSources: new WeakMap(),
94
+ takenNames: collectDeclaredNames(sourceFile),
95
+ helperAliases: new Map(),
96
+ templates: new Map(),
97
+ sourceLocation: options.sourceLocation ?? true,
98
+ diagnostics: []
99
+ }
80
100
  const cleanFilename = filename.split(/[?#]/u, 1)[0] || filename
81
101
  const diagnostics = ts.transpileModule(code, {
82
102
  // Vite appends query strings (for example `?direct`) to module IDs;
@@ -92,9 +112,9 @@ export function compileWithSourceMap(code: string, options: CompileOptions = {})
92
112
  filename,
93
113
  factory: ts.factory,
94
114
  addRuntimeImport(name: string): void {
95
- resolveHelperName(name)
115
+ resolveHelperName(state, name)
96
116
  },
97
- helperRef
117
+ helperRef: (name: string) => helperRef(state, name)
98
118
  }
99
119
 
100
120
  for (const plugin of plugins) plugin.analyze?.(sourceFile, context)
@@ -104,15 +124,19 @@ export function compileWithSourceMap(code: string, options: CompileOptions = {})
104
124
  for (const plugin of plugins) sourceFile = transformPluginNodes(sourceFile, plugin, context)
105
125
 
106
126
  const statements = sourceFile.statements.map(statement =>
107
- ts.isImportDeclaration(statement) ? rebuildImport(statement) : transformStatement(statement)
127
+ ts.isImportDeclaration(statement) ? rebuildImport(state, statement) : transformStatement(state, statement)
108
128
  )
109
- const resultFile = ts.factory.updateSourceFile(sourceFile, [...createRuntimeImports(), ...statements])
129
+ const resultFile = ts.factory.updateSourceFile(sourceFile, [
130
+ ...createRuntimeImports(state),
131
+ ...createTemplateDeclarations(state),
132
+ ...statements
133
+ ])
110
134
 
111
135
  const generated = ts.createPrinter().printFile(resultFile)
112
136
  return {
113
137
  code: generated,
114
- map: buildSourceMap(filename, code, generated, resultFile),
115
- diagnostics: [...diagnostics, ...compileDiagnostics]
138
+ map: buildSourceMap(state, filename, code, generated, resultFile),
139
+ diagnostics: [...diagnostics, ...state.diagnostics]
116
140
  }
117
141
  }
118
142
 
@@ -153,17 +177,17 @@ function buildCodeFrame(
153
177
  * 不支持的 JSX 标签形态(成员表达式 `<Foo.Bar>`、命名空间 `<svg:rect>` 等)。
154
178
  * 诊断以 error 级返回,compile() 与 Vite 插件会直接失败,不再静默产出无效 DOM 标签。
155
179
  */
156
- function reportUnsupportedTag(tagName: ts.JsxTagNameExpression): void {
157
- const sourceFile = tagName.getSourceFile() ?? currentSourceFile
180
+ function reportUnsupportedTag(state: CompileState, tagName: ts.JsxTagNameExpression): void {
181
+ const sourceFile = tagName.getSourceFile() ?? state.sourceFile
158
182
  if (!sourceFile) return
159
183
  const label = tagName.getText()
160
184
  const kindNote = tagName.kind === ts.SyntaxKind.JsxNamespacedName ? '(JSX 命名空间标签)' : ''
161
185
  const { line, column, codeFrame } = buildCodeFrame(sourceFile, tagName.getStart(sourceFile), tagName.getWidth(sourceFile))
162
- compileDiagnostics.push({
186
+ state.diagnostics.push({
163
187
  code: 'VOBS_C101',
164
188
  severity: 'error',
165
189
  message: `不支持的 JSX 标签形态:<${label}>${kindNote}。组件必须是大写开头的标识符,DOM 元素必须是小写标签名。`,
166
- location: { file: currentFilename, line, column },
190
+ location: { file: state.filename, line, column },
167
191
  codeFrame,
168
192
  fix: `把 <${label}> 改为 <Component /> 形式的组件或小写 DOM 标签;Fragment 请使用 <Fragment> 或 <>...</>。`
169
193
  })
@@ -183,6 +207,7 @@ interface MappingSegment {
183
207
  * statement it was produced from.
184
208
  */
185
209
  function buildSourceMap(
210
+ state: CompileState,
186
211
  filename: string,
187
212
  source: string,
188
213
  generated: string,
@@ -190,7 +215,7 @@ function buildSourceMap(
190
215
  ): import('./plugin').VobsSourceMap {
191
216
  const reparsed = ts.createSourceFile(filename, generated, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
192
217
  const segments: MappingSegment[] = []
193
- walkPairedTrees(resultFile, reparsed, reparsed, segments)
218
+ walkPairedTrees(state, resultFile, reparsed, reparsed, segments)
194
219
  segments.sort((a, b) => a.genLine - b.genLine || a.genCol - b.genCol)
195
220
  return {
196
221
  version: 3,
@@ -215,6 +240,7 @@ function statementLists(node: ts.Node): readonly ts.Statement[] | null {
215
240
  * any divergence (length mismatch) simply abandons that subtree.
216
241
  */
217
242
  function walkPairedTrees(
243
+ state: CompileState,
218
244
  original: ts.Node,
219
245
  generated: ts.Node,
220
246
  reparsed: ts.SourceFile,
@@ -227,8 +253,8 @@ function walkPairedTrees(
227
253
  for (let index = 0; index < originalStatements.length; index++) {
228
254
  const originalStatement = originalStatements[index]
229
255
  const generatedStatement = generatedStatements[index]
230
- recordSegment(originalStatement, generatedStatement, reparsed, segments)
231
- walkPairedTrees(originalStatement, generatedStatement, reparsed, segments)
256
+ recordSegment(state, originalStatement, generatedStatement, reparsed, segments)
257
+ walkPairedTrees(state, originalStatement, generatedStatement, reparsed, segments)
232
258
  }
233
259
  return
234
260
  }
@@ -239,17 +265,18 @@ function walkPairedTrees(
239
265
  ts.forEachChild(generated, node => { generatedChildren.push(node) })
240
266
  if (originalChildren.length !== generatedChildren.length) return
241
267
  for (let index = 0; index < originalChildren.length; index++) {
242
- walkPairedTrees(originalChildren[index], generatedChildren[index], reparsed, segments)
268
+ walkPairedTrees(state, originalChildren[index], generatedChildren[index], reparsed, segments)
243
269
  }
244
270
  }
245
271
 
246
272
  function recordSegment(
273
+ state: CompileState,
247
274
  originalStatement: ts.Statement,
248
275
  generatedStatement: ts.Statement,
249
276
  reparsed: ts.SourceFile,
250
277
  segments: MappingSegment[]
251
278
  ): void {
252
- const source = statementSources.get(originalStatement) ?? positionOfOriginalStatement(originalStatement)
279
+ const source = state.statementSources.get(originalStatement) ?? positionOfOriginalStatement(state, originalStatement)
253
280
  if (!source) return
254
281
  const position = reparsed.getLineAndCharacterOfPosition(generatedStatement.getStart(reparsed))
255
282
  segments.push({
@@ -260,10 +287,10 @@ function recordSegment(
260
287
  })
261
288
  }
262
289
 
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)
290
+ function positionOfOriginalStatement(state: CompileState, statement: ts.Statement): SourcePosition | null {
291
+ if (statement.pos < 0 || !state.sourceFile) return null
292
+ const { line, character } = state.sourceFile.getLineAndCharacterOfPosition(
293
+ statement.getStart(state.sourceFile)
267
294
  )
268
295
  return { line, column: character }
269
296
  }
@@ -373,27 +400,27 @@ function isBindingName(node: ts.Identifier): boolean {
373
400
  * the source never binds it; otherwise a collision-free alias is allocated and
374
401
  * the injected import uses the same alias (`import { createElement as _vobs_createElement }`).
375
402
  */
376
- function resolveHelperName(name: string): string {
377
- const existing = helperAliases.get(name)
403
+ function resolveHelperName(state: CompileState, name: string): string {
404
+ const existing = state.helperAliases.get(name)
378
405
  if (existing) return existing
379
406
  let alias = name
380
- if (takenNames.has(alias)) {
407
+ if (state.takenNames.has(alias)) {
381
408
  alias = `_vobs_${name}`
382
409
  let suffix = 1
383
- while (takenNames.has(alias)) alias = `_vobs_${name}_${suffix++}`
410
+ while (state.takenNames.has(alias)) alias = `_vobs_${name}_${suffix++}`
384
411
  }
385
- helperAliases.set(name, alias)
412
+ state.helperAliases.set(name, alias)
386
413
  return alias
387
414
  }
388
415
 
389
416
  /** 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))
417
+ function helperRef(state: CompileState, name: string): ts.Identifier {
418
+ return ts.factory.createIdentifier(resolveHelperName(state, name))
392
419
  }
393
420
 
394
- function createRuntimeImports(): ts.ImportDeclaration[] {
421
+ function createRuntimeImports(state: CompileState): ts.ImportDeclaration[] {
395
422
  const modules = new Map<string, ts.ImportSpecifier[]>()
396
- for (const [name, alias] of helperAliases) {
423
+ for (const [name, alias] of state.helperAliases) {
397
424
  const module = name === 'insertResourceBoundary' ? '@vobs/resource' : '@vobs/vobs'
398
425
  const imported = modules.get(module) ?? []
399
426
  imported.push(ts.factory.createImportSpecifier(
@@ -414,9 +441,9 @@ function createRuntimeImports(): ts.ImportDeclaration[] {
414
441
  ))
415
442
  }
416
443
 
417
- function rebuildImport(node: ts.ImportDeclaration): ts.ImportDeclaration {
444
+ function rebuildImport(state: CompileState, node: ts.ImportDeclaration): ts.ImportDeclaration {
418
445
  if (!ts.isStringLiteral(node.moduleSpecifier)) return node
419
- return tagStatement(ts.factory.createImportDeclaration(
446
+ return tagStatement(state, ts.factory.createImportDeclaration(
420
447
  node.modifiers,
421
448
  node.importClause,
422
449
  ts.factory.createStringLiteral(node.moduleSpecifier.text),
@@ -424,9 +451,9 @@ function rebuildImport(node: ts.ImportDeclaration): ts.ImportDeclaration {
424
451
  ), node)
425
452
  }
426
453
 
427
- function transformStatement(node: ts.Statement): ts.Statement {
454
+ function transformStatement(state: CompileState, node: ts.Statement): ts.Statement {
428
455
  if (ts.isFunctionDeclaration(node) && node.body) {
429
- return tagStatement(ts.factory.updateFunctionDeclaration(
456
+ return tagStatement(state, ts.factory.updateFunctionDeclaration(
430
457
  node,
431
458
  node.modifiers,
432
459
  node.asteriskToken,
@@ -434,24 +461,24 @@ function transformStatement(node: ts.Statement): ts.Statement {
434
461
  node.typeParameters,
435
462
  node.parameters,
436
463
  node.type,
437
- transformBlock(node.body)
464
+ transformBlock(state, node.body)
438
465
  ), node)
439
466
  }
440
467
 
441
- if (ts.isVariableStatement(node)) return transformVariableStatement(node)
468
+ if (ts.isVariableStatement(node)) return transformVariableStatement(state, node)
442
469
  if (ts.isExportAssignment(node) && containsJsx(node.expression)) {
443
- return tagStatement(ts.factory.updateExportAssignment(node, node.modifiers, transformEmbeddedExpression(node.expression)), node)
470
+ return tagStatement(state, ts.factory.updateExportAssignment(node, node.modifiers, transformEmbeddedExpression(state, node.expression)), node)
444
471
  }
445
472
  if (ts.isExpressionStatement(node) && containsJsx(node.expression)) {
446
- return tagStatement(ts.factory.updateExpressionStatement(node, transformEmbeddedExpression(node.expression)), node)
473
+ return tagStatement(state, ts.factory.updateExpressionStatement(node, transformEmbeddedExpression(state, node.expression)), node)
447
474
  }
448
475
  if (ts.isReturnStatement(node) && node.expression && containsJsx(node.expression)) {
449
- return tagStatement(ts.factory.updateReturnStatement(node, transformEmbeddedExpression(node.expression)), node)
476
+ return tagStatement(state, ts.factory.updateReturnStatement(node, transformEmbeddedExpression(state, node.expression)), node)
450
477
  }
451
478
  return node
452
479
  }
453
480
 
454
- function transformVariableStatement(node: ts.VariableStatement): ts.VariableStatement {
481
+ function transformVariableStatement(state: CompileState, node: ts.VariableStatement): ts.VariableStatement {
455
482
  const declarations = node.declarationList.declarations.map(declaration => {
456
483
  const initializer = declaration.initializer
457
484
  if (!initializer || !containsJsx(initializer)) return declaration
@@ -461,7 +488,7 @@ function transformVariableStatement(node: ts.VariableStatement): ts.VariableStat
461
488
  declaration.name,
462
489
  declaration.exclamationToken,
463
490
  declaration.type,
464
- transformEmbeddedExpression(initializer)
491
+ transformEmbeddedExpression(state, initializer)
465
492
  )
466
493
  })
467
494
 
@@ -469,7 +496,7 @@ function transformVariableStatement(node: ts.VariableStatement): ts.VariableStat
469
496
  return node
470
497
  }
471
498
 
472
- return tagStatement(ts.factory.updateVariableStatement(
499
+ return tagStatement(state, ts.factory.updateVariableStatement(
473
500
  node,
474
501
  node.modifiers,
475
502
  ts.factory.updateVariableDeclarationList(node.declarationList, declarations)
@@ -489,14 +516,14 @@ function containsJsx(expression: ts.Expression): boolean {
489
516
  return found
490
517
  }
491
518
 
492
- function transformBlock(block: ts.Block): ts.Block {
519
+ function transformBlock(state: CompileState, block: ts.Block): ts.Block {
493
520
  const statements = block.statements.map(statement => {
494
- if (!ts.isReturnStatement(statement) || !statement.expression) return transformStatement(statement)
521
+ if (!ts.isReturnStatement(statement) || !statement.expression) return transformStatement(state, statement)
495
522
  const expression = ts.isParenthesizedExpression(statement.expression)
496
523
  ? statement.expression.expression
497
524
  : statement.expression
498
525
  return isJsxExpression(expression)
499
- ? tagStatement(ts.factory.updateReturnStatement(statement, transformJsxExpression(expression)), statement)
526
+ ? tagStatement(state, ts.factory.updateReturnStatement(statement, transformJsxExpression(state, expression)), statement)
500
527
  : statement
501
528
  })
502
529
  return ts.factory.updateBlock(block, statements)
@@ -506,61 +533,71 @@ function isJsxExpression(node: ts.Expression): node is ts.JsxElement | ts.JsxSel
506
533
  return ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)
507
534
  }
508
535
 
509
- function transformJsxExpression(node: ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment): ts.Expression {
510
- if (ts.isJsxFragment(node)) return transformFragment(node.children)
536
+ function transformJsxExpression(state: CompileState, node: ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment): ts.Expression {
537
+ if (ts.isJsxFragment(node)) return transformFragment(state, node.children)
511
538
  if (ts.isJsxElement(node)) {
512
- return transformElement(node, node.openingElement.tagName, node.openingElement.attributes, node.children)
539
+ return transformElement(state, node, node.openingElement.tagName, node.openingElement.attributes, node.children)
513
540
  }
514
- return transformElement(node, node.tagName, node.attributes, [])
541
+ return transformElement(state, node, node.tagName, node.attributes, [])
515
542
  }
516
543
 
517
544
  function transformElement(
545
+ state: CompileState,
518
546
  node: ts.JsxElement | ts.JsxSelfClosingElement,
519
547
  tagName: ts.JsxTagNameExpression,
520
548
  attributes: ts.JsxAttributes,
521
549
  children: readonly ts.JsxChild[]
522
550
  ): ts.Expression {
523
- if (isFragmentTag(tagName)) return transformFragment(children)
551
+ if (isFragmentTag(tagName)) return transformFragment(state, children)
524
552
  if (!ts.isIdentifier(tagName)) {
525
553
  // <Foo.Bar>、<svg:rect> 等形态此前会静默编译成无效 DOM 标签(createElement("Foo.Bar"))。
526
554
  // 报结构化诊断后按原路径继续,保证产物结构稳定;compile()/Vite 插件会因 error 诊断直接失败。
527
- reportUnsupportedTag(tagName)
555
+ reportUnsupportedTag(state, tagName)
528
556
  }
529
557
  if (ts.isIdentifier(tagName) && tagName.text === 'ResourceBoundary') {
530
- return transformResourceBoundary(node, attributes, children)
558
+ return transformResourceBoundary(state, node, attributes, children)
531
559
  }
532
560
  if (ts.isIdentifier(tagName) && tagName.text === 'AsyncBoundary') {
533
- return transformAsyncBoundary(node, attributes, children)
561
+ return transformAsyncBoundary(state, node, attributes, children)
534
562
  }
535
563
  if (ts.isIdentifier(tagName) && tagName.text === 'ErrorBoundary') {
536
- return transformErrorBoundary(node, attributes, children)
564
+ return transformErrorBoundary(state, node, attributes, children)
537
565
  }
538
566
  if (ts.isIdentifier(tagName) && tagName.text === 'Profiler') {
539
- return transformProfiler(node, attributes, children)
567
+ return transformProfiler(state, node, attributes, children)
540
568
  }
541
569
  if (ts.isIdentifier(tagName) && /^[A-Z]/.test(tagName.text)) {
570
+ const args: ts.Expression[] = [
571
+ ts.factory.createCallExpression(helperRef(state, 'resolveComponent'), undefined, [
572
+ tagName,
573
+ ts.factory.createStringLiteral(state.filename),
574
+ ts.factory.createStringLiteral(tagName.text)
575
+ ]),
576
+ createComponentProps(state, attributes, children)
577
+ ]
578
+ // 源码位置仅用于错误定位与 DevTools;生产构建可整体剔除(错误仍带组件名,定位走 source map)。
579
+ if (state.sourceLocation) args.push(createSourceLocation(tagName))
580
+ return ts.factory.createCallExpression(helperRef(state, 'createComponent'), undefined, args)
581
+ }
582
+
583
+ // 静态模板提升:完全静态的 DOM 子树(无事件/动态绑定/spread/property 属性)序列化为
584
+ // 模块级模板,运行时一次 cloneNode 替代 createElement + setStaticProps + 逐子插入。
585
+ if (isStaticElement(tagName, attributes, children)) {
542
586
  return ts.factory.createCallExpression(
543
- helperRef('createComponent'),
587
+ helperRef(state, 'cloneTemplate'),
544
588
  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
- ]
589
+ [registerTemplate(state, serializeStaticHtml(node))]
554
590
  )
555
591
  }
556
592
 
557
593
  const elementName = tagName.getText()
558
- const elementId = nextIdentifier('_el')
594
+ const elementId = nextIdentifier(state, '_el')
559
595
  const statements: ts.Statement[] = [
560
596
  createConstStatement(
597
+ state,
561
598
  elementId,
562
599
  ts.factory.createCallExpression(
563
- helperRef('createElement'),
600
+ helperRef(state, 'createElement'),
564
601
  undefined,
565
602
  [ts.factory.createStringLiteral(elementName)]
566
603
  ),
@@ -568,9 +605,9 @@ function transformElement(
568
605
  )
569
606
  ]
570
607
 
571
- appendAttributes(statements, elementId, attributes)
572
- appendChildren(statements, elementId, children)
573
- statements.push(tagStatement(ts.factory.createReturnStatement(elementId), node))
608
+ appendAttributes(state, statements, elementId, attributes)
609
+ appendChildren(state, statements, elementId, children)
610
+ statements.push(tagStatement(state, ts.factory.createReturnStatement(elementId), node))
574
611
 
575
612
  return ts.factory.createCallExpression(
576
613
  ts.factory.createArrowFunction(
@@ -590,13 +627,13 @@ function isFragmentTag(tagName: ts.JsxTagNameExpression): boolean {
590
627
  return tagName.getText() === 'Fragment' || tagName.getText() === 'Vobs.Fragment'
591
628
  }
592
629
 
593
- function transformFragment(children: readonly ts.JsxChild[]): ts.Expression {
594
- const parent = nextIdentifier('_fragmentParent')
595
- const anchor = nextIdentifier('_fragmentAnchor')
630
+ function transformFragment(state: CompileState, children: readonly ts.JsxChild[]): ts.Expression {
631
+ const parent = nextIdentifier(state, '_fragmentParent')
632
+ const anchor = nextIdentifier(state, '_fragmentAnchor')
596
633
  const statements: ts.Statement[] = []
597
- appendChildren(statements, parent, children, anchor)
634
+ appendChildren(state, statements, parent, children, anchor)
598
635
  return ts.factory.createCallExpression(
599
- helperRef('createFragment'),
636
+ helperRef(state, 'createFragment'),
600
637
  undefined,
601
638
  [ts.factory.createArrowFunction(
602
639
  undefined,
@@ -613,6 +650,7 @@ function transformFragment(children: readonly ts.JsxChild[]): ts.Expression {
613
650
  }
614
651
 
615
652
  function transformResourceBoundary(
653
+ state: CompileState,
616
654
  node: ts.JsxElement | ts.JsxSelfClosingElement,
617
655
  attributes: ts.JsxAttributes,
618
656
  children: readonly ts.JsxChild[]
@@ -620,29 +658,31 @@ function transformResourceBoundary(
620
658
  const resource = getAttributeExpression(attributes, 'resource')
621
659
  if (!resource) throw new VobsError({ code: 'VOBS_C002', layer: 'compiler', message: 'ResourceBoundary 必须提供 resource 属性', fix: '为 ResourceBoundary 添加 resource={resource}。' })
622
660
  const options: ts.ObjectLiteralElementLike[] = [
623
- ts.factory.createPropertyAssignment('resource', transformEmbeddedExpression(resource)),
624
- ts.factory.createPropertyAssignment('children', createBoundaryFactory(children))
661
+ ts.factory.createPropertyAssignment('resource', transformEmbeddedExpression(state, resource)),
662
+ ts.factory.createPropertyAssignment('children', createBoundaryFactory(state, children))
625
663
  ]
626
- appendBoundaryOptionalProperty(options, attributes, 'loading')
627
- appendBoundaryOptionalProperty(options, attributes, 'empty')
628
- appendBoundaryOptionalProperty(options, attributes, 'fallback')
629
- return createBoundaryFragment(node, 'insertResourceBoundary', options)
664
+ appendBoundaryOptionalProperty(state, options, attributes, 'loading')
665
+ appendBoundaryOptionalProperty(state, options, attributes, 'empty')
666
+ appendBoundaryOptionalProperty(state, options, attributes, 'fallback')
667
+ return createBoundaryFragment(state, node, 'insertResourceBoundary', options)
630
668
  }
631
669
 
632
670
  function transformErrorBoundary(
671
+ state: CompileState,
633
672
  node: ts.JsxElement | ts.JsxSelfClosingElement,
634
673
  attributes: ts.JsxAttributes,
635
674
  children: readonly ts.JsxChild[]
636
675
  ): ts.Expression {
637
676
  const fallback = getAttributeExpression(attributes, 'fallback')
638
677
  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))
678
+ return createBoundaryFragment(state, node, 'insertErrorBoundary', [
679
+ ts.factory.createPropertyAssignment('children', createBoundaryFactory(state, children)),
680
+ ts.factory.createPropertyAssignment('fallback', transformEmbeddedExpression(state, fallback))
642
681
  ])
643
682
  }
644
683
 
645
684
  function transformAsyncBoundary(
685
+ state: CompileState,
646
686
  node: ts.JsxElement | ts.JsxSelfClosingElement,
647
687
  attributes: ts.JsxAttributes,
648
688
  children: readonly ts.JsxChild[]
@@ -650,17 +690,18 @@ function transformAsyncBoundary(
650
690
  const promise = getAttributeExpression(attributes, 'promise')
651
691
  if (!promise) throw new VobsError({ code: 'VOBS_C002', layer: 'compiler', message: 'AsyncBoundary 必须提供 promise 属性', fix: '为 AsyncBoundary 添加 promise={promise}。' })
652
692
  const options: ts.ObjectLiteralElementLike[] = [
653
- ts.factory.createPropertyAssignment('promise', transformEmbeddedExpression(promise)),
654
- ts.factory.createPropertyAssignment('children', createAsyncFactory(children))
693
+ ts.factory.createPropertyAssignment('promise', transformEmbeddedExpression(state, promise)),
694
+ ts.factory.createPropertyAssignment('children', createAsyncFactory(state, children))
655
695
  ]
656
- appendBoundaryOptionalProperty(options, attributes, 'loading')
657
- appendBoundaryOptionalProperty(options, attributes, 'fallback')
696
+ appendBoundaryOptionalProperty(state, options, attributes, 'loading')
697
+ appendBoundaryOptionalProperty(state, options, attributes, 'fallback')
658
698
  const resetKey = getAttributeExpression(attributes, 'resetKey')
659
699
  if (resetKey) options.push(ts.factory.createPropertyAssignment('resetKey', createGetter(resetKey)))
660
- return createBoundaryFragment(node, 'insertAsyncBoundary', options)
700
+ return createBoundaryFragment(state, node, 'insertAsyncBoundary', options)
661
701
  }
662
702
 
663
703
  function transformProfiler(
704
+ state: CompileState,
664
705
  node: ts.JsxElement | ts.JsxSelfClosingElement,
665
706
  attributes: ts.JsxAttributes,
666
707
  children: readonly ts.JsxChild[]
@@ -673,22 +714,23 @@ function transformProfiler(
673
714
  : null
674
715
  if (!id) throw new VobsError({ code: 'VOBS_C002', layer: 'compiler', message: 'Profiler 必须提供 id 属性', fix: '为 Profiler 添加 id="ComponentName"。' })
675
716
  const options: ts.ObjectLiteralElementLike[] = [
676
- ts.factory.createPropertyAssignment('id', transformEmbeddedExpression(id)),
677
- ts.factory.createPropertyAssignment('children', createBoundaryFactory(children))
717
+ ts.factory.createPropertyAssignment('id', transformEmbeddedExpression(state, id)),
718
+ ts.factory.createPropertyAssignment('children', createBoundaryFactory(state, children))
678
719
  ]
679
- appendBoundaryOptionalProperty(options, attributes, 'onRender')
680
- return createBoundaryFragment(node, 'insertProfiler', options)
720
+ appendBoundaryOptionalProperty(state, options, attributes, 'onRender')
721
+ return createBoundaryFragment(state, node, 'insertProfiler', options)
681
722
  }
682
723
 
683
724
  function createBoundaryFragment(
725
+ state: CompileState,
684
726
  node: ts.JsxElement | ts.JsxSelfClosingElement,
685
727
  helper: 'insertResourceBoundary' | 'insertErrorBoundary' | 'insertAsyncBoundary' | 'insertProfiler',
686
728
  options: readonly ts.ObjectLiteralElementLike[]
687
729
  ): ts.Expression {
688
- const parent = nextIdentifier('_boundaryParent')
689
- const anchor = nextIdentifier('_boundaryAnchor')
730
+ const parent = nextIdentifier(state, '_boundaryParent')
731
+ const anchor = nextIdentifier(state, '_boundaryAnchor')
690
732
  return ts.factory.createCallExpression(
691
- helperRef('createFragment'),
733
+ helperRef(state, 'createFragment'),
692
734
  undefined,
693
735
  [ts.factory.createArrowFunction(
694
736
  undefined,
@@ -699,7 +741,7 @@ function createBoundaryFragment(
699
741
  ],
700
742
  undefined,
701
743
  ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
702
- ts.factory.createBlock([callStatement(helper, [
744
+ ts.factory.createBlock([callStatement(state, helper, [
703
745
  parent,
704
746
  anchor,
705
747
  ts.factory.createObjectLiteralExpression(options, true)
@@ -708,8 +750,8 @@ function createBoundaryFragment(
708
750
  )
709
751
  }
710
752
 
711
- function createBoundaryFactory(children: readonly ts.JsxChild[]): ts.ArrowFunction {
712
- const content = transformFragment(children)
753
+ function createBoundaryFactory(state: CompileState, children: readonly ts.JsxChild[]): ts.ArrowFunction {
754
+ const content = transformFragment(state, children)
713
755
  return ts.factory.createArrowFunction(
714
756
  undefined,
715
757
  undefined,
@@ -720,29 +762,30 @@ function createBoundaryFactory(children: readonly ts.JsxChild[]): ts.ArrowFuncti
720
762
  )
721
763
  }
722
764
 
723
- function createAsyncFactory(children: readonly ts.JsxChild[]): ts.ArrowFunction {
765
+ function createAsyncFactory(state: CompileState, children: readonly ts.JsxChild[]): ts.ArrowFunction {
724
766
  const value = ts.factory.createIdentifier('value')
725
767
  const expressionChild = children.length === 1 && children[0].kind === ts.SyntaxKind.JsxExpression
726
768
  ? (children[0] as ts.JsxExpression).expression
727
769
  : undefined
728
770
  if (expressionChild && ts.isArrowFunction(expressionChild)) {
729
- const transformed = transformEmbeddedExpression(expressionChild)
771
+ const transformed = transformEmbeddedExpression(state, expressionChild)
730
772
  return transformed as ts.ArrowFunction
731
773
  }
732
- const content = transformFragment(children)
774
+ const content = transformFragment(state, children)
733
775
  return ts.factory.createArrowFunction(undefined, undefined, [
734
776
  ts.factory.createParameterDeclaration(undefined, undefined, value)
735
777
  ], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), content)
736
778
  }
737
779
 
738
780
  function appendBoundaryOptionalProperty(
781
+ state: CompileState,
739
782
  properties: ts.ObjectLiteralElementLike[],
740
783
  attributes: ts.JsxAttributes,
741
784
  name: string
742
785
  ): void {
743
786
  const expression = getAttributeExpression(attributes, name)
744
787
  if (expression) {
745
- const transformed = transformEmbeddedExpression(expression)
788
+ const transformed = transformEmbeddedExpression(state, expression)
746
789
  const value = isJsxExpression(unwrapExpression(expression))
747
790
  ? createGetter(transformed)
748
791
  : transformed
@@ -760,11 +803,11 @@ function getAttributeExpression(attributes: ts.JsxAttributes, name: string): ts.
760
803
  return null
761
804
  }
762
805
 
763
- function transformEmbeddedExpression(expression: ts.Expression): ts.Expression {
806
+ function transformEmbeddedExpression(state: CompileState, expression: ts.Expression): ts.Expression {
764
807
  const result = ts.transform(expression, [context => root => {
765
808
  const visit: ts.Visitor = node => {
766
809
  if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) {
767
- return transformJsxExpression(node)
810
+ return transformJsxExpression(state, node)
768
811
  }
769
812
  return ts.visitEachChild(node, visit, context)
770
813
  }
@@ -777,7 +820,125 @@ function transformEmbeddedExpression(expression: ts.Expression): ts.Expression {
777
820
  }
778
821
  }
779
822
 
823
+ /**
824
+ * 静态元素判定:DOM 标签 + 全部属性为字符串字面量或无值 + 全部子节点为文本或递归静态元素。
825
+ * 保守排除项(语义或序列化等价性无把握,走原路径):
826
+ * - property 属性(value/checked/disabled 等):HTML attribute 与 setProperty 初始语义存在差异;
827
+ * - 事件(on*)、ref、spread、key:本身是动态行为;
828
+ * - 嵌套组件/Fragment/Boundary:不是纯 DOM 子树。
829
+ */
830
+ function isStaticElement(
831
+ tagName: ts.JsxTagNameExpression,
832
+ attributes: ts.JsxAttributes,
833
+ children: readonly ts.JsxChild[]
834
+ ): boolean {
835
+ if (!ts.isIdentifier(tagName) || !/^[a-z]/.test(tagName.text)) return false
836
+ for (const attribute of attributes.properties) {
837
+ if (ts.isJsxSpreadAttribute(attribute)) return false
838
+ if (!ts.isJsxAttribute(attribute)) return false
839
+ const name = attribute.name.getText()
840
+ if (name === 'key' || name === 'ref' || name.startsWith('on')) return false
841
+ if (isPropertyAttribute(name)) return false
842
+ const initializer = attribute.initializer
843
+ if (initializer && !ts.isStringLiteral(initializer)) return false
844
+ }
845
+ for (const child of children) {
846
+ if (ts.isJsxText(child)) continue
847
+ if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) {
848
+ const nested = ts.isJsxElement(child)
849
+ ? { tagName: child.openingElement.tagName, attributes: child.openingElement.attributes, children: child.children }
850
+ : { tagName: child.tagName, attributes: child.attributes, children: [] as readonly ts.JsxChild[] }
851
+ if (!isStaticElement(nested.tagName, nested.attributes, nested.children)) return false
852
+ continue
853
+ }
854
+ return false
855
+ }
856
+ return true
857
+ }
858
+
859
+ /** Serialize a fully-static JSX element to HTML, preserving the compiler's text normalization. */
860
+ function serializeStaticHtml(node: ts.JsxElement | ts.JsxSelfClosingElement): string {
861
+ const { tagName, attributes, children } = ts.isJsxElement(node)
862
+ ? { tagName: node.openingElement.tagName, attributes: node.openingElement.attributes, children: node.children }
863
+ : { tagName: node.tagName, attributes: node.attributes, children: [] as readonly ts.JsxChild[] }
864
+ return serializeStaticElement(tagName, attributes, children)
865
+ }
866
+
867
+ function serializeStaticElement(
868
+ tagName: ts.JsxTagNameExpression,
869
+ attributes: ts.JsxAttributes,
870
+ children: readonly ts.JsxChild[]
871
+ ): string {
872
+ const name = tagName.getText()
873
+ let html = `<${name}`
874
+ for (const attribute of attributes.properties) {
875
+ if (!ts.isJsxAttribute(attribute)) continue
876
+ const attributeName = attribute.name.getText() === 'className' ? 'class' : attribute.name.getText()
877
+ const initializer = attribute.initializer
878
+ if (!initializer) {
879
+ html += ` ${attributeName}=""`
880
+ continue
881
+ }
882
+ if (ts.isStringLiteral(initializer)) {
883
+ html += ` ${attributeName}="${escapeHtmlAttribute(initializer.text)}"`
884
+ }
885
+ }
886
+ html += '>'
887
+
888
+ for (const child of children) {
889
+ if (ts.isJsxText(child)) {
890
+ // 与 appendChildren 的文本规范化保持一致,保证提升前后 DOM 文本逐字相同。
891
+ const text = child.text.replace(/\s+/g, ' ').trimStart()
892
+ if (text.trim()) html += escapeHtmlText(text)
893
+ continue
894
+ }
895
+ if (ts.isJsxElement(child)) {
896
+ html += serializeStaticElement(
897
+ child.openingElement.tagName,
898
+ child.openingElement.attributes,
899
+ child.children
900
+ )
901
+ continue
902
+ }
903
+ if (ts.isJsxSelfClosingElement(child)) {
904
+ html += serializeStaticElement(child.tagName, child.attributes, [])
905
+ }
906
+ }
907
+ html += `</${name}>`
908
+ return html
909
+ }
910
+
911
+ function escapeHtmlAttribute(value: string): string {
912
+ return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;')
913
+ }
914
+
915
+ function escapeHtmlText(value: string): string {
916
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
917
+ }
918
+
919
+ /** Register a hoisted template declaration; identical HTML shares one declaration. */
920
+ function registerTemplate(state: CompileState, html: string): ts.Expression {
921
+ const existing = state.templates.get(html)
922
+ if (existing) return existing
923
+ const identifier = nextIdentifier(state, '_tpl')
924
+ state.templates.set(html, identifier)
925
+ return identifier
926
+ }
927
+
928
+ function createTemplateDeclarations(state: CompileState): ts.Statement[] {
929
+ return [...state.templates.entries()].map(([html, identifier]) =>
930
+ ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([
931
+ ts.factory.createVariableDeclaration(identifier, undefined, undefined, ts.factory.createCallExpression(
932
+ helperRef(state, 'createTemplate'),
933
+ undefined,
934
+ [ts.factory.createStringLiteral(html)]
935
+ ))
936
+ ], ts.NodeFlags.Const))
937
+ )
938
+ }
939
+
780
940
  function appendAttributes(
941
+ state: CompileState,
781
942
  statements: ts.Statement[],
782
943
  element: ts.Identifier,
783
944
  attributes: ts.JsxAttributes
@@ -786,7 +947,7 @@ function appendAttributes(
786
947
  const hasSpread = attributes.properties.some(attribute => ts.isJsxSpreadAttribute(attribute))
787
948
  for (const attribute of attributes.properties) {
788
949
  if (ts.isJsxSpreadAttribute(attribute)) {
789
- statements.push(callStatement('spreadProps', [element, transformEmbeddedExpression(attribute.expression)], attribute))
950
+ statements.push(callStatement(state, 'spreadProps', [element, transformEmbeddedExpression(state, attribute.expression)], attribute))
790
951
  continue
791
952
  }
792
953
  if (!ts.isJsxAttribute(attribute)) continue
@@ -795,14 +956,14 @@ function appendAttributes(
795
956
  if (name === 'ref') {
796
957
  const initializer = attribute.initializer
797
958
  if (initializer && ts.isJsxExpression(initializer) && initializer.expression) {
798
- statements.push(callStatement('setRef', [element, transformEmbeddedExpression(initializer.expression)], attribute))
959
+ statements.push(callStatement(state, 'setRef', [element, transformEmbeddedExpression(state, initializer.expression)], attribute))
799
960
  }
800
961
  continue
801
962
  }
802
963
  const initializer = attribute.initializer
803
964
 
804
965
  if (name.startsWith('on') && initializer && ts.isJsxExpression(initializer) && initializer.expression) {
805
- statements.push(callStatement('addEventListener', [
966
+ statements.push(callStatement(state, 'addEventListener', [
806
967
  element,
807
968
  ts.factory.createStringLiteral(name.slice(2).toLowerCase()),
808
969
  initializer.expression
@@ -812,7 +973,7 @@ function appendAttributes(
812
973
 
813
974
  if (!initializer) {
814
975
  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))
976
+ 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
977
  continue
817
978
  }
818
979
  if (isPropertyAttribute(name)) staticProps.push(createStaticProperty(name, ts.factory.createTrue()))
@@ -821,7 +982,7 @@ function appendAttributes(
821
982
  }
822
983
  if (ts.isStringLiteral(initializer)) {
823
984
  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))
985
+ 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
986
  continue
826
987
  }
827
988
  staticProps.push(createStaticProperty(isPropertyAttribute(name) ? name : name === 'className' ? 'class' : name,
@@ -832,14 +993,14 @@ function appendAttributes(
832
993
  const attributeName = name === 'className' ? 'class' : name
833
994
  const propertyAttribute = isPropertyAttribute(name)
834
995
  if (ts.isJsxExpression(initializer) && initializer.expression) {
835
- statements.push(callStatement(propertyAttribute ? 'bindProperty' : 'bindAttribute', [
996
+ statements.push(callStatement(state, propertyAttribute ? 'bindProperty' : 'bindAttribute', [
836
997
  element,
837
998
  ts.factory.createStringLiteral(propertyAttribute ? name : attributeName),
838
999
  createGetter(initializer.expression)
839
1000
  ], attribute))
840
1001
  }
841
1002
  }
842
- if (staticProps.length) statements.splice(1, 0, callStatement('setStaticProps', [
1003
+ if (staticProps.length) statements.splice(1, 0, callStatement(state, 'setStaticProps', [
843
1004
  element,
844
1005
  ts.factory.createObjectLiteralExpression(staticProps, true)
845
1006
  ], attributes))
@@ -856,6 +1017,7 @@ function isPropertyAttribute(name: string): boolean {
856
1017
  }
857
1018
 
858
1019
  function appendChildren(
1020
+ state: CompileState,
859
1021
  statements: ts.Statement[],
860
1022
  element: ts.Identifier,
861
1023
  children: readonly ts.JsxChild[],
@@ -865,9 +1027,9 @@ function appendChildren(
865
1027
  if (ts.isJsxText(child)) {
866
1028
  const text = child.text.replace(/\s+/g, ' ').trimStart()
867
1029
  if (text.trim()) {
868
- statements.push(callStatement('insertBefore', [
1030
+ statements.push(callStatement(state, 'insertBefore', [
869
1031
  element,
870
- ts.factory.createCallExpression(helperRef('createText'), undefined, [
1032
+ ts.factory.createCallExpression(helperRef(state, 'createText'), undefined, [
871
1033
  ts.factory.createStringLiteral(text)
872
1034
  ]),
873
1035
  anchor
@@ -877,9 +1039,9 @@ function appendChildren(
877
1039
  }
878
1040
 
879
1041
  if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {
880
- statements.push(callStatement('insertBefore', [
1042
+ statements.push(callStatement(state, 'insertBefore', [
881
1043
  element,
882
- transformJsxExpression(child),
1044
+ transformJsxExpression(state, child),
883
1045
  anchor
884
1046
  ], child))
885
1047
  continue
@@ -888,30 +1050,31 @@ function appendChildren(
888
1050
  if (child.kind === ts.SyntaxKind.JsxExpression) {
889
1051
  const expression = (child as ts.JsxExpression).expression
890
1052
  if (!expression) continue
891
- const list = transformListExpression(element, expression, anchor)
1053
+ const list = transformListExpression(state, element, expression, anchor)
892
1054
  if (list) {
893
- statements.push(callStatement('insertList', list, child))
1055
+ statements.push(callStatement(state, 'insertList', list, child))
894
1056
  continue
895
1057
  }
896
- const dynamic = transformDynamicExpression(expression)
1058
+ const dynamic = transformDynamicExpression(state, expression)
897
1059
  if (dynamic) {
898
- statements.push(callStatement('insertDynamic', [element, anchor, dynamic], child))
1060
+ statements.push(callStatement(state, 'insertDynamic', [element, anchor, dynamic], child))
899
1061
  continue
900
1062
  }
901
1063
  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))
1064
+ const textId = nextIdentifier(state, '_text')
1065
+ statements.push(createConstStatement(state, textId, ts.factory.createCallExpression(helperRef(state, 'createText'), undefined, [ts.factory.createStringLiteral('')]), child))
1066
+ statements.push(callStatement(state, 'insertBefore', [element, textId, anchor], child))
1067
+ statements.push(callStatement(state, 'bindText', [textId, createGetter(expression)], child))
906
1068
  continue
907
1069
  }
908
- const value = transformEmbeddedExpression(expression)
909
- statements.push(callStatement('insertDynamicValue', [element, anchor, createGetter(value)], child))
1070
+ const value = transformEmbeddedExpression(state, expression)
1071
+ statements.push(callStatement(state, 'insertDynamicValue', [element, anchor, createGetter(value)], child))
910
1072
  }
911
1073
  }
912
1074
  }
913
1075
 
914
1076
  function createComponentProps(
1077
+ state: CompileState,
915
1078
  attributes: ts.JsxAttributes,
916
1079
  children: readonly ts.JsxChild[]
917
1080
  ): ts.ObjectLiteralExpression {
@@ -931,11 +1094,11 @@ function createComponentProps(
931
1094
  } else if (ts.isStringLiteral(initializer)) {
932
1095
  properties.push(ts.factory.createPropertyAssignment(name, ts.factory.createStringLiteral(initializer.text)))
933
1096
  } else if (ts.isJsxExpression(initializer) && initializer.expression) {
934
- properties.push(createGetterProperty(name, transformEmbeddedExpression(initializer.expression)))
1097
+ properties.push(createGetterProperty(name, transformEmbeddedExpression(state, initializer.expression)))
935
1098
  }
936
1099
  }
937
1100
 
938
- const childExpressions = children.flatMap(childToComponentExpression)
1101
+ const childExpressions = children.flatMap(child => childToComponentExpression(state, child))
939
1102
  if (childExpressions.length === 1) {
940
1103
  properties.push(createGetterProperty('children', childExpressions[0]))
941
1104
  } else if (childExpressions.length > 1) {
@@ -955,22 +1118,22 @@ function createSourceLocation(node: ts.Node): ts.ObjectLiteralExpression {
955
1118
  ], true)
956
1119
  }
957
1120
 
958
- function transformDynamicExpression(expression: ts.Expression): ts.ArrowFunction | null {
1121
+ function transformDynamicExpression(state: CompileState, expression: ts.Expression): ts.ArrowFunction | null {
959
1122
  if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
960
1123
  const right = unwrapExpression(expression.right)
961
1124
  if (!isJsxExpression(right)) return null
962
1125
  return createGetter(ts.factory.createConditionalExpression(
963
1126
  expression.left,
964
1127
  ts.factory.createToken(ts.SyntaxKind.QuestionToken),
965
- transformJsxExpression(right),
1128
+ transformJsxExpression(state, right),
966
1129
  ts.factory.createToken(ts.SyntaxKind.ColonToken),
967
1130
  ts.factory.createNull()
968
1131
  ))
969
1132
  }
970
1133
 
971
1134
  if (ts.isConditionalExpression(expression)) {
972
- const whenTrue = transformDynamicBranch(expression.whenTrue)
973
- const whenFalse = transformDynamicBranch(expression.whenFalse)
1135
+ const whenTrue = transformDynamicBranch(state, expression.whenTrue)
1136
+ const whenFalse = transformDynamicBranch(state, expression.whenFalse)
974
1137
  if (!whenTrue && !whenFalse) return null
975
1138
  return createGetter(ts.factory.createConditionalExpression(
976
1139
  expression.condition,
@@ -984,14 +1147,15 @@ function transformDynamicExpression(expression: ts.Expression): ts.ArrowFunction
984
1147
  return null
985
1148
  }
986
1149
 
987
- function transformDynamicBranch(expression: ts.Expression): ts.Expression | null {
1150
+ function transformDynamicBranch(state: CompileState, expression: ts.Expression): ts.Expression | null {
988
1151
  const branch = unwrapExpression(expression)
989
- if (isJsxExpression(branch)) return transformJsxExpression(branch)
1152
+ if (isJsxExpression(branch)) return transformJsxExpression(state, branch)
990
1153
  if (branch.kind === ts.SyntaxKind.NullKeyword || branch.kind === ts.SyntaxKind.FalseKeyword) return branch
991
1154
  return null
992
1155
  }
993
1156
 
994
1157
  function transformListExpression(
1158
+ state: CompileState,
995
1159
  parent: ts.Identifier,
996
1160
  expression: ts.Expression,
997
1161
  anchor: ts.Expression
@@ -1005,7 +1169,7 @@ function transformListExpression(
1005
1169
  if (!isJsxExpression(body) || ts.isJsxFragment(body)) return null
1006
1170
 
1007
1171
  const key = findKeyExpression(body)
1008
- const renderItem = transformListCallback(callback, transformJsxExpression(body))
1172
+ const renderItem = transformListCallback(callback, transformJsxExpression(state, body))
1009
1173
  const args: ts.Expression[] = [
1010
1174
  parent,
1011
1175
  anchor,
@@ -1073,17 +1237,17 @@ function unwrapExpression(node: ts.Expression | ts.ConciseBody): ts.Expression {
1073
1237
  return ts.isParenthesizedExpression(node) ? node.expression : node as ts.Expression
1074
1238
  }
1075
1239
 
1076
- function childToComponentExpression(child: ts.JsxChild): ts.Expression[] {
1240
+ function childToComponentExpression(state: CompileState, child: ts.JsxChild): ts.Expression[] {
1077
1241
  if (ts.isJsxText(child)) {
1078
1242
  const text = child.text.replace(/\s+/g, ' ').trim()
1079
1243
  return text ? [ts.factory.createStringLiteral(text)] : []
1080
1244
  }
1081
1245
  if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {
1082
- return [transformJsxExpression(child)]
1246
+ return [transformJsxExpression(state, child)]
1083
1247
  }
1084
1248
  if (child.kind === ts.SyntaxKind.JsxExpression) {
1085
1249
  const expression = (child as ts.JsxExpression).expression
1086
- return expression ? [transformEmbeddedExpression(expression)] : []
1250
+ return expression ? [transformEmbeddedExpression(state, expression)] : []
1087
1251
  }
1088
1252
  return []
1089
1253
  }
@@ -1115,42 +1279,42 @@ function createGetterProperty(name: string | ts.PropertyName, expression: ts.Exp
1115
1279
  )
1116
1280
  }
1117
1281
 
1118
- function callStatement(name: string, args: ts.Expression[], source?: ts.Node): ts.ExpressionStatement {
1282
+ function callStatement(state: CompileState, name: string, args: ts.Expression[], source?: ts.Node): ts.ExpressionStatement {
1119
1283
  const statement = ts.factory.createExpressionStatement(
1120
- ts.factory.createCallExpression(helperRef(name), undefined, args)
1284
+ ts.factory.createCallExpression(helperRef(state, name), undefined, args)
1121
1285
  )
1122
- return source ? tagStatement(statement, source) : statement
1286
+ return source ? tagStatement(state, statement, source) : statement
1123
1287
  }
1124
1288
 
1125
- function createConstStatement(name: ts.Identifier, initializer: ts.Expression, source?: ts.Node): ts.VariableStatement {
1289
+ function createConstStatement(state: CompileState, name: ts.Identifier, initializer: ts.Expression, source?: ts.Node): ts.VariableStatement {
1126
1290
  const statement = ts.factory.createVariableStatement(
1127
1291
  undefined,
1128
1292
  ts.factory.createVariableDeclarationList([
1129
1293
  ts.factory.createVariableDeclaration(name, undefined, undefined, initializer)
1130
1294
  ], ts.NodeFlags.Const)
1131
1295
  )
1132
- return source ? tagStatement(statement, source) : statement
1296
+ return source ? tagStatement(state, statement, source) : statement
1133
1297
  }
1134
1298
 
1135
1299
  /** 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)
1300
+ function tagStatement<T extends ts.Statement>(state: CompileState, statement: T, source: ts.Node): T {
1301
+ const position = positionOfNode(state, source)
1302
+ if (position) state.statementSources.set(statement, position)
1139
1303
  return statement
1140
1304
  }
1141
1305
 
1142
- function positionOfNode(node: ts.Node): SourcePosition | null {
1306
+ function positionOfNode(state: CompileState, node: ts.Node): SourcePosition | null {
1143
1307
  // ts.transform 产生的节点副本可能丢失 sourceFile 引用,回退到当前编译的源文件。
1144
- const sourceFile = node.getSourceFile() ?? currentSourceFile
1308
+ const sourceFile = node.getSourceFile() ?? state.sourceFile
1145
1309
  if (!sourceFile || node.pos < 0) return null
1146
1310
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
1147
1311
  return { line, column: character }
1148
1312
  }
1149
1313
 
1150
- function nextIdentifier(prefix: string): ts.Identifier {
1314
+ function nextIdentifier(state: CompileState, prefix: string): ts.Identifier {
1151
1315
  let name: string
1152
1316
  do {
1153
- name = `${prefix}${generatedId++}`
1154
- } while (takenNames.has(name))
1317
+ name = `${prefix}${state.generatedId++}`
1318
+ } while (state.takenNames.has(name))
1155
1319
  return ts.factory.createIdentifier(name)
1156
1320
  }
package/src/plugin.ts CHANGED
@@ -36,6 +36,11 @@ export interface CompilerOptions {
36
36
 
37
37
  export interface CompileOptions extends CompilerOptions {
38
38
  filename?: string
39
+ /**
40
+ * 是否为组件调用生成源码位置({ file, line, column },用于错误定位与 DevTools)。
41
+ * 默认 true。生产构建应传 false 以减小产物体积,省略后错误仍带组件名,定位走 source map。
42
+ */
43
+ sourceLocation?: boolean
39
44
  }
40
45
 
41
46
  export interface VobsSourceMap {