Ghost 内部包的 TypeScript 迁移:三提交历史保真与验证工作流实战
本文以 Ghost 仓库中 Agent 技能包 convert-internal-package-to-typescript 的配套参考文档《History and verification reference》为主体,讲解在把 packages/ 下的遗留内部包从 JavaScript/CommonJS 迁移到 TypeScript + ESM「黄金路径」时,如何设计可被 Git 追踪的提交序列,以及提交、打包、打包产物三个层面各自的验证手段。读完本篇,你将掌握一套可直接套用的提交拆分方案(含真实提交案例)、git diff --summary 与 git log --follow 的历史核对命令,以及针对 source 条件、编译产物与运行时资源的完整检查清单,并能对照 Ghost 仓库内已完成的 admin-api-schema 迁移实例逐项印证。
一、背景:黄金路径契约与参考文档的定位
Ghost 的 Node.js 内部库以 packages/README.md 为权威契约:新内部包必须是 private、0.0.0 版本、@tryghost/<name> 命名、"type": "module"、源码位于 src/**/*.ts、由 tsc 编译到 build/,且不做独立 npm 发布。入口通过 source、types、default 三个导出条件暴露,让开发和测试免构建直读原始 TypeScript,而普通 Node 则加载 build/ 下的编译产物。
.agents/skills/convert-internal-package-to-typescript/SKILL.md 定义了完整的迁移工作流:先读透 packages/README.md,再摸清所有 import/require/资产引用与基线测试,然后按「移库 → 改扩展名 → 语义转换」三步提交,并以生产标准完成实现转换。本文聚焦的 history-and-verification.md 正是该技能在规划提交前要求"完整阅读"的参考文档,它回答两个问题:已知的良好提交长什么样,以及每一步如何验证历史与兼容性没有受损。
二、Known-good commit shape:三个刻意分离的提交
参考文档给出的基准案例是 admin-api-schema 包的迁移,它使用了三个刻意独立的提交:
688807ca052 Moved Admin API schema sources from lib to srcbe31b36ffab Changed Admin API schema file extensions to TypeScriptb825dafdc19 Converted Admin API schemas to TypeScript
这个顺序的意义在于:让 Git 和审阅者能够区分文件搬迁、机械性改名与语义转换三类性质完全不同的变更。文档特别强调,这只是"形状"的范例,不是要求复制某个包的实现细节。
对照 SKILL.md 的三步定义,每一步的边界都很严格:
- 第一步(lib → src):用
git mv移动源码树,只更新"不改就无法工作"的引用(测试导入、构建输入、包元数据),不改模块语法、不改扩展名、不动实现; - 第二步(.js → .ts):同样用
git mv改名,只添加让改名后的源码能解析、该提交自洽所需的最小配置,行为保持不变,有意义的类型标注与 ESM 转换留给下一步; - 第三步(语义转换):应用
packages/README.md的包契约——共享配置包、最小化的包内配置、ESM 元数据与 exports、标准脚本、单一编译产物——并通过全部机械检查后,才能把包的migration状态替换为ghostPackage.goldenPath: compliant。
这三个提交在当前仓库的 git 历史中均可查到,且与文档描述完全吻合。以第一个提交 688807ca052 为例,其 git show --stat 显示全部为 {lib => src}/... 形式的零内容改动纯改名(30+ 个 JSON schema、admin-api-schema.js、schemas/index.js 等),仅根目录转发文件 index.js(2 行改动)和 package.json(4 行改动)有实质修改——这正是"只更新必须变更的引用"的落样。
三、History checks:用 git mv 保住文件谱系
参考文档给出的两条核对命令,在每次机械提交之后都应执行:
git diff --summary HEAD^
git log --follow -- packages/<name>/src/<representative-file>.ts
第一条查看上一个提交的文件级摘要(新增/删除/改名),第二条沿一条代表性文件回溯完整生命周期。文档解释了为什么要如此在意:
Git 记录的是快照而非显式的 rename 操作,因此改名检测依赖相似度。一个在同一次提交里既被移动又被大幅重写的文件,很可能被显示为一次删除加一次新增。要把这些操作拆开,历史才可读。
这里有一个值得记住的合法例外:一个很小的 CommonJS 转发文件,当新的 ESM 入口取代它时,合理地以"删除"形式出现是正常的;此时应验证实现代码的谱系,而不是强行制造一个误导性的 rename。这一点在第三个提交 b825dafdc1(Converted Admin API schemas to TypeScript)中得到了真实印证:其变更统计里 packages/admin-api-schema/index.js 标记为 1 -(该文件被删除),语义转换完成后,原本指向旧实现的 CommonJS 转发壳自然退场,无需也不应伪造它的改名记录。
四、Compatibility checks:source 条件与打包产物双向验证
文档要求"在决定输出格式之前先检查真实的消费者",并给出两阶段验证流程:
阶段一:worktree 验证。 先确认一个使用 source 导出条件的消费者,能在开发/测试中解析并加载原始 TypeScript。对照 packages/admin-api-schema/package.json,其 exports 声明为:
{
"source": "./src/index.ts",
"types": "./build/index.d.ts",
"default": "./build/index.js"
}
source 条件在条件顺序上最先,供 monorepo 内免构建开发使用;普通 Node 忽略它、走 default 加载编译产物。
阶段二:构建并打包(build & pack),针对打包产物验证四件事:
- 普通 Node 能 import 编译后的 ESM 入口;
- 既有的 CommonJS 消费者能在 Ghost 支持的 Node 版本上
require()编译入口; - 编译后的模块图中不含顶层
await; - 打包件中的
types、default、main三个目标都指向产物中真实存在的文件。
为什么顶层 await 是硬性约束?packages/README.md 的"TypeScript and ESM"一节解释了底层机制:Ghost Core 本身是 CommonJS,但运行在支持 require(esm) 的 Node 版本上,因此一套 ESM 产物即可同时服务 import 与 require() 两类消费者——前提是整条被引入的模块图都没有顶层 await,且该限制由 ESLint 强制执行。文档同时明确:不要投机性地增加第二套 CommonJS 构建;确有必要时,把例外记录进包 README 和配置并加以测试。
五、Assets and packaging checks:build/ 是运行时唯一事实源
文档要求从干净的包输出开始构建,并逐个检查 build/ 中的运行时文件。对 JSON schema 这类资产,策略是"编译器能输出就尽量从 TypeScript 直接导入;否则使用显式的、可移植的复制步骤"。
admin-api-schema 是前者的现成范例。src/schemas/index.ts 通过 import attributes 从 TypeScript 直接导入 34 个 JSON schema 文件(如 import posts from './posts.json' with { type: 'json' };),配合 tsconfig.json 中 rootDir: src / outDir: build 的设置,tsc 会把 JSON 一并输出到 build/schemas/,无需额外复制步骤。
同一个文件还展示了 SKILL.md 中"用显式的类型化注册表替代动态 CommonJS 发现"的落地方式:schemas 对象以 as const 固定形状,SchemaName = keyof typeof schemas 派生出精确的键类型,actionSchemaNames 数组用 as const satisfies readonly SchemaName[] 做双重校验——注册表即 API,运行时不存在"扫目录找文件"的猜测逻辑。上层入口 src/index.ts 的 get()/list()/validate() 全部构建在这一显式注册表之上,未知名称走类型守卫返回 null 或抛出结构化的 IncorrectUsageError。
文档给出的最终打包核对清单:
- 生产执行不得从
src/读取; files字段包含构建产物,排除手写源码(除非包契约明确另有规定)——admin-api-schema的"files": ["build"]即符合此条;- 消费者行为要针对打包件测试,而不只是 worktree;
- 有现成的 pack 或 release-archive 检查时,优先复用仓库已有手段。
六、Review the final diff:PR 提交前的逐项自查
参考文档的最后一节要求:开 PR 之前,每个提交独立看一遍,总 diff 也看一遍,并专门排查以下六类问题:
- 意外的格式变更(formatting churn);
- 被放松的编译器或 lint 规则;
- 无解释的
unknown或类型断言; - 过期的 CommonJS 配置残留;
- 缺失的运行时资产;
- 仍然绕过包 exports 的消费者。
这份清单与 SKILL.md 的"Hold the conversion to production standards"标准互为表里:后者要求建模真实的输入输出与注册表形状、缺失类型不得用 any 兜底、unknown 只允许出现在真正的不可信边界并尽快收窄、禁用宽泛断言 / 非空断言 / @ts-ignore / lint 关闭、除非显式在范围内否则保持运行时 API 不变、以及不得为了通过转换而放松共享的 TypeScript / ESLint / Vitest 规则。
完成自查后的验证动作包括:运行 packages/README.md 要求的 pnpm build / pnpm test / pnpm lint;让代表性消费者走它们生产环境真实的 import/require() 路径;若 source 与编译产物两种解析方式都被使用,则两条都测;导出、运行时资产或归档包含关系发生变化时,检查打包包或 Ghost release 组件;工作区构建图变化时运行仓库级构建。最后,SKILL.md 补充了合并层面的约定:当这些提交各自独立有效且顺序刻意时,现代化 PR 可以采用 rebase merge;这不改变其前置 subtree 历史导入仍需 merge-commit 的要求。
七、小结:机械变更与语义变更严格分层的工程价值
把 history-and-verification.md 放回 Ghost 的迁移语境,它的核心思想可以浓缩为一句话:让每一类变更在 Git 历史里各归其位。Git 的 rename 检测基于快照相似度,任何"移动 + 重写"的混合提交都会破坏谱系;而 admin-api-schema 三个提交(688807ca052 → be31b36ffa → b825dafdc1)证明了这种拆分是可执行、可验证的——纯改名的 git diff --summary、CommonJS 转发文件的合理删除、JSON 资产经 import attributes 进入 build/、source/types/default 三条件在打包件中的落地,都能从当前仓库源码与 git 历史中直接复核。对于任何想把遗留包迁到 TS + ESM 黄金路径的团队,这套"三提交形状 + 历史核对 + 双向兼容验证 + 资产打包清单 + diff 自查"的组合,本身就是可直接移植的方法论。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00