相信不少用 TypeScript 做全栈开发的朋友都有同感T3 Stack 这套组合Next.js TypeScript Tailwind CSS tRPC第一次用的时候是真香类型从数据库一路通到前端组件整个开发链路几乎不需要 any。但香归香新项目每开一次问题就重复一次——create-t3-app 生成的骨架是标准的目录结构、错误处理、数据库连接、API 统一返回格式这些项目级基础设施却不在它管的范围里。你只能复制上一个项目的代码再手动改一堆名字。次数多了人总会烦躁所以我干脆抽了一个周末写了 t3code 这个脚手架工具。t3code 的目标很直接在 create-t3-app 的基础上把常用的项目模板、依赖和约定化代码一键落进新项目里省掉每次开局两三个小时的重复劳动。这篇文章会把 t3code 的设计动机、核心实现和踩坑过程完整拆开来讲适合正在纠结要不要自己搞一套脚手架或者模板怎么维护才不烂尾的朋友。1. 从每次手动配一遍到一条命令生成项目t3code 的诞生背景1.1 T3 Stack 项目开局时最耗时间的三个环节先说我自己的真实经历。用 create-t3-app 初始化完一个新项目之后它给你的是干净的骨架Next.js 目录、trpc router、prisma schema、tailwind 配置。这些都对但离能直接开发业务还很远。我每次至少要补三块东西。第一块数据库接入的完整链路。create-t3-app 会问你用不用 Prisma但不会帮你把连接池、重试逻辑、开发环境数据库的启动脚本安排好。我习惯本地用 Docker 跑 PostgreSQL所以每次都要新建 docker-compose.yml写 .env 里的 DATABASE_URL再在 package.json 里加两个 scriptdb:up和db:init。这个环节至少二十分钟而且每次都是复制粘贴稍微改个端口就得查半天文档。第二块API 层的约定。不管是 tRPC 还是 REST真实项目里都需要统一返回结构、错误码、日志中间件。create-t3-app 给的 createTRPCContext 和 errorFormatter 只是最基础的空壳我需要自己往里面塞 requestId、统一异常映射、慢查询告警之类的逻辑。这部分代码一旦写成基本所有项目都不会再改但它就是没法被 create-t3-app 覆盖到。第三块前端基础设施。比如 fetch 封装、react-query 的全局配置、Toast 组件、布局组件、权限路由守卫。这些在项目之间高度相似却又是纯体力活。三块加起来一个新项目能跑业务之前差不多要额外花两三个小时。对我这种频繁开原型项目的人来说这个时间实在不划算。t3code 的出发点就是用一条命令把这些体力活自动化。1.2 t3code 的定位不是替代 create-t3-app而是它的增强层在动手之前我明确了一个原则不重复造 create-t3-app 的轮子。原因很简单create-t3-app 背后有一整个社区在维护Next.js、tRPC、Prisma 升级的时候它会及时跟进。我如果自己重新实现一套完整的项目初始化逻辑光是跟着上游版本跑就得累死。所以 t3code 的定位是增强层它仍然依赖 create-t3-app 的产出生成基础骨架之后再把自己维护的那套增强模板叠加进去。你可以把它理解成装修公司里铺地板和刷墙的活地基还是开发商打的。这样一来我的维护成本集中在增量部分统一的错误处理、数据库脚本、目录约定、CI 配置。这些内容变化频率低我可以完全掌控。这个定位决定了整个项目的架构走向t3code 不直接生成项目它做的是拉取基础模板 渲染增强层 执行后置脚本三件事后面所有设计都围绕这个流程展开。1.3 设计目标与范围边界定完定位我给 t3code 列了几个硬性目标写在了项目 README 最上面一条命令完成基础脚手架 增强模板 依赖安装 环境初始化全程交互式问答不超过 6 个问题。生成的项目必须能直接pnpm dev跑起来不需要再手改任何配置。模板内容用独立的 Git 仓库维护t3code 本体与模板内容解耦避免发版耦合。支持非交互模式--yes方便脚本化和 CI 测试。跨平台可用Windows、macOS、Linux 都要没问题。同时我明确划掉了几个不做的范围不做 monorepo 管理不做后端服务端的脚手架不做云平台一键部署。这些不是不重要是贪多必失先把单项目脚手架做到极致再说。2. 架构选型与项目布局CLI 本体、模板仓库、生成器三分离2.1 为什么单独维护模板仓库而不是把模板打进 CLI 包这是 t3code 架构上最重要的一个决定。我最早的想法特别朴素把模板文件直接放进 CLI 项目的 templates 目录打包发布到 npm用户装完 CLI 本地就有模板了。听起来很顺但实际操作一周后就发现不行。问题出在版本耦合上。CLI 版本是 0.x 快速迭代的但模板内容的更新频率比 CLI 高得多——比如我加了一个新的 API 错误处理中间件或者把 Prisma 连接池参数调了一下。如果每次改模板都要发一个 CLI 新版本npm 包版本号会涨得飞快而且用户得主动升级 CLI 才能拿到新模板太反直觉。后来我改成双仓库结构t3code 本体仓库只放 CLI 代码模板仓库 templates 单独维护。CLI 运行时直接从模板仓库拉取代码默认拉最新的也可以指定--template-version v1.2.0锁定版本。这样模板更新只动模板仓库CLI 不用每次发版用户拿到新模板也只需要重新跑一下命令。实测维护成本至少降了一半。除了解耦单独模板仓库还有一个附带好处我可以在模板仓库里放自己的测试项目直接跑 CI 验证模板在任何 Node 版本下都能构建。CLI 仓库和模板仓库各自有独立的 CI互不干扰。2.2 CLI 技术栈选型Commander、Inquirer、Handlebars选 CLI 技术栈这件事我纠结了几天最后定下来三个库都很成熟commander处理命令行参数和子命令。t3code 目前只有new、list、doctor三个子命令commander 完全够用。inquirer做交互式问答。它支持单选、多选、输入框、confirm 四种基础交互满足我的 6 个问题绰绰有余。handlebars模板渲染。选它是因为支持条件块、循环、自定义 helper而且语法简洁模板文件写起来不太费劲。这三个库加起来不到 5MB 依赖CLI 本身用 TypeScript 写编译成 CommonJS 发布。启动速度我测过交互模式从敲命令到出现第一个问题大概 300ms 左右体感算很快了。有一个小细节值得提inquirer 的跨平台交互依赖 readline在 Windows 上有时会出现箭头键错乱的问题。如果读者碰到先更新 inquirer 到 v9新版对 Windows Terminal 的支持好很多。我自己的开发机有 macOS 也有 Windows实测 v9 之后没再出过问题。2.3 仓库结构与模板组织方式CLI 仓库的 src 目录结构是这样的src/ commands/ new.ts # 新建项目主流程 list.ts # 列出可用模板分支 doctor.ts # 检查本地环境node 版本、pnpm、docker core/ prompter.ts # 交互问答定义 renderer.ts # 模板渲染核心 installer.ts # 依赖安装与后置脚本 gitHelper.ts # 拉取模板仓库 utils/ nameCase.ts # 项目名转各种命名风格 fsWalk.ts # 目录遍历 index.ts # CLI 入口模板仓库这边的组织方式我按版本分支 功能切片来管理。主干分支是最干净的基础增强模板只包含错误处理、目录约定、数据库脚本这些通用部分prisma、next-auth、eslint 增强这些属于可选切片每个切片是一个独立的目录生成时按用户的选择决定要不要复制进去。为什么不用 Git 分支来区分功能因为功能之间会组合。比如用户要prisma next-auth如果用分支就得维护 prisma-auth 这个组合分支如果将来再加一个 redis 切片组合数量会爆炸。用目录切片 复制逻辑组合关系由 CLI 代码控制模板仓库只需要维护互不影响的切片即可。这个设计让我后期加功能时轻松很多。3. 核心功能拆解交互问答、模板渲染、依赖安装的完整流程3.1 交互项设计哪些问题该问哪些不该问t3code 设计之初我给自己定了一条规矩交互问题不超过 6 个。问得越少越容易用脚本自动化问得越多用户越烦。我最后保留的 6 个问题分别是项目名称必填非交互模式下用参数指定包管理器npm / pnpm / yarn是否启用 Prisma PostgreSQL 基础设施是否启用 NextAuth 集成是否生成 CI 工作流GitHub Actions是否需要安装开发调试工具如 trpc playground、swagger-ui这些问题背后有一条默认逻辑凡是能在配置文件里写的绝对不问用户凡是能给合理默认值的先给默认值。比如是否需要 Docker 环境这个问题我直接默认需要因为本地开发用 Docker 跑数据库是绝大多数项目的配置真有人不需要他可以在生成后手动删掉 docker-compose.yml成本很低。这里有个反直觉的经验问题越少用户对你工具的信任感越强。我第一版问过 12 个问题后来自己用的时候都觉得烦果断砍到 6 个。现在每次生成项目从输入命令到项目跑起来基本稳定在 3-5 分钟。3.2 模板渲染机制占位符、条件块与命名转换模板渲染是 t3code 的核心。我的做法是模板仓库里的文件分两类一类是纯文本直接复制比如 .gitignore、.prettierrc另一类是含 Handlebars 语法的模板文件扩展名统一用 .hbs。举个例子docker-compose.yml.hbs 的内容长这样version: 3.8 services: db: image: postgres:16-alpine environment: POSTGRES_USER: {{dbUser}} POSTGRES_PASSWORD: {{dbPassword}} POSTGRES_DB: {{dbName}} ports: - {{dbPort}}:5432 volumes: - pgdata:/var/lib/postgresql/data volumes: pgdata:渲染的时候renderer 会扫描整个目标目录遇到 .hbs 文件就用 handlebars 渲染然后把输出文件名的 .hbs 后缀去掉遇到普通文件就直接拷贝。这里有个重要的细节自动拷贝目录时一定要跳过 .git、node_modules、dist 这类目录否则会把模板仓库的 Git 历史带进用户项目里产生一堆垃圾文件。命名转换我单独写了一个 nameCase 模块。因为用户输入的项目名通常是 kebab-case比如 my-blog-app但代码里需要几种不同的命名目录名、package.json 的 name保持 kebab-caseTS 变量名、常量名转成 camelCasemyBlogAppReact 组件名、类名转成 PascalCaseMyBlogApp环境变量前缀转成 SCREAMING_SNAKEMY_BLOG_APP这套转换逻辑在渲染时通过 handlebars helper 暴露给模板比如模板里写{{pascalCase projectName}}renderer 在注册 helper 时把 projectName 变量的值传进去就能在任意文件里自动换名字。模板里需要用到项目名的地方统一走这套 helper绝不用手写拼接。3.3 生成后的自动化流程装依赖、初始化数据库、提交 Git模板渲染完项目目录已经存在了接下来是收官三步。第一步安装依赖。这一步不用 npm而是根据用户选的包管理器执行pnpm install或npm install。安装过程用 spawn 子进程执行把 stdio 直接继承给用户终端这样用户能看到真实的安装进度而不是干等。选 pnpm 的原因除了快还有一个硬理由pnpm 的 symlink 结构对本地开发调试更友好生成的 lockfile 也更稳定。第二步初始化数据库。如果用户选了 Prisma我会在生成结束后依次执行prisma generate和docker compose up -d等数据库起来后用prisma db push把 schema 推进去。这一步最容易出问题所以我在代码里加了健康检查轮询 PostgreSQL 的 5432 端口最多等 30 秒起不来的话直接报错并提示用户检查 Docker 是否运行。第三步初始化 Git 仓库并做首次提交。很多人会忽略这一步但新项目没有 Git 历史后续开发体验会很难受。t3code 会在项目根目录执行git init git add -A git commit -m chore: init from t3code用户拿到手就是一个干净、有初始提交的仓库。这三步全部成功之后CLI 会打印一段简短的启动提示告诉你pnpm dev就可以开了。4. 落地过程中踩过的坑版本漂移、跨平台、命名规范4.1 模板仓库与脚手架版本漂移试过三种方案才找到平衡点第一个坑是版本漂移。create-t3-app 差不多每两三个月就会升一次主版本Next.js 一升级App Router 的写法可能变tRPC 的初始化代码也可能变。如果模板仓库里的基础骨架是三个月前拉的生成的代码在新的 create-t3-app 版本下可能直接编译失败。我一开始的方案是不锁版本每次生成时实时拉最新 create-t3-app 模板。听起来很灵活但实际用下来问题更大生成的结果不可复现。同一个 t3code 版本周一生成的项目和周五生成的项目底层骨架可能差了一个大版本后续排错非常头疼。现在的方案是在模板仓库里做版本快照 分支管理。每次 create-t3-app 大版本更新我在模板仓库开一个新的版本分支比如base-next-15、base-next-14并把对应的基础模板固定进去。t3code 默认取最新的稳定分支但用户可以在命令里用--base-version next-14指定旧版本。方案优点缺点结论实时拉最新模板永远最新结果不可复现排错困难放弃锁死一个版本最稳定升级成本高跟不上上游不推荐版本快照 分支管理兼顾可复现与可升级需要维护分支采用这个坑给我的教训是任何脚手架工具可复现性比最新版本重要得多。用户要的是一个能稳定工作的项目不是你帮他追新版本的义务。4.2 Windows 与 macOS 的路径和脚本差异一个小细节能省一天我主要开发环境在 macOS最早写 fsWalk 时直接用了 Node 的path.join大部分逻辑没问题。但坑出现在两个地方。第一个是模板仓库里的脚本。最开始我在模板里写了几个 shell 脚本比如scripts/dev-db.sh。这个脚本在 macOS 上跑得很欢Windows 上直接报错——因为 Windows 没有 bash也没有 /dev/null。后来我把所有 shell 脚本替换成 Node 脚本比如scripts/dev-db.mjs用 Node 的 child_process 去执行 docker compose跨平台立刻稳定了。如果你也在维护脚手架记住这个经验能在 Node 里做的事尽量不要依赖 shell 脚本。第二个是文件路径分隔符。我在渲染时需要把一些相对路径字符串写进 tsconfig 或者 import 语句最早直接用了 path.join 的结果在 Windows 上产出src\lib\trpc这种带反斜杠的路径放进 import 语句里直接报废。正确做法是路径统一转成正斜杠再写进代码文件比如path.join(...).split(path.sep).join(/)或者直接手动拼接正斜杠。这个坑很隐蔽不跨平台测一遍根本发现不了。4.3 包名、目录名与组件名的命名规范统一第三个坑是命名。用户输入的项目名五花八门有的带下划线有的带点有的甚至首字母大写。如果直接把My.Project塞进 package.json 的 name 字段npm 虽然能接受但有些工具链会炸。我的 nameCase 模块里加了一层规范化管道输入先转成 kebab-case所有非法字符转成连字符连续连字符压缩成一个首尾连字符去掉。校验合法性如果规范化后是空字符串直接提示用户重新输入。基于规范化的 kebab-case 再派生出 camelCase、PascalCase、SCREAMING_SNAKE 三种命名。这套管道处理之后之前遇到的诡异情况基本都消失了。唯一还留的一个已知限制camelCase 派生时如果项目名里含有连续两个以上的连字符转换结果可能不符合直觉比如my--app会变成myApp。所以我在生成时会顺带提醒用户项目名尽量用标准的 kebab-case别整花活。后来干脆在前端校验里直接拒绝包含连续连字符的输入省得后面解释。5. 验证与回归我如何保证 t3code 生成的项目开箱即用5.1 用临时目录批量生成的自动化测试脚手架工具最怕的就是你改了 A 功能B 功能悄悄挂了。我的做法是写一套批量生成测试脚本每次 CLI 代码有改动自动跑一遍。脚本的逻辑很简单在系统临时目录里创建 N 个测试目录每个目录用非交互模式生成一个不同配置组合的项目比如一组 pnpm prisma no-auth、一组 pnpm no-prisma auth然后对每个项目依次执行三条命令pnpm install pnpm exec tsc --noEmit pnpm exec next build三行命令全部通过这个配置组合才算合格。如果任何一个项目出现类型错误、构建失败CI 就标红。这套脚本救了我不下五次最典型的是有一次我改 renderer 的正则表达式普通文件复制路径偶尔会带上 .hbs 后缀导致next.config.hbs直接被当成配置文件构建直接崩了。没有批量测试这种问题很难被发现。另外我还在 CI 里加了 Node 版本矩阵Node 18、20、22 各跑一轮。因为模板里有些依赖对 Node 版本有要求提前在矩阵里抓到问题总比用户在自己机器上报错再找我来得强。5.2 人工验收清单生成项目后我还盯着看什么自动化测试只能覆盖能不能编译覆盖不了体验顺不顺。我自己每次给模板加一个新切片时还会按一份人工验收清单过一遍生成后的项目目录里有没有多余文件比如不小心拷进来的 .git、README 里残留的旧项目名。pnpm dev启动后首页能否正常渲染tRPC 的 health check 接口能否返回 200。数据库容器是否自动起来了Prisma 的 seed 脚本能否跑通。如果启用了 NextAuth登录页能否正常跳转session 能否写入数据库。错误处理中间件是否能正确返回统一错误结构而不是默认的堆栈。这份清单看起来很基础但真的能拦住很多自动化测不出来的体验问题。有一次我在模板里加了国际化配置自动化测试全绿但人工验收时发现浏览器语言切换之后布局错乱——因为那个组件里有个硬编码的宽度。这种问题只有人眼点开页面才看得出来。5.3 后续可以扩展的方向monorepo、插件机制、模板搜索t3code 目前的版本能用、够用但离理想中的状态还有距离。我列了三个最想做的方向。第一个是 monorepo 支持。很多产品项目不止一个应用会有 admin、web、api 分包。如果能用 t3code 一次生成 monorepo 结构比如用 pnpm workspace turbo会比单项目脚手架再手动拆分省事很多。第二个是插件机制。现在加新切片的方式是直接在模板仓库里加目录 在 CLI 代码里写复制逻辑本质上还是硬编码。如果做成插件机制让每个切片自带目录 渲染规则 后置脚本用户以后想贡献自己的模板就不用改 CLI 内核了。第三个是模板搜索与发现。类似 create-t3-app 的社区模板列表t3code 可以提供一个交互式浏览社区模板的命令让用户直接选择别人的项目模板来生成。这会把 t3code 从个人工具变成一个平台不过对维护和内容审核的要求也会高很多我倾向于先做好单项目体验再说。最后聊一点个人体会。t3code 这个项目让我最意外的收获不是省了多少时间而是它逼着我把项目基础设施这件事彻底想清楚了。以前每次手动配 Docker、错误处理、CI其实都是一种无意识的重复从来没有系统思考过我到底需要什么、为什么需要。当我必须把这些问题变成模板、变成代码、变成测试时我才意识到很多所谓的习惯其实只是懒得优化的默认路径。如果你也在纠结要不要自己搞一套脚手架我的建议是先别想着做通用平台先把自己的痛点写成脚本跑通两三个真实项目再决定要不要产品化。t3code 就是从一个小脚本长成现在这个样子的。工具不一定要多复杂能让你自己每周省下三小时就已经值回票价了。