拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Rolldown Module ID 解析:字符串路径身份的归一化设计与跨平台一致性

Rolldown Module ID 解析字符串路径身份的归一化设计与跨平台一致性【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldownModule ID 是 Rolldown 整个打包器的主键——模块图、增量缓存、插件 API、HMR、文件监听都以它为键。本文基于 Rolldown 仓库内部设计文档 internal-docs/module-id/implementation.md系统讲解 Module ID 的字符串路径身份机制Rollup 如何用单点归一化解决路径一致性问题、Rolldown 的ModuleId三分类设计与StableModuleId稳定化方案以及路径身份在哪些边界上存在静默失效风险。读完本文你将理解模块 ID 的完整生命周期、各子系统的键约定以及路径比较中字符串相等与PathBuf组件比较的本质差异。Module ID整个打包器的主键Rolldown 中Module ID 是整个 bundler 的主键primary key贯穿六大子系统模块图module graph以 Module ID 唯一标识每个模块节点缓存caches增量构建时以 ID 为键保存扫描阶段结果插件 APIget_module_info()、resolveId、load、transform等钩子中插件所见、所传的都是模块 IDHMR客户端与服务端之间通过稳定 ID 对齐模块watch 文件文件监听事件需要匹配到模块graph.watchFiles插件通过addWatchFile()声明的监听文件集合。在 Rolldown 中Module ID 是基于ArcStr原子引用计数的不可变字符串实现的因此路径身份path identity完全取决于精确的字符串相等性exact string equality——/foo/bar与/foo/bar/是同一个文件却是两个不同的 Module ID。本文要回答的核心问题就是路径如何在这些子系统之间流转、失配mismatch会发生在哪里以及 Rollup 是如何解决同一问题的。Rollup 的做法单点归一化设计Rollup 采用**单点归一化single normalization point**设计resolveId钩子及其默认实现path.resolve()是路径被归一化的唯一位置。解析结果成为模块 ID 后被用于所有下游场景——模块图、缓存、graph.watchFiles、插件钩子等。关键事实Module ID 使用操作系统原生分隔符。在 Windows 上模块 ID 包含\分隔符例如D:\project\src\main.jspath.resolve()的输出原样存储不对模块 ID 应用任何分隔符归一化。Rollup 确实有一个把\转为/的normalize函数位于rollup/src/utils/path.tsconst BACKSLASH_REGEX /\\/g; export function normalize(path) { return path.replace(BACKSLASH_REGEX, /); }但该函数只在下游/输出上下文downstream/output contexts中使用而非核心模块 ID 流水线pluginFilter.ts—— 在匹配 include/exclude 模式前归一化 IDChunk.ts—— 生成preserveModules的 chunk 文件名renderChunks.ts—— source map 的源路径relativeId.ts—— 计算相对导入路径MetaProperty.ts——import.meta相对路径。而addWatchFile()这类插件 API不做任何归一化——它信任调用方提供与模块 ID 约定一致的路径。Rolldown 当前实现ModuleId 的三分类设计Rolldown 的ModuleId定义于 crates/rolldown_common/src/types/module_id.rs其内部是一个Repr枚举在构造时按字符串形态分为三类// crates/rolldown_common/src/types/module_id.rs pub struct ModuleId { repr: Repr } enum Repr { Path(ArcStr), // absolute filesystem path — path operations are meaningful Virtual(ArcStr), // virtual id, prefixed with \0 (Rollup convention) Bare(ArcStr), // bare specifier (react), URL, data URI, relative specifier, … }分类逻辑源码classify函数非常直观fn classify(inner: ArcStr) - Repr { if inner.starts_with(\0) { Repr::Virtual(inner) } else if Path::new(inner.as_str()).is_absolute() { Repr::Path(inner) } else { Repr::Bare(inner) } }即以\0开头 → 虚拟模块绝对路径 → 真实文件路径其余裸说明符如react、URL、data URI、相对说明符→ 其他。相等性、哈希与排序仍按原始字符串关键设计点是ModuleId的PartialEq、Ord、Hash实现全部基于as_str()的原始字符串字节比较忽略 kind 判别值。这意味着一个ModuleId与其字符串的哈希完全相同通过impl Borrowstr源码第 239 行str可以直接作为HashMapModuleId, _的查找键——map.get(/some/path)无需先构造ModuleId相同的字符串永远分类到同一个变体以ModuleId为键的映射保持一致。分类只门控路径逻辑分类的作用是让路径操作只对真正的路径执行避免把每个 ID 都当作路径往返Path/to_string_lossyas_path()仅对Path种类返回Some(Path)这是一个零成本的Path::new视图虚拟 ID、裸说明符、URL 等返回Noneis_in_node_modules()、representative_name()等辅助方法建立在该门控之上命名naming是一种启发式而非路径操作因此representative_name()不依赖is_path——例如虚拟模块\0…/empty.js?x仍会产出empty这个名字与历史行为一致new_empty()构造browser: false被忽略模块的哨兵 ID前缀\0rolldown/empty.js?保留原解析路径以便区分每个被忽略的模块。与 Rollup 的对比解析器oxc_resolver返回PathBufRolldown 通过full_path().to_str()转为字符串后原样存储不做分隔符归一化。在 Windows 上模块 ID 包含原生\分隔符并被归类为Path种类。RollupRolldownWindows 上的 Module IDC:\Users\project\src\file.jsC:\Users\project\src\file.jsLinux 上的 Module ID/home/user/project/src/file.js/home/user/project/src/file.js归一化无原生 OS 分隔符无原生 OS 分隔符是否平台相关前缀和分隔符前缀和分隔符Rollup 与 Rolldown 在此保持一致——两者都按原样存储path.resolve()/ 解析器输出使用原生 OS 分隔符。Rollup 的normalize函数只在下游/输出上下文生效见上文不作用于模块 ID。需要说明的是某些插件在字符串匹配模块 ID 时可能内部假设/分隔符。这是插件层面的关注点而非 Rollup 与 Rolldown 的行为分歧。StableModuleId跨机器稳定的 IDStableModuleId定义于 crates/rolldown_common/src/types/stable_module_id.rs是ModuleId的稳定化版本相对 cwd当前工作目录、使用正斜杠归一化。用于需要跨机器稳定的场景——source map、HMR 客户端的模块引用。// Absolute → relative from cwd, forward slashes // \0foo → \\0foo (virtual module escape) // fs → fs (non-path specifiers unchanged)其构造逻辑基于ModuleId已完成的分类源码StableModuleId::newModuleIdKind::Path绝对路径→ 通过relative_path_to_slash转为相对 cwd 的正斜杠路径ModuleIdKind::Virtual虚拟模块→ 将\0前缀转义为\\0ModuleIdKind::Bare裸说明符 / URL 等→ 原样返回廉价的Arc克隆。仓库自带的单元测试stable_module_id.rs中test_stabilize_id给出了精确的预期行为// absolute path → relative to cwd StableModuleId::with_str(cwd.join(src).join(main.js), cwd).as_str() src/main.js StableModuleId::with_str(cwd.join(..).join(src).join(main.js), cwd).as_str() ../src/main.js // non-path specifier → unchanged StableModuleId::with_str(fs, cwd).as_str() fs StableModuleId::with_str(https://deno.land/x/oak/mod.ts, cwd).as_str() https://deno.land/x/oak/mod.ts // virtual module → escaped StableModuleId::with_str(\0foo, cwd).as_str() \\0foo路径身份在哪些子系统起作用下表汇总了各子系统使用的键类型、归一化策略与风险点源自原设计文档子系统键类型归一化风险模块图查找ModuleId(ArcStr)无解析器输出必须保持一致扫描阶段缓存ModuleId→VisitState无同一路径被不同解析 重复模块module_idx_by_abs_pathArcStr插入时to_slash()HMR 变更文件路径必须匹配插件get_module_info()str查找无插件必须使用精确的模块 ID插件add_watch_file()ArcStr存入FxDashSet无watch 集合使用原始字符串watch 文件比较ArcStreq#[cfg(windows)]反斜杠回退脆弱解析器包缓存PathBufPathBuf 组件比较可处理分隔符差异以 crates/rolldown/src/types/scan_stage_cache.rs 为例可以看到两条路径索引的实际形态// Usage: Map file path emitted by watcher to corresponding module index pub module_idx_by_abs_path: FxHashMapArcStr, ModuleIdx, // Usage: Map module stable id injected to client code to corresponding module index pub module_idx_by_stable_id: FxHashMapStableModuleId, ModuleIdx,其中module_idx_by_abs_path在插入时做了normal_module.id.as_arc_str().to_slash()归一化build_module_index_maps与merge两处把原生分隔符统一为正斜杠——这正是为了让 HMR 中由 watcher 报告的文件变更路径同样经to_slash()能够精确匹配。在 crates/rolldown/src/hmr/hmr_stage.rs 的compute_hmr_update_for_file_changes中可以看到完整的对照流程watcher 传来的changed_file_path先to_slash()再作为键查询module_idx_by_abs_path定位受影响的模块插件hotUpdate钩子返回的 ID 也以同样的to_slash()方式归一化后回查。而add_watch_file()在 crates/rolldown_plugin/src/plugin_context/plugin_context.rs 等四处插件上下文中均直接接收str存储不做任何归一化——插件必须自行保证与模块 ID 约定一致这与 Rollup 的行为对齐。现有的归一化工具自 sugar_path 3 起推荐统一使用 crates/rolldown_std_utils/src/path_ext.rs 中的辅助函数而不是在调用点手写sugar_path组合避免重新引入relative(...).to_slash_lossy().into_owned()这类有损或双重分配链条relative_path_to_slash(target, base)—— 从base到target的词法相对路径输出/分隔的 UTF-8 字符串relative_path_as_js_specifier(target, base)—— 格式化为 JS 风格的相对说明符相同路径 →.离开 base..开头→ 原样否则 →./…absolute_path_to_relative_slash(path, cwd)—— 绝对路径转相对 cwd 的正斜杠路径稳定 ID / 诊断用normalize_path_buf_to_slash(path)—— 归一化自有路径后转为正斜杠字符串join产物首选消费型链避免拷贝path_buf_to_slash(path)、absolutize_path_buf(path)、strip_path_prefix_to_slash(path, prefix)等。这些工具遵循一个贯穿 Rolldown 的不变量模块与文件系统路径已知为合法 UTF-8因此使用expect_to_str()/expect_to_slash()这类严格转换非法 UTF-8 时 panic而非有损的to_string_lossy()。完整的工具族清单见 crates/rolldown_std_utils/src/lib.rs风格指南见 internal-docs/path-manipulation/style-guide.md。核心问题四处路径来源的表示不一致模块 ID 是字符串而系统中不同部分产生路径字符串的方式不同Resolver解析器—— 产生绝对路径平台原生分隔符Plugins插件—— 通过addWatchFile()提供路径不保证归一化notify crate—— 报告 OS 原生路径的文件变更事件HMR client—— 发送稳定 ID相对路径 正斜杠。如果任意两方对同一文件的表示方式不一致查找就会静默失败模块找不到、缓存未命中、watch 文件匹配不上、HMR 更新被丢弃——没有任何显式报错。当前之所以大体能工作是因为解析器自洽resolver is consistent with itself且大多数查找在两侧都使用解析器输出。脆弱点集中在边界boundaries——外部产生的路径notify 事件、插件输入、HMR 客户端与解析器产生的模块 ID 进行比较的地方。PathBuf 的比较行为组件比较而非字节比较Path/PathBuf的比较基于组件components而非原始字节。根据 Rust 官方文档归一化忽略重复分隔符、非开头的.组件、尾部分隔符在 Windows 上/和\都被视为分隔符。因此字符串相等与PathBuf相等在以下场景中表现不同场景str相等PathBuf相等/foo/barvs/foo/bar/falsetrue/foo//barvs/foo/barfalsetrue/foo/./barvs/foo/barfalsetrue/foo/../foo/barvs/foo/barfalsefalse(Windows)C:\foo\barvsC:/foo/barfalsetrue/foo/Barvs/foo/barfalsefalse大小写敏感哈希与相等性一致——因此PathBuf可以安全地用于HashSet/HashMap。局限性PathBuf比较不解析..段也不解析符号链接。要处理这两者需要fs::canonicalize()但它有自己的代价会解析符号链接且对不存在的路径可能失败。这一差异正好解释了设计文档表格中解析器包缓存使用PathBuf组件比较能处理分隔符差异的原因——解析器内部对同一路径的不同分隔符写法可以正确命中同一缓存项而字符串键的模块图则必须依赖解析器输出的一致性。未解决的问题原设计文档明确列出三个悬而未决的问题可作为理解当前实现边界的参考是否应在创建时归一化模块 IDRollup 不归一化模块 ID 分隔符——在 Windows 上插件看到的是带\的 ID。Rolldown 目前对齐该行为。若 Rolldown 选择在ModuleId::new()中统一归一化为/会改变 Windows 上可观察到的模块 ID但可能简化插件过滤器匹配和内部比较。watch 文件集合是否应改用PathBuf而非ArcStrPathBuf能处理尾部斜杠、双斜杠、.段和 Windows 分隔符代价是失去廉价的ArcStr克隆和str查找。watch 专属讨论见 internal-docs/watch-mode/implementation.md。..段与符号链接——PathBuf比较和字符串比较都无法处理这两者。实际上..不应出现在解析器输出中解析器会 canonicalize符号链接是罕见边界情况。Rolldown 是否应对此做出任何保证深入阅读internal-docs/module-id/implementation.md —— 本文对应的原始设计文档crates/rolldown_common/src/types/module_id.rs ——ModuleId类型三分类、Borrowstr查找、相等性实现crates/rolldown_common/src/types/stable_module_id.rs ——StableModuleId类型及其单元测试crates/rolldown_std_utils/src/path_ext.rs —— 路径归一化工具族relative_path_to_slash、relative_path_as_js_specifier等crates/rolldown/src/types/scan_stage_cache.rs ——module_idx_by_abs_path/module_idx_by_stable_id两条路径索引的实际定义与构建crates/rolldown/src/hmr/hmr_stage.rs —— HMR 中文件变更路径与模块 ID 的匹配流程crates/rolldown_plugin/src/plugin_context/plugin_context.rs ——add_watch_file()插件 APIinternal-docs/watch-mode/implementation.md —— watch 文件集合的路径匹配讨论internal-docs/path-manipulation/style-guide.md —— 路径操作风格指南【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门