TypeScript / JavaScript / Node 的语言层约定:运行时版本策略、包管理器与锁文件、tsconfig 严格选项、模块格式、lint 与格式化、单元测试与属性测试、运行时校验,以及查询层、迁移工具和 OpenAPI 生成器的选型。
TS-001硬约束检查:自动TypeScript / JavaScript / Node 生产环境的 Node 只用仍在维护期的 LTS 主线并跟进安全补丁;不用已停止维护、奇数号短期版本或预发布版本
怎么检查:CI 打印 node --version 并对照官方发布计划;容器镜像扫描标出停止维护的运行时
为什么、来源
为什么:停止维护的版本不再有安全修复;短期版本和预发布版本的支持期太短或不稳定。
- 新项目选当前活跃的 LTS;存量项目可以留在另一条仍在维护期的 LTS 上,但要在其停止维护前完成升级。
- Bun、Deno 上生产前,要在目标平台上验证所用的原生扩展、信号、子进程和文件系统行为。
TS-002硬约束检查:自动TypeScript / JavaScript / Node 只提交一种锁文件,CI 用冻结锁文件的方式安装(pnpm install --frozen-lockfile 或 npm ci)
怎么检查:CI 检查仓库里只有一种锁文件;安装命令带冻结参数,锁文件与清单不一致时失败
为什么、来源
为什么:混用包管理器或不冻结安装,每次装到的依赖都可能不同,问题无法复现,也给投毒版本留了口子。
TS-003软指导检查:自动TypeScript / JavaScript / Node package.json 用 engines 声明支持的 Node 范围,用 packageManager 写包管理器的精确版本
怎么检查:脚本检查两个字段存在,且 CI、镜像里的版本落在声明范围内
为什么、来源
为什么:只在 README 里写版本没人看;写进清单后,版本管理工具和 CI 才能读同一份。
本机用 mise 等版本管理工具读取同一份声明,不在多个地方各写一遍。
TS-004硬约束检查:半自动TypeScript / JavaScript / Node 构建和 CI 里不临时安装 latest 版本的工具,不用 npx 包名@latest 这类不锁版本的调用
怎么检查:在 CI 配置、Dockerfile、脚本里搜索 @latest、npm install -g 和不带版本的 npx,评审确认
为什么、来源
为什么:不锁版本的工具每次构建都可能变,而且绕过了锁文件和冷却期,是供应链攻击的常见入口。
TS-005硬约束检查:自动TypeScript / JavaScript / Node 依赖的安装脚本默认不执行,只对审查过的包逐个放行;禁止整体放开
怎么检查:pnpm 保持 strictDepBuilds 开启,用 allowBuilds 列白名单;CI 检查没有 dangerouslyAllowAllBuilds;白名单文件写进 CODEOWNERS
为什么、来源
为什么:安装脚本能在开发机和 CI 上执行任意代码,近年的 npm 投毒事件大多经由它窃取凭据。
- pnpm 新版默认就不执行未放行的安装脚本,且装到未放行的包会失败;不要为了省事把它关掉。
- 用 npm 的项目用
--ignore-scripts 安装,再对需要编译的包单独执行。
- 安装依赖的 CI 步骤不持有发布令牌和生产凭据。
TS-006软指导检查:自动TypeScript / JavaScript / Node 开启发布冷却期:刚发布的新版本过一段时间才允许安装;紧急安全修复用精确版本单独放行
怎么检查:检查 pnpm 配置里的 minimumReleaseAge 没有被设为 0;例外写进 minimumReleaseAgeExclude 并经评审
参考值:pnpm 新版默认 1440 分钟(1 天);风险高的项目可以设得更长,这是参考做法,没有统一标准。
为什么、来源
为什么:被投毒的版本通常在发布后几小时内被发现并撤下,冷却期能避开这段窗口。
TS-007硬约束检查:自动TypeScript / JavaScript / Node供别人引用的库或 SDK 发布到 npm 的包用 OIDC 可信发布并生成来源证明,CI 里不保存长期有效的 npm 令牌
怎么检查:检查发布工作流使用可信发布配置、带来源证明;仓库和 CI 密钥里没有 npm 长期令牌
为什么、来源
为什么:长期令牌一旦泄漏,攻击者就能以你的名义发布恶意版本。
TS-008软指导检查:自动TypeScript / JavaScript / Node供别人引用的库或 SDK 库发布前用 npm pack 打包,在一个干净的项目里安装,验证导入、类型和最低支持的 Node 版本
怎么检查:CI 任务:打包、在空目录安装、分别用 ESM 导入和类型检查,在最低和最新 Node 版本上各跑一次
为什么、来源
为什么:在单仓里能用不代表发出去能用,漏打包文件、exports 写错只有在真实消费方那里才暴露。
TS-009硬约束检查:自动TypeScript / JavaScript / Node tsconfig 显式写 strict 为 true,不靠默认值;不得为了通过检查关掉 strict 的子项
怎么检查:脚本检查生效的 tsconfig(tsc --showConfig)里 strict 为 true,且没有把 strictNullChecks 等子项设为 false
为什么、来源
为什么:TypeScript 7 改了多项默认值,不同版本、不同工具读到的默认值可能不同;关掉 strict 子项会把空值和隐式 any 错误放到运行时。
TS-010软指导检查:自动TypeScript / JavaScript / Node 新项目在 strict 之外再开启 noUncheckedIndexedAccess、noImplicitOverride、noFallthroughCasesInSwitch、verbatimModuleSyntax,并显式写 types
怎么检查:脚本检查 tsconfig 中这些选项
为什么、来源
为什么:这几项能抓到下标越界、漏写 override、switch 穿透、类型导入混进运行时等常见错误,代价很小。
types 在新版中默认不再自动包含所有 @types 包,Node 项目写 ["node"],测试目录按需补测试库的类型。
exactOptionalPropertyTypes 更严格,存量项目开启改动面大,可按模块逐步开。
- 大项目可以保留
skipLibCheck: true 以控制检查时间,但要知道它会跳过依赖的声明文件错误。
TS-011硬约束检查:自动TypeScript / JavaScript / Node CI 必须单独跑一次类型检查(tsc --noEmit);Node 直接运行 .ts、esbuild、tsx、Vite 编译成功都不算类型通过
怎么检查:CI 中有独立的类型检查步骤,失败即阻断合并
为什么、来源
为什么:这些工具只把类型擦掉再运行,不检查类型;不单独检查,类型错误会一路带到生产。
TS-012硬约束检查:自动TypeScript / JavaScript / Node 直接用 Node 运行 .ts 的项目必须开启 erasableSyntaxOnly 和 verbatimModuleSyntax,不用 enum、带运行时代码的 namespace、参数属性,类型导入写 import type
怎么检查:tsconfig 开启这两项后,tsc --noEmit 会直接报出不可擦除的语法和缺少 type 的类型导入
为什么、来源
为什么:Node 只会把类型语法替换成空白,遇到需要生成代码的语法会报错,没写 type 的类型导入会被当成值导入,运行时失败。
- Node 不读 tsconfig,所以
paths 别名、target 等设置对它无效;也不会处理 node_modules 里的 .ts 文件。
- enum 改用
as const 对象加联合类型;参数属性改成显式字段赋值。
TS-013软指导检查:半自动TypeScript / JavaScript / Node 测试、脚本和工具配置文件也纳入类型检查,不用 exclude 把它们排除在外
怎么检查:检查 tsc --listFilesOnly 的输出包含测试和脚本目录;需要不同设置时用单独的 tsconfig 并在 CI 里一起检查
为什么、来源
为什么:构建配置排除测试目录很常见,但如果检查也用同一份配置,测试代码就从没被检查过。
TS-014软指导检查:自动TypeScript / JavaScript / Node 不用 any、非空断言 ! 和 as 绕过类型错误;必须抑制时用 @ts-expect-error 写明原因,禁用 @ts-ignore 和 @ts-nocheck
怎么检查:开启 lint 的相应规则(Biome 的 noExplicitAny、noNonNullAssertion,typescript-eslint 的 ban-ts-comment 等);例外按 02 的要求写理由
为什么、来源
为什么:这些写法让编译器闭嘴,错误照样会在运行时出现,而且以后没人知道这里本来有问题。
未知结构的数据用 unknown 接住,再经校验收窄;生成的代码目录可以在专门的例外配置里排除。
TS-015硬约束检查:半自动TypeScript / JavaScript / Node 来自外部的数据(HTTP 请求、环境变量、消息、文件、数据库 JSON 列、第三方和模型返回的结果)进入业务前,用运行时校验库解析,不得只用 as 声明类型
怎么检查:lint 找候选(JSON.parse 或 response.json() 后直接 as、typescript-eslint 的 no-unsafe-* 规则);评审检查每个入口都有 schema 解析
为什么、来源
为什么:TypeScript 的类型在运行时全部擦除,外部数据长什么样只有运行时检查才知道;从 OpenAPI 生成的类型同样不做运行时检查。
- 一条链路选一种校验方式:用 Fastify 的就用它的 JSON Schema 校验,否则用 Zod;不要同一个入口校验两遍。
- 校验失败返回明确的错误,不把原始输入写进日志。
TS-016软指导检查:自动TypeScript / JavaScript / Node 配置只在一个入口模块里读取环境变量并用 schema 解析,其他代码从这个模块取值,不直接读 process.env
怎么检查:lint 禁止在配置模块以外使用 process.env(如 Biome 的 noProcessEnv 或 ESLint 的 n/no-process-env)
为什么、来源
为什么:到处读 process.env 无法在启动时一次性发现缺失和格式错误的配置,也查不清有哪些配置项。
TS-017软指导检查:自动TypeScript / JavaScript / Node 新项目统一用 ESM:package.json 设 "type": "module";Node 直接运行的代码用 NodeNext 模块解析,相对导入写文件扩展名
怎么检查:检查 package.json 和 tsconfig 的模块设置;tsc --noEmit 在 NodeNext 下会报出缺扩展名的相对导入
为什么、来源
为什么:ESM 是 JavaScript 的标准模块格式;CJS 和 ESM 混用时的互操作问题很难排查。
- 源码里写
./x.ts 时,用 rewriteRelativeImportExtensions 让编译输出变成 ./x.js。
- 由打包工具处理的前端代码可以用
bundler 模块解析,见 07。
TS-018软指导检查:自动TypeScript / JavaScript / Node 不依赖 tsconfig 的 paths 别名在运行时生效;需要内部别名时用 package.json 的 imports(以 # 开头的子路径导入)
怎么检查:检查 tsconfig 是否配置了 paths;有的话在评审时确认运行时有对应的解析方式
为什么、来源
为什么:tsconfig 的 paths 只影响类型检查,Node 不认识,构建输出能过检查却在运行时找不到模块。
TS-019软指导检查:半自动TypeScript / JavaScript / Node供别人引用的库或 SDK 库默认只发布 ESM,用 exports 字段声明入口和类型文件;确有 CJS 使用方时才发布双格式
怎么检查:检查 package.json 的 exports;双格式包要说明使用方并在 TS-008 的打包测试里覆盖两种导入
为什么、来源
为什么:双格式包容易出现同一个模块被加载两份、类型和实现对不上的问题。
TS-020硬约束检查:自动TypeScript / JavaScript / Node 仓库只用一个格式化工具(默认 Biome),CI 用检查模式运行,有差异即失败
怎么检查:CI 运行 biome ci .(或用 Prettier 的项目运行 prettier --check .);检查没有同时配置第二个格式化工具
为什么、来源
为什么:两个格式化工具会互相改对方的输出,每次提交都出现无意义的差异。
TS-021硬约束检查:自动TypeScript / JavaScript / Node 每个 Promise 都要 await、return,或显式用 void 标记并处理错误;用 lint 的漏 await 规则检查
怎么检查:开启 typescript-eslint 的 no-floating-promises 和 no-misused-promises(或 Biome 的对应规则,覆盖不全时见 TS-022)
为什么、来源
为什么:漏掉的 await 会让错误变成未处理的拒绝,Node 默认会因此让进程退出;也会让操作在事务或请求结束后才执行。
TS-022软指导检查:半自动TypeScript / JavaScript / Node 需要完整的类型感知 lint 时,在 Biome 之外只补 typescript-eslint 的类型规则,格式化仍只归一个工具
怎么检查:检查 ESLint 配置只启用类型相关规则、没有格式化类规则;CI 里两者都跑
参考值:Biome 官方称其漏 await 规则的检出率约为 typescript-eslint 的 75%(初步测试,2025 年发布时的数据)。
为什么、来源
为什么:Biome 自己实现了类型推断,部分类型规则的检出率还不如 typescript-eslint;只补缺的那部分,避免两套工具规则重叠。
TS-023软指导检查:半自动TypeScript / JavaScript / Node 发出网络请求和其他 IO 时传入 AbortSignal 并设超时,取消信号一路传到最底层的调用
怎么检查:评审时检查 fetch 和客户端库调用是否带 signal;lint 找出没有 signal 参数的 fetch 作为候选
为什么、来源
为什么:没有超时和取消的调用会一直挂着,占住连接和内存;只在外层取消、内层不传,底下的请求照样在跑。
AbortSignal.timeout(ms) 只负责产生取消信号,被调用的库忽略 signal 时依然会挂住,选库时要确认它支持取消。
TS-024软指导检查:半自动TypeScript / JavaScript / Node 对数量不定的集合并发处理时限制并发数,不直接 Promise.all(items.map(...))
怎么检查:评审时检查对查询结果、文件列表等集合的 Promise.all;用 p-limit 等库限制并发
为什么、来源
为什么:无界并发会一次打出成千上万个请求,压垮下游或耗尽内存和连接池。
一个失败就该终止全部时,失败后调用 abort() 取消其余任务;需要收集全部结果时用 Promise.allSettled。
TS-025硬约束检查:半自动TypeScript / JavaScript / Node 启动子进程用参数数组(spawn、execFile),不把含外部输入的字符串交给 shell(exec、shell 设为 true)
怎么检查:SAST 规则或 lint 标出 exec、execSync 和 shell 设为 true 的调用,评审确认参数不含外部输入
为什么、来源
为什么:交给 shell 的字符串一旦混进外部输入,就是命令注入。
子进程要限制运行时间和输出大小,并在退出或取消时回收整棵进程树;只杀主进程会留下孙进程。
TS-026硬约束检查:人工TypeScript / JavaScript / Node 不把 Node 的 vm 模块或权限模型(--permission)当作运行不可信代码的沙箱
怎么检查:评审时检查执行外部提交代码、插件或 Agent 生成代码的地方,确认隔离靠容器、虚拟机或独立系统用户
为什么、来源
为什么:Node 官方明确说明权限模型和 vm 都不能防御恶意代码,只能减少误操作。
TS-027软指导检查:自动TypeScript / JavaScript / Node 单元测试默认用 Vitest;一个仓库只维护一套单元测试框架
怎么检查:检查依赖清单里没有同时出现两套单元测试框架(存量迁移期间除外,需写明期限)
为什么、来源
为什么:两套框架意味着两套断言、模拟和配置,新人和 Agent 都会写混。
零依赖的小库或 CLI 可以用 Node 自带的 node:test;存量 Jest 项目可以保留,迁移不是必须的。
TS-028软指导检查:半自动TypeScript / JavaScript / Node 解析、序列化、校验、金额和日期计算这类有明确不变量的代码,用 fast-check 写属性测试
怎么检查:评审时检查这类模块是否有属性测试;失败时记录种子和缩减后的最小样例(见 04)
为什么、来源
为什么:手写的样例只覆盖想得到的情况,属性测试能自动找出边界上的反例。
TS-029硬约束检查:自动TypeScript / JavaScript / Node 不把 .only 提交进主干,不用 || true、跳过测试或删断言的方式让 CI 变绿
怎么检查:lint 禁止 .only(如 @vitest/eslint-plugin 的 no-focused-tests);CI 配置检查测试命令没有 || true;新增 .skip 要求写原因和任务编号
为什么、来源
为什么:这些写法让测试看起来通过,实际什么都没测。
TS-030硬约束检查:半自动TypeScript / JavaScript / Node SQL 一律参数化或用查询构建器拼装,不把外部输入拼进 SQL 字符串;原始 SQL 接口的调用要评审
怎么检查:SAST 规则标出字符串拼接的 SQL 和 sql.raw、$queryRawUnsafe 等原始接口,评审确认没有外部输入
为什么、来源
为什么:SQL 注入是最常见也最严重的漏洞之一。
Kysely、Drizzle 的 sql 模板标签会把插值变成参数,是安全的;名字里带 raw 或 unsafe 的接口不会。
TS-031软指导检查:半自动TypeScript / JavaScript / Node 数据库迁移用所选查询层或独立迁移工具的 CLI 生成 SQL 文件入库,SQL 经人工评审后再执行
怎么检查:迁移目录写进 CODEOWNERS;评审时看生成的 SQL,不只看 schema 定义的变化
为什么、来源
为什么:工具生成的迁移可能把改名变成删列再加列,只有看 SQL 才发现得了。
迁移何时执行、能否回滚、怎么和旧版本代码兼容,见 17。
TS-032软指导检查:半自动TypeScript / JavaScript / Node对外提供服务的后端 每个进程只创建一个数据库连接池,连接上限和获取超时可配置;不在每个请求里新建连接或连接池
怎么检查:评审时搜索 new Pool、new Client 的位置;压测时观察数据库连接数
为什么、来源
为什么:每请求建连接会在流量高峰时耗尽数据库连接。
TS-033硬约束检查:自动TypeScript / JavaScript / Node 从 OpenAPI 生成的 TypeScript 类型和客户端放在只读目录,不手改;CI 重新生成后用 git diff --exit-code 检查
怎么检查:CI 运行生成命令,有差异即失败
为什么、来源
为什么:手改的生成代码下次生成就会丢,客户端也会和契约悄悄对不上。
TS-034软指导检查:自动TypeScript / JavaScript / Node对外提供服务的后端 服务端用结构化日志库(默认 Pino)并配置敏感字段脱敏,不用 console.log 打业务日志
怎么检查:lint 在服务代码里禁止 console(如 Biome 的 noConsole),CLI 和脚本可放开;测试里检查日志不含令牌等字段
为什么、来源
为什么:console 输出没有级别和字段,也不会自动脱敏。
日志字段和级别的含义见 02,采集和链路关联见 26。
TS-035软指导检查:半自动TypeScript / JavaScript / Node 升级 Node 或 TypeScript 的大版本时,先在 CI 里并行跑新旧两个版本,确认编译、lint、测试、生成器和构建工具都支持后再切换
怎么检查:升级 PR 附版本矩阵的 CI 结果;评审检查依赖编译器接口的工具是否兼容
为什么、来源
为什么:大版本会改默认值和接口;TypeScript 7 暂时没有稳定的编程接口,依赖它的工具不能直接跟着升级。
TypeScript 7 下,typescript-eslint 的类型规则、ts-morph 等依赖编译器接口的工具,暂时要配合 TypeScript 6 的接口包使用,类型检查本身用 TypeScript 7 的 tsc。
没有符合条件的规则。
运行时
Node.js 仍在维护期的 LTS 主线
备选:Bun(想要一体化工具或更快启动,并已在目标平台验证原生扩展、信号和子进程行为);Deno(需要默认权限隔离的脚本或边缘运行环境,并已验证所用 npm 包的兼容性)
不推荐:已停止维护的版本、奇数号短期版本、预发布版本
Node 版本管理
mise(读取项目里声明的版本)
备选:fnm 或 nvm(团队已经在用,且只需管 Node 一种运行时)
不推荐:只在 README 里写版本
包管理器
pnpm(含 workspace) · MIT(待核实)
备选:npm(单包项目,不想多装一个工具);Bun 的包管理器(运行时也用 Bun)
不推荐:一个仓库里混用多种包管理器和锁文件
单仓任务编排
pnpm workspace 自带的过滤和递归命令
备选:Turborepo 或 Nx(包多、构建慢,需要任务图和缓存)
不推荐:包很少时就上任务图和远端缓存
类型检查
TypeScript 7 的 tsc --noEmit · Apache-2.0(待核实)
备选:TypeScript 6 的接口包(typescript-eslint 类型规则、ts-morph 等工具需要编译器编程接口时,与 TypeScript 7 并存)
不推荐:把 esbuild、tsx、Node 能运行当成类型通过;依赖编译器插件或 transformer 的方案
开发时直接运行 .ts
Node 原生的类型擦除
备选:tsx(代码里还有 enum、参数属性等不可擦除的语法,或需要 tsconfig 的 paths)
不推荐:用运行成功代替类型检查
构建
服务和 CLI 直接用 tsc 输出 JavaScript,不打包
备选:tsdown(发布库,需要生成声明文件、多入口和 exports 映射);esbuild(需要单文件产物或冷启动敏感)
不推荐:发布未编译的 .ts 源码
格式化与 lint
Biome(一个工具同时管格式化和 lint) · MIT(待核实)
备选:ESLint + typescript-eslint + Prettier(需要 Biome 没有的插件生态(如框架专用规则),或存量项目已稳定使用);Oxlint(存量 ESLint 项目想先提速 lint,格式化另选一个工具)
不推荐:同时开两个以上格式化工具
类型感知 lint 的补充
typescript-eslint 的类型规则(如 no-floating-promises、no-misused-promises、no-unsafe-*),只启用 Biome 覆盖不全的那部分 · MIT(待核实)
单元测试
Vitest · MIT(待核实)
备选:node:test(零依赖的小库或 CLI);Jest(存量项目已在用)
不推荐:一个仓库同时维护两套单元测试框架
属性测试
fast-check · MIT(待核实)
变异测试
StrykerJS · Apache-2.0(待核实)
HTTP 模拟
MSW(在网络层拦截请求) · MIT(待核实)
不推荐:到处手写 fetch 的替身
运行时校验
Zod · MIT(待核实)
备选:Valibot(前端或边缘环境对包体积敏感);框架自带的 JSON Schema 校验(Fastify / Ajv)(服务端已用 Fastify,校验和 OpenAPI 文档共用一份 schema)
不推荐:req.body as T 或手写字段检查
服务端 HTTP 框架
Fastify · MIT(待核实)
备选:Hono(需要在 Node、Bun、Deno、边缘环境之间通用,或服务很小);NestJS(团队大、模块多,明确需要依赖注入和统一的组织方式);Express(存量项目)
不推荐:用 Node 自带的 http 模块手写路由和中间件
查询构建器 / ORM
Kysely(类型安全的 SQL 查询构建器)+ node-postgres · Kysely MIT(待核实);Drizzle ORM Apache-2.0;Prisma ORM Apache-2.0
备选:Drizzle ORM(希望 schema 写在 TypeScript 里并由它生成迁移);Prisma ORM(团队已熟悉,需要它的关系查询和工具链;断网环境要先验证代码生成步骤不需联网)
不推荐:手写字符串拼 SQL
数据库迁移工具
跟随查询层:Kysely 自带迁移器、Drizzle Kit、Prisma Migrate
备选:node-pg-migrate(只用 PostgreSQL,查询层不带迁移);dbmate 等与语言无关的工具(多种语言的服务共用一个数据库时,统一用一个工具(见 17))
不推荐:一个数据库由两套迁移工具同时管理
PostgreSQL 驱动
node-postgres(pg)+ 连接池
备选:postgres.js(偏好模板字符串写 SQL,且不用依赖 pg 的查询层)
OpenAPI 的 TypeScript 生成器
openapi-typescript(生成类型)+ openapi-fetch(轻量客户端) · MIT(待核实)
备选:Orval(前端需要直接生成 TanStack Query 的 hooks);Hey API(需要生成完整的 SDK(仍是 0.x,升级注意破坏性变化))
不推荐:多个生成器同时产出同一份客户端;手写一份接口类型在前后端之间复制
日志
Pino · MIT(待核实)
备选:Winston(存量项目)
并发控制
p-limit · MIT(待核实)
备选:p-queue(还需要优先级、暂停或速率限制)
✕ 错误做法:Node 直接运行 .ts 成功,就认为类型没问题
✓ 应该:CI 单独跑 tsc --noEmit
✕ 错误做法:JSON.parse(body) as User
✓ 应该:用 Zod 等库解析后再使用
✕ 错误做法:tsconfig 不写 strict,依赖默认值
✓ 应该:显式写 strict: true
✕ 错误做法:用 tsconfig 的 paths 别名,构建后运行时报找不到模块
✓ 应该:用 package.json 的 imports 子路径导入
✕ 错误做法:Biome、Prettier、ESLint 的格式化规则同时开
✓ 应该:一个格式化工具,ESLint 只补类型规则
✕ 错误做法:为了装上某个原生包,把安装脚本整体放开
✓ 应该:只把这个包加入白名单
✕ 错误做法:CI 里 npx 某工具@latest
✓ 应该:把工具写进 devDependencies,用锁文件固定
✕ 错误做法:对几万条记录 Promise.all(rows.map(fetchSomething))
✓ 应该:用 p-limit 限制并发,或改成流式处理
✕ 错误做法:exec(\git clone ${url}`)`
✓ 应该:execFile('git', ['clone', url])
✕ 错误做法:用 vm 模块运行用户提交的代码
✓ 应该:放进容器或虚拟机隔离(见 32)
✕ 错误做法:每个请求里 new Pool()
✓ 应该:进程启动时创建一个连接池