My App
基础项目搭建

用 ts-morph 生成 TypeScript 代码

基于 ts-morph 以编程方式生成与写入 TS 文件

ts-morph 简介

在已有「commander + prompts + chalk + ora」的 CLI 上,使用 ts-morph 可以以编程方式创建、修改 TypeScript 源码并写回磁盘。它基于 TypeScript Compiler API,用面向对象的方式操作 AST,适合做代码生成、重构、批量修改等。

特点概览:

  • 创建/修改源码Project + createSourceFile / addSourceFileAtPath 等,无需手写字符串
  • 结构描述 — 用 StructureKind(如 EnumClass)描述节点,自动生成合法 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/cg
2. 导入 ts-morph 模块

src/cmd/root.ts 顶部增加 ProjectStructureKind 的导入。

src/cmd/root.ts
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) 辅助函数。

src/cmd/root.ts
const program = new Command()
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms))

// ... 其他代码
4. 在 action 中用 ts-morph 生成文件

在默认执行的逻辑里:先让用户输入文件名(不含 .ts);用 ora 分步展示「创建项目…」「生成代码…」「格式化代码…」「保存文件…」,每步用 delayspinner.text / spinner.color 更新提示;用 Project + createSourceFile 生成包含枚举与类的 TS 文件,再 formatText()saveSync(),最后 spinner.succeed。继续用 try/catch 对 ExitPromptError 静默退出。

src/cmd/root.ts
/**
 * 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. 完整代码示例
src/cmd/root.ts
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. 运行测试

在项目根目录执行(或按你项目入口调整):

run.sh
bun run packages/cg/src/cmd/root.ts

查看帮助:

bun run packages/cg/src/cmd/root.ts -h

预期效果: 先输出「代码生成cmd工具」;提示「输入文件名(不含 .ts)」;输入并回车后,依次出现「创建项目…」「生成代码…」「格式化代码…」(黄色)、「保存文件…」(绿色),最后 ✓「已生成 ./xxx.ts」。当前目录下会多出 xxx.ts,内容包含 MyEnumMyClass。若在输入时按 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.sh
git add .
git commit -m "feat: 使用 ts-morph 生成 TypeScript 代码文件"

On this page