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