使用 @effect/docgen 为 Effect 项目自动生成 API 文档:安装配置、JSDoc 约定与源码实现解析
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
@effect/docgen 是 Effect 生态中一个"固执己见"(opinionated)的文档生成器,它读取 TypeScript 源码中的 JSDoc 注释,自动产出面向 GitHub Pages 的 Markdown API 文档站点,并内置示例代码的类型检查与运行校验。本文以仓库中的 .repos/effect-smol/packages/tools/docgen/README.md 为主体,结合 docgen 的源码实现,完整讲解其安装、零配置使用、docgen.json配置、JSDoc 标签约定、CLI 参数以及"解析—校验—生成"的底层流水线,帮助你在自己的 Effect 项目中一键生成高质量、可持续维护的 API 文档。
项目定位:为什么需要一个专门的文档生成器
@effect/docgen面向 Effect 项目设计,核心思路是把"写文档"这件事沉淀为一套可强制的工程规范:
- 注释即文档:通过标准的 JSDoc 标签(
@category、@example、@since、@deprecated、@internal、@ignore)描述每个模块导出,生成器负责排版输出; - 示例即测试:
@example中的代码片段会被tsc类型检查,还能用tsx实际运行,配合 Node.js 内置assert模块实现"文档即测试"; - 规范可强制执行:通过
enforceDescriptions/enforceExamples/enforceVersion开关,让 CI 在缺少描述、示例或版本标注时直接失败。
它借鉴了 docs-ts 可以看到其自身就是 Effect 风格代码:CLI 用effect/unstable/cli构建,配置加载用 Effect 的Config/ConfigProvider,整个程序跑在Effect运行时上。
安装与快速上手
环境要求
[!WARNING] 使用
@effect/docgen需要Node.js v18 或以上。
这一点在源码中有双重印证:package.json 声明了"engines": { "node": ">=18.0.0" },且bin入口直接指向需要原生process与子进程能力的脚本(bin.ts)。
安装
以rc版本安装为开发依赖:
npm install -D @effect/docgen@rc作为 peer 依赖,项目还需要 TypeScript 与 tsx(用于运行示例):
typescript:>=5.8.2 <7.0.0tsx:>=4.19.3 <5.0.0
三步完成接入
- (可选)创建
docgen.json配置文件,并让编辑器获得 JSON Schema 校验支持:
{ "$schema": "node_modules/@effect/docgen/schema.json" }- 在
package.json中添加脚本:
{ "scripts": { "docgen": "docgen" } }- 运行:
npm run docgen零配置默认行为
默认情况下 docgen 无需任何配置即可工作:从src目录搜索 TypeScript 文件,向docs目录输出生成的 Markdown 文档(README 原文:"By default, docgen will search for files in thesrcdirectory and will output generated files into adocsdirectory")。srcDir/outDir的默认值在 Configuration.ts 的ConfigurationSchema中有明确声明(default: "src"与default: "docs")。
生成的文档站点包含哪些文件
从 Core.ts 的getMarkdown与writeMarkdown可以看到,每次运行会生成/维护四类产物:
| 产物 | 路径(相对 outDir) | 说明 |
|---|---|---|
| 首页 | index.md | Jekyll front matter(title: Home,nav_order: 1) |
| 模块索引 | modules/index.md | title: Modules、has_children: true |
| GitHub Pages 配置 | _config.yml | 写入remote_theme、search_enabled与aux_links |
| 模块文档 | modules/<路径>.md | 每个源码模块一个文件,含目录(TOC)与分类导出版块 |
其中_config.yml是"智能合并"的:若目标文件已存在,resolveConfigYML 会保留用户已有内容,只替换remote_theme:、search_enabled:和 Auxiliary Links 三处;若不存在则生成完整默认文件。此外,写入前会先删除 outDir 下所有旧的**/*.ts.md文件,避免残留脏文档。
支持的 JSDoc 标签
docgen 通过doctrine解析 JSDoc(见 Parser.ts 的parseComment),支持的标签如下:
| 标签 | 说明 | 默认值 |
|---|---|---|
@category | 在生成的文档中为相关模块导出分组 | 'utils' |
@example | 为源码提供使用示例。所有示例都会用tsc做类型检查;还可以用tsx实际运行,并可用 Node.js 的assert模块做即时测试 | |
@since | 标注某段源码最近一次更新的库版本 | |
@deprecated | 标记弃用,生成的文档中对应模块或函数名会显示为 | false |
@internal | 阻止 docgen 为标注的代码块生成文档;若tsconfig.json中stripInternal为true,TypeScript 也不会为其输出声明 | |
@ignore | 阻止 docgen 为标注的代码块生成文档 |
标签行为背后的源码逻辑
@internal与@ignore的处理:在 Parser.ts 中,shouldIgnore(doc)直接检查doc.tags中是否存在"internal"或"ignore",命中即跳过该导出。注意二者语义差异:@internal还附带 TypeScript 编译器stripInternal的配合(README 明确说明),而@ignore纯粹是 docgen 层面的排除。@deprecated的渲染:Doc模型保存deprecated标签数组(Domain.ts),Printer 据此在名称上渲染删除线。@example的测试能力:示例代码块会被抽取成独立.ts文件先tsc类型检查、再(可选)tsx执行,执行环境中可直接import assert from "node:assert"做断言——即 README 所述"on-the-fly testing"。
docgen.json 配置详解
docgen默认是零配置 CLI 工具,需要自定义行为时,在项目根目录创建docgen.json。该文件由 Effect Schema 定义并校验(Configuration.ts 的ConfigurationSchema),对应的 JSON Schema 文件位于 schema.json,注意其中additionalProperties: false,多余的字段会被判定为非法配置。
docgen.json遵循以下 TypeScript 接口(README 原文):
interface Config { readonly projectHomepage?: string readonly srcLink?: string readonly srcDir?: string readonly outDir?: string readonly theme?: string readonly enableSearch?: boolean readonly enforceDescriptions?: boolean readonly enforceExamples?: boolean readonly enforceVersion?: boolean readonly tscExecutable?: string readonly exclude?: ReadonlyArray<string> readonly parseCompilerOptions?: string | Record<string, unknown> readonly examplesCompilerOptions?: string | Record<string, unknown> }参数说明与默认值
| 参数 | 说明 | 默认值 |
|---|---|---|
projectHomepage | 在生成文档的 Auxiliary Links(右上角导航区)中链接到项目主页 | package.json中的homepage字段 |
srcLink | 链接到项目源码 | {projectHomepage}/blob/main/src/ |
srcDir | docgen 搜索待解析 TypeScript 文件的目录 | 'src' |
outDir | docgen 输出 Markdown 文档的目录 | 'docs' |
theme | 写入生成_config.yml的 GitHub Pages 主题 | 'mikearnaldi/just-the-docs' |
enableSearch | 是否在生成_config.yml中开启搜索 | true |
enforceDescriptions | 是否强制要求每个模块导出都有描述 | false |
enforceExamples | 是否强制要求每个模块导出都有@example(注意:模块级文档不强制) | false |
enforceVersion | 是否强制要求每个模块导出都有@since | true |
tscExecutable | docgen 以编程方式调用编译器时使用的 TypeScript 编译器可执行文件路径 | 'tsc' |
exclude | glob 字符串数组,指定排除在文档之外的源码文件 | [] |
parseCompilerOptions | 解析源码用的 tsconfig 选项(或 tsconfig 文件路径) | {} |
examplesCompilerOptions | 示例代码用的 tsconfig 选项(或 tsconfig 文件路径) | {} |
两个值得注意的细节:
enforceVersion默认开启:也就是说,除非显式设置"enforceVersion": false,所有导出的函数、类、常量、接口、类型别名、命名空间都必须带@since标签,否则 docgen 会报错终止。这与 Checker.ts 中checkEntry的逻辑一致:@since缺失即生成 "Missing@sincetag" 错误,并附带@babel/code-frame定位的源码片段。parseCompilerOptions与examplesCompilerOptions可为字符串:传入字符串时被视为 tsconfig 文件路径,docgen 用tsconfck解析(readTSConfig),从中提取compilerOptions;传入对象时直接作为编译选项使用(Configuration.ts 的resolveCompilerOptions)。
配置优先级:CLI > 环境变量 > docgen.json > 默认值
从 Configuration.ts 的configProviderLayer可以看出完整的取值链条:
- CLI 标志优先(
loadConfiguration中先处理命令行参数); - 其次读环境变量:以
DOCGEN_为前缀、常量大小写命名(如DOCGEN_ENABLE_SEARCH=false),对应ConfigProvider.fromEnv({ env }).pipe(ConfigProvider.nested("DOCGEN"), ConfigProvider.constantCase); - 再读
docgen.json文件; - 最后落到内置默认值(
enableSearch: true、enforceDescriptions: false、enforceExamples: false、enforceVersion: true等)。
其中projectName与projectHomepage必须来自package.json(Schema 要求name与homepage均为字符串,缺失会直接报错),srcLink未配置时由 homepage 推导。
官方示例配置
{ "exclude": ["src/internal/**/*.ts"], "parseCompilerOptions": { "noEmit": true, "strict": true, "skipLibCheck": true, "moduleResolution": "Bundler", "target": "ES2022", "lib": ["ES2022", "DOM"], "paths": { "@effect/<project-name>": ["./src/index.js"], "@effect/<project-name>/test/*": ["./test/*.js"], "@effect/<project-name>/examples/*": ["./examples/*.js"], "@effect/<project-name>/*": ["./src/*.js"] } }, "examplesCompilerOptions": { "noEmit": true, "strict": true, "skipLibCheck": true, "moduleResolution": "Bundler", "target": "ES2022", "lib": ["ES2022", "DOM"], "paths": { "@effect/<project-name>": ["../../src/index.js"], "@effect/<project-name>/test/*": ["../../test/*.js"], "@effect/<project-name>/examples/*": ["../../examples/*.js"], "@effect/<project-name>/*": ["../../src/*.js"] } } }对该配置的实操解读:
exclude: ["src/internal/**/*.ts"]与@internal标签相互配合,是 Effect 生态的标准做法——内部实现既不出现在文档中,也不参与解析;parseCompilerOptions.paths把@effect/<project-name>映射到本地./src/*.js,保证解析阶段能正确解析模块间引用;examplesCompilerOptions.paths使用../../相对路径,是因为示例代码会被抽取到outDir/examples/下执行(见下文),需要从该目录回指项目根部的src;- 未显式设置的
runExamples保持默认false,即示例只做类型检查、不实际运行。
CLI 命令行参数
docgen 的 CLI 由effect/unstable/cli构建(CLI.ts),所有配置项都暴露为命令行标志,用法形如docgen --src ./lib --out ./api-docs:
| 标志 | 对应配置 | 说明 |
|---|---|---|
--homepage <url> | projectHomepage | 项目主页链接(显示在生成文档的 Auxiliary Links) |
--srcLink <url> | srcLink | 项目源码链接 |
--src <dir> | srcDir | 搜索待解析 TS 文件的目录(必须存在) |
--out <dir> | outDir | 输出 Markdown 的目录 |
--theme <theme> | theme | 生成文档使用的 Jekyll 主题 |
--disable-search/--enable-search | enableSearch | 是否在生成文档中启用搜索 |
--enforce-descriptions | enforceDescriptions | 强制要求每个模块导出有描述 |
--enforce-examples | enforceExamples | 强制要求每个模块导出有@example(模块级文档不强制) |
--no-enforce-version/--enforce-version | enforceVersion | 是否强制要求@since标签 |
--run-examples | runExamples | 是否实际执行源码中发现的示例 |
--exclude <glob> | exclude | glob 数组,排除指定文件(可多次传入) |
--parse-tsconfig-file <file> | parseCompilerOptions | 解析源码使用的 tsconfig 文件路径 |
--parse-compiler-options <json> | parseCompilerOptions | 解析源码使用的编译器选项(JSON 字符串) |
--examples-tsconfig-file <file> | examplesCompilerOptions | 示例使用的 tsconfig 文件路径 |
--examples-compiler-options <json> | examplesCompilerOptions | 示例使用的编译器选项(JSON 字符串) |
需要注意的约束(CLI.ts 的loadConfiguration中明确校验):
--parse-tsconfig-file与--parse-compiler-options不能同时使用,否则抛出InvalidValue错误;--examples-tsconfig-file与--examples-compiler-options同理;- 内联 JSON 选项会先经
Schema.decodeUnknownEffect校验,必须是合法的 JSON 对象,否则报错并提示expected: a JSON record。
示例代码的类型检查与运行:文档即测试
@example与描述文本中的代码围栏(code fence)会被 docgen 抽取、类型检查并(可选)运行,这是它区别于普通文档工具的核心能力。完整流水线位于 Core.ts 的typeCheckAndRunExamples:
- 抽取:
extractFencedCode用正则匹配```与~~~两种围栏;只有语言标记以ts/typescript开头、且不包含skip-type-checking元数据的代码块才会被抽取(支持```ts title="..."这类带元数据的围栏)。若发现未闭合的围栏会输出警告。 - 落盘:示例写入
outDir/examples/,文件命名规则为<模块路径>-<kind>-<导出名>-<序号>.ts(kind 包括module、class、interface、typealias、constant、function、namespace、export以及类的method/staticmethod),同时生成一个聚合入口index.ts(逐行import所有示例)和examples/tsconfig.json(内容来自examplesCompilerOptions)。 - 类型检查:用
tsc --noEmit --project <outDir>/examples/tsconfig.json检查全部示例;非零退出码即报错,错误信息中会携带tsc的 stdout。 - 运行:仅当
runExamples为true(配置或--run-examples)时,用tsx --tsconfig ... index.ts执行所有示例,执行失败同样导致 docgen 失败。 - 清理:每次运行前后都会删除
outDir/examples目录,保证示例永远从最新源码重新生成(目录中的文件标记为可覆写isOverwriteable: true)。
在 Windows 上,工具会自动改用.cmd变体(tsc.cmd/tsx.cmd)并以 shell 模式执行。
如果想跳过某段代码的类型检查,可在围栏元数据中加入skip-type-checking:
```ts skip-type-checking // 这段代码不会被 docgen 抽取与类型检查 ```工作原理解析:解析 → 校验 → 生成的流水线
docgen 的入口程序定义在 Core.ts 的program中,整体分为两个并发 Fiber,最后Fiber.joinAll汇合:
读取源码文件(glob src/**/*.ts + exclude) │ ▼ 解析为模块模型(Parser:ts-morph AST + doctrine JSDoc) │ ┌────┴─────────────────────────────┐ ▼ ▼ 校验模块(Checker) 生成 Markdown(Printer) │ ├─ 强制 @since ├─ 每个模块一个 .md │ ├─ 强制描述(可选) ├─ 首页 / 模块索引 / _config.yml │ └─ 强制示例(可选) └─ 清理旧 *.ts.md ▼ 类型检查并(可选)运行示例解析层(Parser.ts)
基于ts-morph的Project+doctrine的 JSDoc 解析,构建出 Domain.ts 中的Module模型。每个模块模型包含classes、interfaces、functions、typeAliases、constants、exports、namespaces七类成员,解析规则值得了解:
- 函数:既支持
export function声明,也支持export const fn = () => ...箭头函数形式的导出变量;签名统一渲染为declare const <name>: <类型>; - 常量:导出的、非函数类型的变量声明(如
export const x = 1); - 类:导出类的实例方法、静态方法(均取第一个重载的 JSDoc)与实例属性,属性签名保留
readonly与可选标记; - 命名空间:支持嵌套命名空间,文档中通过
前缀-命名空间名的方式区分配平后的重名导出(extractPrefixedNestedNamespaces); - 显式导出:包括命名导出(
export { x } from ...,支持前置 JSDoc 注释)与export * from .../export * as ns from ...(后者会被自动注入一段描述:"Re-exports all named exports from the ... module"); - 模块级文档:取源码文件第一个语句前的
/** ... */注释作为模块描述; - 解析选项:
createProject会把parseCompilerOptions合并进默认的{ strict: true, moduleResolution: "node" },再交给 ts-morph 建立工程。
校验层(Checker.ts)
checkModules遍历所有模块,对每个导出调用checkEntry按配置检查:
enforceDescriptions→ 缺少描述报错;enforceExamples→doc.examples.length === 0报错;enforceVersion(默认开启)→ 缺少@since报错。
错误信息使用@babel/code-frame输出带行列定位的源码片段。类的方法、静态方法、属性以及嵌套命名空间中的成员会递归校验,但方法/属性级不强制@since(enforceVersion: false)。
生成层(Printer.ts + markdown-toc)
- 每个模块生成
docs/modules/<路径>.md,front matter 中写入nav_order; - 用
@effect/markdown-toc生成 TOC,插入到<!-- toc -->占位符处,产出"Exports Grouped by Category"目录; - 最终内容经
prettier格式化后写盘。
常见问题
Q:重载函数能否分别记录每个重载的文档?
A:不能。docgen 在生成输出时只使用函数第一个重载的文档。这一行为在 Parser.ts 的getFunctionDeclarationJSDocs中得到印证:fd.getOverloads()非空时,直接取第一个重载的 JSDoc。因此请把完整的用法说明写在第一个重载上,或在实现上补充模块级文档。
更多参考
- 配置 Schema:schema.json
- 配置加载与默认值:Configuration.ts
- 主流水线:Core.ts
- JSDoc 解析:Parser.ts
- 规范校验:Checker.ts
- 数据模型:Domain.ts
- 命令行入口:CLI.ts
许可证
MIT License。
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考