Deno 的工具链与 Node.js 兼容性:NPM、LSP 与 Tools 目录
前置知识
- ›第 1–4 篇文章
- ›Node.js/npm 生态系统(package.json、node_modules、npm registry)
- ›LSP 协议基础概念
Deno 的工具链与 Node.js 兼容性:NPM、LSP 与 Tools 目录
在前四篇文章中,我们从进程入口与子命令分发(第 1 篇)出发,依次深入了解了 Rust-V8 扩展桥接机制(第 2 篇)、模块加载与 TypeScript 编译(第 3 篇),以及 Worker 引导与权限系统(第 4 篇)。本篇是系列的收尾,将从更宏观的视角审视 Deno 的整体能力:替代 prettier、eslint、jest 和 esbuild 的集成工具链;deno compile 独立二进制构建流程;支持两种解析模式的深度 npm 兼容层;200 多个 Node.js API polyfill;完整的 LSP 实现;以及用于验证上述一切的规范测试框架。
Tools 目录:内置开发工具链
cli/tools/mod.rs 中声明了 23 个工具模块:
pub mod bench;
pub mod bundle;
pub mod check;
pub mod clean;
pub mod compile;
pub mod coverage;
pub mod deploy;
pub mod doc;
pub mod fmt;
pub mod info;
pub mod init;
pub mod installer;
pub mod jupyter;
pub mod lint;
pub mod pm; // package management (add, remove, audit, outdated)
pub mod publish;
pub mod repl;
pub mod run;
pub mod serve;
pub mod task;
pub mod test;
pub mod upgrade;
pub mod x;
每个工具模块都由第 1 篇中介绍的 run_subcommand() 进行分发,接收一个 Arc<Flags> 以及各自的子命令专属 flags 结构体。各工具通过 CliFactory 共享基础设施——按需调用 factory.cli_options()、factory.module_graph_builder() 或 factory.type_checker(),而 Deferred<T> 确保每个服务只初始化一次。
graph LR
subgraph "Tools"
FMT[fmt]
LINT[lint]
TEST[test]
BENCH[bench]
DOC[doc]
COMPILE[compile]
PUBLISH[publish]
TASK[task]
REPL[repl]
end
subgraph "Shared via CliFactory"
FF[FileFetcher]
MGB[ModuleGraphBuilder]
TC[TypeChecker]
RF[ResolverFactory]
end
FMT --> FF
LINT --> FF
TEST --> MGB
TEST --> TC
BENCH --> MGB
DOC --> MGB
COMPILE --> MGB
PUBLISH --> MGB
PUBLISH --> TC
TASK --> RF
这里有一个关键设计思路值得关注:deno fmt 完全不会触碰类型检查器,deno lint 不需要模块图构建器,deno task 几乎不依赖这些服务中的任何一个。第 1 篇介绍的 Deferred<T> 模式确保启动开销与每个工具的实际需求成正比。
提示: 有些工具的实现有繁简之分。
cli/tools/fmt.rs只是单个文件,而cli/tools/test/则是一个完整的目录,包含用于测试发现、运行器、报告器和覆盖率集成的多个子模块。添加新工具时,可以参考类似工具的结构。
deno compile:独立二进制构建流程
deno compile 命令能生成一个自包含的可执行文件——目标机器上无需安装 Deno。cli/standalone/binary.rs 中的构建流程分为以下几个阶段:
flowchart TD
SRC["Source module<br/>main.ts"]
GRAPH["Build ModuleGraph<br/>(all dependencies)"]
RESOLVE["Resolve npm packages<br/>+ node_modules"]
VFS["Build VirtualFs<br/>Embed files in memory"]
META["Serialize Metadata<br/>flags, import map, lockfile"]
BASE["Fetch base Deno binary<br/>(for target platform)"]
APPEND["Append eszip + VFS + metadata<br/>to binary"]
MAGIC["Write MAGIC_BYTES trailer"]
OUT["Standalone executable"]
SRC --> GRAPH
GRAPH --> RESOLVE
RESOLVE --> VFS
VFS --> META
META --> BASE
BASE --> APPEND
APPEND --> MAGIC
MAGIC --> OUT
cli/standalone/virtual_fs.rs 模块负责处理 VirtualFs——一个嵌入在二进制文件中的文件系统,用于承载 node_modules、本地源文件及其他资源。VfsBuilder 会对文件进行去重(相同内容只存储一份),output_vfs() 函数则以树形结构展示已嵌入的文件及其大小:
pub fn output_vfs(vfs: &BuiltVfs, executable_name: &str) {
if !log::log_enabled!(log::Level::Info) {
return;
}
let display_tree = vfs_as_display_tree(vfs, executable_name);
log::info!("\n{}\n", deno_terminal::colors::bold("Embedded Files"));
log::info!("{}", text.trim());
}
序列化到二进制文件中的 Metadata 结构体包含了重建运行时环境所需的全部信息:CLI flags、workspace resolver 配置、npm 解析快照、CJS 导出分析结果,以及 import map。运行时,独立二进制文件从 MAGIC_BYTES 尾部读取这些数据,并重建一个由嵌入式 eszip 和虚拟文件系统支撑的、配有自定义 ModuleLoader 的 MainWorker。
NPM 集成深度解析
Deno 支持两种 npm 解析模式,具体取决于是否存在 node_modules/ 目录:
托管模式(Managed mode,默认):Deno 在 $DENO_DIR 下管理自己的 npm 缓存。包会被解析、下载并全局缓存,除非显式指定,否则不会创建 node_modules/ 目录。该模式由 libs/npm_installer/ crate 实现。
BYONM 模式(Bring Your Own Node Modules):当项目中已存在 node_modules/ 目录(通常由 npm install 或 pnpm install 创建)时,Deno 会直接读取该目录。通过 --node-modules-dir 选项或检测到已有 node_modules/ 时,Deno 会自动切换到此模式。
graph TD
REQ["npm:express@4"]
MODE{Resolution mode?}
REQ --> MODE
MODE -->|Managed| MGLOBAL["Global npm cache<br/>$DENO_DIR/npm/"]
MGLOBAL --> RESOLVE_M["Resolve version<br/>from registry"]
RESOLVE_M --> CACHE["Cache tarball<br/>+ extract"]
CACHE --> LINK["Create virtual<br/>node_modules layout"]
MODE -->|BYONM| LOCAL["Read node_modules/<br/>on disk"]
LOCAL --> RESOLVE_B["Resolve from<br/>package.json"]
RESOLVE_B --> FOUND["Use installed<br/>package"]
LINK --> READY["Package ready"]
FOUND --> READY
cli/npm.rs 文件定义了 CLI 专用的 npm 类型,包括 CliNpmInstallerFactory、CliNpmInstaller 和 CliNpmCache——它们都以系统 trait CliSys 为泛型参数。生命周期脚本执行器负责处理 preinstall、install 和 postinstall 脚本,其中有一个值得关注的检查:is_broken_default_install_script() 会识别并跳过在 Deno 环境中已知会失败的安装脚本。
deno add 和 deno remove 命令(分发至 tools::pm)会修改 deno.json 或 package.json,更新依赖声明并执行安装。deno outdated 命令则检查所有依赖是否有新版本可用。这些包管理命令共享 libs/npm/、libs/npm_cache/ 和 libs/npm_installer/ crate 提供的基础设施。
Node.js Polyfill 层(ext/node/)
ext/node/lib.rs 扩展是 Deno 中体量最大的单个扩展,通过 Rust ops 和 JavaScript polyfill 两种方式提供了 200 多个 Node.js API shim:
pub use node_resolver::DENO_SUPPORTED_BUILTIN_NODE_MODULES
as SUPPORTED_BUILTIN_NODE_MODULES;
该扩展在两个层面上发挥作用:
-
JavaScript polyfill(
ext/node/polyfills/):以纯 JavaScript 实现的 Node.js 模块,涵盖path、url、events、buffer、stream、crypto(部分)、assert等。 -
Rust ops(
ext/node/ops/):用 Rust 实现的性能敏感操作,包括node:crypto操作(委托给ext/node_crypto/)、node:child_process(映射到 Deno 的子进程系统)、node:fs(委托给ext/fs),以及node:vm(自定义 V8 context 管理)。
graph TD
subgraph "Node.js Compatibility"
REQUIRE["require('fs')"]
IMPORT["import fs from 'node:fs'"]
RESOLVER["Node Module Resolver<br/>libs/node_resolver/"]
POLYFILL["JavaScript Polyfills<br/>ext/node/polyfills/"]
RUST_OPS["Rust Ops<br/>ext/node/ops/"]
DENO_EXT["Deno Extensions<br/>ext/fs, ext/net, ext/crypto"]
end
REQUIRE --> RESOLVER
IMPORT --> RESOLVER
RESOLVER --> POLYFILL
POLYFILL --> RUST_OPS
RUST_OPS --> DENO_EXT
NodeRequireLoader trait 抽象允许在 CLI、standalone 和测试等不同上下文中使用不同的 require() 实现。is_maybe_cjs() 方法根据最近的 package.json 中的 "type" 字段、文件扩展名及其他启发式规则,判断一个模块应以 CJS(require() 语义)还是 ESM 方式加载。
提示:
ext/node/ops/vm.rs中的VM_CONTEXT_INDEX常量以及create_v8_context()/init_global_template()函数实现了 Node 的vm.createContext()——它们会创建一个拥有独立全局对象的 V8 context,这正是jsdom等依赖该 API 的 npm 包所需要的。
LSP 架构
cli/lsp/mod.rs 包含 34 个子模块,基于 tower-lsp 实现了完整的 Language Server Protocol 服务器:
mod analysis;
mod cache;
mod capabilities;
mod client;
mod code_lens;
mod completions;
mod config;
mod diagnostics;
mod documents;
mod jsr;
mod lint;
mod npm;
mod performance;
mod refactor;
mod registries;
mod resolver;
mod semantic_tokens;
mod testing;
mod tsc;
mod tsgo;
// ... more
服务器由 lsp::start() 启动,创建一个 tower-lsp 服务:
pub async fn start() -> Result<(), AnyError> {
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
let builder = LspService::build(|client| {
LanguageServer::new(client::Client::from_tower(client))
});
// ...
}
LanguageServer 结构体负责管理文档状态、诊断信息、TypeScript 服务集成以及客户端通信。与 CLI 类型检查相比,LSP 架构有以下几点关键差异:
- 以文档为中心:LSP 追踪已打开的文档及其依赖,而非只关注入口文件
- 增量更新:单个文件的变更只触发针对性的重分析,而非重建完整的模块图
- 双重 TypeScript 支持:
lsp/tsc.rs(传统方式)和lsp/tsgo.rs(基于 Go 的新方式)均可提供类型信息 - 诊断聚合流水线:来自多个来源(TypeScript、deno_lint、import 解析)的诊断信息会被合并与去重
flowchart LR
subgraph "LSP Server"
DOC["Document Manager<br/>Open files + dependencies"]
DIAG["Diagnostics Pipeline<br/>TS + lint + resolution"]
TSC["TypeScript Service<br/>Completions, hover, refs"]
CONFIG["Config Manager<br/>deno.json, workspace settings"]
end
EDITOR["Editor / IDE"] <-->|LSP Protocol| DOC
DOC --> DIAG
DOC --> TSC
CONFIG --> DIAG
CONFIG --> TSC
DIAG -->|publishDiagnostics| EDITOR
TSC -->|completion, hover| EDITOR
测试基础设施:规范测试与集成测试
Deno 的主要集成测试格式是"规范测试(spec test)",相关说明记录在 CLAUDE.md 中。每个测试是一个目录,其中包含一个 __test__.jsonc 文件,用于描述 CLI 命令和预期输出:
{
"tests": {
"basic_case": {
"args": "run main.ts",
"output": "expected.out"
},
"with_flag": {
"steps": [{
"args": "run --allow-net main.ts",
"output": "[WILDCARD]success[WILDCARD]"
}]
}
}
}
输出匹配语言支持通配符:
| 模式 | 含义 |
|---|---|
[WILDCARD] |
匹配 0 个或多个任意字符(类似 .*),可跨越换行 |
[WILDLINE] |
匹配 0 个或多个任意字符,在行尾停止 |
[WILDCHAR] |
精确匹配一个字符 |
[WILDCHARS(5)] |
精确匹配 5 个字符 |
[UNORDERED_START]...[UNORDERED_END] |
以任意顺序匹配行 |
[# comment] |
行注释,忽略 |
这套通配符系统对于测试一个输出中包含文件路径(因机器而异)、时间戳及其他不确定性内容的运行时至关重要。[UNORDERED_START/END] 块则用于处理并发输出中的竞争条件。
第 1 篇中提到的运行回退到任务(run-to-task fallback)本身也是一个值得关注的 UX 设计决策。should_fallback_on_run_error() 函数用于判断在模块未找到时,是否将 deno run <name> 重试为 deno task <name>:
fn should_fallback_on_run_error(script_err: &str) -> bool {
if script_err.starts_with(MODULE_NOT_FOUND)
|| script_err.starts_with(UNSUPPORTED_SCHEME)
{
return true;
}
let re = lazy_regex::regex!(
r"Import 'file:///.+?' failed\.\n\s+0: .+ \(os error \d+\)"
);
re.is_match(script_err)
}
对应的规范测试会验证回退行为本身,以及当模块和任务均不存在时的错误提示信息。
系列总结
通过这五篇文章,我们完整地梳理了 Deno 的整体架构:
- 架构概览:75 个 crate 分布在五个目录,分层依赖图,带有懒初始化服务的
CliFactory - V8 桥接层:扩展将 ops 和 JS 打包在一起,
#[op2]生成 V8 绑定,快照在构建时序列化 JS 堆 - 模块加载:通过多层栈进行 specifier 解析,
deno_graph预分析,tsc 与 tsgo 双重 TypeScript 检查 - Worker 机制:
MainWorker创建过程,跨 Rust-V8-JS 边界的引导流程,支持 broker 的 8 种权限类型系统 - 工具链:23 个集成工具,
deno compile虚拟文件系统,npm 托管/BYONM 两种模式,200 多个 Node.js polyfill,完整的 LSP 实现
Deno 的代码库体量庞大,但结构清晰。libs/、ext/、runtime/ 与 cli/ 之间的分层不仅仅是代码组织上的考量——它同时也是编译防火墙和抽象边界。扩展是可组合的单元,ops 是安全边界,而 Deferred<T> 模式确保你只为实际用到的功能付出代价。无论你是在修复 bug、添加 op,还是基于 deno_core 构建自己的运行时,这五层架构都是你最可靠的参考蓝图。