用 ts-morph 生成 TypeScript 代码
基于 ts-morph 以编程方式生成与写入 TS 文件
ts-morph 简介
在已有「commander + prompts + chalk + ora」的 CLI 上,使用 ts-morph 可以以编程方式创建、修改 TypeScript 源码并写回磁盘。它基于 TypeScript Compiler API,用面向对象的方式操作 AST,适合做代码生成、重构、批量修改等。
特点概览:
- 创建/修改源码 —
Project+createSourceFile/addSourceFileAtPath等,无需手写字符串 - 结构描述 — 用
StructureKind(如Enum、Class)描述节点,自动生成合法 TS 代码 - 格式化 —
sourceFile.formatText()统一缩进与换行 - 同步/异步写入 —
saveSync()或save()写回文件 - 类型安全 — 完整 TypeScript 类型,适合在 CLI/脚本中做代码生成
常用概念:
Project— 管理一组源文件与编译选项的入口createSourceFile(filePath, structure, options)— 在内存中创建新文件(可overwrite: true)StructureKind.Enum/StructureKind.Class— 描述枚举、类等结构sourceFile.formatText()— 格式化当前文件sourceFile.saveSync()— 同步写入磁盘
初始化步骤
1. 安装 ts-morph
在项目根目录安装为开发依赖。官方仓库: ts-morph
npm install --save-dev ts-morph -w @repo/cg2. 导入 ts-morph 模块
在 src/cmd/root.ts 顶部增加 Project 与 StructureKind 的导入。
import { Command } from 'commander'
import { input } from '@inquirer/prompts'
import chalk from 'chalk'
import ora from 'ora'
import { Project, StructureKind } from 'ts-morph'
// ... 其他代码3. 添加 delay 辅助函数
后续步骤中会用短延迟配合 ora 展示「创建项目 → 生成代码 → 格式化 → 保存」的进度,可先加一个 delay(ms) 辅助函数。
const program = new Command()
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms))
// ... 其他代码4. 在 action 中用 ts-morph 生成文件
在默认执行的逻辑里:先让用户输入文件名(不含 .ts);用 ora 分步展示「创建项目…」「生成代码…」「格式化代码…」「保存文件…」,每步用 delay 与 spinner.text / spinner.color 更新提示;用 Project + createSourceFile 生成包含枚举与类的 TS 文件,再 formatText()、saveSync(),最后 spinner.succeed。继续用 try/catch 对 ExitPromptError 静默退出。
/**
* bun run src/cmd/root.ts
* bun run src/cmd/root.ts -h
*/
program
.name('code-gen')
.description('代码生成器 CLI 工具')
.version('1.0.0')
.action(async () => {
try {
console.log('代码生成cmd工具\n')
const answer = await input({ message: '输入文件名(不含 .ts)' })
const fileName = answer.trim() || 'output'
const filePath = `./${fileName}.ts`
const spinner = ora('创建项目...').start()
await delay(600)
spinner.text = '生成代码...'
const project = new Project()
const sourceFile = project.createSourceFile(
filePath,
{
statements: [
{
kind: StructureKind.Enum,
name: 'MyEnum',
members: [{ name: 'member' }],
},
{
kind: StructureKind.Class,
name: 'MyClass',
},
],
},
{ overwrite: true },
)
await delay(600)
spinner.text = chalk.yellow('格式化代码...')
spinner.color = 'yellow'
sourceFile.formatText()
await delay(600)
spinner.text = chalk.green('保存文件...')
spinner.color = 'green'
sourceFile.saveSync()
await delay(600)
spinner.succeed(chalk.green(`已生成 ${filePath}`))
} catch (err) {
// Ctrl+C 会触发 ExitPromptError,静默退出不打印堆栈
if (err instanceof Error && err.name === 'ExitPromptError') {
process.exit(0)
}
throw err
}
})
program.parse()说明: createSourceFile 的第二个参数用 statements 描述顶层节点,StructureKind.Enum / StructureKind.Class 会生成合法 TS;overwrite: true 表示若文件已存在则覆盖。formatText() 只改内存中的内容,saveSync() 才写入磁盘。
5. 完整代码示例
import { Command } from 'commander'
import { input } from '@inquirer/prompts'
import chalk from 'chalk'
import ora from 'ora'
import { Project, StructureKind } from 'ts-morph'
const program = new Command()
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms))
/**
* bun run src/cmd/root.ts
* bun run src/cmd/root.ts -h
*/
program
.name('code-gen')
.description('代码生成器 CLI 工具')
.version('1.0.0')
.action(async () => {
try {
console.log('代码生成cmd工具\n')
const answer = await input({ message: '输入文件名(不含 .ts)' })
const fileName = answer.trim() || 'output'
const filePath = `./${fileName}.ts`
const spinner = ora('创建项目...').start()
await delay(600)
spinner.text = '生成代码...'
const project = new Project()
const sourceFile = project.createSourceFile(
filePath,
{
statements: [
{
kind: StructureKind.Enum,
name: 'MyEnum',
members: [{ name: 'member' }],
},
{
kind: StructureKind.Class,
name: 'MyClass',
},
],
},
{ overwrite: true },
)
await delay(600)
spinner.text = chalk.yellow('格式化代码...')
spinner.color = 'yellow'
sourceFile.formatText()
await delay(600)
spinner.text = chalk.green('保存文件...')
spinner.color = 'green'
sourceFile.saveSync()
await delay(600)
spinner.succeed(chalk.green(`已生成 ${filePath}`))
} catch (err) {
// Ctrl+C 会触发 ExitPromptError,静默退出不打印堆栈
if (err instanceof Error && err.name === 'ExitPromptError') {
process.exit(0)
}
throw err
}
})
program.parse()6. 运行测试
在项目根目录执行(或按你项目入口调整):
bun run packages/cg/src/cmd/root.ts查看帮助:
bun run packages/cg/src/cmd/root.ts -h预期效果: 先输出「代码生成cmd工具」;提示「输入文件名(不含 .ts)」;输入并回车后,依次出现「创建项目…」「生成代码…」「格式化代码…」(黄色)、「保存文件…」(绿色),最后 ✓「已生成 ./xxx.ts」。当前目录下会多出 xxx.ts,内容包含 MyEnum 与 MyClass。若在输入时按 Ctrl+C,会静默退出。
7. 更多用法示例
// 在已有文件中追加接口
const project = new Project()
const file = project.addSourceFileAtPath('src/existing.ts')
file.addInterface({
name: 'MyInterface',
properties: [{ name: 'id', type: 'number' }],
})
file.saveSync()
// 仅在内存中生成代码字符串(不写文件)
const project = new Project()
const sf = project.createSourceFile('temp.ts', {
statements: [
{
kind: StructureKind.Enum,
name: 'Status',
members: [{ name: 'Ok' }, { name: 'Err' }],
},
],
})
console.log(sf.getFullText())Git 提交
git add .
git commit -m "feat: 使用 ts-morph 生成 TypeScript 代码文件"