首页
/ Astro 源码注释规范:面向贡献者的 JSDoc 契约、行内注释与"删除测试"

Astro 源码注释规范:面向贡献者的 JSDoc 契约、行内注释与"删除测试"

2026-09-05 20:14:51作者:凤尚柏Louis

Astro 仓库维护了一套严格的源码注释规范,它不是给最终用户看的 API 文档,而是写给"数月乃至数年后、拿着仓库 HEAD 版本、却完全没有你当前上下文"的贡献者读的。读完本篇,你将掌握 Astro 贡献者注释的三层分工(模块概述 / 条目文档 / 行内注释)、"删除测试"判据、变通方案(workaround)必须附 issue 链接的硬性要求、七类被明令禁止的注释反模式,以及 @deprecatedTODO: remove in Astro N 等仓库专属惯例,并在需要向 Astro 提交 PR 时写出符合社区评审标准的注释。

适用边界:贡献者注释 vs 用户文档

这条规范首先划清了一条容易踩线的边界:它只管贡献者面向的 .ts/.js 源码注释,不适用于面向最终用户的文档。仓库中有三类"注释"不在此管辖范围内,编辑它们需要走完全不同的流程:

  1. @docs 标签的 JSDoc 块——位于 packages/astro/src/types/public/config.tspackages/astro/src/core/errors/errors-data.ts 中,会被外部 docgen 工具抓取并发布到 Astro 官方文档站。编写这类内容需遵循 packages/astro/src/core/errors/README.md 中的错误信息写作指南,并且必须经过文档团队(docs team)评审——CI 会在 types/public/** 变动时重新生成参考文档。
  2. types/public/** 下的其他 JSDoc——它们通过编辑器的 IntelliSense 直接暴露给用户。要站在"Astro 用户正在建站"的视角写作,而不是"贡献者正在读源码"的视角。
  3. 除此之外,规范正文讨论的所有内容,都针对贡献者在 HEAD 上读取的源码

仓库中的真实 @docs 样例

config.ts 中每个配置项都带有结构化 JSDoc 块,包含 @name@type@default@version@description 等标签,例如 config.ts 中的:

/**
 * @name server.port
 * @type {number}
 * @default `4321`
 * @description
 * Set which port the dev server should listen on.
 *
 * If the given port is already in use, Astro will automatically try the next available port.
 */
port?: number;

这些块的用户是"配置 astro.config.mjs 的建站者",描述的是配置契约本身,绝不允许夹带内部实现说明(比如某个内部归一化函数的行为细节)——这正是该技能文档边界章节反复强调的反面案例。

errors-data.ts 的头部注释本身就是一条边界声明,见 errors-data.ts

// BEFORE ADDING AN ERROR: Please look at the README.md in this folder for general guidelines on writing error messages
// Additionally, this code, much like `types/public/config.ts`, is used to generate documentation, so make sure to pass
// your changes by our wonderful docs team before merging!

文件内每条错误都按 README.md 的规范用 @docs@message@see@description 标签组织,例如 UnknownCompilerErrorerrors-data.ts)就是一个完整样例:@description 从用户视角解释发生了什么、为什么、该怎么做,而不是描述抛错逻辑的内部实现。

读者的定义:一个只有 HEAD 的陌生人

规范把目标读者定义得非常具体:一位精通 TypeScript、但完全没有你当前上下文的 Astro 贡献者——他看不到你这轮对话、看不到你的 PR、看不到关联 issue、也看不到你的 diff,他只能看到仓库 HEAD 这一份代码。

由这个前提直接推出两条铁律:

  1. 永远不要叙述变更历史。 "now"(现在)、"previously"(以前)、"no longer"(不再)、"the new approach"(新方案)这类词在 HEAD 上毫无意义——那里只存在一种做法。注释要陈述代码如何工作,而不是它如何演变而来。唯一的例外是 @deprecated 声明(见后文"本仓库的惯例"),因为它描述的是契约的未来,而读者确实需要知道。
  2. 永远不要对评审者说话。 注释不是用来论证"我的改动是正确的"——"this properly handles X"(这正确处理了 X)这类辩解属于 PR 描述,不属于源码。注释必须为代码现状提供永久性辩护,而不是为你的改动辩护。

三类注释,三件不同的事

类型 语法 职责 内容
文件 / 模块概述 文件顶部的 /** */ 解释(Explanation) 模块为什么存在、它定义的概念和术语、各部分如何关联、设计动因
条目文档(Item docs) 直接位于声明上方的 /** */ 参考(Reference) 契约:行为、参数、返回值、抛出的错误、不变量。中性、事实性
行内注释 函数体内的 // 动机(Rationale) 只有代码自己说不出来的东西:约束、带 issue 链接的变通方案、不明显的耦合

这三类职责严格分离,对应 Diátaxis 文档框架中"解释 / 参考 / 动机"的三分法(技能文档在 References 一节明确以此为理论来源)。两个典型的混淆方向被点名禁止:

  • 实现细节不属于 /** */ 契约——应下沉为函数体内的 // 注释;
  • 契约不应散落在行内注释里——应挂在声明上方的文档块上。

行为文档:为人类读者写契约,而非翻译实现

当一条声明确实需要条目文档时,写作目标是给人类读者描述契约,而不是把实现逐行翻译成文字。规范同时强调这并不意味着"每个函数都要写 JSDoc"——名称、类型和结构本身应当能承载直观行为,承载不了就先改名字。

具体写作要求:

  • 先用一句通俗语言说明函数返回什么、完成了什么
  • 使用短句或中句,每句只承载一个主要思想;
  • 调用方不需要时,避免内部术语(Astro 黑话)。确需使用技术术语,就在同一段里解释;
  • 描述调用方可见的、可能令人意外的注意点:回退行为、工作量的上限、含糊的结果、重载的匹配顺序、副作用,以及返回 undefinednull、空结果或其他不确定值的条件;
  • 当调用方需要区分或恢复时,文档化抛出的错误;
  • 除非调用方理解行为或安全使用 API 必须,否则不描述实现细节。

何时必须加示例

当行为依赖于签名无法清晰表达的关系时,应添加 @example。Astro 与 TypeScript 中最常见的五类场景:

  1. 哪个重载会被选中;
  2. 参数如何映射到可选参数或 rest 参数;
  3. 哪个公开的 Astro 入口点暴露了实现在别处的符号(re-export 场景);
  4. 含糊路由、不完整配置或缺失内容时的回退行为;
  5. 含义无法从类型直接看出的返回值。

示例的写法也有规范:先用散文引出代码块,说明它演示什么、预期结果是什么;代码片段保持最小、自包含,并且站在 Astro 用户或拥有该契约的内部调用方的视角书写。

模块文档:描述持久的概念

模块级 /** */ 应当描述一个持久的概念、架构边界或设计理由。禁止罗列文件中的各个导出项来"总结"文件——随着符号被增删改名,这类清单很快就会过时(stale)。如果一个模块没有值得解释的持久概念,那就写一行简短描述,或者干脆不写概述。

删除测试(The Deletion Test)

写任何注释之前先问自己:这条注释陈述的信息,读者能否从代码本身恢复出来?

  • 如果信息已经被名称、类型或结构承载,就不要写注释;如果名字承载不了,去改进名字。
  • 真正有资格获得注释的信息:一个不变量、一个动机、与别处代码的耦合、带链接的变通方案、某个依赖令人意外的行为、以及该模块定义的专业术语。

编辑旧代码时同一测试反向适用:不再通过的注释应当删除,而不是留着慢慢腐烂。技能文档把这句收在自检清单的末尾,并给出总基调:"删除是默认选项;缺失的注释比误导性的注释更便宜。"

变通方案必须链接 issue 或 PR

这是本仓库一条高度一致、可被机器核验的硬性规则:**任何解释变通方案(workaround)、HACK、回归防护(regression guard)或依赖意外行为的注释,必须链接到动机所在的 GitHub issue 或 PR。**链接的存在让未来的读者能判断这个变通方案是否仍然必要——技能文档的原话是"没有链接的变通方案与一个错误无法区分"。

规范给出的标准示例:

// Handle recommended nanostores. Only @nanostores/preact is required from our testing!
// Full explanation and related bug report: https://github.com/withastro/astro/pull/3667
'@nanostores/preact',

这条规则在仓库中并非纸上谈兵。packages/astro/src/vite-plugin-environment/index.ts 里的 ALWAYS_NOEXTERNAL 列表就是规范风格的活体样本,每个条目都是"一行理由 + issue/PR 编号":

const ALWAYS_NOEXTERNAL = [
	// This is only because Vite's native ESM doesn't resolve "exports" correctly.
	'astro',
	// Vite fails on nested `.astro` imports without bundling
	'astro/components',
	// Handle recommended nanostores. Only @nanostores/preact is required from our testing!
	// Full explanation and related bug report: https://github.com/withastro/astro/pull/3667
	'@nanostores/preact',
	// Must be bundled so the prerender output resolves Astro's own copy, not an
	// older hoisted version from another dependency. See https://github.com/withastro/astro/issues/17508
	'neotraverse',
];

注意其中的措辞都是现在时动机("Vite fails on..."、"Must be bundled so..."),而不是"我们改了它因为……"式的历史叙述——恰好同时满足"读者铁律"和"变通链接"两条规则。

七类被禁止的注释模式

技能文档逐一点名并给出了改写对照,这是全篇最具操作性的部分:

1. 复述下一行代码。 看到就删:

// Increment the generation counter
generation += 1;

2. 叙述变更历史。 改写为现在时的动机说明:

// BAD: We now resolve lightningcss from the user's root instead of ours.
// GOOD: lightningcss is an optional peer dep, so it resolves from the user's project root.

3. 对评审者的辩解。 论据移回 PR 描述:

// BAD: This correctly handles the multi-encoded path from the bug report.
// GOOD: A path still encoded after MAX_DECODE_ITERATIONS is rejected, so
//       middleware and routing can never disagree on the decoded path.

4. 换了个说法的 JSDoc。 复述声明名的文档块等于没说:

// BAD:
/** Compiles the styles. */
function compileStyles(...)

// GOOD:
/** Rewrites relative `url()` references in `css` against `base`, leaving
 *  absolute and data URLs untouched. */
function compileSkills(...)

(注意示例中 GOOD 版本说明了行为契约——重写相对 url()、不动绝对与 data URL——而不是翻译函数名。)

5. 含糊的套话。 "some cases"、"various reasons"、"handles edge cases"、"etc."——要么点名具体是什么,要么删掉整句。

6. Emoji。 源码中全面禁用,注释也不例外(这是仓库级政策)。

7. 临时分区横幅(如 // ----- helpers -----// ==== TYPES ====)。本仓库没有 // #region 折叠惯例,不要添加横幅。如果一个文件长到你伸手去找横幅,那是该拆分文件的信号,不是该装饰它的信号。

本仓库的注释惯例

以下惯例是 Astro 源码特有的约定,脱离仓库语境可能不成立:

JSDoc 标签。 @param name - description@returns@throws 用于陈述契约;只有当签名本身有歧义时才用花括号包裹类型(如 @returns {Promise<string>});非显而易见的用法配 @example```js 围栏代码块。

交叉引用。 使用 {@link Symbol} / {@linkcode Symbol} 而不是裸写符号名,这样符号改名时引用会自动更新,编辑器也能跳转到目标。

@internal 标记不属于公开 API 面的符号。它只是约定——这个仓库没有 typedoc 或 api-extractor 来剥离它——所以它表达的是意图,不能替代真正的访问控制手段。

@deprecated 先说迁移方案,再说移除时间点,仓库示例:

/** @deprecated Use the instance method `cookies.consume()` instead. This will be removed in Astro 7 */

关键是说"替代方案是什么",而不只是"此符号已废弃"。这条面向未来的陈述属于读者需要的契约信息,不属于被禁止的历史叙述——这正是"读者铁律第 1 条"预留的例外通道。

TODO。 延后工作用 // TODO:;有 issue 跟踪就附链接;受破坏性变更窗口约束的工作使用既定句式 // TODO: remove in Astro <N>。本仓库不存在 FIXME——不要引入它。这句约定在源码中有直接证据,例如 packages/astro/src/content/runtime.ts 中的两处:

// TODO: remove in Astro 8
warnForPropertyAccess(
	logger,
	result.data,
	'slug',
	`[content] Attempted to access deprecated property on "${collection}" entry.\nThe "slug" property is no longer automatically added to entries. Please use the "id" property instead.`,
);

这里的措辞细节值得品味:TODO 本体只说"在 Astro 8 移除",而给用户看的警告字符串才解释替代路径——两条信息各归其位,互不污染。

编辑已有代码时的三条纪律

  • 保留既有注释。 如果你的改动改变了行为,就扩展或修正对应的那段散文——绝不替换成通用套话。删除来之不易的上下文,比留一条略微过时的注释更糟。
  • 注释被你的改动说假了,就在同一个 diff 里修好。 过时的注释比没有注释更坏。
  • 匹配周边的注释密度。 文档密集的模块,新增条目应达到同等水准;也不要给本来就稀疏的模块泼一身注释。

收尾自检:只重读 diff 里的注释

技能文档要求:完成任何触碰注释的任务后,把 diff 中新增的注释单独摘出来、脱离代码改动重读一遍,逐条过四问:

  1. 每一条是否通过删除测试?
  2. 是否有引用对话、引用本次改动本身、或对评审者说话的?
  3. 每个变通方案是否链接了它的 issue 或 PR?
  4. 一个看不到 diff 的读者能否独立理解每一条?

不达标的,修掉或删掉。删除是默认选项。

自动化评估:规则如何被验证

这些规范并非仅靠人工评审执行。仓库中 .agents/skills/writing-comments/evals/evals.json 为"写作注释"技能定义了三个可断言的评估场景,恰好覆盖规范的核心分支:

  1. 变通场景:给定一段"无上下文"的 resolveLightningcss 源码,要求补注释。断言包括:注释必须位于函数体内(//)而非声明 JSDoc、必须说明"lightningcss 是可选 peer dependency、须从用户项目根解析"、必须包含追踪 issue 的准确 URL、禁止出现叙述 import/return 的行、禁止 now/previously/correctly handles 等历史或辩解措辞、禁止 emoji 与横幅。
  2. 契约场景:给定 resolveEntry(collection, slug)(精确匹配 → 回退 → undefined 的语义无法从签名看出),要求补写完整条目 JSDoc。断言包括:开头散文描述调用方可见的结果而非复述函数名、JSDoc 覆盖精确匹配/回退/返回 undefined 三种条件、包含 @param collection -@param slug -@returns、带 js 围栏代码块的 @example、且不得宣称函数会抛错、不得谈论 Map.get 等实现细节。
  3. 边界场景:要求把一段 normalizeAssets() 的内部实现动机写进 types/public/config.ts@docs 块——正确行为是拒绝修改,识别出 @docs 块属于生成的用户文档、不在贡献者注释规则管辖内,且需要遵循 errors/README.md 一类的仓库指引并取得 docs 团队评审。

这三个场景与本技能规范文档(.agents/skills/writing-comments/SKILL.md)构成"规范—验证"闭环:凡你在 Astro 中写下的注释,都可以用同样的断言清单自测。

参考

登录后查看全文
热门项目推荐
相关项目推荐