t3code 是我从去年年底开始一直在打磨的一个命令行工具项目。起初只是觉得每次新建一个 TypeScript 项目都要重复那套“装依赖、配置 eslint、搭目录、写 tsconfig”的活实在太烦,后来干脆把它做成了一整套可交互的脚手架与项目管理工作流。t3code 这个名字听起来像 T3 Stack 的亲戚,确实受了不少启发,但定位不太一样:它不只是帮你初始化一个全栈项目,还管你日常的开发命令、模板复用和依赖追加。如果你也是那种“新建项目就头疼,配置环境就崩溃”的开发者,这篇复盘应该能帮你少走不少弯路。
t3code 解决的痛点很直接:项目模板散落、脚本命令记不住、团队新成员上手慢。核心能力是四个字,初始化、添加、运行、检查。你可以用它初始化一个带 Next.js、TailwindCSS、Prisma 的完整全栈项目,也能用它往现有项目里单独添加某个技术模块,还能把 dev/build/start 这类命令统一收口到 t3code dev 上。整个工具的核心逻辑基于 TypeScript + Node.js,配置走 ts 文件,模板支持变量注入,整体设计参考了 create-t3-app、shadcn/ui 和 Turborepo 的思路,但在日常维护上做得更重。
这篇文章我只讲三件事:为什么这个工具要这样设计、核心模块是怎么实现的、真实使用里有哪些文档里根本不会写的坑。适合对 CLI 工具开发、Node.js 生态和全栈工程化有兴趣的开发者,也适合想自建团队脚手架的人参考。
1. 为什么做 t3code:从“脚手架”到“工作流”
1.1 脚手架的泛滥与我的真实痛点
现在前端和 Node 生态里的脚手架工具多得数不过来。官方有 create-vite、create-next-app、create-t3-app,社区还有各种带 UI 的初始化器。工具多了以后问题反而更明显:每个工具都只负责项目生命周期的第一步,也就是初始化,到了后面加依赖、调配置、重装依赖的时候,你还是要回到原始命令。时间一长,项目里的 package.json scripts 越来越多,新人根本分不清pnpm dev、pnpm dev:web、pnpm dev:api这几个命令到底谁先跑。
我真正想解决的问题,是让“启动一个项目”这件事变得统一。不管是 clone 下来的仓库,还是刚用 t3code 生成的目录,都通过同一个命令入口去理解项目结构。t3code 本质上是在项目描述信息之上加了一层命令抽象,它不替代包管理器,也不替代构建工具,而是把它们编排起来。
1.2 t3code 的产品定位
t3code 的项目定位可以用一句话概括:它是 TypeScript 全栈项目的“总装车间”。初始化只是第一个环节,后续的模块添加、环境诊断、模板升级都属于这个工具的覆盖范围。工具本身分两层:
- 底层是模板引擎和文件生成器,负责把模板仓库复制到目标目录,并处理变量替换、文件重命名、依赖合并。
- 上层是命令编排层,通过读项目的 t3.config.ts 配置,把 dev/build/test/lint 这类动作统一转发给正确的子命令。
这个定位和我在团队里见到的场景完全吻合。后端同学拿到前端项目,最怕的不是代码看不懂,而是“我不知道在哪运行”。t3code 能把这个门槛降到最低,输入t3code dev就能按照配置依次拉起 API 服务和前端 Dev Server。
2. 核心命令与配置体系设计
2.1 命令设计:init、dev、add、doctor
t3code 的命令设计我打磨了好几轮,最终稳定在六个子命令:
| 命令 | 作用 | 说明 |
|---|---|---|
t3code init <name> | 初始化新项目 | 交互式询问技术栈,支持模板选择 |
t3code dev | 启动开发环境 | 根据配置文件依次启动依赖服务 |
t3code add [module] | 向现有项目添加模块 | 支持 zod、prisma、docker、eslint 等 |
t3code doctor | 环境诊断 | 检查 Node、包管理器、Docker 版本 |
t3code list | 查看模板与模块列表 | 用于确认当前可用内容 |
t3code update | 更新本地模板缓存 | 拉取最新模板仓库 |
init 命令看起来和 create-t3-app 差不多,但内部设计有本质区别。create-t3-app 是单次生成、生成完就退出;t3code 的 init 会把项目的配置信息持久化成 t3.config.ts,后续所有命令都依赖这个文件存在。这也意味着 t3code 不只是“初始化器”,它更像一个项目级的状态管理器。
2.2 配置文件 t3.config.ts 的设计逻辑
配置文件是整个工具的“项目蓝图”。init 结束后会生成类似这样的文件:
import { defineConfig } from 't3code/config' export default defineConfig({ name: 'my-blog', packageManager: 'pnpm', runtime: 'node20', stack: { framework: 'next', styling: 'tailwind-v4', orm: 'prisma', auth: 'authjs', }, scripts: { dev: 'pnpm --parallel dev', build: 'pnpm build', }, include: ['apps/**', 'packages/**'], })选 TypeScript 而不是 JSON 作为配置格式,是因为项目结构里经常需要表达式、条件逻辑和注释。JSON 写死了就只能改数据,而 ts 配置文件可以用process.env做环境判断,也方便批量复用。加载配置我用了 jiti 而不是 ts-node,主要原因是 jiti 对 Node 版本要求更低,启动更快,而且能够直接加载 ESM/CJS 混合模块。配置解析这块踩了很多坑,后面专门讲。
2.3 模板仓库如何管理依赖与变量
模板不是内置在 npm 包里的,而是独立成仓库,通过 t3code.json 描述元数据。这样的好处是模板能绕过 npm 发版,直接走 Git 更新。模板目录示例:
my-template/ template/ package.json tsconfig.json next.config.ts src/ pages/ {{name}}.tsx t3code.jsont3code.json 里声明模板接受的参数、默认依赖版本和变量列表。文件生成前,工具会读取template/下所有文件名和内容,把{{name}}、{{packageName}}这类占位符替换成用户输入。文件级别的重命名也走同样的逻辑,比如{{name}}.ts会被实际生成foo.ts。
3. 核心实现:从命令注册到文件生成
3.1 CLI 框架选型与入口设计
命令行框架我对比过 Commander、meow 和 cac。t3code 最终选的是 Commander,虽然它比 meow 啰嗦,但胜在子命令解析成熟、参数校验完善、中间件机制好用。入口文件保持最薄:
#!/usr/bin/env node import { Command } from 'commander' import { initCommand } from './commands/init.js' import { devCommand } from './commands/dev.js' import { addCommand } from './commands/add.js' import { doctorCommand } from './commands/doctor.js' const program = new Command() program .name('t3code') .description('TypeScript 全栈项目初始化与管理工作流') .version('0.5.0') program .command('init') .argument('<name>', '项目名称') .option('--template <template>', '指定模板名称') .option('--no-interactive', '跳过交互式提问') .action(initCommand) program .command('dev') .action(devCommand) program.parseAsync(process.argv)Commander 的.command()和.action()分离,让每个命令逻辑都能独立成模块,测试也好写。initCommand 是一个纯函数,接收项目名和 options,内部再去调核心生成器,这样能保证命令层不掺杂文件 IO 逻辑。
3.2 交互式提问与进度反馈
交互式 UI 用了 prompts 这个库,而不是更重的 Inquirer。prompts 的 Promise 风格 API 和 Commander 配合得更顺手。比如选择模板类型:
import prompts from 'prompts' const response = await prompts([ { type: 'select', name: 'templateType', message: 'Which template do you want to start with?', choices: [ { title: 'Fullstack App (Next.js + API)', value: 'fullstack' }, { title: 'API Only (Node + Hono)', value: 'api-only' }, { title: 'Library (TS Package)', value: 'library' }, ], }, ])这里有个很容易被忽略的问题:CI 环境里没有 TTY,prompts 会直接抛错。我的处理是给每个命令都加--no-interactive通道,如果检测到!process.stdin.isTTY就自动跳过提问,改用默认值或者从 options 里取值。这个设计在 GitHub Actions 里跑了上百次,稳定没出过问题。
3.3 文件生成引擎:复制、变量替换与 gitignore 处理
文件生成是 t3code 最核心的部分。逻辑拆成了三步:
第一步,遍历模板目录,建立完整文件清单。这一步用 fast-glob,保留相对路径,并过滤掉 node_modules、.git 和临时文件。
第二步,判断文件是否需要变量替换。文本文件走 chardet 检测编码后做替换,二进制文件则是字节级复制,避免模板管道的{{...}}在图片、字体上引发误替换。
第三步,写入目标目录。文件名和内容都经过 render 函数处理,例如{{name}}.test.ts会先转成my-app.test.ts,再把里面的{{NAME}}替换成大写变量。
这里我一开始犯过一个错:直接对整个文件做字符串替换,导致 PNG 图标头信息被破坏。后来在 t3code.json 里增加了binaryExtensions白名单,.png/.jpg/.woff2 这类后缀一律跳过模板渲染。
还有一个隐藏大坑:npm 发布包时会忽略.gitignore,如果模板目录中只有一个.gitignore,发布到 npm 之后这个文件会变成“凭空消失”。解决方案是在模板里放.npmignore和.gitignore两个文件,发布前用脚本把.gitignore重命名为_gitignore,生成时再恢复。
4. 实操全过程:用 t3code 初始化一个全栈博客
4.1 安装与第一个命令
先看安装。全局安装或临时执行都可以:
npm install -g t3code # 或者用 pnpm 不需要全局安装 pnpm dlx t3code init my-blog我个人推荐pnpm dlx t3code init my-blog这种方式,不会污染全局环境,而且每次都会拿到新版本。init 的交互流程大概是:
✔ Project name: my-blog ✔ Select template: Fullstack App (Next.js + API) ✔ Select ORM: Prisma ✔ Select Auth: Auth.js ✔ Install dependencies after generation? Yes这个流程我最终精简到最多 6 步,超过 6 步用户会明显烦躁。生成完成后,目录里会出现my-blog,带上完整的 src/app、prisma、src/server、eslintrc 等文件。
4.2 添加模块与依赖的实战用法
一个常见场景是:初学项目初始化时没带 ORM,跑了两天后发现需要数据库。按传统做法,你得自己手动安装 prisma,初始化 schema,还要改 tsconfig。t3code 的做法是:
cd my-blog t3code add prismaadd 命令会读取现有 t3.config.ts,确认项目还没装 prisma,然后再去模板仓库拉取 prisma 模块对应的文件,自动安装依赖,并更新配置里的 stack.orm。这里我没有重新生成整个项目,是因为 t3code 在 init 时建立了文件 hash 索引,能区分“用户改过的文件”和“模板生成的文件”,add 只追加不覆盖,用户的改动永远不会被冲掉。
4.3 通过 t3code dev 统一启动开发环境
项目里跑t3code dev的时候,工具先读取配置里的 scripts.dev 选项,再解析包含的 workspace。以博客项目为例,它可能会先启动 API 服务再启动前端,两个进程都挂在同一个终端下,Ctrl+C 能一次性退出。实现上使用的是 concurrently 的逻辑,但不是简单拼接命令,而是先检测端口占用,如果 3000 端口被占,就自动提示并改用 3001。
说实话,这个命令的代码量不大,但实际体验提升非常明显。它让一个新人只需要知道 t3code dev 这一个命令,就能跑起整个项目。
5. 真实开发中踩过的坑与排查实录
5.1 文件大小写问题导致的跨平台事故
模板仓库里有一个文件叫LogoComponent.tsx,在 macOS 上 init 没问题,但在 Linux 的 CI 环境里克隆下来就变成了logocomponent.tsx。根因是 Git 默认的 core.ignorecase 在不同平台表现不一致,文件在 macOS 写入后大小写丢失了。这个问题最致命的是:本地能跑,push 之后 CI 必挂。解决方法是两个:
- 模板仓库统一用 kebab-case 或 PascalCase 规范,禁止混用。
- CI 里执行
git config core.ignorecase false,并在文件存在性检查中增加大小写校验。
5.2 jiti 加载 TypeScript 配置的冲突
一开始我用 jiti 加载 t3.config.ts,结果在用户项目里频繁报错:Cannot find module '@t3code/config'。看报错是 node_modules 解析路径出了问题。原因是 jiti 默认从配置文件所在目录开始解析依赖,而 t3code 是全局安装的,全局路径下面找不到项目本地的 node_modules。解决办法是给 jiti 显式指定alias和interopDefault,把 t3code/config 直接映射到工具自带的类型包,加载配置只做类型获取,不实际执行项目里的业务代码。后来我甚至让 t3code 优先读取.json或.cjs配置,只有在文件后缀明确为 .ts 时才走 jiti。
5.3 保留用户自定义文件的合并策略
早期 t3code add 会无脑覆盖同名文件,为此丢过用户的大改内容。后来我加入了三向合并策略:如果目标文件是模板生成的,并且用户没改过,就直接覆盖;如果用户改动过,则自动生成.orig备份文件并把改动项合并到新文件里;如果模板文件本身更新了逻辑,则用关键词标记冲突位置。这个策略参考了 git merge 的 least-surprise 原则。用起来确实多了一层保障,再也没人抱怨“我的代码去哪了”。
6. 常见问题排查速查
这里整理了一份我在 issue 里处理过最多问题的速查表,基本覆盖了 t3code 日常使用的绝大多数异常场景。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| init 执行后目录为空 | 模板仓库未匹配或分支名不对 | 执行t3code list查看当前模板,确认默认分支不是 main 而是 master |
t3code dev报 ENOTDIR | 配置文件 scripts 里路径写错 | 检查 t3.config.ts 的 workspace 字段,路径必须相对项目根目录 |
| Node 版本报错 | 模板要求 Node 20,本机是 18 | 使用 nvm 切换后,执行t3code doctor验证 |
| add 命令卡在依赖安装 | 包管理器代理环境问题 | 设置 pnpm config set registry,再重试 |
| 生成的 .gitignore 消失 | npm 包发布过滤规则 | 更新到 0.5.x 以上版本,使用 _gitignore 机制 |
| 模板变量被错误替换到二进制文件 | 缺少 binaryExtensions 配置 | 在 t3code.json 加后缀白名单 |
| Windows 下 init 生成的文件名带问号 | Windows 文件系统不支持某些字符 | 项目名不要包含: * ? " < > | |
| 远程模板更新后本地不生效 | 模板缓存未失效 | 执行t3code update强制刷新缓存 |
| dev 命令启动了两个同一端口服务 | concurrently 未等待前一个进程启动 | 检查配置文件是否在 dev 脚本前加了wait-on |
排查思路一般从最简单的地方开始:先跑t3code doctor确认环境,再看 t3.config.ts 是否符合预期,最后查模板缓存目录。相比无头绪地重装依赖,这套顺序能省下至少一个小时。
最后说一个我在真实使用过程中总结出来的经验:t3code 这类工具,最大的价值不是减少敲命令的次数,而是把“项目长什么样”这个信息从人的记忆里搬到了配置文件里。任何一个人 clone 仓库后,不需要看 README 就知道怎么启动、有哪些模块、用什么包管理器。我后来把团队的 onboarding 文档从十几页简化成三行:“装好 Node 20,执行 pnpm dlx t3code init,或者对老项目直接 t3code dev。”这个工具对我来说已经不再是脚手架,而是一套能让项目自己解释自己的工程规范。