xterm 补丁重新生成¶
范围¶
Orca 随附的 @xterm/xterm 包含四个它需要但上游尚未接受的源码更改:IME 组合钩子、它们触发的 xterm-composition-* 自定义事件、这些钩子扩展的 ICompositionHelper 接口,以及一个 SortedList 修复。pnpm 通过 config/patches/@xterm__xterm@<version>.patch 应用它们。
该补丁涉及八个文件。其中四个是手工编写的源码(src/browser/CoreBrowserTerminal.ts、src/browser/Types.ts、src/browser/input/CompositionHelper.ts、src/common/SortedList.ts),另外四个是这些源码产生的构建输出(lib/xterm.js、lib/xterm.mjs,以及两个 sourcemap)。bundle 部分有 7.3 MB 的压缩代码。它是生成的,本文档的存在就是为了确保没有人手工编辑它。
两部分是同一组更改以两种方式生成的 diff,因此生成器要求每个源文件逐字节匹配。已发布补丁无法命名的 hunk——上游的 .npmignore 会移除 src/**/*.test.ts——会在下一次 --write 中被丢弃,因此它会使运行失败。
config/patches/xterm-src/@xterm__xterm@<version>.src.patch 是事实来源。其他所有内容都由 config/scripts/regenerate-xterm-patches.mjs 从它派生,该脚本固定到发布 tarball 所基于的确切上游提交。
@xterm/addon-webgl、@xterm/addon-search、@xterm/addon-serialize 和 @xterm/addon-image 以相同方式生成,来自 config/patches/xterm-src/ 下各自的源码补丁。它们的条目仅在 packageDir 和构建步骤上不同;以下内容适用于全部五个。@xterm/addon-ligatures 是仍然手工编写的补丁——参见 已知差距。
图像补丁按最大 WASM 容量限制待处理的 Kitty 解码器,并按字节大小限制传输的图像 blob。两者都使用已配置的存储预算;上游的显示像素预算不覆盖这些分配。字节预算驱逐会先丢弃未放置的 payload,因此当被放弃的 blob 仍占用预算时,新上传不会擦除可见图像;只有当这不够时才会移除已显示图像,因为该上限是硬性边界。传入的图像始终会被存储,因此上限最多超出一个 payload,而不是丢弃协议已经以 OK 确认的图像。Orca 使用固定的 32 MB 存储和 8 MiB 序列限制,而不是任意的 addon 配置。
config/scripts/xterm-image-memory-contract.test.mjs 使用未完成的上传、块续传、两种驱逐顺序和释放来测试已安装的 bundle。
该补丁还在拼接已解码块之前限制解压缩,在原生解码之前验证 PNG 尺寸,并在重置、禁用或释放后关闭过期的异步图像结果。config/scripts/xterm-image-lifecycle-contract.test.mjs 针对已安装的 addon 测试这些边界。字体缩放会缩放可见 tile,而不会创建放大的完整图像画布;
config/scripts/xterm-image-resize-contract.test.mjs 检查分配和 tile 映射。
规则¶
- 永远不要编辑
config/patches/@xterm__*@<version>.patch。编辑源码补丁并重新生成。 - 永远不要编辑打过补丁的
node_modules树中的lib/,然后重新运行pnpm patch-commit。这正是 bundle hunk 与其源码不再匹配的方式。 - 每个源码更改必须与重新生成的 bundle hunk 以及
pnpm-lock.yaml哈希更新一起,在一个提交中落地。 - 上游提交位于
config/patches/xterm-upstream.json中,而不是注释中。如果版本升级使其过期,生成器会失败,而不会静默地修补错误的树。 - Sourcemap 随 bundle 一起移动,并且永远不会被静默省略。补丁会移动代码,因此只丢弃 map hunk 会发布指向错误行的偏移量。
sourcemaps.policy只接受include:它占用输出补丁约 5.8 MB,并且是必需的,因为src/renderer/src/components/terminal-pane/terminal-ime-xterm-transaction-events.test.ts读取lib/*.map并断言映射的Version.ts与运行时版本匹配。删除 map 曾经是一个选项;执行该操作的代码已作为不可达代码移除,因此重新添加该策略意味着重新添加那段代码。 --check是锁文件的权威,而不是pnpm install。pnpm 在两个位置写入补丁哈希——patchedDependencies和每个依赖被打补丁包的 resolution key——在热存储上,它会保留 resolution key 的先前值同时报告成功。这会在本地安装,并在 CI 的冷存储上漂移。对于版本升级,遵循 版本升级 工作流直到第 5 步(最终的--check);如果安装后报告过期哈希,重新运行--write。对于仅源码编辑,上述四步工作流以--check结束。
工作流¶
# 1. Edit the source hunks.
$EDITOR config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch
# 2. Rebuild the bundle hunks, the full patch, and the lockfile hash.
node config/scripts/regenerate-xterm-patches.mjs --write
# 3. Reinstall so node_modules picks up the new patch hash.
pnpm install
# 4. Confirm the tree is self-consistent.
node config/scripts/regenerate-xterm-patches.mjs --check
手工编辑补丁文件对于超过一行的更改都很笨拙。 对于实质性更改,请改为在生成器自己的 checkout 中操作——在任何运行之后,它都会保留在固定提交上,并应用源码补丁:
node config/scripts/regenerate-xterm-patches.mjs --check --work-dir=/tmp/xterm
$EDITOR /tmp/xterm/upstream/src/browser/input/CompositionHelper.ts
git -C /tmp/xterm/upstream diff -- src/ > config/patches/xterm-src/@xterm__xterm@6.1.0-beta.303.src.patch
node config/scripts/regenerate-xterm-patches.mjs --write --work-dir=/tmp/xterm
对于 addon,请在 addons/<name>/ 下编辑,并使用 --relative 从该目录获取 diff,使补丁以与发布 tarball 相同的方式以包为根:
$EDITOR /tmp/xterm/upstream/addons/addon-webgl/src/TextureAtlas.ts
git -C /tmp/xterm/upstream/addons/addon-webgl diff --relative -- src/ \
> config/patches/xterm-src/@xterm__addon-webgl@0.20.0-beta.299.src.patch
--write 会将源码补丁重写为重新 diff 时会输出的规范形式,因此手工生成的 git diff 会在第一次运行时被规范化,而不是一直与 --check 对抗。
将 checkout 放在本仓库之外运行。其下的构建树会使 tsgo 向上遍历进入 Orca 自己的 node_modules,并以 TS2300: Duplicate identifier 失败;这是树所在位置的症状,而不是补丁的问题。
如何确定提交¶
上游 bin/publish.js 会在 npm publish 之前设置 packageJson.commit,因此每个发布的 tarball 都会标明构建它的提交。生成器会将该标记与 xterm-upstream.json 进行断言,然后逐文件比较 tarball 中的 src/ 与 checkout 中的文件。只有 src/common/Version.ts 允许不同,因为 publish.js 会在打包前立即重写版本;生成器会应用相同的标记。
正是这一对检查使重建可信。没有它们,错误的提交仍会生成一个看似合理的 7 MB 补丁。
构建顺序¶
上游的发布路径是 npm ci → 标记 Version.ts → npm run package。npm run package 会为 lib/xterm.js 运行 webpack,然后通过 postpackage 为 lib/xterm.mjs 运行 bin/esbuild_all.mjs --prod。
一个 addon 需要按此顺序执行三个步骤,而第一步很容易被忽略:
- 根目录
npm run build。 addon 自己的npm run build是针对一个 tsconfig 运行tsgo -p .,该 tsconfig 的files和include都为空,并且只列出项目引用。在-p模式下,tsgo 不会构建引用,因此它会成功但不输出任何内容,随后 addon 的 webpack 会因缺少./out/而失败。根目录构建正是用来填充该目录的。 - addon
npm run package— addon 自己的 webpack,输出 CJSlib/addon-*.js。根目录的package脚本从不构建它。 - 根目录
npm run esbuild-package—bin/esbuild_all.mjs --prod,一次性为所有 addon 输出 ESMlib/addon-*.mjs。
不要在打包构建之后运行 npm run setup。 setup 是开发用的 esbuild 构建,且 minify: false。之后运行它会用未压缩的 bundle 和一个不再匹配的 map 覆盖 lib/xterm.mjs,导致生成的补丁静默出错——失败模式是 .mjs 比发布版本大 50%,这在 7 MB 的 diff 中很容易漏掉。manifest 中的 forbiddenBuildScripts 编码了这一规则,生成器会拒绝运行命名了其中任一脚本的构建步骤。
生成器还会先构建 未修改的 提交,并在输出任何内容之前断言它能逐字节复现已发布的 lib/。因此,工具链或构建顺序问题会表现为明确的“未能复现已发布 bundles”错误,而不是 7 MB 的神秘 diff。
从手工编辑的 bundle 中恢复¶
在 2026-08-09 到 2026-08-17 之间,此 harness 尚不存在,四个修复是通过直接编辑压缩后的 bundles 落地的。识别特征是压缩器不会输出的代码:在一个否则只使用 let 的 bundle 中出现 const,以及 $rl、$hp、$tid 这样的标识符。
恢复不是重写。手工编辑也应用到了 src/,因此已发布补丁中的源码 hunks 本来就是正确的,--write 会从它们重新推导 bundles。发生变化的是表面性的,并且是预期的:
- 手工编写的局部变量会重新收缩为压缩器名称,这会改变 esbuild 按频率排序的分配,并可能在全 bundle 范围内交换两个短名称(
.mjs中的i↔t,.js中的w↔y)。大多数有差异的行长度相同。 - 手工编写的等价形式会规范化为工具链实际输出的形式(
!!x变回Boolean(x),转义的\u200E变回字面字符)。
要确认重新生成在语义上是无操作而不是回退,请比较新旧 bundle 之间的标识符多重集,而不是阅读 diff:每个不是单字母压缩器局部变量的名称,在两者中出现的次数都应相同。其他任何情况都是真实变更,需要解释。
锁文件随补丁一起变更¶
pnpm 根据补丁文件本身的 sha256 派生 pnpm-lock.yaml 中的 patchedDependencies 哈希——以及 .pnpm/@xterm+xterm@<version>_patch_hash=<hash>/ 存储目录名。重新生成的补丁如果没有 lockfile 更新,会在除作者机器之外的所有机器上导致 pnpm install --frozen-lockfile 失败。--write 会进行该编辑;如果缺少该编辑,--check 会失败。
config/scripts/regenerate-xterm-patches.test.mjs 在没有网络或构建的情况下断言同一件事,因此普通测试任务可以在毫秒内捕获 lockfile 漂移,即使完整重建运行在它自己的 CI 通道中。
工具链固定¶
manifest 中的 toolchain 记录上游 package-lock.json 在固定提交处解析到的内容,如果 npm ci 产生其他结果,生成器会失败。重要的条目是 @typescript/native-preview(tsgo),上游将其固定到一个带日期的开发构建——撰写时为 7.0.0-dev.20260521.1。它是一个真实发布的版本,npm 也不会清理旧发布,但它是该方案中唯一不是稳定发布的依赖。
如果该版本变得无法解析,生成器会失败并给出一个点名该版本的工具链错误。恢复方法是把固定点移动到下一个 package-lock.json 可解析的上游提交,重新验证重建仍能复现已发布的 bundles,然后重新生成。已提交的补丁在此期间始终可用——只有重新生成被阻塞,因此这永远不会造成中断。
补丁路径根¶
已发布的 tarball 以包为根,因此 addon 的补丁使用 src/TextureAtlas.ts,而不是 addons/addon-webgl/src/TextureAtlas.ts。有两处必须与之保持一致,如果它们不一致,两者都会静默失败:
- checkout diff 传递
--relative,它必须位于CHECKOUT_DIFF_FLAGS中--分隔符之前。放在其后时,git 会将其读取为 pathspec,并保留相对于仓库根的路径,随后每个源码 hunk 都会从输出的补丁中脱落。 git apply从仓库根目录运行,并使用--directory=<packageDir>。如果改为从子目录运行,git 仍会从仓库根目录解析补丁路径,跳过每个 hunk,并退出 0。生成器通过以下方式保护:当应用源码补丁后 checkout 未发生变化时,使其失败。
版本升级¶
上游仅在自身输出发生变化时才发布每个包,因此四个包共享同一个提交,却带有不同的 beta 编号——撰写本文时,@xterm/xterm@6.1.0-beta.303 和 @xterm/headless@6.1.0-beta.302 都构建自 d3e32b3。应匹配 package.json.commit,而不是版本字符串;xterm-user-scrolling-contract.test.ts 会为 headless 和 core 断言这种配对关系。
升级 @xterm/xterm 的步骤是:
- 更新
package.json中的版本并运行pnpm install。 - 将两个补丁文件重命名为新版本,并更新
xterm-upstream.json中的patch、sourcePatch和version。 - 将
upstream.commit更新为新 tarball 的package.json中的commit字段,并将toolchain更新为新package-lock.json解析出的内容。 node config/scripts/regenerate-xterm-patches.mjs --write。pnpm install,然后--check。在升级时,lockfile 在新键下还没有条目,因此--write会报告缺失,并把哈希留给pnpm install;--check用于证明之后两者一致。
第 4 步是真正的上游冲突出现的地方:对源补丁执行 git apply 会在新代码树上失败。在检出中解决它,重新 diff,然后重跑。bundle 的 hunk 在任何时候都不需要处理。
为什么不内置一个 Fork¶
一个内置的 @xterm/xterm fork 可以完全移除补丁,但它会让 Orca 脱离已发布的包,因此每个上游 beta 都会变成一次合并,而不是版本升级,并且 Orca 会继承构建和发布一个它并不拥有的包的责任。该补丁只是针对一个可逐字节复现的提交的四个小型源码 hunk;对于相同的结果,fork 会带来大得多的持续成本。
为什么不在运行时处理组合¶
CompositionHelper 挂钩了在 onData 之前的四个私有调用点,而 SortedList 完全没有公开接口。没有任何受支持的扩展点能够触及其中任何一个,因此运行时 shim 意味着要深入 _core 内部,而上游会在 beta 之间随意重命名这些内部实现。补丁是风险更小的选择。
CI 契约¶
.github/workflows/pr.yml 中的 xterm_patch_sync 会在每个 PR 上运行 regenerate-xterm-patches.mjs --check,并且是 verify 聚合的一部分。它会克隆固定提交,安装上游工具链,构建两次,并将结果与已提交的补丁进行逐字节比较。两次构建和 diff 加起来大约八秒;为上游工具链执行 npm ci 才是该作业实际花费分钟数的地方,而缓存键是清单。
config/scripts/regenerate-xterm-patches.test.mjs 覆盖了纯逻辑部分——pnpm 的 diff 标志和规范化、hunk 拆分、往返稳定性、提交和构建顺序断言,以及 lockfile 耦合——无需网络且无需构建,因此它们运行在普通测试分片中。
已知缺口¶
@xterm/addon-ligatures 仍然通过手工打补丁,并且可以保持这种方式:该补丁是一处十五行的 package.json 编辑,重新指向 module 并添加一个 exports 块,不涉及 bundle 和 sourcemap。它没有任何生成内容,因此该工具链没有可验证的内容。
这些 addon 于 2026-08-29 被合并进这份清单。在那之前,它们是手工编辑的压缩 bundle,压缩代码中带有字面量 /* PATCH(orca): ... */ 注释,存在解析器往返伪影(!0 被重新打印为 true,局部变量从 i 重命名为 i5),并且完全没有 .map hunk——因此两者都附带了 sourcemap,其偏移量与旁边的 bundle 不匹配。现在,全部四个 @xterm/addon-webgl 产物和全部四个 @xterm/addon-serialize 产物都能从固定提交逐字节复现,这也正是解决该问题的原因。
仍然未得到证明的一点是,这一点在上游修订之间成立,而不仅仅在当前提交成立。addon-serialize.js.map 在这里第一次尝试时未能复现;原因是错误的构建顺序留下了过期的 out/,而不是上游非确定性,一旦先运行根构建,它就精确复现了。在证明相反结论之前,应将未来任何无法复现的产物视为构建顺序 bug。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 orca48/orca