深入理解 ts-morph 项目中的代码生成(Emit)机制
什么是代码生成(Emit)
在 TypeScript 生态中,代码生成(Emit)是指将 TypeScript 源代码转换为 JavaScript 代码(生成 .js
文件)和类型声明文件(生成 .d.ts
文件)的过程。ts-morph 作为一个强大的 TypeScript AST 操作工具,提供了完整的代码生成功能,让开发者能够灵活控制整个编译输出流程。
基础代码生成示例
让我们从一个最简单的例子开始,了解如何使用 ts-morph 进行代码生成:
const project = new Project({
compilerOptions: {
outDir: "dist",
declaration: true
}
});
project.createSourceFile("MyFile.ts", "const num = 1;");
await project.emit(); // 异步生成
// 或者
project.emitSync(); // 同步生成(性能较低)
执行上述代码后,会在 dist
目录下生成两个文件:
// MyFile.js
var num = 1;
// MyFile.d.ts
declare const num = 1;
高级代码生成功能
1. 单文件生成
有时候我们只需要生成特定的源文件,而不是整个项目:
const sourceFile = project.getSourceFileOrThrow("MyFile.ts");
await sourceFile.emit(); // 异步生成单个文件
// 或者
sourceFile.emitSync(); // 同步生成单个文件
如果想直接获取生成内容而不写入文件系统:
const emitOutput = sourceFile.getEmitOutput();
console.log(emitOutput.getEmitSkipped()); // 检查是否跳过了生成
for (const outputFile of emitOutput.getOutputFiles()) {
console.log(outputFile.getFilePath()); // 输出文件路径
console.log(outputFile.getText()); // 输出文件内容
}
2. 仅生成声明文件
在某些场景下,我们可能只需要类型声明文件:
await project.emit({ emitOnlyDtsFiles: true });
3. 处理生成诊断信息
代码生成过程中可能会出现各种问题,检查诊断信息是必要的:
const emitResult = await project.emit();
for (const diagnostic of emitResult.getDiagnostics()) {
console.error(diagnostic.getMessageText()); // 输出错误信息
}
诊断信息会告诉你为什么某些文件没有被生成,这在调试时非常有用。
高级定制:自定义转换器
ts-morph 允许我们在代码生成过程中插入自定义转换逻辑,这为代码转换提供了极大的灵活性。
自定义转换器示例
下面的例子展示了如何将所有数字字面量转换为字符串字面量:
await project.emit({
customTransformers: {
before: [context => sourceFile =>
visitSourceFile(sourceFile, context, numericLiteralToStringLiteral)
],
after: [],
afterDeclarations: [],
},
});
// 辅助函数:遍历AST节点
function visitSourceFile(
sourceFile: ts.SourceFile,
context: ts.TransformationContext,
visitNode: (node: ts.Node, context: ts.TransformationContext) => ts.Node,
) {
return visitNodeAndChildren(sourceFile) as ts.SourceFile;
function visitNodeAndChildren(node: ts.Node): ts.Node {
return ts.visitEachChild(visitNode(node, context), visitNodeAndChildren, context);
}
}
// 转换逻辑:数字字面量转字符串
function numericLiteralToStringLiteral(node: ts.Node, context: ts.TransformationContext) {
if (ts.isNumericLiteral(node))
return context.factory.createStringLiteral(node.text);
return node;
}
在这个例子中,我们定义了三种转换器:
before
: 在标准JS转换前执行after
: 在标准JS转换后执行afterDeclarations
: 在声明文件生成后执行
内存中的代码生成
有时我们不想直接写入文件系统,ts-morph 提供了内存生成功能:
const project = new Project({ compilerOptions: { outDir: "dist" } });
project.createSourceFile("MyFile.ts", "const num = 1;");
const result = project.emitToMemory();
// 查看生成内容
for (const file of result.getFiles()) {
console.log("----");
console.log(file.filePath);
console.log("----");
console.log(file.text);
}
// 可选:将内存结果保存到文件系统
await result.saveFiles();
内存生成后的进一步处理
我们可以将内存生成的结果加载到新项目中进行进一步处理:
const result = project.emitToMemory();
const newProject = new Project();
// 加载生成的JS文件到新项目
for (const file of result.getFiles()) {
newProject.createSourceFile(file.filePath, file.text, { overwrite: true });
}
// 在这里可以对JS文件进行进一步操作
// ...
// 最后保存修改
await newProject.save();
不过需要注意的是,如果只是简单的转换需求,使用前面提到的自定义转换器性能会更好。
性能考虑
- 异步 vs 同步:优先使用
emit()
而非emitSync()
,因为异步生成不会阻塞事件循环 - 范围控制:只生成必要的文件,使用
sourceFile.emit()
而非整个项目生成 - 转换器选择:对于简单转换,优先使用
customTransformers
而非内存生成后处理
总结
ts-morph 提供了强大而灵活的代码生成功能,从简单的文件输出到复杂的内存操作和自定义转换,能够满足各种TypeScript代码生成需求。掌握这些功能可以帮助开发者构建更高效的TypeScript工具链和构建流程。
在实际项目中,建议根据具体需求选择最合适的生成策略,并始终检查生成诊断信息以确保生成过程的正确性。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考