Plate 仓库 TypeScript 6 升级实战:从 5.8.3 到 6.0.2 的迁移清单与排障指南
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
导读
本文以 Plate 仓库内 TypeScript 6 upgrade for PR 4887 升级计划文档为主线,完整还原一个大型 pnpm + Turbo monorepo(多包富文本编辑器项目)从 TypeScript 5.8.3 升级到 6.0.2 的完整决策路径:先确认生态兼容性,再复现失败、修复配置、清理真实代码回归,最后按固定顺序通过安装、构建、类型检查与 lint 四道验证闸门。读完本文,你将掌握 TS6 对baseUrl、moduleResolution: "node"、环境类型自动发现、CSS 副作用导入等行为的收紧点,以及如何避免被"混入 src/dist 的 TS 程序"和"Turbo 过滤式 typecheck 假失败"误导,做到一次升级、事实清晰、验证可复现。
升级目标与初始上下文
原始计划文档把目标定义得非常克制且明确:让当前 checkout 在 TypeScript 6 下通过,或者在生态尚未就绪时精确定位阻塞点。也就是说,这次升级不是"无脑升版本",而是先做可证伪的尝试。
初始上下文包含三个关键事实:
- 根 package.json 当时的
typescript版本被钉在5.8.3(升级后已变为6.0.2,并同时更新了 apps/www/package.json 的对应 pin)。 - 仓库中已有两处"已知陷阱"的文档沉淀,分别是工作区
src/dist混合图问题,以及 Turbo 过滤式 typecheck 的误导性失败。 - 升级前必须先拿到工具链兼容性的第一手证据(primary-source confirmation),不能只凭迁移指南就改版本。
这三点决定了整个计划的执行顺序:先取证、后复现、再动手、最后按固定顺序验证。
升级前必须识别的两个"已知陷阱"
计划文档明确要求,在动版本之前先消化仓库内两篇解决方案文档,因为它们会直接影响你对升级失败信号的解读。
陷阱一:工作区别名"脑裂"(split-brain)
来源文档:TypeScript workspace subpath aliases inapps/www。
问题本质:apps/www进入 pnpm workspace 后,开发态与类型检查态"各说各话":
- Next dev 通过
next.config.ts把工作区导入指向各包的src入口以便 HMR; - 而
apps/www里的tsc却通过链接包的 exports 解析到了dist; - 一旦
src与dist同时进入同一个 TS program,错误输出会变得"噪音爆炸"且极具误导性(典型表现是SlateEditor、插件配置类型与内部生成的dist符号之间的大段不兼容链)。
当时的修复策略是拆分类型检查策略:
- apps/www/tsconfig.json 检查应用代码及其 workspace 源码图;
- apps/www/tsconfig.package-integration.json 让
src/__tests__/package-integration/**按构建产物契约检查; - 在 app tsconfig 中,只保留宽泛
@platejs/*规则用于包根导入,并为platejs/react、platejs/static及每一个真实存在的@platejs/<pkg>/react、@platejs/<pkg>/static写入精确别名,同时把../../packages/*/src/**/*.d.ts与../../packages/udecode/*/src/**/*.d.ts纳入 include,保证源码级 typecheck 能看到包构建时才能看到的 ambient 声明。
文档还给出了一个"真相探测器"命令——检查 app 源码图里是否泄漏了 dist:
pnpm --dir apps/www exec tsc --noEmit -p tsconfig.json --listFilesOnly | rg '/packages/.*/dist/'对于 app 源码图,它应该什么都不打印。这个"先确认 TS program 到底在检查哪份代码,再决定是否追查错误"的方法论,正是本次 TS6 升级中判断"哪些失败是真的"的底层依据。
陷阱二:Turbo 过滤式 typecheck 会"撒谎"
来源文档:Turbo filtered typecheck can lie when package typecheck passes。
现象:pnpm turbo typecheck --filter=...在某个包上失败,但同一个包单独跑pnpm --filter @platejs/<pkg> run typecheck却通过。Turbo 会打印出具体的 TS 错误,看起来像真实包债务,但信号本身可能是假的。
根因是 turbo.json 中www#typecheck覆写曾清空了^build依赖:
"typecheck": { "dependsOn": ["^build"], "outputs": [], "cache": true }, "www#typecheck": { "dependsOn": [], "outputs": [] }这让www的 typecheck 可以在依赖包 build 任务还在重写 workspacedist输出时启动,而apps/www是有意检查"源码 + 构建产物"混合图的,并行改写就产生了转瞬即逝的"模块找不到"垃圾错误(如platejs/staticnot found、源码文件报缺少platejsexports、隐式any级联)。持久修复是把^build依赖加回 app 覆写:
"www#typecheck": { "dependsOn": ["^build"], "outputs": [] }因此计划文档给出的行动准则是:过滤式 Turbo typecheck 失败时,先直接验证包本身,再串行重跑,最后才宣布存在真实债务,即:
pnpm --filter @platejs/<pkg> run typecheck pnpm turbo typecheck --concurrency=1 --filter=./packages/<...>这两条"已知陷阱"叠加起来,决定了 TS6 升级期间的错误解读顺序:先看是不是 src/dist 混合、再看是不是 Turbo 并发噪音,最后才轮到真实的 TS6 兼容问题。
六步执行计划:先取证、后动手
计划文档给出了清晰的执行序列,每步都有明确产出:
- 从第一手来源确认 TS6 发布状态与关键工具链兼容性——例如
@typescript-eslint/*、tsdown、Bun 测试运行时等是否已支持 TS6,避免盲目升版本。 - 在当前 checkout 上复现失败检查——升级前先把失败信号抓出来,作为后续对比基线。
- 升级 TS6 所需的最小依赖/配置集——只动必要的 pin 与 tsconfig,不做无关重构。
- 修复升级暴露出的真实损坏点——区分"配置收紧导致的噪音"与"真正需要改代码的回归"。
- 按固定顺序运行安装/构建/类型检查/lint 验证。
- 判断这次工作是否产出可复用知识,若是则沉淀文档——本仓库的解决方案文档体系正是这样逐步累积的。
验证闸门:四道命令按序执行
计划文档明确列出验证闸门,且强调"按所需顺序"(in the required order):
pnpm install pnpm turbo build ... pnpm turbo typecheck ... pnpm lint:fix对应到根 package.json 的脚本体系,完整形态还包含:
pnpm build # g:build:turbo 过滤 packages 构建,失败时降级 --concurrency=1 pnpm typecheck # g:typecheck:先 pnpm g:build,再 turbo --filter "./packages/**" typecheck --only pnpm lint:fix # biome check . --fix pnpm lint # biome check . && eslint pnpm --filter www typecheck其中g:typecheck的设计值得注意:它强制先构建再类型检查,正是为了规避上文提到的"dist 正在被重写时并行 typecheck"的竞态;g:build中(turbo ... || turbo ... --concurrency=1)的降级分支也体现了对 Turbo 并行噪音的防御姿态。升级完成后,实测通过的命令集(见解决方案文档)为:
pnpm install pnpm build pnpm typecheck pnpm --filter www typecheck pnpm --filter www build:registry pnpm lint:fix pnpm lint升级中真正会踩到的四个 TS6 行为收紧点
配套解决方案文档 TypeScript 6 upgrade needs explicit paths and Bun test typing 把失败原因归结为一句话:升级失败不是因为代码突然不会写类型了,而是 TS6 收紧了仓库一直在悄悄依赖的几个配置行为。
1.baseUrl被弃用并升级为报错
TS5 时代baseUrl常被当作"静默路径前缀"使用。TS6 直接将其视为错误。本次升级中,根 tsconfig.json、apps/www/tsconfig.json、apps/www/scripts/tsconfig.scripts.json、tooling/config/tsconfig.test.json、packages/udecode/depset/tsconfig.json 五处都需要清掉baseUrl。
注意 tooling/config/tsconfig.test.json 目前仍保留"baseUrl": "../../"——这正是升级时必须改写的文件:仅仅删除baseUrl还不够,像"@/components/*": ["apps/www/src/components/*"]这样的路径目标在失去baseUrl前缀语义后必须改成显式相对路径:
"@/components/*": ["../../apps/www/src/components/*"]升级后的根 tsconfig.json 也印证了这一点:其paths全部使用以仓库根为起点的显式相对路径,例如"platejs": ["./packages/plate/src/index.tsx"]、"@platejs/*": ["./packages/*/src/index.ts", ...]、"@udecode/*": ["./packages/udecode/*/src/index.ts", ...],且moduleResolution已改为"bundler"。
2.moduleResolution: "node"被弃用,需切换为"bundler"
packages/udecode/depset/tsconfig.json 的moduleResolution已从"node"改为"bundler"。原因在于 TS6 不再接受老的 node 解析模式,而现代 bundler 驱动的 monorepo 本来就走 ESM + 导出映射,"bundler"才是与工具链一致的解析策略。
3. 独立 tsconfig 不再免费获得环境类型,需显式声明types
TS5 的 ambient 类型自动发现(auto-discovery)在 TS6 下失效:独立 tsconfig 不会再把@types/*包自动纳入程序。典型受影响者是 packages/udecode/depset/tsconfig.json,它专门运行 Bun 测试,必须在compilerOptions.types中显式加上"bun-types":
"types": ["bun-types"]根 tsconfig.json 同样采用了显式声明:"types": ["node", "@testing-library/jest-dom", "bun-types", "jest"]。
4. CSS 副作用导入默认被检查,需显式放行
TS6 默认检查 side-effect import 的模块存在性,导致apps/www对@/app/globals.css、@excalidraw/excalidraw/index.css、katex/dist/katex.min.css等导入开始报错。这些 CSS 副作用导入在本仓库是有意的(bundler 管理样式),因此在根 tsconfig.json 中显式设置:
"noUncheckedSideEffectImports": false升级暴露出的真实代码回归:两处小而精的修复
清完配置噪音后,剩下的真实损坏点只有两个,都得到了源码级验证。
1.Blob构造与Uint8Array<ArrayBufferLike>的类型收紧
在 packages/docx-io/src/lib/html-to-docx.ts 中,new Blob([buffer])在 TS6 下开始报错,因为jszip的generateAsync({ type: 'uint8array' })返回的是Uint8Array<ArrayBufferLike>,TS6 不再允许它直接作为BlobPart。修复方式是先拷贝成普通Uint8Array(该文件 L60-L63 即为修复后的真实代码):
const buffer = await resultZip.generateAsync({ type: 'uint8array' }); const blobBuffer = new Uint8Array(buffer); return new Blob([blobBuffer], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', });2. 全局spyOn类型过宽,需显式锚定到 Bun 的类型
apps/www/src/tests/package-integration/core-html/HtmlPlugin.slow.tsx 中原本用let jsonParseSpy: ReturnType<typeof spyOn>声明 spy,TS6 下它解析到了一个过宽的Spy类型,不再暴露mockReturnValue——运行时完全正常,撒谎的是类型。修复是直接从bun:test导入并显式标注:
import { afterEach, describe, expect, it, spyOn, type Mock } from 'bun:test'; let jsonParseSpy: Mock<typeof JSON.parse>; jsonParseSpy = spyOn(JSON, 'parse').mockReturnValue(/* ... */);这样既避免了错误的全局重载,又恢复了mockReturnValue/mockRestore的完整类型。
遗留事项与生态边界(升级后的诚实记录)
解决方案文档记录了几个升级后仍需记住的边界,写在这里供后续维护者参考:
- Contentlayer 的 prebuild 警告:
pnpm --filter www typecheck已通过,但 Contentlayer 在 prebuild 期间仍打印Config option 'compilerOptions.baseUrl' not found in "tsconfig.json",该警告非阻塞,构建可正常完成。 - 根级裸
tsc -p tsconfig.json仍会崩溃:pnpm --package=typescript@6.0.2 dlx tsc -p tsconfig.json --noEmit会命中_tsc.js中的 TypeScriptDebug Failure。这条路径不在仓库常规验证流程内,不阻塞升级,但若未来把裸根tsc作为 CI 闸门需要先解决。 - 生态 peer range 尚未完全跟上:
pnpm install可以成功,但@typescript-eslint/*、typescript-eslint、tsdown@0.16.6仍声明 TypeScript<6的 peer range,这些警告不阻塞已验证的构建与类型检查流程。
总结:这次升级留下的方法论
从计划到执行的完整闭环是:
- 先取证:确认 TS6 发布与关键工具链兼容性,不要拿迁移指南当唯一依据;
- 先复现:在当前 checkout 上跑出失败,让信号可对比;
- 先排除已知陷阱:用
--listFilesOnly检查 src/dist 是否混合、用--concurrency=1排除 Turbo 并发噪音,再谈真实债务; - 配置先行:清
baseUrl、改moduleResolution、显式声明types、放行 CSS 副作用导入,把 TS6 的行为收紧变成显式配置; - 再修代码:剩余的损坏点通常小而真实(
Blob类型收紧、测试 spy 类型锚定); - 按序验证:
pnpm install→ build → typecheck → lint,把验证过程固化为可复用知识沉淀进仓库文档。
对任何大型 TS monorepo 而言,这套"计划先行、证据驱动、验证闸门化"的升级姿势,比"升完版本再追着错误跑"要可靠得多——而这正是本仓库在 PR 4887 中实践并沉淀下来的真实路径。
延伸阅读(仓库内相关文档)
- TypeScript 6 upgrade needs explicit paths and Bun test typing:本次升级的完整排障与修复细节
- TypeScript workspace subpath aliases in
apps/www:src/dist 脑裂问题的根因与拆分解法 - Turbo filtered typecheck can lie when package typecheck passes:Turbo 过滤式 typecheck 假失败的识别与持久修复
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考