首页
/ Deno 源码仓库的开发者工具链详解:tools/format.js、lint.js、wgpu_sync.js 与 copyright_checker.js 的原理与用法

Deno 源码仓库的开发者工具链详解:tools/format.js、lint.js、wgpu_sync.js 与 copyright_checker.js 的原理与用法

2026-09-06 13:19:35作者:申梦珏Efrain

本文基于 Deno 仓库的 tools/README.md 展开,系统讲解 Deno 官方源码仓库 tools/ 目录下的四类核心开发工具脚本:代码格式化(format.js)、代码静态检查(lint.js)、WebGPU 上游同步(wgpu_sync.js)与版权头校验(copyright_checker.js)。结合各脚本的源码实现,你将理解这些脚本在 CI 前的准入流程中扮演什么角色、底层如何并行调度 dprint / deno lint / clippy,以及如何安全地执行上游 vendor 同步与版权合规检查。

tools/ 目录定位:Deno 开发的准入检查层

tools/README.md 开篇一句话说明了该目录的定位:

Documentation for various tooling in support of Deno development.

tools/ 目录下的脚本共同服务于 Deno 自身的开发流程,其中 format.js 与 lint.js 被明确标注为 代码提交前必须执行的前置步骤("It is a prerequisite to run this before code check in")。这与仓库根目录的 CLAUDE.md 中的开发约定相互印证:

- Before committing, make sure `tools/format.js` is run to format your code
- ...`tools/lint.js --js` and fix any lint errors before committing
- If you changed Rust code, make sure to run `tools/lint.js` and fix any lint

也就是说,对 Deno 贡献代码的标准流程是:先 format,再 lint,全部通过后才提交。下面逐个脚本展开。

format.js:统一入口的代码格式化

基本用法

tools/README.md 的说明,format.js 使用 dprint(Rust 代码侧使用 rustfmt)来格式化代码库,运行方式为:

deno run --allow-read --allow-write --allow-run ./tools/format.js

源码实现:dprint 统一配置

阅读 tools/format.js 可以发现,脚本逻辑非常短,核心是拼装并执行一条 dprint 命令:

const subcommand = Deno.args.includes("--check") ? "check" : "fmt";
const configFile = join(ROOT_PATH, ".dprint.json");
const cmd = new Deno.Command("deno", {
  args: [
    "run",
    "-A",
    "--no-config",
    "npm:dprint@0.47.2",
    subcommand,
    "--config=" + configFile,
  ],
  cwd: ROOT_PATH,
  stdout: "inherit",
  stderr: "inherit",
});

从源码结构看有三个值得注意的设计点:

  1. dprint 版本锁定:通过 npm:dprint@0.47.2 精确固定 dprint 版本,保证所有开发者与 CI 看到一致的格式化结果,避免“在我机器上是好的”式漂移。
  2. --check 子命令:脚本解析 Deno.args,当传入 --check 时向 dprint 转发 check 子命令(只检测不修改),否则执行 fmt。这意味着你可以用 deno run ./tools/format.js --check 做“格式是否干净”的快速校验,适合在提交前使用。
  3. 统一配置源:所有格式规则收敛在仓库根的 .dprint.json 中,脚本本身不包含任何格式规则,规则变更只需改一个配置文件。
  4. 退出码透传Deno.exit(code) 将 dprint 的退出码原样传出,因此该脚本可以直接作为 CI 任务或 Git hook 的判定依据。

lint.js:JS 与 Rust 双栈的静态检查调度器

基本用法

tools/README.md 说明 lint.js 使用 deno lintclippy 检查代码库,同样是提交前必跑步骤:

deno run --allow-read --allow-write --allow-run ./tools/lint.js

文档还给出了一条实用技巧:可以用 cargo 运行当前正在构建(或已构建)的本地 deno 可执行文件来执行这些工具脚本,从而让工具链检查的是你正在开发的那个 Deno 版本,而不是 PATH 里安装的版本:

cargo run -- run --allow-read --allow-write --allow-run ./tools/<script>

这条建议对 Deno 内核贡献者尤其重要——比如验证新的 lint 规则时,需要用包含该规则的那个构建去跑 tools/lint.js

参数与并行调度模型

tools/lint.js 支持 --js--rs 参数控制检查范围,两个都不传则 JS 与 Rust 都检查:

let js = Deno.args.includes("--js");
let rs = Deno.args.includes("--rs");
if (!js && !rs) {
  js = true;
  rs = true;
}

各检查项被推入 promises 数组后用 Promise.allSettled(promises) 并行执行,任一失败则打印错误并以退出码 1 结束。JS 侧包含 6 类检查,Rust 侧包含 3 类,其中 checkCopyright(版权检查)在同时启用 --js--rs 时才跑。这种设计让 tools/lint.js --js 只查 JS 侧、--rs 只查 Rust 侧,方便在 CI 矩阵中拆分任务。

clippy 侧:deny 清单与分 crate 策略

clippy() 函数是 lint.js 中最复杂的部分。它首先定义了一组拒绝级 lint 规则:

const clippyDenyFlags = [
  "--",
  "-D", "warnings",
  "--deny", "clippy::unused_async",
  "--deny", "clippy::print_stderr",
  "--deny", "clippy::print_stdout",
  "--deny", "clippy::large_futures",
  "--deny", "clippy::allow_attributes_without_reason",
];

源码注释解释了为什么禁用 print_stdout / print_stderrRust 标准输出宏在管道断开时(例如 deno test | head)会 panic,因此 Deno 代码库要求用 log crate 打诊断日志、用 deno_printdrop_println! 宏打 stdout。更细致的是,脚本会实时捕获 clippy 的 stderr 流,一旦匹配到 clippy::print[-_]std(out|err) 字样就额外打印这条提示,把“报错原因 + 修复方向”直接喂给开发者。

在 workspace 层面,由于 Cargo 的 --all-features 无法表达互斥的引擎后端,脚本通过 cargo metadata --no-deps 解析出所有 workspace 成员的全部 feature(剔除 QuickJS 与平台相关 V8 模式),显式拼接 --features 传给 clippy,并 --exclude deno_coredeno_core 则单独按固定 feature 集(default,unsafe_runtime_options,unsafe_use_unprotected_platform,v8)再跑一次 clippy。这种“分 crate、分 feature”的策略保证每个 crate 都在其真实的 feature 组合下被检查。

此外 clippy 检查还会遵循 buildMode()(来自 tools/util.js):传入 --release 时 clippy 也加 --release,使检查目标与实际构建模式一致。

几个高信息量的内置约定检查

除 deno lint 与 clippy 外,lint.js 还内嵌了几个纯 JS 实现的“仓库宪法”检查:

  • ensureNoNonPermissionCapitalLetterShortFlags:正则扫描 libs/cli_parser/src/defs.rs 中所有 .short('X') 声明,断言大写短选项只能是权限相关标志(A/E/I/N/R/S/W 等),并维护一份白名单。源码注释说明了动机:大写短选项只关联权限,是为了让用户审查命令时能“多一分警惕”。
  • ensureDisallowedMethodsEnforced:强制 ext/libs/runtime/ 下每个 crate 都有 clippy.toml,且其中必须列出禁止直接调用的方法清单(如 std::fs::readstd::fs::writestd::path::Path::canonicalizeurl::Url::to_file_path 等),libs 类 crate 还要额外禁止 std::env::varstd::time::SystemTime::now 等。这从静态层面保证 Deno 扩展必须走 ops 抽象访问文件系统/环境变量/时间,而非直接调用 std。
  • ensureNoUnusedOutFiles:遍历 tests/specs/ 下所有 __test__.jsonc,把其中 output 字段引用的 .out 文件从集合中删除,剩下的即为“无人引用的 .out 文件”,报错要求清理——防止测试快照膨胀。
  • ensureNoNewTopLevelEntries:通过 gitLsFiles 列出仓库顶层条目,与白名单(.cargoclidocextlibsruntimeteststoolsx 及各根配置文件)比对,新增任何顶层目录或文件都会被拒绝,注释明确写着“Keep the root of the repository clean”,且向白名单添加条目必须先讨论。
  • ensureWorkflowYmlsUpToDate:逐个运行 .github/workflows/*.ts 生成器(带 --lint),确保所有 .generated.yml 工作流文件与其 TypeScript 源同步。

文件收集层的 Git / Jujutsu 双兼容

上述检查都依赖 tools/util.js 中的 getSources(baseDir, patterns) 收集文件。值得留意的是 gitLsFiles 的实现:如果检测到 .jj 目录(即仓库用 Jujutsu 管理),会把 git pathspec 翻译成 jj 的 glob: 文件集(包括把 *.ext 映射成 **/*.ext、把结尾 / 的目录映射为 ** 等归一化逻辑),否则走 git ls-files -z --exclude-standard --cached --modified --others。这让工具脚本在 git 与 jj 工作流之间无缝切换,也说明 Deno 团队正在并行试验版本控制系统。

wgpu_sync.js:把 gfx-rs/wgpu 的 deno_webgpu 树 vendor 进来

tools/README.md 对 wgpu_sync.js 的说明是:

wgpu_sync.js streamlines updating deno_webgpu from gfx-rs/wgpu. It essentially vendors the deno_webgpu tree with a few minor patches applied on top, somewhat similar to git subtree.

即该脚本从上游 gfx-rs/wgpu 仓库整树拷贝(vendor)deno_webgpu 到本仓库 ext/webgpu/ 目录,再打少量补丁,类似 git subtree 的语义。官方推荐的操作流程为四步:

  1. 更新 tools/wgpu_sync.js 中的 COMMITV_WGPU
  2. 运行 ./tools/wgpu_sync.js
  3. 仔细复查改动,必要时手工打补丁;
  4. 提交并发送 PR。

源码走读:同步流水线

tools/wgpu_sync.js 的头部定义了同步的两个关键变量:

const COMMIT = "ae87ffe28041a7ebd82d8d3c2fa0e2343f0f0234";
const REPO = "gfx-rs/wgpu";
const V_WGPU = "29.0.1";
const TARGET_DIR = join(ROOT_PATH, "ext", "webgpu");

main() 按固定顺序执行四个阶段:

await main();  // => clearTargetDir → checkoutUpstream → patchCargo → patchReadme → format.js
  1. clearTargetDirrm -r ext/webgpu/*,清空本地 vendor 目录,保证每次同步从干净状态开始(这也是 README 要求“double check changes”的原因——上游文件被整体替换)。
  2. checkoutUpstream:通过 GitHub API 拉取指定 COMMIT 的 tarball,再用 tar --strip=2 只解压其中的 deno_webgpu/ 子树到 ext/webgpu/
    curl -L https://api.github.com/repos/gfx-rs/wgpu/tarball/${COMMIT} | \
      tar -C 'ext/webgpu' -xzvf - --strip=2 'gfx-rs-wgpu-ae87ffe/deno_webgpu/'
    
    注意这一步依赖网络与 bash,因此脚本 shebang 声明了 --allow-read --allow-write --allow-run
  3. patchCargo:对 vendor 进来的 ext/webgpu/Cargo.toml 做正则替换,把 version 对齐到根 Cargo.toml 中登记的 deno_webgpu 版本,把 authors/license/repository 归并到 workspace 继承(authors.workspace = true 等);同时把根 Cargo.tomlwgpu-corewgpu-types 版本统一改成 V_WGPU。这一步保证 vendor 代码与本仓库 Cargo workspace 规范兼容。
  4. patchReadme:幂等地向 ext/webgpu/README.md 注入/替换 ## Source 段落,声明该目录的 canonical source 在上游仓库、本副本由该脚本同步。脚本会先删除已有的 Source 段落再插入,因此重复执行不会累积重复内容
  5. 最后调用 tools/format.js 对新拉入的代码统一跑一遍 dprint 格式化,保证 vendor 代码也符合本仓库格式规范。

这种“整树替换 + 确定性补丁 + 复用本仓库格式化”的模式,是维护第三方 vendor 代码的典型做法:上游升级时 diff 可控、补丁位置固定、格式统一。

copyright_checker.js:全仓版权头合规检查

tools/README.md 对版权检查器的描述:

copyright_checker.js is used to check copyright headers in the codebase. ... it will check all code files in the repository and report any files that are not properly licensed.

运行方式:

deno run --allow-read --allow-run  ./tools/copyright_checker.js

源码走读:检查规则与豁免清单

阅读 tools/copyright_checker.js,其核心逻辑是逐文件读取开头一段字节(readFirstPartOfFile 只读前 1KB,避免为检查版权头而加载整个文件),验证是否以期望的版权行开头:

const copyrightYear = 2026;
const COPYRIGHT_LINE =
  `Copyright 2018-${copyrightYear} the Deno authors. MIT license.`;

几个值得展开的规则细节:

  • 文件类型与豁免清单:通过 getSources 收集 *.js*.mjs*.jsx*.ts*.tsx*.rs*.c 以及所有 Cargo.toml,并用 :!: 排除模式跳过第三方 vendor 与测试数据,例如 cli/tsc/*typescript.js(TypeScript 编译器本体)、cli/tsc/dts/**(内置类型声明)、ext/node/polyfills/deps/**tests/registry/**tests/wpt/suite/** 等。这份清单本身就是“哪些代码属于 Deno 自研、哪些是引入的上游”的一份隐性边界描述。
  • 允许的版权行前缀ACCEPTABLE_LINES 允许版权行之前出现 // deno-lint-* 指令、// Copyright* 续行、// Ported*(移植代码)、空行或 shebang 行;超过这些容忍范围仍不以版权行开头则报错,并附上“出错的行是……”的诊断。
  • Cargo.toml 单独处理:期望以 # Copyright 2018-2026 the Deno authors. MIT license. 开头(TOML 注释风格),而 JS/TS/Rust/C 文件期望 // Copyright ...(C 风格注释)。
  • LICENSE.md 年份联动:脚本最后还检查仓库根 LICENSE.md 是否包含 Copyright 2018-2026 the Deno authors,年份过期会单独报 LICENSE.md has old copyright year
  • 错误合并输出:所有错误先收集、最后一次性 console.error(errors.join("\n")) 输出,注释解释原因是避免与并行运行的其他工具脚本的输出交错。

该脚本还被 tools/lint.js 以模块方式复用(import { checkCopyright } from "./copyright_checker.js"),所以跑 tools/lint.js 时会顺带完成版权检查。

小结:从 tools/README.md 到完整提交前检查清单

回到 tools/README.md 的主线,可以把整个工具链浓缩成一份可操作的提交前清单:

步骤 命令 底层工具 检查/修改范围
格式化 deno run --allow-read --allow-write --allow-run ./tools/format.js dprint 0.47.2(配置 .dprint.json),支持 --check 只检测 全仓文本文件
检查(全量) deno run --allow-read --allow-write --allow-run ./tools/lint.js deno lint + clippy + 多个内置约定检查 + 版权检查 JS/TS 与 Rust 双栈
检查(仅 JS) ... ./tools/lint.js --js deno lint + 工作流同步、顶层条目、.out 快照等检查 JS/TS 侧
用本地构建跑脚本 cargo run -- run --allow-read --allow-write --allow-run ./tools/<script> cargo 构建中的 deno 二进制
WebGPU 上游同步 修改 COMMIT/V_WGPU 后运行 ./tools/wgpu_sync.js curl + tar + 正则补丁 + format.js ext/webgpu/
版权检查 deno run --allow-read --allow-run ./tools/copyright_checker.js 自研脚本 全仓源码头 + LICENSE.md

从源码结构看,这套工具链的设计哲学很清晰:所有格式与静态检查规则收敛在单一入口脚本与单一配置文件中(dprint 配置只有一个 .dprint.json,clippy 规则集中在 tools/lint.js 的 deny 清单与各 crate 的 clippy.toml),上游 vendor 用可重放的脚本管理tools/wgpu_sync.js),仓库结构本身也被当作被检查对象(顶层条目白名单、工作流文件生成同步)。对希望参与 Deno 开发或研究其工程实践的读者,直接阅读 tools/ 目录下的这些脚本,是理解 Deno 如何大规模维护一个 Rust + JS 双栈代码库的最快入口。

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