@vobs/compiler 1.2.1 → 1.3.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/dist/compile.cjs +349 -254
- package/dist/compile.cjs.map +1 -1
- package/dist/compile.js +90 -23
- package/dist/compile.js.map +1 -1
- package/dist/i18n-extractor.cjs +45 -16
- package/dist/i18n-extractor.cjs.map +1 -1
- package/dist/i18n-extractor.js +6 -5
- package/dist/i18n-extractor.js.map +1 -1
- package/dist/index.cjs +361 -260
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +99 -28
- package/dist/index.js.map +1 -1
- package/dist/plugin.cjs +18 -2
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +6 -0
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.js +0 -2
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -2
- package/src/compile.test.ts +74 -0
- package/src/compile.ts +113 -17
- package/src/plugin.ts +6 -0
package/src/compile.test.ts
CHANGED
|
@@ -170,6 +170,80 @@ describe('compiler', () => {
|
|
|
170
170
|
expect(result).not.toContain('setAttribute(_el2, "key"')
|
|
171
171
|
})
|
|
172
172
|
|
|
173
|
+
it('编译嵌套三元的全部分支(不再只保留第一个分支)', () => {
|
|
174
|
+
const result = compile(`const el = <div>{flag ? <A /> : other ? <B /> : <C />}</div>`)
|
|
175
|
+
|
|
176
|
+
expect(result).toContain('insertDynamic')
|
|
177
|
+
// 三个分支全部编译为组件调用
|
|
178
|
+
expect(result.match(/createComponent\(resolveComponent/gu)).toHaveLength(3)
|
|
179
|
+
expect(result).not.toContain('React')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('编译 && 与嵌套动态节点组合', () => {
|
|
183
|
+
const result = compile(`const el = <div>{flag && (other ? <A /> : <B />)}</div>`)
|
|
184
|
+
|
|
185
|
+
expect(result).toContain('insertDynamic')
|
|
186
|
+
expect(result.match(/createComponent\(resolveComponent/gu)).toHaveLength(2)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('编译 if 块内的 JSX 早返回(不再泄漏到 React 降级路径)', () => {
|
|
190
|
+
const code = `
|
|
191
|
+
function App() {
|
|
192
|
+
if (items.value.length === 0) return <Empty />
|
|
193
|
+
return <div><Footer /></div>
|
|
194
|
+
}
|
|
195
|
+
`
|
|
196
|
+
const result = compile(code)
|
|
197
|
+
|
|
198
|
+
// 早返回分支与主 return 分支都编译为 vobs 组件调用,源码中不残留 JSX
|
|
199
|
+
expect(result.match(/createComponent\(resolveComponent/gu)).toHaveLength(2)
|
|
200
|
+
expect(result).not.toContain('<Empty')
|
|
201
|
+
expect(result).not.toContain('<Footer')
|
|
202
|
+
expect(result).not.toContain('React')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('编译函数体内部初始化器与嵌套函数中的 JSX', () => {
|
|
206
|
+
const code = `
|
|
207
|
+
function App() {
|
|
208
|
+
const render = () => <Inner />
|
|
209
|
+
if (cond.value) { slot = <Aside /> }
|
|
210
|
+
return <div>{render()}</div>
|
|
211
|
+
}
|
|
212
|
+
`
|
|
213
|
+
const result = compile(code)
|
|
214
|
+
|
|
215
|
+
expect(result.match(/createComponent\(resolveComponent/gu)).toHaveLength(2)
|
|
216
|
+
expect(result).not.toContain('<Inner')
|
|
217
|
+
expect(result).not.toContain('<Aside')
|
|
218
|
+
expect(result).not.toContain('React')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('hmrModuleId 将模块顶层 state 包装为 HMR 保鲜引用', () => {
|
|
222
|
+
const code = `import { state } from '@vobs/reactivity'
|
|
223
|
+
export const count = state(0)
|
|
224
|
+
function helper() {
|
|
225
|
+
const local = state(1)
|
|
226
|
+
return local.value
|
|
227
|
+
}
|
|
228
|
+
`
|
|
229
|
+
const result = compile(code, { hmrModuleId: 'src/stores/counter.ts' })
|
|
230
|
+
|
|
231
|
+
// 顶层声明被包装,函数内的局部声明不受影响
|
|
232
|
+
expect(result).toContain('hmrStateRef("src/stores/counter.ts#count"')
|
|
233
|
+
expect(result).toContain('() => state(0, "count")')
|
|
234
|
+
expect(result).not.toContain('hmrStateRef("src/stores/counter.ts#local"')
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('hmrModuleId 保留显式 debugName 且兼容别名导入', () => {
|
|
238
|
+
const code = `import { state as st } from '@vobs/reactivity'
|
|
239
|
+
export const width = st(50, 'doc.width')
|
|
240
|
+
`
|
|
241
|
+
const result = compile(code, { hmrModuleId: 'src/stores/doc.ts' })
|
|
242
|
+
|
|
243
|
+
expect(result).toContain('hmrStateRef("src/stores/doc.ts#width"')
|
|
244
|
+
expect(result).toContain("() => st(50, 'doc.width')")
|
|
245
|
+
})
|
|
246
|
+
|
|
173
247
|
it('在 JSX 转换前执行编译器插件的分析、程序与节点钩子', () => {
|
|
174
248
|
const filenames: string[] = []
|
|
175
249
|
const plugin: CompilerPlugin = {
|
package/src/compile.ts
CHANGED
|
@@ -39,6 +39,8 @@ interface CompileState {
|
|
|
39
39
|
localBindings: ReadonlySet<string>
|
|
40
40
|
/** 编译器自身产出的诊断(如不支持的 JSX 形态),与 TypeScript 解析诊断合并返回。 */
|
|
41
41
|
diagnostics: CompilerDiagnostic[]
|
|
42
|
+
/** HMR 模块标识:提供后模块顶层 state() 声明包装为 hmrStateRef,跨热更新保活信号。 */
|
|
43
|
+
hmrModuleId: string | null
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
interface SourcePosition {
|
|
@@ -101,7 +103,8 @@ export function compileWithSourceMap(code: string, options: CompileOptions = {})
|
|
|
101
103
|
sourceLocation: options.sourceLocation ?? true,
|
|
102
104
|
stateAliases: collectStateAliases(sourceFile),
|
|
103
105
|
localBindings: collectLocallyDeclaredNames(sourceFile),
|
|
104
|
-
diagnostics: []
|
|
106
|
+
diagnostics: [],
|
|
107
|
+
hmrModuleId: options.hmrModuleId ?? null
|
|
105
108
|
}
|
|
106
109
|
const cleanFilename = filename.split(/[?#]/u, 1)[0] || filename
|
|
107
110
|
const diagnostics = ts.transpileModule(code, {
|
|
@@ -130,16 +133,17 @@ export function compileWithSourceMap(code: string, options: CompileOptions = {})
|
|
|
130
133
|
for (const plugin of plugins) sourceFile = transformPluginNodes(sourceFile, plugin, context)
|
|
131
134
|
|
|
132
135
|
const statements = sourceFile.statements.map(statement =>
|
|
133
|
-
ts.isImportDeclaration(statement) ? rebuildImport(state, statement) : transformStatement(state, statement)
|
|
136
|
+
ts.isImportDeclaration(statement) ? rebuildImport(state, statement) : transformStatement(state, statement, true)
|
|
134
137
|
)
|
|
135
138
|
// 模板声明必须先于 runtime import 生成:声明里的 createTemplate 依赖
|
|
136
139
|
// helperRef 注册别名,import 需要在别名全部就绪后再构建。
|
|
137
140
|
const templateDeclarations = createTemplateDeclarations(state)
|
|
138
|
-
|
|
141
|
+
let resultFile = ts.factory.updateSourceFile(sourceFile, [
|
|
139
142
|
...createRuntimeImports(state),
|
|
140
143
|
...templateDeclarations,
|
|
141
144
|
...statements
|
|
142
145
|
])
|
|
146
|
+
resultFile = transformResidualJsx(state, resultFile)
|
|
143
147
|
|
|
144
148
|
const generated = ts.createPrinter().printFile(resultFile)
|
|
145
149
|
return {
|
|
@@ -490,7 +494,7 @@ function rebuildImport(state: CompileState, node: ts.ImportDeclaration): ts.Impo
|
|
|
490
494
|
), node)
|
|
491
495
|
}
|
|
492
496
|
|
|
493
|
-
function transformStatement(state: CompileState, node: ts.Statement): ts.Statement {
|
|
497
|
+
function transformStatement(state: CompileState, node: ts.Statement, moduleScope = false): ts.Statement {
|
|
494
498
|
if (ts.isFunctionDeclaration(node) && node.body) {
|
|
495
499
|
return tagStatement(state, ts.factory.updateFunctionDeclaration(
|
|
496
500
|
node,
|
|
@@ -504,7 +508,7 @@ function transformStatement(state: CompileState, node: ts.Statement): ts.Stateme
|
|
|
504
508
|
), node)
|
|
505
509
|
}
|
|
506
510
|
|
|
507
|
-
if (ts.isVariableStatement(node)) return transformVariableStatement(state, node)
|
|
511
|
+
if (ts.isVariableStatement(node)) return transformVariableStatement(state, node, moduleScope)
|
|
508
512
|
if (ts.isExportAssignment(node) && containsJsx(node.expression)) {
|
|
509
513
|
return tagStatement(state, ts.factory.updateExportAssignment(node, node.modifiers, transformEmbeddedExpression(state, node.expression)), node)
|
|
510
514
|
}
|
|
@@ -517,7 +521,7 @@ function transformStatement(state: CompileState, node: ts.Statement): ts.Stateme
|
|
|
517
521
|
return node
|
|
518
522
|
}
|
|
519
523
|
|
|
520
|
-
function transformVariableStatement(state: CompileState, node: ts.VariableStatement): ts.VariableStatement {
|
|
524
|
+
function transformVariableStatement(state: CompileState, node: ts.VariableStatement, moduleScope = false): ts.VariableStatement {
|
|
521
525
|
let changed = false
|
|
522
526
|
const declarations = node.declarationList.declarations.map(declaration => {
|
|
523
527
|
const initializer = declaration.initializer
|
|
@@ -526,6 +530,9 @@ function transformVariableStatement(state: CompileState, node: ts.VariableStatem
|
|
|
526
530
|
let nextInitializer = inferStateDebugName(state, declaration, initializer) ?? initializer
|
|
527
531
|
if (containsJsx(nextInitializer)) {
|
|
528
532
|
nextInitializer = transformEmbeddedExpression(state, nextInitializer)
|
|
533
|
+
} else if (moduleScope && state.hmrModuleId !== null) {
|
|
534
|
+
// HMR 状态保鲜仅限模块顶层:函数内局部 state 每次调用都应创建新信号
|
|
535
|
+
nextInitializer = wrapStateWithHmrRef(state, declaration, nextInitializer) ?? nextInitializer
|
|
529
536
|
}
|
|
530
537
|
if (nextInitializer === initializer) return declaration
|
|
531
538
|
|
|
@@ -578,7 +585,41 @@ function inferStateDebugName(
|
|
|
578
585
|
])
|
|
579
586
|
}
|
|
580
587
|
|
|
581
|
-
|
|
588
|
+
/**
|
|
589
|
+
* HMR 状态保鲜:模块热更新重执行时,模块级 state() 会创建全新信号实例,与未重执行的
|
|
590
|
+
* 导入方持有旧实例并存,形成"两份状态"(症状:编辑不生效、页面半边失灵,全量刷新也无法
|
|
591
|
+
* 消除)。开启 hmrModuleId 后,模块顶层的 state 声明改经运行时注册表取值:
|
|
592
|
+
* `const x = state(init)` → `const x = hmrStateRef(moduleId, 'x', () => state(init, 'x'))`。
|
|
593
|
+
* 首次执行照常创建;模块重执行时直接复用既有信号,模块逻辑(副作用、导出绑定)照常重跑。
|
|
594
|
+
*/
|
|
595
|
+
function wrapStateWithHmrRef(
|
|
596
|
+
state: CompileState,
|
|
597
|
+
declaration: ts.VariableDeclaration,
|
|
598
|
+
initializer: ts.Expression
|
|
599
|
+
): ts.Expression | undefined {
|
|
600
|
+
let call = initializer
|
|
601
|
+
while (ts.isParenthesizedExpression(call) || ts.isAsExpression(call) || ts.isTypeAssertionExpression(call) || ts.isSatisfiesExpression(call)) {
|
|
602
|
+
call = call.expression
|
|
603
|
+
}
|
|
604
|
+
if (!ts.isCallExpression(call)) return undefined
|
|
605
|
+
const callee = call.expression
|
|
606
|
+
if (!ts.isIdentifier(callee) || !state.stateAliases.has(callee.text)) return undefined
|
|
607
|
+
if (state.localBindings.has(callee.text)) return undefined
|
|
608
|
+
if (!ts.isIdentifier(declaration.name)) return undefined
|
|
609
|
+
return ts.factory.createCallExpression(helperRef(state, 'hmrStateRef'), undefined, [
|
|
610
|
+
ts.factory.createStringLiteral(`${state.hmrModuleId}#${declaration.name.text}`),
|
|
611
|
+
ts.factory.createArrowFunction(
|
|
612
|
+
undefined,
|
|
613
|
+
undefined,
|
|
614
|
+
[],
|
|
615
|
+
undefined,
|
|
616
|
+
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
|
617
|
+
initializer
|
|
618
|
+
)
|
|
619
|
+
])
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function containsJsx(expression: ts.Expression | ts.SourceFile): boolean {
|
|
582
623
|
let found = false
|
|
583
624
|
const visit = (node: ts.Node): void => {
|
|
584
625
|
if (isJsxExpression(node as ts.Expression)) {
|
|
@@ -895,6 +936,28 @@ function transformEmbeddedExpression(state: CompileState, expression: ts.Express
|
|
|
895
936
|
}
|
|
896
937
|
}
|
|
897
938
|
|
|
939
|
+
|
|
940
|
+
function transformResidualJsx(state: CompileState, sourceFile: ts.SourceFile): ts.SourceFile {
|
|
941
|
+
if (!containsJsx(sourceFile)) return sourceFile
|
|
942
|
+
const result = ts.transform(sourceFile, [context => root => {
|
|
943
|
+
const visit: ts.Visitor = node => {
|
|
944
|
+
if (ts.isReturnStatement(node) && node.expression && containsJsx(node.expression)) {
|
|
945
|
+
return ts.factory.updateReturnStatement(node, transformEmbeddedExpression(state, node.expression))
|
|
946
|
+
}
|
|
947
|
+
if (isJsxExpression(node as ts.Expression)) {
|
|
948
|
+
return transformJsxExpression(state, node as ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment)
|
|
949
|
+
}
|
|
950
|
+
return ts.visitEachChild(node, visit, context)
|
|
951
|
+
}
|
|
952
|
+
return ts.visitNode(root, visit) as ts.SourceFile
|
|
953
|
+
}])
|
|
954
|
+
try {
|
|
955
|
+
return result.transformed[0]
|
|
956
|
+
} finally {
|
|
957
|
+
result.dispose()
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
898
961
|
/**
|
|
899
962
|
* 静态元素判定:DOM 标签 + 全部属性为字符串字面量或无值 + 全部子节点为文本或递归静态元素。
|
|
900
963
|
* 保守排除项(语义或序列化等价性无把握,走原路径):
|
|
@@ -1194,38 +1257,71 @@ function createSourceLocation(node: ts.Node): ts.ObjectLiteralExpression {
|
|
|
1194
1257
|
}
|
|
1195
1258
|
|
|
1196
1259
|
function transformDynamicExpression(state: CompileState, expression: ts.Expression): ts.ArrowFunction | null {
|
|
1260
|
+
const converted = convertDynamicNodeExpression(state, expression)
|
|
1261
|
+
return converted ? createGetter(converted) : null
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* 把产出节点的动态表达式(`cond ? <A/> : <B/>`、`cond && <A/>`,含任意嵌套组合)
|
|
1266
|
+
* 转换为条件表达式树;各分支中的 JSX 递归编译为节点工厂,由 insertDynamic 挂载/卸载。
|
|
1267
|
+
* 返回 null 表示没有任何分支产出节点(纯文本/数值场景走 insertDynamicValue 文本绑定)。
|
|
1268
|
+
*/
|
|
1269
|
+
function convertDynamicNodeExpression(state: CompileState, expression: ts.Expression): ts.ConditionalExpression | null {
|
|
1197
1270
|
if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
1198
1271
|
const right = unwrapExpression(expression.right)
|
|
1199
|
-
if (
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1272
|
+
if (isJsxExpression(right)) {
|
|
1273
|
+
return createNodeConditional(expression.left, transformJsxExpression(state, right), null)
|
|
1274
|
+
}
|
|
1275
|
+
// 右侧是嵌套的动态节点表达式(如 cond && (sub ? <A/> : <B/>))时递归转换,
|
|
1276
|
+
// 转换失败(纯文本分支)则整体回落为动态值绑定,保持语义可静态判定。
|
|
1277
|
+
const convertedRight = convertDynamicNodeExpression(state, right)
|
|
1278
|
+
if (convertedRight) return createNodeConditional(expression.left, convertedRight, null)
|
|
1279
|
+
return null
|
|
1207
1280
|
}
|
|
1208
1281
|
|
|
1209
1282
|
if (ts.isConditionalExpression(expression)) {
|
|
1210
1283
|
const whenTrue = transformDynamicBranch(state, expression.whenTrue)
|
|
1211
1284
|
const whenFalse = transformDynamicBranch(state, expression.whenFalse)
|
|
1212
1285
|
if (!whenTrue && !whenFalse) return null
|
|
1213
|
-
return
|
|
1286
|
+
return ts.factory.createConditionalExpression(
|
|
1214
1287
|
expression.condition,
|
|
1215
1288
|
ts.factory.createToken(ts.SyntaxKind.QuestionToken),
|
|
1216
1289
|
whenTrue ?? ts.factory.createNull(),
|
|
1217
1290
|
ts.factory.createToken(ts.SyntaxKind.ColonToken),
|
|
1218
1291
|
whenFalse ?? ts.factory.createNull()
|
|
1219
|
-
)
|
|
1292
|
+
)
|
|
1220
1293
|
}
|
|
1221
1294
|
|
|
1222
1295
|
return null
|
|
1223
1296
|
}
|
|
1224
1297
|
|
|
1298
|
+
function createNodeConditional(
|
|
1299
|
+
condition: ts.Expression,
|
|
1300
|
+
whenTrue: ts.Expression,
|
|
1301
|
+
whenFalse: ts.Expression | null
|
|
1302
|
+
): ts.ConditionalExpression {
|
|
1303
|
+
return ts.factory.createConditionalExpression(
|
|
1304
|
+
condition,
|
|
1305
|
+
ts.factory.createToken(ts.SyntaxKind.QuestionToken),
|
|
1306
|
+
whenTrue,
|
|
1307
|
+
ts.factory.createToken(ts.SyntaxKind.ColonToken),
|
|
1308
|
+
whenFalse ?? ts.factory.createNull()
|
|
1309
|
+
)
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* 转换单个分支:JSX → 节点工厂;null/false 原样保留;嵌套的三元与 `&&`
|
|
1314
|
+
* 动态节点表达式递归转换(此前嵌套三元只编译第一个分支,其余分支被静默丢弃)。
|
|
1315
|
+
* 其余表达式(字符串、数值等)返回 null,由调用方回落为 null 分支。
|
|
1316
|
+
*/
|
|
1225
1317
|
function transformDynamicBranch(state: CompileState, expression: ts.Expression): ts.Expression | null {
|
|
1226
1318
|
const branch = unwrapExpression(expression)
|
|
1227
1319
|
if (isJsxExpression(branch)) return transformJsxExpression(state, branch)
|
|
1228
1320
|
if (branch.kind === ts.SyntaxKind.NullKeyword || branch.kind === ts.SyntaxKind.FalseKeyword) return branch
|
|
1321
|
+
if (ts.isConditionalExpression(branch)
|
|
1322
|
+
|| (ts.isBinaryExpression(branch) && branch.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)) {
|
|
1323
|
+
return convertDynamicNodeExpression(state, branch)
|
|
1324
|
+
}
|
|
1229
1325
|
return null
|
|
1230
1326
|
}
|
|
1231
1327
|
|
package/src/plugin.ts
CHANGED
|
@@ -41,6 +41,12 @@ export interface CompileOptions extends CompilerOptions {
|
|
|
41
41
|
* 默认 true。生产构建应传 false 以减小产物体积,省略后错误仍带组件名,定位走 source map。
|
|
42
42
|
*/
|
|
43
43
|
sourceLocation?: boolean
|
|
44
|
+
/**
|
|
45
|
+
* HMR 模块标识(dev 由 Vite 插件注入,通常为模块绝对路径)。提供后,模块顶层的
|
|
46
|
+
* state() 声明会包装为 hmrStateRef(...):热更新重执行模块时复用既有信号实例,
|
|
47
|
+
* 避免"新旧两份模块实例、两份状态"导致的页面半边失灵。
|
|
48
|
+
*/
|
|
49
|
+
hmrModuleId?: string
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
export interface VobsSourceMap {
|