@vobs/compiler 1.0.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 ADDED
@@ -0,0 +1,1156 @@
1
+ import ts from 'typescript'
2
+ import { VobsError } from '@vobs/runtime/error'
3
+ import type {
4
+ CompilerContext,
5
+ CompilerOptions,
6
+ CompilerPlugin,
7
+ CompileOptions,
8
+ CompileResult,
9
+ CompilerDiagnostic,
10
+ VobsCompiler
11
+ } from './plugin'
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[] = []
23
+
24
+ interface SourcePosition {
25
+ readonly line: number
26
+ readonly column: number
27
+ }
28
+
29
+ export function createCompiler(options: CompilerOptions = {}): VobsCompiler {
30
+ const basePlugins = options.plugins ?? []
31
+
32
+ return {
33
+ compile(code: string, overrides: CompileOptions = {}): string {
34
+ return compile(code, {
35
+ ...overrides,
36
+ plugins: [...basePlugins, ...(overrides.plugins ?? [])]
37
+ })
38
+ },
39
+ compileWithSourceMap(code: string, overrides: CompileOptions = {}): CompileResult {
40
+ return compileWithSourceMap(code, {
41
+ ...overrides,
42
+ plugins: [...basePlugins, ...(overrides.plugins ?? [])]
43
+ })
44
+ }
45
+ }
46
+ }
47
+
48
+ export function compile(code: string, options: CompileOptions = {}): string {
49
+ const result = compileWithSourceMap(code, options)
50
+ const firstError = result.diagnostics.find(diagnostic => diagnostic.severity === 'error')
51
+ if (firstError) {
52
+ throw new VobsError({
53
+ code: firstError.code,
54
+ layer: 'compiler',
55
+ message: firstError.message,
56
+ location: firstError.location,
57
+ codeFrame: firstError.codeFrame,
58
+ fix: firstError.fix
59
+ })
60
+ }
61
+ return result.code
62
+ }
63
+
64
+ export function compileWithSourceMap(code: string, options: CompileOptions = {}): CompileResult {
65
+ generatedId = 0
66
+ const filename = options.filename ?? 'component.tsx'
67
+ currentFilename = filename
68
+ statementSources = new WeakMap()
69
+ let sourceFile = ts.createSourceFile(
70
+ filename,
71
+ code,
72
+ ts.ScriptTarget.Latest,
73
+ true,
74
+ ts.ScriptKind.TSX
75
+ )
76
+ currentSourceFile = sourceFile
77
+ takenNames = collectDeclaredNames(sourceFile)
78
+ helperAliases = new Map()
79
+ compileDiagnostics = []
80
+ const cleanFilename = filename.split(/[?#]/u, 1)[0] || filename
81
+ const diagnostics = ts.transpileModule(code, {
82
+ // Vite appends query strings (for example `?direct`) to module IDs;
83
+ // strip them so TypeScript still recognizes TSX syntax for diagnostics.
84
+ fileName: cleanFilename,
85
+ reportDiagnostics: true,
86
+ compilerOptions: { jsx: ts.JsxEmit.Preserve, target: ts.ScriptTarget.Latest }
87
+ }).diagnostics?.map(diagnostic => toCompilerDiagnostic(diagnostic, sourceFile, cleanFilename)) ?? []
88
+ const plugins = options.plugins ?? []
89
+ validatePlugins(plugins)
90
+
91
+ const context: CompilerContext = {
92
+ filename,
93
+ factory: ts.factory,
94
+ addRuntimeImport(name: string): void {
95
+ resolveHelperName(name)
96
+ },
97
+ helperRef
98
+ }
99
+
100
+ for (const plugin of plugins) plugin.analyze?.(sourceFile, context)
101
+ for (const plugin of plugins) {
102
+ sourceFile = plugin.transform?.program?.(sourceFile, context) ?? sourceFile
103
+ }
104
+ for (const plugin of plugins) sourceFile = transformPluginNodes(sourceFile, plugin, context)
105
+
106
+ const statements = sourceFile.statements.map(statement =>
107
+ ts.isImportDeclaration(statement) ? rebuildImport(statement) : transformStatement(statement)
108
+ )
109
+ const resultFile = ts.factory.updateSourceFile(sourceFile, [...createRuntimeImports(), ...statements])
110
+
111
+ const generated = ts.createPrinter().printFile(resultFile)
112
+ return {
113
+ code: generated,
114
+ map: buildSourceMap(filename, code, generated, resultFile),
115
+ diagnostics: [...diagnostics, ...compileDiagnostics]
116
+ }
117
+ }
118
+
119
+ function toCompilerDiagnostic(
120
+ diagnostic: ts.Diagnostic,
121
+ sourceFile: ts.SourceFile,
122
+ filename: string
123
+ ): CompilerDiagnostic {
124
+ const start = diagnostic.start ?? 0
125
+ const length = diagnostic.length ?? 1
126
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')
127
+ const { line, column, codeFrame } = buildCodeFrame(sourceFile, start, length)
128
+ return {
129
+ code: `VOBS_C${String(diagnostic.code).padStart(3, '0')}`,
130
+ severity: diagnostic.category === ts.DiagnosticCategory.Warning ? 'warning' : 'error',
131
+ message,
132
+ location: { file: filename, line, column },
133
+ codeFrame
134
+ }
135
+ }
136
+
137
+ function buildCodeFrame(
138
+ sourceFile: ts.SourceFile,
139
+ start: number,
140
+ length: number
141
+ ): { line: number; column: number; codeFrame: string } {
142
+ const position = sourceFile.getLineAndCharacterOfPosition(start)
143
+ const lineText = sourceFile.text.split(/\r?\n/u)[position.line] ?? ''
144
+ const markerLength = Math.max(1, Math.min(length, Math.max(1, lineText.length - position.character)))
145
+ return {
146
+ line: position.line + 1,
147
+ column: position.character + 1,
148
+ codeFrame: `${position.line + 1} | ${lineText}\n${' '.repeat(String(position.line + 1).length + 3 + position.character)}${'^'.repeat(markerLength)}`
149
+ }
150
+ }
151
+
152
+ /**
153
+ * 不支持的 JSX 标签形态(成员表达式 `<Foo.Bar>`、命名空间 `<svg:rect>` 等)。
154
+ * 诊断以 error 级返回,compile() 与 Vite 插件会直接失败,不再静默产出无效 DOM 标签。
155
+ */
156
+ function reportUnsupportedTag(tagName: ts.JsxTagNameExpression): void {
157
+ const sourceFile = tagName.getSourceFile() ?? currentSourceFile
158
+ if (!sourceFile) return
159
+ const label = tagName.getText()
160
+ const kindNote = tagName.kind === ts.SyntaxKind.JsxNamespacedName ? '(JSX 命名空间标签)' : ''
161
+ const { line, column, codeFrame } = buildCodeFrame(sourceFile, tagName.getStart(sourceFile), tagName.getWidth(sourceFile))
162
+ compileDiagnostics.push({
163
+ code: 'VOBS_C101',
164
+ severity: 'error',
165
+ message: `不支持的 JSX 标签形态:<${label}>${kindNote}。组件必须是大写开头的标识符,DOM 元素必须是小写标签名。`,
166
+ location: { file: currentFilename, line, column },
167
+ codeFrame,
168
+ fix: `把 <${label}> 改为 <Component /> 形式的组件或小写 DOM 标签;Fragment 请使用 <Fragment> 或 <>...</>。`
169
+ })
170
+ }
171
+
172
+ interface MappingSegment {
173
+ readonly genLine: number
174
+ readonly genCol: number
175
+ readonly srcLine: number
176
+ readonly srcCol: number
177
+ }
178
+
179
+ /**
180
+ * Build a real statement-level source map. The generated file is re-parsed and
181
+ * paired structurally with the compiled tree (statements map 1:1 inside every
182
+ * block), so each emitted statement points back to the JSX or original
183
+ * statement it was produced from.
184
+ */
185
+ function buildSourceMap(
186
+ filename: string,
187
+ source: string,
188
+ generated: string,
189
+ resultFile: ts.SourceFile
190
+ ): import('./plugin').VobsSourceMap {
191
+ const reparsed = ts.createSourceFile(filename, generated, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
192
+ const segments: MappingSegment[] = []
193
+ walkPairedTrees(resultFile, reparsed, reparsed, segments)
194
+ segments.sort((a, b) => a.genLine - b.genLine || a.genCol - b.genCol)
195
+ return {
196
+ version: 3,
197
+ file: filename,
198
+ sources: [filename],
199
+ sourcesContent: [source],
200
+ names: [],
201
+ mappings: encodeMappings(segments, generated.split('\n').length)
202
+ }
203
+ }
204
+
205
+ /** Statements only nest inside these container kinds. */
206
+ function statementLists(node: ts.Node): readonly ts.Statement[] | null {
207
+ if (ts.isSourceFile(node) || ts.isBlock(node) || ts.isModuleBlock(node)) return node.statements
208
+ if (ts.isCaseClause(node) || ts.isDefaultClause(node)) return node.statements
209
+ return null
210
+ }
211
+
212
+ /**
213
+ * Pair the compiled tree with the re-parsed generated tree node by node.
214
+ * Both trees were printed from the same AST, so their shapes are identical;
215
+ * any divergence (length mismatch) simply abandons that subtree.
216
+ */
217
+ function walkPairedTrees(
218
+ original: ts.Node,
219
+ generated: ts.Node,
220
+ reparsed: ts.SourceFile,
221
+ segments: MappingSegment[]
222
+ ): void {
223
+ const originalStatements = statementLists(original)
224
+ const generatedStatements = statementLists(generated)
225
+ if (originalStatements && generatedStatements) {
226
+ if (originalStatements.length !== generatedStatements.length) return
227
+ for (let index = 0; index < originalStatements.length; index++) {
228
+ const originalStatement = originalStatements[index]
229
+ const generatedStatement = generatedStatements[index]
230
+ recordSegment(originalStatement, generatedStatement, reparsed, segments)
231
+ walkPairedTrees(originalStatement, generatedStatement, reparsed, segments)
232
+ }
233
+ return
234
+ }
235
+
236
+ const originalChildren: ts.Node[] = []
237
+ const generatedChildren: ts.Node[] = []
238
+ ts.forEachChild(original, node => { originalChildren.push(node) })
239
+ ts.forEachChild(generated, node => { generatedChildren.push(node) })
240
+ if (originalChildren.length !== generatedChildren.length) return
241
+ for (let index = 0; index < originalChildren.length; index++) {
242
+ walkPairedTrees(originalChildren[index], generatedChildren[index], reparsed, segments)
243
+ }
244
+ }
245
+
246
+ function recordSegment(
247
+ originalStatement: ts.Statement,
248
+ generatedStatement: ts.Statement,
249
+ reparsed: ts.SourceFile,
250
+ segments: MappingSegment[]
251
+ ): void {
252
+ const source = statementSources.get(originalStatement) ?? positionOfOriginalStatement(originalStatement)
253
+ if (!source) return
254
+ const position = reparsed.getLineAndCharacterOfPosition(generatedStatement.getStart(reparsed))
255
+ segments.push({
256
+ genLine: position.line,
257
+ genCol: position.character,
258
+ srcLine: source.line,
259
+ srcCol: source.column
260
+ })
261
+ }
262
+
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)
267
+ )
268
+ return { line, column: character }
269
+ }
270
+
271
+ function encodeMappings(segments: readonly MappingSegment[], lineCount: number): string {
272
+ const lines: string[][] = Array.from({ length: lineCount }, () => [])
273
+ let prevGenLine = -1
274
+ let prevGenCol = 0
275
+ let prevSrcLine = 0
276
+ let prevSrcCol = 0
277
+ for (const segment of segments) {
278
+ if (segment.genLine !== prevGenLine) {
279
+ prevGenCol = 0
280
+ prevGenLine = segment.genLine
281
+ }
282
+ const values = [
283
+ segment.genCol - prevGenCol,
284
+ 0,
285
+ segment.srcLine - prevSrcLine,
286
+ segment.srcCol - prevSrcCol
287
+ ]
288
+ lines[segment.genLine].push(values.map(encodeVlq).join(''))
289
+ prevGenCol = segment.genCol
290
+ prevSrcLine = segment.srcLine
291
+ prevSrcCol = segment.srcCol
292
+ }
293
+ return lines.map(line => line.join(',')).join(';')
294
+ }
295
+
296
+ const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
297
+
298
+ function encodeVlq(value: number): string {
299
+ let encoded = value < 0 ? ((-value) << 1) | 1 : value << 1
300
+ let result = ''
301
+ do {
302
+ let digit = encoded & 31
303
+ encoded >>>= 5
304
+ if (encoded > 0) digit |= 32
305
+ result += base64Chars[digit]
306
+ } while (encoded > 0)
307
+ return result
308
+ }
309
+
310
+ function validatePlugins(plugins: readonly CompilerPlugin[]): void {
311
+ const names = new Set<string>()
312
+ for (const plugin of plugins) {
313
+ if (!plugin.name) throw new VobsError({ code: 'VOBS_C007', layer: 'compiler', message: '编译器插件必须提供 name', fix: '为插件添加稳定且唯一的 name。' })
314
+ if (names.has(plugin.name)) throw new VobsError({ code: 'VOBS_C007', layer: 'compiler', message: `检测到重复插件: ${plugin.name}`, fix: '为每个编译器插件使用唯一的 name。' })
315
+ names.add(plugin.name)
316
+ }
317
+ }
318
+
319
+ function transformPluginNodes(
320
+ sourceFile: ts.SourceFile,
321
+ plugin: CompilerPlugin,
322
+ context: CompilerContext
323
+ ): ts.SourceFile {
324
+ const transformNode = plugin.transform?.node ?? plugin.transformNode
325
+ if (!transformNode) return sourceFile
326
+
327
+ const transformer: ts.TransformerFactory<ts.SourceFile> = transformContext => root => {
328
+ const visit: ts.Visitor = node => {
329
+ const replacement = transformNode(node, context)
330
+ if (replacement === null) return undefined
331
+ return ts.visitEachChild(replacement ?? node, visit, transformContext)
332
+ }
333
+ return ts.visitNode(root, visit) as ts.SourceFile
334
+ }
335
+
336
+ const result = ts.transform(sourceFile, [transformer])
337
+ try {
338
+ return result.transformed[0]
339
+ } finally {
340
+ result.dispose()
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Collect every binding name declared anywhere in the source (imports, variables,
346
+ * functions, parameters, ... including nested scopes). If a helper name is bound
347
+ * in ANY scope, the injected runtime import must switch to an alias: generated
348
+ * references uniformly use the alias, so user code is never captured or duplicated.
349
+ */
350
+ function collectDeclaredNames(sourceFile: ts.SourceFile): Set<string> {
351
+ const names = new Set<string>()
352
+ const visit = (node: ts.Node): void => {
353
+ if (ts.isIdentifier(node) && isBindingName(node)) names.add(node.text)
354
+ ts.forEachChild(node, visit)
355
+ }
356
+ visit(sourceFile)
357
+ return names
358
+ }
359
+
360
+ /** 标识符是否为某个声明的绑定名(import、变量、函数、参数、类成员等)。 */
361
+ function isBindingName(node: ts.Identifier): boolean {
362
+ const parent = node.parent
363
+ if (!parent) return false
364
+ // 属性访问(document.createElement)与 JSX 属性名不是绑定名
365
+ if (ts.isPropertyAccessExpression(parent) && parent.name === node) return false
366
+ if (ts.isQualifiedName(parent) && parent.right === node) return false
367
+ if (ts.isJsxAttribute(parent)) return false
368
+ return (parent as { name?: ts.Node }).name === node
369
+ }
370
+
371
+ /**
372
+ * Resolve the reference name for a runtime helper. The plain name is kept when
373
+ * the source never binds it; otherwise a collision-free alias is allocated and
374
+ * the injected import uses the same alias (`import { createElement as _vobs_createElement }`).
375
+ */
376
+ function resolveHelperName(name: string): string {
377
+ const existing = helperAliases.get(name)
378
+ if (existing) return existing
379
+ let alias = name
380
+ if (takenNames.has(alias)) {
381
+ alias = `_vobs_${name}`
382
+ let suffix = 1
383
+ while (takenNames.has(alias)) alias = `_vobs_${name}_${suffix++}`
384
+ }
385
+ helperAliases.set(name, alias)
386
+ return alias
387
+ }
388
+
389
+ /** 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))
392
+ }
393
+
394
+ function createRuntimeImports(): ts.ImportDeclaration[] {
395
+ const modules = new Map<string, ts.ImportSpecifier[]>()
396
+ for (const [name, alias] of helperAliases) {
397
+ const module = name === 'insertResourceBoundary' ? '@vobs/resource' : '@vobs/vobs'
398
+ const imported = modules.get(module) ?? []
399
+ imported.push(ts.factory.createImportSpecifier(
400
+ false,
401
+ alias === name ? undefined : ts.factory.createIdentifier(name),
402
+ ts.factory.createIdentifier(alias)
403
+ ))
404
+ modules.set(module, imported)
405
+ }
406
+ return [...modules.entries()].map(([module, imported]) => ts.factory.createImportDeclaration(
407
+ undefined,
408
+ ts.factory.createImportClause(
409
+ false,
410
+ undefined,
411
+ ts.factory.createNamedImports(imported)
412
+ ),
413
+ ts.factory.createStringLiteral(module)
414
+ ))
415
+ }
416
+
417
+ function rebuildImport(node: ts.ImportDeclaration): ts.ImportDeclaration {
418
+ if (!ts.isStringLiteral(node.moduleSpecifier)) return node
419
+ return tagStatement(ts.factory.createImportDeclaration(
420
+ node.modifiers,
421
+ node.importClause,
422
+ ts.factory.createStringLiteral(node.moduleSpecifier.text),
423
+ node.attributes
424
+ ), node)
425
+ }
426
+
427
+ function transformStatement(node: ts.Statement): ts.Statement {
428
+ if (ts.isFunctionDeclaration(node) && node.body) {
429
+ return tagStatement(ts.factory.updateFunctionDeclaration(
430
+ node,
431
+ node.modifiers,
432
+ node.asteriskToken,
433
+ node.name,
434
+ node.typeParameters,
435
+ node.parameters,
436
+ node.type,
437
+ transformBlock(node.body)
438
+ ), node)
439
+ }
440
+
441
+ if (ts.isVariableStatement(node)) return transformVariableStatement(node)
442
+ if (ts.isExportAssignment(node) && containsJsx(node.expression)) {
443
+ return tagStatement(ts.factory.updateExportAssignment(node, node.modifiers, transformEmbeddedExpression(node.expression)), node)
444
+ }
445
+ if (ts.isExpressionStatement(node) && containsJsx(node.expression)) {
446
+ return tagStatement(ts.factory.updateExpressionStatement(node, transformEmbeddedExpression(node.expression)), node)
447
+ }
448
+ if (ts.isReturnStatement(node) && node.expression && containsJsx(node.expression)) {
449
+ return tagStatement(ts.factory.updateReturnStatement(node, transformEmbeddedExpression(node.expression)), node)
450
+ }
451
+ return node
452
+ }
453
+
454
+ function transformVariableStatement(node: ts.VariableStatement): ts.VariableStatement {
455
+ const declarations = node.declarationList.declarations.map(declaration => {
456
+ const initializer = declaration.initializer
457
+ if (!initializer || !containsJsx(initializer)) return declaration
458
+
459
+ return ts.factory.updateVariableDeclaration(
460
+ declaration,
461
+ declaration.name,
462
+ declaration.exclamationToken,
463
+ declaration.type,
464
+ transformEmbeddedExpression(initializer)
465
+ )
466
+ })
467
+
468
+ if (declarations.every((declaration, index) => declaration === node.declarationList.declarations[index])) {
469
+ return node
470
+ }
471
+
472
+ return tagStatement(ts.factory.updateVariableStatement(
473
+ node,
474
+ node.modifiers,
475
+ ts.factory.updateVariableDeclarationList(node.declarationList, declarations)
476
+ ), node)
477
+ }
478
+
479
+ function containsJsx(expression: ts.Expression): boolean {
480
+ let found = false
481
+ const visit = (node: ts.Node): void => {
482
+ if (isJsxExpression(node as ts.Expression)) {
483
+ found = true
484
+ return
485
+ }
486
+ ts.forEachChild(node, visit)
487
+ }
488
+ visit(expression)
489
+ return found
490
+ }
491
+
492
+ function transformBlock(block: ts.Block): ts.Block {
493
+ const statements = block.statements.map(statement => {
494
+ if (!ts.isReturnStatement(statement) || !statement.expression) return transformStatement(statement)
495
+ const expression = ts.isParenthesizedExpression(statement.expression)
496
+ ? statement.expression.expression
497
+ : statement.expression
498
+ return isJsxExpression(expression)
499
+ ? tagStatement(ts.factory.updateReturnStatement(statement, transformJsxExpression(expression)), statement)
500
+ : statement
501
+ })
502
+ return ts.factory.updateBlock(block, statements)
503
+ }
504
+
505
+ function isJsxExpression(node: ts.Expression): node is ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment {
506
+ return ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)
507
+ }
508
+
509
+ function transformJsxExpression(node: ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment): ts.Expression {
510
+ if (ts.isJsxFragment(node)) return transformFragment(node.children)
511
+ if (ts.isJsxElement(node)) {
512
+ return transformElement(node, node.openingElement.tagName, node.openingElement.attributes, node.children)
513
+ }
514
+ return transformElement(node, node.tagName, node.attributes, [])
515
+ }
516
+
517
+ function transformElement(
518
+ node: ts.JsxElement | ts.JsxSelfClosingElement,
519
+ tagName: ts.JsxTagNameExpression,
520
+ attributes: ts.JsxAttributes,
521
+ children: readonly ts.JsxChild[]
522
+ ): ts.Expression {
523
+ if (isFragmentTag(tagName)) return transformFragment(children)
524
+ if (!ts.isIdentifier(tagName)) {
525
+ // <Foo.Bar>、<svg:rect> 等形态此前会静默编译成无效 DOM 标签(createElement("Foo.Bar"))。
526
+ // 报结构化诊断后按原路径继续,保证产物结构稳定;compile()/Vite 插件会因 error 诊断直接失败。
527
+ reportUnsupportedTag(tagName)
528
+ }
529
+ if (ts.isIdentifier(tagName) && tagName.text === 'ResourceBoundary') {
530
+ return transformResourceBoundary(node, attributes, children)
531
+ }
532
+ if (ts.isIdentifier(tagName) && tagName.text === 'AsyncBoundary') {
533
+ return transformAsyncBoundary(node, attributes, children)
534
+ }
535
+ if (ts.isIdentifier(tagName) && tagName.text === 'ErrorBoundary') {
536
+ return transformErrorBoundary(node, attributes, children)
537
+ }
538
+ if (ts.isIdentifier(tagName) && tagName.text === 'Profiler') {
539
+ return transformProfiler(node, attributes, children)
540
+ }
541
+ if (ts.isIdentifier(tagName) && /^[A-Z]/.test(tagName.text)) {
542
+ return ts.factory.createCallExpression(
543
+ helperRef('createComponent'),
544
+ 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
+ ]
554
+ )
555
+ }
556
+
557
+ const elementName = tagName.getText()
558
+ const elementId = nextIdentifier('_el')
559
+ const statements: ts.Statement[] = [
560
+ createConstStatement(
561
+ elementId,
562
+ ts.factory.createCallExpression(
563
+ helperRef('createElement'),
564
+ undefined,
565
+ [ts.factory.createStringLiteral(elementName)]
566
+ ),
567
+ node
568
+ )
569
+ ]
570
+
571
+ appendAttributes(statements, elementId, attributes)
572
+ appendChildren(statements, elementId, children)
573
+ statements.push(tagStatement(ts.factory.createReturnStatement(elementId), node))
574
+
575
+ return ts.factory.createCallExpression(
576
+ ts.factory.createArrowFunction(
577
+ undefined,
578
+ undefined,
579
+ [],
580
+ undefined,
581
+ ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
582
+ ts.factory.createBlock(statements, true)
583
+ ),
584
+ undefined,
585
+ []
586
+ )
587
+ }
588
+
589
+ function isFragmentTag(tagName: ts.JsxTagNameExpression): boolean {
590
+ return tagName.getText() === 'Fragment' || tagName.getText() === 'Vobs.Fragment'
591
+ }
592
+
593
+ function transformFragment(children: readonly ts.JsxChild[]): ts.Expression {
594
+ const parent = nextIdentifier('_fragmentParent')
595
+ const anchor = nextIdentifier('_fragmentAnchor')
596
+ const statements: ts.Statement[] = []
597
+ appendChildren(statements, parent, children, anchor)
598
+ return ts.factory.createCallExpression(
599
+ helperRef('createFragment'),
600
+ undefined,
601
+ [ts.factory.createArrowFunction(
602
+ undefined,
603
+ undefined,
604
+ [
605
+ ts.factory.createParameterDeclaration(undefined, undefined, parent),
606
+ ts.factory.createParameterDeclaration(undefined, undefined, anchor)
607
+ ],
608
+ undefined,
609
+ ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
610
+ ts.factory.createBlock(statements, true)
611
+ )]
612
+ )
613
+ }
614
+
615
+ function transformResourceBoundary(
616
+ node: ts.JsxElement | ts.JsxSelfClosingElement,
617
+ attributes: ts.JsxAttributes,
618
+ children: readonly ts.JsxChild[]
619
+ ): ts.Expression {
620
+ const resource = getAttributeExpression(attributes, 'resource')
621
+ if (!resource) throw new VobsError({ code: 'VOBS_C002', layer: 'compiler', message: 'ResourceBoundary 必须提供 resource 属性', fix: '为 ResourceBoundary 添加 resource={resource}。' })
622
+ const options: ts.ObjectLiteralElementLike[] = [
623
+ ts.factory.createPropertyAssignment('resource', transformEmbeddedExpression(resource)),
624
+ ts.factory.createPropertyAssignment('children', createBoundaryFactory(children))
625
+ ]
626
+ appendBoundaryOptionalProperty(options, attributes, 'loading')
627
+ appendBoundaryOptionalProperty(options, attributes, 'empty')
628
+ appendBoundaryOptionalProperty(options, attributes, 'fallback')
629
+ return createBoundaryFragment(node, 'insertResourceBoundary', options)
630
+ }
631
+
632
+ function transformErrorBoundary(
633
+ node: ts.JsxElement | ts.JsxSelfClosingElement,
634
+ attributes: ts.JsxAttributes,
635
+ children: readonly ts.JsxChild[]
636
+ ): ts.Expression {
637
+ const fallback = getAttributeExpression(attributes, 'fallback')
638
+ 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))
642
+ ])
643
+ }
644
+
645
+ function transformAsyncBoundary(
646
+ node: ts.JsxElement | ts.JsxSelfClosingElement,
647
+ attributes: ts.JsxAttributes,
648
+ children: readonly ts.JsxChild[]
649
+ ): ts.Expression {
650
+ const promise = getAttributeExpression(attributes, 'promise')
651
+ if (!promise) throw new VobsError({ code: 'VOBS_C002', layer: 'compiler', message: 'AsyncBoundary 必须提供 promise 属性', fix: '为 AsyncBoundary 添加 promise={promise}。' })
652
+ const options: ts.ObjectLiteralElementLike[] = [
653
+ ts.factory.createPropertyAssignment('promise', transformEmbeddedExpression(promise)),
654
+ ts.factory.createPropertyAssignment('children', createAsyncFactory(children))
655
+ ]
656
+ appendBoundaryOptionalProperty(options, attributes, 'loading')
657
+ appendBoundaryOptionalProperty(options, attributes, 'fallback')
658
+ const resetKey = getAttributeExpression(attributes, 'resetKey')
659
+ if (resetKey) options.push(ts.factory.createPropertyAssignment('resetKey', createGetter(resetKey)))
660
+ return createBoundaryFragment(node, 'insertAsyncBoundary', options)
661
+ }
662
+
663
+ function transformProfiler(
664
+ node: ts.JsxElement | ts.JsxSelfClosingElement,
665
+ attributes: ts.JsxAttributes,
666
+ children: readonly ts.JsxChild[]
667
+ ): ts.Expression {
668
+ const idAttribute = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && attribute.name.getText() === 'id')
669
+ const id = idAttribute && ts.isJsxAttribute(idAttribute) && idAttribute.initializer && ts.isStringLiteral(idAttribute.initializer)
670
+ ? ts.factory.createStringLiteral(idAttribute.initializer.text)
671
+ : idAttribute && ts.isJsxAttribute(idAttribute) && idAttribute.initializer && ts.isJsxExpression(idAttribute.initializer)
672
+ ? idAttribute.initializer.expression
673
+ : null
674
+ if (!id) throw new VobsError({ code: 'VOBS_C002', layer: 'compiler', message: 'Profiler 必须提供 id 属性', fix: '为 Profiler 添加 id="ComponentName"。' })
675
+ const options: ts.ObjectLiteralElementLike[] = [
676
+ ts.factory.createPropertyAssignment('id', transformEmbeddedExpression(id)),
677
+ ts.factory.createPropertyAssignment('children', createBoundaryFactory(children))
678
+ ]
679
+ appendBoundaryOptionalProperty(options, attributes, 'onRender')
680
+ return createBoundaryFragment(node, 'insertProfiler', options)
681
+ }
682
+
683
+ function createBoundaryFragment(
684
+ node: ts.JsxElement | ts.JsxSelfClosingElement,
685
+ helper: 'insertResourceBoundary' | 'insertErrorBoundary' | 'insertAsyncBoundary' | 'insertProfiler',
686
+ options: readonly ts.ObjectLiteralElementLike[]
687
+ ): ts.Expression {
688
+ const parent = nextIdentifier('_boundaryParent')
689
+ const anchor = nextIdentifier('_boundaryAnchor')
690
+ return ts.factory.createCallExpression(
691
+ helperRef('createFragment'),
692
+ undefined,
693
+ [ts.factory.createArrowFunction(
694
+ undefined,
695
+ undefined,
696
+ [
697
+ ts.factory.createParameterDeclaration(undefined, undefined, parent),
698
+ ts.factory.createParameterDeclaration(undefined, undefined, anchor)
699
+ ],
700
+ undefined,
701
+ ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
702
+ ts.factory.createBlock([callStatement(helper, [
703
+ parent,
704
+ anchor,
705
+ ts.factory.createObjectLiteralExpression(options, true)
706
+ ], node)], true)
707
+ )]
708
+ )
709
+ }
710
+
711
+ function createBoundaryFactory(children: readonly ts.JsxChild[]): ts.ArrowFunction {
712
+ const content = transformFragment(children)
713
+ return ts.factory.createArrowFunction(
714
+ undefined,
715
+ undefined,
716
+ [],
717
+ undefined,
718
+ ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
719
+ content
720
+ )
721
+ }
722
+
723
+ function createAsyncFactory(children: readonly ts.JsxChild[]): ts.ArrowFunction {
724
+ const value = ts.factory.createIdentifier('value')
725
+ const expressionChild = children.length === 1 && children[0].kind === ts.SyntaxKind.JsxExpression
726
+ ? (children[0] as ts.JsxExpression).expression
727
+ : undefined
728
+ if (expressionChild && ts.isArrowFunction(expressionChild)) {
729
+ const transformed = transformEmbeddedExpression(expressionChild)
730
+ return transformed as ts.ArrowFunction
731
+ }
732
+ const content = transformFragment(children)
733
+ return ts.factory.createArrowFunction(undefined, undefined, [
734
+ ts.factory.createParameterDeclaration(undefined, undefined, value)
735
+ ], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), content)
736
+ }
737
+
738
+ function appendBoundaryOptionalProperty(
739
+ properties: ts.ObjectLiteralElementLike[],
740
+ attributes: ts.JsxAttributes,
741
+ name: string
742
+ ): void {
743
+ const expression = getAttributeExpression(attributes, name)
744
+ if (expression) {
745
+ const transformed = transformEmbeddedExpression(expression)
746
+ const value = isJsxExpression(unwrapExpression(expression))
747
+ ? createGetter(transformed)
748
+ : transformed
749
+ properties.push(ts.factory.createPropertyAssignment(name, value))
750
+ }
751
+ }
752
+
753
+ function getAttributeExpression(attributes: ts.JsxAttributes, name: string): ts.Expression | null {
754
+ for (const attribute of attributes.properties) {
755
+ if (!ts.isJsxAttribute(attribute) || attribute.name.getText() !== name) continue
756
+ if (attribute.initializer && ts.isJsxExpression(attribute.initializer)) {
757
+ return attribute.initializer.expression ?? null
758
+ }
759
+ }
760
+ return null
761
+ }
762
+
763
+ function transformEmbeddedExpression(expression: ts.Expression): ts.Expression {
764
+ const result = ts.transform(expression, [context => root => {
765
+ const visit: ts.Visitor = node => {
766
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) {
767
+ return transformJsxExpression(node)
768
+ }
769
+ return ts.visitEachChild(node, visit, context)
770
+ }
771
+ return ts.visitNode(root, visit) as ts.Expression
772
+ }])
773
+ try {
774
+ return result.transformed[0]
775
+ } finally {
776
+ result.dispose()
777
+ }
778
+ }
779
+
780
+ function appendAttributes(
781
+ statements: ts.Statement[],
782
+ element: ts.Identifier,
783
+ attributes: ts.JsxAttributes
784
+ ): void {
785
+ const staticProps: ts.ObjectLiteralElementLike[] = []
786
+ const hasSpread = attributes.properties.some(attribute => ts.isJsxSpreadAttribute(attribute))
787
+ for (const attribute of attributes.properties) {
788
+ if (ts.isJsxSpreadAttribute(attribute)) {
789
+ statements.push(callStatement('spreadProps', [element, transformEmbeddedExpression(attribute.expression)], attribute))
790
+ continue
791
+ }
792
+ if (!ts.isJsxAttribute(attribute)) continue
793
+ const name = attribute.name.getText()
794
+ if (name === 'key') continue
795
+ if (name === 'ref') {
796
+ const initializer = attribute.initializer
797
+ if (initializer && ts.isJsxExpression(initializer) && initializer.expression) {
798
+ statements.push(callStatement('setRef', [element, transformEmbeddedExpression(initializer.expression)], attribute))
799
+ }
800
+ continue
801
+ }
802
+ const initializer = attribute.initializer
803
+
804
+ if (name.startsWith('on') && initializer && ts.isJsxExpression(initializer) && initializer.expression) {
805
+ statements.push(callStatement('addEventListener', [
806
+ element,
807
+ ts.factory.createStringLiteral(name.slice(2).toLowerCase()),
808
+ initializer.expression
809
+ ], attribute))
810
+ continue
811
+ }
812
+
813
+ if (!initializer) {
814
+ 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))
816
+ continue
817
+ }
818
+ if (isPropertyAttribute(name)) staticProps.push(createStaticProperty(name, ts.factory.createTrue()))
819
+ else staticProps.push(createStaticProperty(name === 'className' ? 'class' : name, ts.factory.createStringLiteral('')))
820
+ continue
821
+ }
822
+ if (ts.isStringLiteral(initializer)) {
823
+ 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))
825
+ continue
826
+ }
827
+ staticProps.push(createStaticProperty(isPropertyAttribute(name) ? name : name === 'className' ? 'class' : name,
828
+ ts.factory.createStringLiteral(initializer.text)))
829
+ continue
830
+ }
831
+
832
+ const attributeName = name === 'className' ? 'class' : name
833
+ const propertyAttribute = isPropertyAttribute(name)
834
+ if (ts.isJsxExpression(initializer) && initializer.expression) {
835
+ statements.push(callStatement(propertyAttribute ? 'bindProperty' : 'bindAttribute', [
836
+ element,
837
+ ts.factory.createStringLiteral(propertyAttribute ? name : attributeName),
838
+ createGetter(initializer.expression)
839
+ ], attribute))
840
+ }
841
+ }
842
+ if (staticProps.length) statements.splice(1, 0, callStatement('setStaticProps', [
843
+ element,
844
+ ts.factory.createObjectLiteralExpression(staticProps, true)
845
+ ], attributes))
846
+ }
847
+
848
+ function createStaticProperty(name: string, value: ts.Expression): ts.PropertyAssignment {
849
+ return ts.factory.createPropertyAssignment(ts.factory.createStringLiteral(name), value)
850
+ }
851
+
852
+ function isPropertyAttribute(name: string): boolean {
853
+ return name === 'value' || name === 'checked' || name === 'selected' || name === 'disabled'
854
+ || name === 'multiple' || name === 'readOnly' || name === 'required'
855
+ || name === 'autofocus' || name === 'hidden' || name === 'tabIndex'
856
+ }
857
+
858
+ function appendChildren(
859
+ statements: ts.Statement[],
860
+ element: ts.Identifier,
861
+ children: readonly ts.JsxChild[],
862
+ anchor: ts.Expression = ts.factory.createNull()
863
+ ): void {
864
+ for (const child of children) {
865
+ if (ts.isJsxText(child)) {
866
+ const text = child.text.replace(/\s+/g, ' ').trimStart()
867
+ if (text.trim()) {
868
+ statements.push(callStatement('insertBefore', [
869
+ element,
870
+ ts.factory.createCallExpression(helperRef('createText'), undefined, [
871
+ ts.factory.createStringLiteral(text)
872
+ ]),
873
+ anchor
874
+ ], child))
875
+ }
876
+ continue
877
+ }
878
+
879
+ if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {
880
+ statements.push(callStatement('insertBefore', [
881
+ element,
882
+ transformJsxExpression(child),
883
+ anchor
884
+ ], child))
885
+ continue
886
+ }
887
+
888
+ if (child.kind === ts.SyntaxKind.JsxExpression) {
889
+ const expression = (child as ts.JsxExpression).expression
890
+ if (!expression) continue
891
+ const list = transformListExpression(element, expression, anchor)
892
+ if (list) {
893
+ statements.push(callStatement('insertList', list, child))
894
+ continue
895
+ }
896
+ const dynamic = transformDynamicExpression(expression)
897
+ if (dynamic) {
898
+ statements.push(callStatement('insertDynamic', [element, anchor, dynamic], child))
899
+ continue
900
+ }
901
+ 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))
906
+ continue
907
+ }
908
+ const value = transformEmbeddedExpression(expression)
909
+ statements.push(callStatement('insertDynamicValue', [element, anchor, createGetter(value)], child))
910
+ }
911
+ }
912
+ }
913
+
914
+ function createComponentProps(
915
+ attributes: ts.JsxAttributes,
916
+ children: readonly ts.JsxChild[]
917
+ ): ts.ObjectLiteralExpression {
918
+ const properties: ts.ObjectLiteralElementLike[] = []
919
+
920
+ for (const attribute of attributes.properties) {
921
+ if (ts.isJsxSpreadAttribute(attribute)) {
922
+ properties.push(ts.factory.createSpreadAssignment(attribute.expression))
923
+ continue
924
+ }
925
+
926
+ const name = propertyName(attribute.name.getText())
927
+ if (attribute.name.getText() === 'key') continue
928
+ const initializer = attribute.initializer
929
+ if (!initializer) {
930
+ properties.push(ts.factory.createPropertyAssignment(name, ts.factory.createTrue()))
931
+ } else if (ts.isStringLiteral(initializer)) {
932
+ properties.push(ts.factory.createPropertyAssignment(name, ts.factory.createStringLiteral(initializer.text)))
933
+ } else if (ts.isJsxExpression(initializer) && initializer.expression) {
934
+ properties.push(createGetterProperty(name, transformEmbeddedExpression(initializer.expression)))
935
+ }
936
+ }
937
+
938
+ const childExpressions = children.flatMap(childToComponentExpression)
939
+ if (childExpressions.length === 1) {
940
+ properties.push(createGetterProperty('children', childExpressions[0]))
941
+ } else if (childExpressions.length > 1) {
942
+ properties.push(createGetterProperty('children', ts.factory.createArrayLiteralExpression(childExpressions)))
943
+ }
944
+
945
+ return ts.factory.createObjectLiteralExpression(properties, true)
946
+ }
947
+
948
+ function createSourceLocation(node: ts.Node): ts.ObjectLiteralExpression {
949
+ const sourceFile = node.getSourceFile()
950
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
951
+ return ts.factory.createObjectLiteralExpression([
952
+ ts.factory.createPropertyAssignment('file', ts.factory.createStringLiteral(sourceFile.fileName)),
953
+ ts.factory.createPropertyAssignment('line', ts.factory.createNumericLiteral(position.line + 1)),
954
+ ts.factory.createPropertyAssignment('column', ts.factory.createNumericLiteral(position.character + 1))
955
+ ], true)
956
+ }
957
+
958
+ function transformDynamicExpression(expression: ts.Expression): ts.ArrowFunction | null {
959
+ if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
960
+ const right = unwrapExpression(expression.right)
961
+ if (!isJsxExpression(right)) return null
962
+ return createGetter(ts.factory.createConditionalExpression(
963
+ expression.left,
964
+ ts.factory.createToken(ts.SyntaxKind.QuestionToken),
965
+ transformJsxExpression(right),
966
+ ts.factory.createToken(ts.SyntaxKind.ColonToken),
967
+ ts.factory.createNull()
968
+ ))
969
+ }
970
+
971
+ if (ts.isConditionalExpression(expression)) {
972
+ const whenTrue = transformDynamicBranch(expression.whenTrue)
973
+ const whenFalse = transformDynamicBranch(expression.whenFalse)
974
+ if (!whenTrue && !whenFalse) return null
975
+ return createGetter(ts.factory.createConditionalExpression(
976
+ expression.condition,
977
+ ts.factory.createToken(ts.SyntaxKind.QuestionToken),
978
+ whenTrue ?? ts.factory.createNull(),
979
+ ts.factory.createToken(ts.SyntaxKind.ColonToken),
980
+ whenFalse ?? ts.factory.createNull()
981
+ ))
982
+ }
983
+
984
+ return null
985
+ }
986
+
987
+ function transformDynamicBranch(expression: ts.Expression): ts.Expression | null {
988
+ const branch = unwrapExpression(expression)
989
+ if (isJsxExpression(branch)) return transformJsxExpression(branch)
990
+ if (branch.kind === ts.SyntaxKind.NullKeyword || branch.kind === ts.SyntaxKind.FalseKeyword) return branch
991
+ return null
992
+ }
993
+
994
+ function transformListExpression(
995
+ parent: ts.Identifier,
996
+ expression: ts.Expression,
997
+ anchor: ts.Expression
998
+ ): ts.Expression[] | null {
999
+ if (!ts.isCallExpression(expression) || expression.arguments.length !== 1) return null
1000
+ if (!ts.isPropertyAccessExpression(expression.expression) || expression.expression.name.text !== 'map') return null
1001
+
1002
+ const callback = expression.arguments[0]
1003
+ if (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) return null
1004
+ const body = unwrapExpression(callback.body)
1005
+ if (!isJsxExpression(body) || ts.isJsxFragment(body)) return null
1006
+
1007
+ const key = findKeyExpression(body)
1008
+ const renderItem = transformListCallback(callback, transformJsxExpression(body))
1009
+ const args: ts.Expression[] = [
1010
+ parent,
1011
+ anchor,
1012
+ createGetter(expression.expression.expression),
1013
+ renderItem
1014
+ ]
1015
+ if (key) args.push(createKeyCallback(callback, key))
1016
+ return args
1017
+ }
1018
+
1019
+ function transformListCallback(
1020
+ callback: ts.ArrowFunction | ts.FunctionExpression,
1021
+ body: ts.Expression
1022
+ ): ts.Expression {
1023
+ if (ts.isArrowFunction(callback)) {
1024
+ return ts.factory.updateArrowFunction(
1025
+ callback,
1026
+ callback.modifiers,
1027
+ callback.typeParameters,
1028
+ callback.parameters,
1029
+ callback.type,
1030
+ callback.equalsGreaterThanToken,
1031
+ body
1032
+ )
1033
+ }
1034
+
1035
+ return ts.factory.updateFunctionExpression(
1036
+ callback,
1037
+ callback.modifiers,
1038
+ callback.asteriskToken,
1039
+ callback.name,
1040
+ callback.typeParameters,
1041
+ callback.parameters,
1042
+ callback.type,
1043
+ ts.factory.createBlock([ts.factory.createReturnStatement(body)], true)
1044
+ )
1045
+ }
1046
+
1047
+ function createKeyCallback(
1048
+ callback: ts.ArrowFunction | ts.FunctionExpression,
1049
+ key: ts.Expression
1050
+ ): ts.ArrowFunction {
1051
+ return ts.factory.createArrowFunction(
1052
+ undefined,
1053
+ undefined,
1054
+ callback.parameters,
1055
+ undefined,
1056
+ ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
1057
+ key
1058
+ )
1059
+ }
1060
+
1061
+ function findKeyExpression(node: ts.JsxElement | ts.JsxSelfClosingElement): ts.Expression | null {
1062
+ const attributes = ts.isJsxElement(node) ? node.openingElement.attributes : node.attributes
1063
+ for (const attribute of attributes.properties) {
1064
+ if (!ts.isJsxAttribute(attribute) || attribute.name.getText() !== 'key') continue
1065
+ if (attribute.initializer && ts.isJsxExpression(attribute.initializer)) {
1066
+ return attribute.initializer.expression ?? null
1067
+ }
1068
+ }
1069
+ return null
1070
+ }
1071
+
1072
+ function unwrapExpression(node: ts.Expression | ts.ConciseBody): ts.Expression {
1073
+ return ts.isParenthesizedExpression(node) ? node.expression : node as ts.Expression
1074
+ }
1075
+
1076
+ function childToComponentExpression(child: ts.JsxChild): ts.Expression[] {
1077
+ if (ts.isJsxText(child)) {
1078
+ const text = child.text.replace(/\s+/g, ' ').trim()
1079
+ return text ? [ts.factory.createStringLiteral(text)] : []
1080
+ }
1081
+ if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {
1082
+ return [transformJsxExpression(child)]
1083
+ }
1084
+ if (child.kind === ts.SyntaxKind.JsxExpression) {
1085
+ const expression = (child as ts.JsxExpression).expression
1086
+ return expression ? [transformEmbeddedExpression(expression)] : []
1087
+ }
1088
+ return []
1089
+ }
1090
+
1091
+ function propertyName(name: string): ts.PropertyName {
1092
+ return /^[$A-Z_a-z][$\w]*$/u.test(name)
1093
+ ? ts.factory.createIdentifier(name)
1094
+ : ts.factory.createStringLiteral(name)
1095
+ }
1096
+
1097
+ function createGetter(expression: ts.Expression): ts.ArrowFunction {
1098
+ return ts.factory.createArrowFunction(
1099
+ undefined,
1100
+ undefined,
1101
+ [],
1102
+ undefined,
1103
+ ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
1104
+ expression
1105
+ )
1106
+ }
1107
+
1108
+ function createGetterProperty(name: string | ts.PropertyName, expression: ts.Expression): ts.GetAccessorDeclaration {
1109
+ return ts.factory.createGetAccessorDeclaration(
1110
+ undefined,
1111
+ typeof name === 'string' ? propertyName(name) : name,
1112
+ [],
1113
+ undefined,
1114
+ ts.factory.createBlock([ts.factory.createReturnStatement(expression)], true)
1115
+ )
1116
+ }
1117
+
1118
+ function callStatement(name: string, args: ts.Expression[], source?: ts.Node): ts.ExpressionStatement {
1119
+ const statement = ts.factory.createExpressionStatement(
1120
+ ts.factory.createCallExpression(helperRef(name), undefined, args)
1121
+ )
1122
+ return source ? tagStatement(statement, source) : statement
1123
+ }
1124
+
1125
+ function createConstStatement(name: ts.Identifier, initializer: ts.Expression, source?: ts.Node): ts.VariableStatement {
1126
+ const statement = ts.factory.createVariableStatement(
1127
+ undefined,
1128
+ ts.factory.createVariableDeclarationList([
1129
+ ts.factory.createVariableDeclaration(name, undefined, undefined, initializer)
1130
+ ], ts.NodeFlags.Const)
1131
+ )
1132
+ return source ? tagStatement(statement, source) : statement
1133
+ }
1134
+
1135
+ /** 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)
1139
+ return statement
1140
+ }
1141
+
1142
+ function positionOfNode(node: ts.Node): SourcePosition | null {
1143
+ // ts.transform 产生的节点副本可能丢失 sourceFile 引用,回退到当前编译的源文件。
1144
+ const sourceFile = node.getSourceFile() ?? currentSourceFile
1145
+ if (!sourceFile || node.pos < 0) return null
1146
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
1147
+ return { line, column: character }
1148
+ }
1149
+
1150
+ function nextIdentifier(prefix: string): ts.Identifier {
1151
+ let name: string
1152
+ do {
1153
+ name = `${prefix}${generatedId++}`
1154
+ } while (takenNames.has(name))
1155
+ return ts.factory.createIdentifier(name)
1156
+ }