理解 Redux Toolkit 文档构建中的remark-typescript-tools:TypeScript 7 时代的 vendored 插件移植
【免费下载链接】redux-toolkitThe official, opinionated, batteries-included toolset for efficient Redux development项目地址: https://gitcode.com/gh_mirrors/re/redux-toolkit
本篇技术指南聚焦于 Redux Toolkit 仓库(gh_mirrors/re/redux-toolkit)中website/plugins/remark-typescript-tools这一 vendored(内嵌复制)插件:它承担着 Docusaurus 文档站中"代码块类型检查与 TS/JS 双 Tab 渲染"和"从源码 JSDoc 抽取文档块"两大核心职责。读完本文,你将掌握该插件在 TypeScript 7 原生编译器(Go 内核)下如何通过 overlay 文件系统、oxc-transform与oxfmt完成编译与格式化管线,理解其与经典ts.*API 的差异,以及仓库为何选择以 vendored 形式而不是 npm 依赖接入。
背景:Docusaurus 文档站为什么需要它
Redux Toolkit 的官方文档(位于 docs 目录)由 Docusaurus 构建。文档中大量.mdx代码块需要在构建期被真实地"当作 TypeScript 项目"编译检查——这不仅包括语法检查,还包括类型检查;同时文档需要同时展示 TypeScript 与编译后的 JavaScript 两个版本,形成 Tabs 切换。
这两项能力分别由插件包内的两条 remark 管线提供,最终统一从 index.ts 导出:
transpileCodeblocks:将ts/tsx代码块编译、类型检查,并替换为@theme/Tabs+@theme/TabItem结构,同时展示 TS 与 JS;linkDocblocks:把形如docblock://的链接展开为源码中对应的 JSDoc/TSDoc 注释内容。
在 website/docusaurus.config.ts 中,两者被注册为 Docusaurus 的remarkPlugins。值得注意的细节是:transpileCodeblocks只在 CI 环境启用(process.env.CI判断),注释明确说明"因为它较慢"(// Only transpile codeblocks in CI, as it's slow)。
为什么是 vendored 而不是发布版依赖
README.md 明确解释了原因,核心是 TypeScript 7 的破坏性变化:
- TypeScript 7 的主导出只有
{ version, versionMajorMinor },不再有typescript.js与typescript.d.ts。任何依赖经典ts.*API 的工具都会失效。 - 上游
remark-typescript-tools重度使用经典 API:手写的LanguageServiceHost、getEmitOutput做转译、AST 遍历做 docblock 抽取,因此在 TS 7 下无法运行。 - 该移植唯一未完成的部分是声明文件输出:
rollup-plugin-dts仍然驱动经典 TypeScript API,在 TS 7 下会崩溃,因此上游包无法发布新版本。
采用 vendored 方案后,插件源码被 website/docusaurus.config.ts 直接以源码方式导入(main: "index.ts"),不再需要声明文件。
此外,README 特别强调这是一个"桥梁(bridge),不是 fork":不在该目录开发新功能,上游 PR 承载同样的移植,一旦上游发布新版本就删除本目录并重新依赖 npm 包。同时仓库在 .oxfmtrc.json 中将website/plugins/**列入ignorePatterns,以便这份副本与上游保持格式化一致、易于 diff。
从 package.json 可以看到完整依赖栈:typescript@^7.0.2、oxc-transform@^0.144.0、oxfmt@^0.63.0、make-synchronized@^0.8.0、@microsoft/tsdoc@^0.15.0、unified@^11.0.5、unist-util-visit@^5.0.0、unist-util-flatmap@^1.0.0、vfile@^6.0.3以及mdast-util-mdx-jsx、mdast-util-mdxjs-esm。
ESM 兼容桥:ts7.cjs的作用
TypeScript 7 是 ESM-only 的。Docusaurus 通过 jiti 加载docusaurus.config.ts,而 jiti 会把沿途所有依赖转译成 CommonJS——但这一转译会保留 TypeScript 的import.meta,导致加载时报错"Cannot use 'import.meta' outside a module"。
ts7.cjs 正是为解决这个兼容问题而生:它是纯 CommonJS、不含任何 ESM 语法,jiti 对它无可转译,于是交给 Node 原生require(esm)处理,Node 可以正确加载 TypeScript 7。该文件只导出运行时(value)导入:API、SyntaxKind、getLeadingCommentRanges、getTrailingCommentRanges、isIdentifier、isVariableDeclaration、isVariableStatement。import type在运行时前即被擦除,因此插件其余部分仍直接从typescript/unstable/ast导入类型。
transpileCodeblocks:代码块编译管线
transpileCodeblocks的实现位于 transpileCodeblocks/plugin.ts,核心是Compiler类(compiler.ts)。
插件层处理流程
插件的 transformer 对每个.mdx文件执行以下步骤:
- 跳过非目标文件:仅处理
fileExtensions中列出的扩展名(默认['.mdx'])。 - 自动注入 Tabs 导入:通过
visit检查 AST 中是否已导入@theme/Tabs与@theme/TabItem,若没有则在根节点插入对应的mdxjsEsm导入声明。 - 遍历代码块:对每个
lang为ts或tsx的code节点递增codeBlock计数,并跳过带no-transpilemeta 标签的代码块。 - 拆分为虚拟文件:
splitFiles用正则^\/\/ file: ([\w\-./\[\]]+)(?: (.*))?\s*$将单个代码块按// file:标记拆分成多个虚拟文件,存放到${virtualFilepath}/codeBlock_N/虚拟目录下;默认文件名index.ts;noEmit标记对应skip标志,用于"只检查不展示"的场景。 - 编译并收集诊断:调用
compiler.compile(),若存在line/character级别的诊断,则通过file.fail()触发构建失败,并附带诊断上下文行号与代码片段。 - 组装替换节点:
defaultAssembleReplacementNodes将原代码块替换为Tabs(groupId: 'language'、defaultValue: 'ts',values为[{ label: 'TypeScript', value: 'ts' }, { label: 'JavaScript', value: 'js' }]),内含两个TabItem:TS 版本展示postProcessTs的结果,JS 版本展示postProcessTranspiledJs的结果,并把meta中的.ts/.tsx标题改写为.js/.jsx。
Compiler:overlay 文件系统 + 单实例 API
Compiler的核心设计是基于overlay(覆盖层)文件系统的虚拟文件模型:
// compiler.ts 中的 createOverlay() 核心片段 readFile: (fileName: string) => this.virtual.get(slash(fileName)), fileExists: (fileName: string) => this.virtual.has(slash(fileName)) ? true : undefined, directoryExists: (dirName: string) => this.virtualDirs.has(slash(dirName)) ? true : undefined,关键点在于:TS 7 的FileSystem回调返回undefined即回退到真实文件系统。因此虚拟代码块与真实的node_modules树可以共存,而无需创建临时目录。代码块路径形如docs/api/createAction.mdx/codeBlock_2/,所以addVirtualFile会为每个祖先目录注册到virtualDirs——即使磁盘上真实存在createAction.mdx这个文件,在 overlay 中它也必须"报告为目录"。
另一个关键点是externalResolutions的处理。注释说明:TS 7 的 checker 运行在 Go 进程中,无法回调 JavaScript 模块解析器,因此externalResolutions不再通过resolveModuleNames生效,而是注入paths条目到生成的 tsconfig 中;packageId在 TS 7 中没有对应物,被忽略。
编译主流程compile():
- 将每个虚拟文件的内容写入 overlay,空行会被替换为
//__NEWLINE__注释标记(避免编译器删除空行),emit 后再移除。 - 生成一份与真实 tsconfig 相邻的
__remark_typescript_tools.tsconfig.json,其结构为:
{ "extends": "./<用户tsconfig文件名>", "compilerOptions": { "noEmit": true, "paths": { "模块名": ["解析后的真实路径"] } }, "include": [], "files": ["<所有虚拟文件绝对路径>"] }include: []+ 显式files至关重要——用户的 tsconfig 没有files/include,若不固定根文件,编译器会对每个代码块拉取整个 docs 目录树。 3. 调用api.updateSnapshot({ openProjects: [configPath], fileChanges })增量更新项目快照,fileChanges区分created/deleted/changed(首次快照为undefined),从快照中取出program。 4. 收集诊断:getConfigFileParsingDiagnostics()+getProgramDiagnostics()+ 每个文件的getSyntacticDiagnostics()+getSemanticDiagnostics(),通过flattenMessage展开messageChain,并用SourceFile.getLineAndCharacterOfPosition换算行列号。 5. 最后emit()调用transformSync(fileName, source, { jsx: 'preserve' })进行类型擦除与 JSX 转换——因为类型检查已完成,这里只需语法级转换,随后把//__NEWLINE__标记删除。
Compiler实例通过WeakMap<CompilerSettings, Compiler>在多次调用间复用,并提供dispose()关闭 API。
后处理:用 oxfmt 替代 Prettier
postProcessing.ts 实现了两个默认后处理器:
defaultPostProcessTs:对每个虚拟文件调用formatCode并trim()。defaultPostProcessTranspiledJs:先移除转译产物中的@ts-ignore/@ts-expect-error注释行(正则/(\n\s*|)\/\/ (@ts-ignore|@ts-expect-error).*$/gm),再格式化,并将文件名后缀从.ts/.tsx改写为.js/.jsx(name.replace(/.t(sx?)$/, '.j$1'))。
格式化使用oxfmt而非 Prettier。oxfmt的format是异步的(跨越 napi 边界),而 remark 遍历管线是同步的,因此通过make-synchronized在 worker 中运行并用Atomics.wait阻塞等待——这与旧版@prettier/sync对 Prettier 的处理是同一技术。
配置解析resolveFormatConfig从parentFile所在目录向上逐层查找.oxfmtrc.json、.oxfmtrc、oxfmt.json(带缓存),使代码块与所在仓库的格式化风格保持一致。仓库根目录的 .oxfmtrc.json 配置了semi: false、singleQuote: true、printWidth: 80等默认值,并通过overrides对packages/rtk-query-codegen-openapi/**、packages/rtk-codemods/**等目录定制选项。
与旧 Prettier 实现的关键行为差异(README 与源码注释均明确):缺少配置时不再跳过格式化,而是回退到 oxfmt 默认配置——将未格式化的编译器输出直接放进文档比用默认配置格式化更糟,且旧行为失败时仅输出一行日志、静默无效。
linkDocblocks:从源码抽取文档块
linkDocblocks(linkDocblocks/plugin.ts)允许在 MDX 中写docblock://链接,把源码注释渲染进文档。其 MDX 语法约定为:链接的host + pathname是相对basedir的源码文件,query 参数token必填(否则抛错token name must be provided as query parameter 'token'),overload用于选择重载序号(默认0)。链接文字(children[0].value)为逗号分隔的 section 列表,合法值为:summary、remarks、overloadSummary、overloadRemarks、examples、params。
sectionMapping定义了各 section 的渲染方式,例如:
summary→ 渲染summarySection并移除@summary标记;params→ 用* **$1**将@param x - desc改写为 Markdown 加粗列表;examples→ 渲染examples自定义块。
渲染后的结果会再次经过 MDX 解析器(this.parse)变为 AST 节点插入原位置。
在 website/docusaurus.config.ts 中,linkDocblocks的extractorSettings配置为:
tsconfig: ../docs/tsconfig.jsonbasedir: ../packages/toolkit/srcrootFiles: ['index.ts', 'query/index.ts', 'query/createApi.ts', 'query/endpointDefinitions.ts', 'query/react/index.ts', 'query/react/ApiProvider.tsx', 'query/core/buildMiddleware/cacheCollection.ts']
这意味着文档中的 docblock 链接全部指向packages/toolkit/src下的核心源码注释,例如createApi、endpointDefinitions等。
Extractor:AST 定位 + TSDoc 解析
extract.ts 中的Extractor类与Compiler共享同一套程序构造思路:生成__remark_typescript_tools.docblocks.tsconfig.json覆盖层配置(extends用户 tsconfig、noEmit: true、include: []、显式files),通过 overlay 提供,不落盘;随后api.updateSnapshot({ openProjects: [configPath] })拿到program。
findTokens递归遍历 AST 定位 token:支持点号路径(如createApi.something),对VariableStatement、VariableDeclaration处理"外部(outside)样式"与"内部(inside)样式"两种注释挂载方式。getComment(token, fileName, overload)找到 AST 节点后:
- 用 utils.ts 中移植自编译器
ts.getJSDocCommentRanges的getJSDocCommentRanges提取/**开头的注释区间(对Parameter、TypeParameter、FunctionExpression、ArrowFunction、ParenthesizedExpression、VariableDeclaration、VariableStatement额外收集尾部注释),排除/**/这种退化写法; - 用
@microsoft/tsdoc的TSDocParser解析,并注册自定义块标签@overloadSummary、@overloadRemarks; - 返回的
docComment额外附带了parserContext、buffer、overloadSummary、overloadRemarks、examples字段。
renderDocNode把 TSDoc 节点渲染回 Markdown:对DocFencedCode,识别// codeblock-meta ...前缀提取代码块 meta;对DocExcerpt输出其文本内容。
两条管线的共同底层:单例 API 与增量快照
Compiler与Extractor均通过new API({ fs, cwd })构造 TypeScript 7 的同步 API 实例(来自typescript/unstable/sync),并用updateSnapshot({ openProjects })建立项目快照。两个插件分别用WeakMap缓存单例,保证同一设置下只创建一个 API 实例。
这种"生成配置 + overlay 文件系统 + 快照增量更新"的架构直接对应 TypeScript 7 的 Go 内核架构:编译器进程不再能回调 JS,一切输入(虚拟文件、生成配置、外部解析)都通过 overlay 与paths注入,检查结果以快照/诊断形式返回 JS 侧。
版本基线
README 记录的上游基线:upstream 基于main@5c375b1,移植分支为feat/typescript-7-and-oxfmt@b2ce7f2。在 Redux Toolkit 仓库中,该插件以 website/plugins/remark-typescript-tools 目录存在,依赖版本可从 package.json 查看,根目录格式化约定见 .oxfmtrc.json,接入方式见 website/docusaurus.config.ts。
小结
Redux Toolkit 文档站通过 vendored 的remark-typescript-tools实现了两条关键能力:transpileCodeblocks在构建期真实编译、类型检查所有 TS/TSX 代码块并生成 TS/JS 双 Tab 展示,linkDocblocks将源码 TSDoc 注释直接嵌入文档。移植的核心思路是在 TypeScript 7 的 Go 内核架构下,用 overlay 文件系统替代LanguageServiceHost、用oxc-transform替代getEmitOutput、用oxfmt替代 Prettier,并通过ts7.cjs解决 ESM-only 依赖在 jiti 转译链中的兼容问题。理解这条管线,有助于你在阅读 Redux Toolkit 文档源码或为其他 Docusaurus 项目接入"代码块类型检查 + 注释抽取"能力时,快速定位实现与配置。
【免费下载链接】redux-toolkitThe official, opinionated, batteries-included toolset for efficient Redux development项目地址: https://gitcode.com/gh_mirrors/re/redux-toolkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考