Ghost 内部包 TypeScript 与 ESM 迁移实战:保留 Git 历史的三步转换工作流

Ghost 内部包 TypeScript 与 ESM 迁移实战:保留 Git 历史的三步转换工作流 Ghost 内部包 TypeScript 与 ESM 迁移实战保留 Git 历史的三步转换工作流【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost本文基于 Ghost monorepo 中定义内部包迁移工作流的技能文档 SKILL.md 及其配套参考 history-and-verification.md 展开完整讲解如何将packages/下的遗留 JavaScript/CommonJS 内部包按金路径golden path契约转换为 TypeScript ESM 单一构建产物。读完本文你将掌握迁移前的适用性判定、三个职责单一 commit 的拆分策略lib目录搬迁、扩展名改写、实现转换、生产级类型规范、打包与兼容性验证清单并能对照仓库中已完成的admin-api-schema案例与强制校验脚本pnpm lint:packages复现整个流程。一、背景Ghost 的 packages 金路径契约Ghost 仓库中packages/目录存放的是仅供 monorepo 内部使用的 Node.js 库。packages/README.md 是这些包整个生命周期内架构约定的权威来源而本文讨论的迁移工作流文档只负责怎么转换不负责定义转换成什么样——这正是两份文档的分工边界。金路径对新内部包的定义是私有的、TypeScript 专属的 ESM 库具体契约包括见 packages/README.md#L11-L21包名使用tryghost/name前缀version: 0.0.0且private: true声明ghostPackage: {goldenPath: compliant}type: module源码放在src/**/*.ts、测试放在test/**/*.ts用tsc将生产代码编译到build/不做独立的 npm 发布。每个私有包都必须在ghostPackage.goldenPath字段中声明生命周期状态见 packages/README.md#L27-L41状态含义compliant该包已符合金路径会被机械化规则逐条检查migration过渡状态保留历史的导入已合入正在等待独立的现代化 PRexempt有意保留的长期例外如纯测试辅助包、多运行时包migration与exempt都必须附带非空的ghostPackage.reason。这些状态和所有可机械化执行的规则由 scripts/check-internal-packages.js 在pnpm lint:packages中统一校验——该脚本在 根 package.json#L52 中被挂到仓库级 lint 流程里。值得注意的是该脚本对标准脚本与 devDependencies 的期望值是硬编码常量注释中明确说明这是刻意与 _template 模板 解耦的避免模板漂移后自己批准自己。金路径的构建细节同样值得在迁移前读懂见 packages/README.md#L128-L140共享 TypeScript 配置使用 NodeNext 语义相对导入必须写真实的.ts扩展名编译器在输出时改写为.jsGhost Core 本身是 CommonJS但运行在支持require(esm)的 Node 版本上因此单个 ESM 构建可同时服务import与require()两类消费者——前提是整个模块图不得出现顶层await由 ESLint 强制执行默认不添加 CommonJS 构建或转发 shim只有经过验证的消费者无法使用标准 ESM 产物时才允许多格式。仓库中的共享 TS 配置 configs/typescript/esm.json 把这些约定落实为具体编译器选项module: nodenext、moduleResolution: nodenext、allowImportingTsExtensions: true、rewriteRelativeImportExtensions: true、resolveJsonModule: true、strict: true并叠加noUncheckedIndexedAccess、noUnusedLocals、noUnusedParameters等严格开关。这些选项正是后文生产级标准在工具链层面的兜底。二、迁移前确认工作流适用迁移文档要求先完整阅读packages/README.md再检查目标包、它的消费者以及可对比的现行包确认该包仅内部使用且能够采用文档化的单一构建 TypeScript ESM 契约。动手编辑之前必须完成四步准备对应 SKILL.md#L19-L31找到该包的所有导入点、require()调用、导出、运行时资源与路径引用检查包元数据、构建与测试配置、发布/归档archive包含关系以及任何动态模块加载运行包现有测试和有代表性的消费者检查建立基线baseline识别出那些无法加载金路径 ESM 产物的受支持消费者。文档同时划定了停止条件如果包拥有活跃的独立发布线、第三方支持契约或需要与金路径冲突的输出格式应停下来先为其确立支持契约而不是机械套用本工作流。一个容易误判的细节是历史上已废弃的 npm 版本本身不构成迁移障碍——只有当前的支持契约才能阻止转换。此外文档要求在规划 commit 之前完整阅读 references/history-and-verification.md。该参考文档提供的内容在下一节展开。三、用三个聚焦 commit 保留文件历史这是整个工作流的核心方法论把机械搬迁与语义改写严格分离让 Git 的重命名检测rename detection能够工作。参考文档解释了原理Git 记录的是快照而非显式重命名操作重命名识别依赖内容相似度如果同一个 commit 里既移动文件又大幅改写内容Git 很可能把它呈现为删除 新增历史就此断裂。三个 commit 各自的要求如下对应 SKILL.md#L37-L691. 将源码从 lib 迁到 src使用git mv移动整棵源码树只更新必须随路径变化的引用测试导入、构建输入、包元数据本 commit不改动模块语法、文件扩展名或任何实现若改动内容与之相符commit subject 使用Moved package sources from lib to src。2. 将文件扩展名改为 TypeScript用git mv完成.js到.ts的批量重命名只添加让重命名后的源码能够解析、让本 commit 保持自洽所必需的最小配置与语法调整保持行为不变把有意义的类型标注和 ESM 转换推迟到第三个 commitsubject 使用Changed package file extensions to TypeScript。3. 将实现转换为 TypeScript 与 ESM按 packages/README.md 的包契约落地共享配置包、最小化的包内配置、ESM 元数据与 exports、标准脚本以及单一编译产物除非有已验证的消费者需要例外只有当所有金路径机械化检查全部通过后才把包的migration状态替换为ghostPackage.goldenPath: compliantsubject 使用Converted package to TypeScript。文档对机械 commit 还有一条纪律避免夹带格式化或顺手清理opportunistic cleanup让每个 commit 都能独立通过验证。已验证的 commit 形态admin-api-schema 案例参考文档 history-and-verification.md 记录了admin-api-schema这次转换使用的三个 commit688807ca052 Moved Admin API schema sources from lib to srcbe31b36ffab Changed Admin API schema file extensions to TypeScriptb825dafdc19 Converted Admin API schemas to TypeScript文档强调这个顺序的价值在于让 Git 与评审者能够区分重定位、机械改名、语义转换三类变更它是形态示例而非实现模板不应照搬其中的包特定实现。今天的 packages/admin-api-schema/ 正是转换完成后的样子可以作为目标态的实物参照package.jsontype: module、private: true、ghostPackage.goldenPath: compliantexports 按source→types→default顺序暴露./src/index.ts、./build/index.d.ts、./build/index.jsfiles只含build——与 packages/README.md#L51-L65 给出的契约模板逐字段一致tsconfig.json继承internal/cfg-typescript/esm.json仅覆写rootDir: src、outDir: buildinclude只含生产源码test/tsconfig.json继承源配置、noEmit: true同时 include 源码与测试供test:types脚本使用vitest.config.ts一行createVitestConfig()来自共享的internal/cfg-vitest。这些包内配置保持最小的样例恰好呼应了 packages/README 中只在行为偏离共享契约时才加 override的原则。四、转换必须达到的生产级标准迁移文档列出的质量标准SKILL.md#L71-L88可以归纳为六条纪律每一条都对应仓库里已有的工具链支撑1. 类型要建模真实形状禁止用 any 兜底。对输入、输出与注册表registry形状建模不得以any替代缺失的类型。仓库的共享 TS 配置开了strict与noUncheckedIndexedAccess见 configs/typescript/esm.json转换后代码必须在这套开关下干净通过。2.unknown只出现在真正不可信边界且要立刻收窄。这是防止未知输入一路裸奔的关键约束。3. 禁止用宽泛断言、非空断言、ts-ignore或 lint disable 来让编译器闭嘴。参考文档在最终 diff 审查清单中再次点名unexplained unknown or casts作为重点检查项。4. 保持运行时 API 不变。除非 API 变更被明确纳入范围且所有消费者同步更新否则包对外暴露的行为必须原样保留。5. NodeNext 下相对导入必须写显式.ts扩展名。这不是风格偏好而是 configs/typescript/esm.json 中allowImportingTsExtensionsrewriteRelativeImportExtensions的组合所要求的写法——编译器会在 emit 时把.ts改写为.js。6. ESM 表达不了旧的 CJS 动态加载模式时用显式的类型化注册表替代。典型场景是旧代码靠__dirname 目录扫描动态require()一堆模块ESM 无法安全表达这种发现式加载正确做法是改成静态、带类型的注册表结构。此外还有两条面向产物的约束JSON 等运行时资源必须被发射并可在build/中找到。packages/README.md#L142-L150 要求生产行为只能依赖build/单独工作编译器能拷贝的如通过resolveJsonModule导入的 JSON就直接从 TS 导入否则加一个显式、可移植的拷贝步骤。仓库中 packages/admin-api-schema/src/schemas/ 下的大批量*.json校验模式posts、pages、members 等 Admin API 请求 schema正是这类运行时资源的典型实例避免顶层await以便受支持的 CommonJS 消费者能够通过 Node 的require(esm)互操作直接require()该包——这是单一 ESM 构建同时服务两类消费者的前提。最后一条红线不得为了通过转换而放宽共享的 TypeScript、ESLint 或 Vitest 规则。这一点由 scripts/check-internal-packages.js 机械化保障——它硬编码了标准脚本build: tsc、test:unit、test:types、lint:code等和标准 devDependencies 的期望值任何在包内私改共享规则或偷换脚本的行为都会在pnpm lint:packages中暴露。五、验证体系从工作区到打包产物迁移的验证分四层逐层收紧证据边界。1. 标准命令在包目录内运行pnpm build、pnpm test、pnpm lint即 packages/README Verification 一节要求的最小集合。2. 消费者侧双路径验证参考文档要求按顺序做两件事history-and-verification.md#L34-L50先在工作区worktree验证source条件确认某个使用source条件的消费者能在开发/测试中解析并加载原始 TypeScriptsource: ./src/index.ts这一条 exports 就是为此设计的纯 Node 会忽略它而加载build/中的编译产物再构建并pack包针对打包后的产物验证四件事纯 Node 能 import 编译后的 ESM 入口现有的 CommonJS 消费者能在 Ghost 支持的 Node 版本上require()编译入口编译后的模块图中不存在顶层await产物中的types、default、main目标都能解析到真实存在的文件。文档同时警告不要投机性地把第二个 CommonJS 构建加回去任何必需的例外必须记录在包的 README 与配置中并被测试覆盖。3. 资源与打包检查从干净的包输出开始构建逐一检查build/中每个运行时文件JSON schema 之类的资源编译器能发射就从 TS 导入否则用显式拷贝步骤复用仓库既有的 pack / 发布归档检查确认三件事生产执行不从src/读文件files包含构建产物、排除手写源码除非包契约明确另有规定消费者行为是针对产物而非仅工作区测试的。4. 历史可读性与最终 diff 审查对每个机械 commit 运行并检查重命名是否被正确识别git diff --summary HEAD^ git log --follow -- packages/name/src/representative-file.ts参考文档补充了一个边界情形一个小的 CommonJS 转发文件在新 ESM 入口替换它时合理地显示为删除是可接受的——此时应验证实现代码的血缘而不是硬造一个有误导性的 rename。开 PR 前除了总 diff还要逐 commit 独立审查重点排查意外的格式化噪音、被削弱的编译器/lint 规则、来历不明的unknown或类型断言、过时的 CommonJS 配置、缺失的运行时资源、以及绕过包 exports 直取内部路径的消费者。关于合并方式SKILL.md 明确当这些 commit 独立有效且顺序有意为之时现代化 PR 可以 rebase-merge这不影响前置子树历史导入所要求的 merge-commit 规则。六、快速核对清单结合仓库证据执行一次完整迁移可按下表自查环节动作仓库依据适用性确认包为内部使用、无独立发布/第三方契约废弃的历史 npm 版本不阻塞SKILL.md基线跑通现有测试与代表性消费者同上Commit 1git mv将lib/移到src/不动语法与扩展名SKILL.md#L42-L49Commit 2git mv批量.js→.ts只加最小解析配置SKILL.md#L51-L58Commit 3落地 TS ESM 契约检查全过后再改goldenPath: compliantSKILL.md#L60-L69类型纪律真实建模、unknown仅限边界、禁 any/断言/ts-ignore、保持运行时 APISKILL.md#L71-L88配置形态继承共享 cfg 包、source/types/defaultexports、标准脚本面packages/README.md、packages/_template/package.json验证build/test/lint → worktreesource条件 → pack 产物四查 → 资源与归档检查 →git diff --summary/git log --followhistory-and-verification.md机械化兜底pnpm lint:packages校验脚本面、devDeps 与 goldenPath 状态scripts/check-internal-packages.js、根 package.json#L52七、总结这套工作流的价值在于把一次语言升级拆成了可审计的历史事件lib→src的重定位、.js→.ts的机械改名、以及真正的语义转换各自独立成立Git 重命名检测因此全程有效评审者与未来的git log --follow读者都能看到清晰的文件血缘。而ghostPackage.goldenPath状态机配合scripts/check-internal-packages.js的机械化校验则保证声明 compliant与实际通过金路径检查是同一件事。对维护者而言这套方法可以直接作为 Ghostpackages/下任何遗留内部包现代化的操作手册对读者而言packages/admin-api-schema/ 是转换完成后的完整参照实现。【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考