@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/LICENSE +21 -0
- package/README.md +59 -0
- package/package.json +21 -0
- package/src/compile.test.ts +505 -0
- package/src/compile.ts +1156 -0
- package/src/i18n-extractor.ts +51 -0
- package/src/index.ts +19 -0
- package/src/plugin.ts +72 -0
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
|
+
}
|