Rolldown 缓存架构深度解析:ScanStageCache 与增量构建的实现原理
【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown
本文依据仓库内
internal-docs/cache/implementation.md展开,并结合crates/rolldown与crates/rolldown_common等目录下的实际源码进行印证。Rolldown 是一套用 Rust 编写、API 兼容 Rollup 的 JavaScript/TypeScript 打包器,其增量构建与 HMR(热模块替换)能力全部建立在一套多层次的缓存体系之上。读完本文,你将掌握 Rolldown 全部 14 个*Cache型缓存的分工,深入理解架构核心ScanStageCache的数据结构、模块身份模型、部分扫描合并算法(merge),以及完整的读写方清单。
缓存总览:Rolldown 的六类缓存机制
Rolldown 拥有多种截然不同的缓存机制。按字面命名统计,类型名恰好以*Cache结尾的共有14 个。它们按用途可归为六类:增量构建缓存、跨构建失效状态、构建内记忆化、插件临时状态、JS 侧缓存与监听模式的文件系统缓存。其中架构上最核心的是ScanStageCache——它是 bundler 级别的模块解析图快照,正是它让增量构建与 HMR 成为可能;其余缓存则是构建内的记忆化(memoization)、插件临时状态以及一个 JS 侧存储。
1. 增量构建缓存(Incremental-build cache)
| 类型 | 位置 | 存储内容 |
|---|---|---|
ScanStageCache | crates/rolldown/src/types/scan_stage_cache.rs | 模块图快照 + 模块索引映射表。本文剩余篇幅将围绕它展开。 |
2. 跨构建失效状态(Cross-build invalidation state)
它们并非结果缓存,而是与ScanStageCache一同持久化,供下一次增量构建判断该失效什么、或回答插件的查询。
| 数据 | 位置 | 说明 |
|---|---|---|
transform_dependencies | crates/rolldown_plugin/src/plugin_driver/ | addWatchFile()记录的依赖;模块 → 它依赖的文件。详见 internal-docs/bundler-data-lifecycle/implementation.md。 |
module_infos | crates/rolldown_plugin/src/plugin_driver/ | 插件填充的模块元数据,服务于this.getModuleInfo。详见 internal-docs/bundler-data-lifecycle/implementation.md。 |
3. 构建内记忆化(Within-build memoization)
| 类型 | 位置 | 存储内容 |
|---|---|---|
SideEffectCache(枚举) | crates/rolldown/src/stages/link_stage/tree_shaking/determine_side_effects.rs | None/Visited/Cache(DeterminedSideEffects);link 阶段副作用遍历期间的临时局部记忆。 |
PackageJsonCache | crates/rolldown_plugin_vite_resolve/src/package_json_cache.rs | side_effects_cache: FxDashMap<PathBuf, Arc<PackageJson>>、optional_peer_dep_cache: FxDashMap<PathBuf, Arc<…>>。 |
ResolverCaches | crates/rolldown_plugin_vite_resolve/src/resolver.rs | package_json: PackageJsonCache、importer_exists: FxDashSet<String>。 |
TsconfigCache | crates/rolldown_binding/src/transform_cache.rs | resolver: Arc<Resolver>、cache: FxDashMap<PathBuf, Arc<TsConfig>>。通过#[napi]暴露给 Node 侧。 |
RawTransformOptions的cache字段 | crates/rolldown_common/src/inner_bundler_options/types/transform_options.rs | tsconfig → 编译后的 Oxc transform 选项。 |
| oxc_resolver 内部缓存 | 外部 crate,由 bundler 层级的SharedResolver持有 | 文件系统/路径元数据。 |
4. 插件临时状态(位于PluginContext.meta())
命名为*Cache,但功能上只是每次构建内、在插件 hook 调用之间传递数据的共享 Map。全部位于 crates/rolldown_plugin_utils/src/。
| 类型 | 位置 | 存储内容 |
|---|---|---|
AssetCache | crates/rolldown_plugin_utils/src/file_to_url.rs | FxDashMap<String, String> |
PublicAssetUrlCache | crates/rolldown_plugin_utils/src/public_file_to_built_url.rs | FxDashMap<String, String> |
CSSEntriesCache | crates/rolldown_plugin_utils/src/constants.rs | FxDashMap<ArcStr, ArcStr> |
CSSModuleCache | crates/rolldown_plugin_utils/src/constants.rs | FxDashMap<String, FxHashMap<String, String>> |
CSSChunkCache | crates/rolldown_plugin_utils/src/constants.rs | FxDashMap<ArcStr, String> |
RemovedPureCSSFilesCache | crates/rolldown_plugin_utils/src/constants.rs | FxDashMap<ArcStr, Arc<OutputChunk>> |
CSSUrlCache | crates/rolldown_plugin_utils/src/constants.rs | FxDashMap<String, String> |
同一文件内还有未以Cache命名的关联结构:ViteMetadata、HTMLProxyResult、HTMLProxyMap、CSSStyles、PureCSSChunks。
5. JS 侧缓存
| 类型 | 位置 | 存储内容 |
|---|---|---|
PluginContextData | packages/rolldown/src/plugin/plugin-context-data.ts | moduleOptionMap、resolveOptionsMap、loadModulePromiseMap、renderedChunkMeta、normalizedInputOptions、normalizedOutputOptions。 |
InvalidateJsSideCache | crates/rolldown_common/src/inner_bundler_options/types/invalidate_js_side_cache.rs | Arc<InvalidateJsSideCacheFn>——一个由 Rust 持有、回调进 JS 的函数指针。 |
FilterExprCache | crates/rolldown_binding/src/options/plugin/binding_plugin_options.rs | 预编译的插件 hook 过滤器表达式(NAPI binding,按插件隔离)。 |
其中InvalidateJsSideCache在 crates/rolldown_binding/src/utils/normalize_binding_options.rs 中被接线;在 JS 侧(packages/rolldown/src/utils/bindingify-input-options.ts)它被绑定到PluginContextData.clear。调用它即清空 JS 侧的PluginContextData。
6. 监听模式的文件系统缓存
notifycrate 的RecommendedCache被持有在 crates/rolldown_fs_watcher/src/ 的去抖器(debouncer)内部,用于跟踪文件系统元数据以进行事件去抖。
ScanStageCache——增量构建的基石
存放位置与两层模型
ScanStageCache是bundler 级数据:它跨构建存活。在构建期间,它被临时移入每次构建的Bundle,构建结束后再移回。这段移入/移出由with_cached_bundle/with_cached_bundle_experimental完成(crates/rolldown/src/bundler/impl_bundler_incremental_build.rs)。
从源码看,with_cached_bundle的实现非常讲究失败安全(crates/rolldown/src/bundler/impl_bundler_incremental_build.rs):
let cache = mem::take(&mut self.cache); let mut bundle = self.bundle_factory.create_bundle(bundle_mode, Some(cache))?; // 注意:这里绝不能用 `?` 提前返回 // 一旦 Err 提前退出,bundle 会被 drop,Bundler::cache 停留在 mem::take 安装的 default() // 状态(snapshot = None),下一次 HMR 周期会在 get_snapshot() 中 panic let ret = with_fn(&mut bundle).await; self.cache = bundle.cache; ret“bundler 级 vs bundle 级”的两层数据模型记录在 internal-docs/bundler-data-lifecycle/implementation.md 中,该文档同时覆盖了构建失败时的缓存完整性保证。
结构体定义
scan_stage_cache.rs 中的完整定义:
#[derive(Default, Debug)] pub struct ScanStageCache { snapshot: Option<NormalizedScanStageOutput>, pub barrel_state: BarrelState, pub module_id_to_idx: FxHashMap<ModuleId, VisitState>, pub importers: IndexVec<ModuleIdx, Vec<ImporterRecord>>, /// 其 importers 记录被部分扫描变更的模块 pub modules_with_changed_importers: FxHashSet<ModuleIdx>, /// 一次中止(并已回滚)的部分扫描留下的文件 pub pending_rescans: Vec<ResolvedId>, pub user_defined_entry: FxHashSet<ModuleId>, pub module_idx_by_abs_path: FxHashMap<ArcStr, ModuleIdx>, pub module_idx_by_stable_id: FxHashMap<StableModuleId, ModuleIdx>, }各字段职责如下表:
| 字段 | 用途 |
|---|---|
snapshot | 完整模块图。None是合法的临时状态;它是private的,只能通过下述方法访问。 |
barrel_state | Barrel 再导出解析状态(BarrelState)。 |
module_id_to_idx | ModuleId→ModuleIdx的注册表/分配器(见“模块身份模型”一节)。 |
importers | 反向依赖图:对每个模块,记录谁导入了它。 |
modules_with_changed_importers | 其importers记录被部分扫描变更的模块;merge会 drain 它,为这些模块重新推导物化的 importer 集合(见下)。 |
pending_rescans | 工作队列:一次中止的部分扫描所涉及的文件(其变更已被ModuleLoader::revert_partial_scan回滚);下一次部分扫描会重试它们,让错误持续浮出水面直到文件被修复。 |
user_defined_entry | 配置的根入口ModuleId集合。 |
module_idx_by_abs_path | 绝对路径 →ModuleIdx,供 watcher 使用。路径统一使用斜杠规范化。 |
module_idx_by_stable_id | StableModuleId→ModuleIdx,供 HMR 使用。 |
其中module_idx_by_abs_path与module_idx_by_stable_id是派生数据——每当set_snapshot运行时,build_module_index_maps会清空并从快照重建这两张映射表(仅将Module::Normal的绝对路径以to_slash()形式写入前者)。
快照访问器
scan_stage_cache.rs 提供了一组访问器:
set_snapshot(L46)——安装快照并重建索引映射。get_snapshot(L78)——返回&NormalizedScanStageOutput;若snapshot为None则 panic(unwrap)。get_snapshot_mut(L53)——返回&mut;为None时同样 panic。take_snapshot(L58)——把快照移出,留下None。has_snapshot(L91)——非 panic 变体,仅判断快照是否存在。derive_importers_from_snapshot(L101)——从快照重新推导importers边列表。update_defer_sync_data(L62)——取出快照、运行defer_sync_scan_data、在所有结果路径上恢复快照,再传播任何错误。源码注释解释得很清楚:若用?提前退出会 drop 快照,使self.snapshot == None,导致下一个 HMR 周期的get_snapshot()panic;一个“部分同步”的快照是可恢复的,而“缺失”的快照不可恢复。merge(L123)——把一次扫描输出拼接入快照(见下文)。create_output(L324)——为本次构建产出NormalizedScanStageOutput。
BundleMode:决定缓存的创建、保留与复用
crates/rolldown_common/src/types/bundle_mode.rs 定义了三种模式:
| 模式 | 缓存入 | 缓存出 | 适用场景 |
|---|---|---|---|
FullBuild | None | 丢弃 | 一次性构建,非增量的 watch |
IncrementalFullBuild | 全新 | 保存 | 首次增量构建,或 dev 模式在构建失败后的恢复 |
IncrementalBuild | 已有 | 更新 | 后续的增量构建 |
辅助方法:is_full_build()对FullBuild与IncrementalFullBuild为真;is_incremental()对IncrementalFullBuild与IncrementalBuild为真。二者组合可以精确表达“这是一次完整扫描但结果要入缓存”(首次增量)与“这是一次只针对变更文件的部分扫描”(后续增量)的区别。
从 impl_bundler_incremental_build.rs 可以看到模式决策逻辑:若self.cache.has_snapshot()为真则沿用传入的scan_mode,否则强制ScanMode::Full(没有快照就没有可叠加的图,必须完整扫描);随后ScanMode::Full对应IncrementalFullBuild,ScanMode::Partial(_)对应IncrementalBuild。
快照类型:NormalizedScanStageOutput
定义在 crates/rolldown/src/stages/scan_stage.rs。字段包括module_table、index_ecma_ast(每个模块解析出的 AST)、stmt_infos、entry_points、symbol_ref_db、runtime、dynamic_import_exports_usage_map、user_defined_entry_modules、tla_module_count、tla_keyword_span_map等。
make_copy(scan_stage.rs)克隆快照,但对symbol_ref_db使用clone_without_scoping克隆(性能优化——符号表作用域在构建之后才恢复,见merge_immutable_fields_for_cache)。
ScanStageOutputvsNormalizedScanStageOutput
ScanStageOutput(scan_stage.rs)是扫描阶段的产出。它的module_table、index_ecma_ast、stmt_infos是HybridIndexVec,而快照中的同名字段是稠密的基于IndexVec的结构。转换发生在merge(部分扫描)或TryFrom<ScanStageOutput>(完整扫描,scan_stage.rs)中。
模块身份模型:理解merge的前提
ModuleId与ModuleIdx的双重身份
一个模块拥有两种身份:
ModuleId——解析后的文件路径(+ query)。稳定不变,相当于模块的“名字”。ModuleIdx——小整数(u32的新类型包装)。相当于槽位号/数组下标。在 bundler 会话期间永久有效。
Module::id()(crates/rolldown_common/src/module/mod.rs)返回&ModuleId;Module::idx()(mod.rs)返回模块结构体idx字段中存储的ModuleIdx。
module_id_to_idx——注册表兼分配器
module_id_to_idx: FxHashMap<ModuleId, VisitState>是“模块名字 → 槽位”的唯一事实来源。它是单调的:新模块总是被分配idx = module_id_to_idx.len()。槽位按0, 1, 2, …无空洞地发放,且永不复用。
VisitState:新鲜度标记
定义于 crates/rolldown/src/module_loader/module_loader.rs:
pub enum VisitState { Seen(ModuleIdx), Invalidate(ModuleIdx) }两个变体都携带 idx。变体本身是一个新鲜度标记:
Seen(i)——模块是最新的;loader 跳过它(不重新扫描)。Invalidate(i)——模块已过期;loader 重新扫描它,复用i作为槽位。
IndexVec/Map/HybridIndexVec
IndexVec<ModuleIdx, T>——以ModuleIdx为下标的Vec。稠密:对0..len中每个i都存在槽位。FxHashMap<ModuleIdx, T>——稀疏:只持有被插入的键。HybridIndexVec<ModuleIdx, T>(crates/rolldown_common/src/types/hybrid_index_vec.rs)——枚举,要么是IndexVec(..)要么是Map(..)。Default为IndexVec变体。
完整扫描产出所有模块 → 稠密IndexVec。部分扫描只产出“变更 + 新发现”的模块 → 稀疏Map。
六条不变量
- 模块的
ModuleIdx在首次解析时被分配恰好一次,之后永不改变、也永不复用。 - idx 从 0 起稠密分配;已分配集合恰好是
0..module_id_to_idx.len()。 - 缓存快照是稠密且全量的:
module_table与每个并行侧表(index_ecma_ast、stmt_infos、symbol_ref_db的局部 DB)对每个已分配 idx 都有槽位。 - 部分扫描输出是稀疏的:恰好包含 {变更} ∪ {新增} 模块,未变更模块缺席。
- 对部分扫描输出中的模块,“新增” ⟺ 其 idx ≥ 合并时缓存的当前模块计数。
- 模块 loader 为每个模块分配唯一的
ModuleIdx,并用同一个值作为扫描输出的Map键、Module.idx字段与module_id_to_idx的值(见try_spawn_new_task)。因此对任意模块这三者相等。
module_id_to_idx的更新生命周期
module_id_to_idx生活在ScanStageCache中。ModuleLoader持有同一缓存的可变借用——cache: &'a mut ScanStageCache(module_loader.rs)——因此 loader 的写入直接变更 bundler 真正的缓存,不存在副本。
module_id_to_idx在扫描阶段由 loader 急切地更新。merge在扫描之后运行,且只读module_id_to_idx——它从不向其中插入。
写入点(全部在module_loader.rs,扫描期间)
| 写入点 | 位置 | 效果 |
|---|---|---|
| 运行时模块 | fetch_modules,L304–L308 | Entry::Vacant→ 插入Seen(idx)(一次)。 |
| 使变更文件失效 | fetch_modules,L348–L350 | 对每个 watcher 上报的文件:Entry::Occupied→insert(Invalidate(idx))。idx 不变。 |
Seen(idx)分支 | try_spawn_new_task,L230–L244 | 不写;直接返回 idx,模块不被重新扫描。 |
Invalidate(idx)分支 | try_spawn_new_task,L246–L251 | insert(Seen(idx))——模块正在被重新扫描。 |
None,部分扫描 | try_spawn_new_task,L252–L259 | 新模块:insert(id, Seen(len)),其中len = module_id_to_idx.len()。 |
None,完整扫描 | try_spawn_new_task,L260–L264 | 新模块:insert(id, Seen(alloc()))。 |
单条目状态机
(absent) --首次解析--> Seen(idx) --文件变更--> Invalidate(idx) ^ | | loader 重新扫描该模块 | +--------------------------+idx 在诞生时固定;后续转换只翻转Seen/Invalidate标记。
构建内的时序
在部分扫描中,fetch_modules处理每个 watcher 上报的文件时,先把它翻转为Invalidate,再调用try_spawn_new_task——后者命中Invalidate分支,把它翻回Seen并重新扫描。中间的Invalidate状态正是强制重新扫描的关键:若直接是Seen,try_spawn_new_task会立即返回而不重扫。同时它天然去重:一旦翻回Seen,后续解析到同一模块的 importer 只取回 idx。
推论:扫描输出中出现的每个模块,都已在merge运行之前由 loader 登记进module_id_to_idx。
ScanStageCache::merge——写入路径
定义于 scan_stage_cache.rs。签名:merge(&mut self, scan_stage_output: ScanStageOutput, plugin_driver: &PluginDriver) -> BuildResult<()>。
调用方
- bundle.rs——在
normalize_scan_stage_output_and_update_cache的非完整扫描分支中。 hmr_stage.rs——HMR 更新与 lazy-compile 路径(crates/rolldown/src/hmr/)。
完整扫描的构建路径不调用merge,而是走set_snapshot(bundle.rs)。当前所有调用方传入的都是部分扫描输出,其module_table为HybridIndexVec::Map——这正是merge中IndexVec匹配分支为unreachable!()的原因。
从 bundle.rs 可以看到完整流程:
if is_full_scan_mode { // 完整扫描:try_into 归一化 → defer_sync_scan_data →(增量模式)set_snapshot let mut output: NormalizedScanStageOutput = output.try_into()...; defer_sync_scan_data(&self.options, &self.cache.module_id_to_idx, &mut output).await; if is_incremental { self.cache.set_snapshot(output.make_copy()); } return Ok(output); } // 部分扫描:merge 拼接入快照 → 同步 deferred 数据 → 产出本次构建的输出 self.cache.merge(output, &self.plugin_driver)?; self.cache.update_defer_sync_data(&self.options).await?; Ok(self.cache.create_output())算法分步解析
- 首次构建逃生通道(L134–L139)——若
snapshot为None,把整个输出通过try_into转换后直接作为快照返回。 - 提取
modules(L140–L149)——匹配module_table:IndexVec分支为unreachable!();Map分支收集为Vec并按 idx 排序。排序把已有模块(idx < 缓存长度)排在新模块(idx ≥ 缓存长度)之前,并将新模块按升序排列,使随后的push恰好落在其分配的槽位上。 - 逐模块循环(L152–L216):
new_idx是Map键(索引扫描输出);idx是module_id_to_idx[new_module.id()].idx()(索引缓存)。由不变量 6 二者相等。- 更新
module_idx_by_abs_path(仅普通模块,斜杠规范化)与module_idx_by_stable_id。 - 新模块(
new_idx ≥ cache.module_table.modules.len()):把模块/AST/stmt infos/局部符号 DB push 到并行的集合上;调整tla_module_count与tla_keyword_span_map。 - 已有模块:在
idx处覆盖同样的集合;按旧↔新差值调整 TLA 计数;替换或移除 TLA span。 - 所有载荷都用
mem::take/take/mem::replace/mem::swap移动出扫描输出——绝不克隆。
- 重新推导受影响的缓存模块的 importer 集合(L221–L229)——drain
modules_with_changed_importers(由ModuleLoader::mark_module_importers_changed在每次变更importers边列表时填充),对列出的每个模块,用EcmaView::rebuild_importer_sets从边列表重建其物化的importers/importers_idx/dynamic_importers集合。扫描只为它产出的模块做这件事;一个缓存模块的 importer 若增删或改类了对它的导入,就在这里被刷新。 - 合并入口点(L231–L253)——收集部分扫描中出现的所有模块,先从每个缓存的动态入口中移除这些模块的引用,再合并新行。清理是全局的,因为删除或重定向导入不会为旧目标产生行;丢弃失去调用点的陈旧条目;合并可重新添加空
related_stmt_infos的无 span 导入。 - 修补 barrel 模块(L256–L264)——drain
barrel_state.resolved_barrel_modules,把解析出的导入记录写回缓存模块。 - 重算用户定义入口(L266–L291)——从扫描输出的集合出发,加回仍然解析到存活模块的持久化配置根入口(
self.user_defined_entry)。这是每次构建重建集合,而非单调扩展。同时把emitFile(type: 'chunk')产出的入口模块也保留在集合中。 - 刷新面向插件的
ModuleInfo(L295–L303)——对步骤 4 中重新推导的模块,重跑to_module_info与plugin_driver.set_module_info,使this.getModuleInfo(id).importers与合并后的图保持一致(扫描只为它产出的模块刷新)。
merge有两个 panic 面:module_id_to_idx[new_module.id()]下标表达式(键缺失时 panic——只有违反不变量 6 才会触达)以及unreachable!()分支。Module::idx()返回与module_id_to_idx查找相同的值且不可失败。
读写方清单
ScanStageCache的写入方
| 写入方 | 位置 | 写入内容 |
|---|---|---|
ScanStage::scan(scan_mode, &mut self.cache) | 调用于bundle.rs:104 | 通过 loader 写入非快照字段。 |
ModuleLoader(cache: &'a mut ScanStageCache) | module_loader.rs 及各方法 | module_id_to_idx、barrel_state(如失效时移除barrel_infos)、importers、modules_with_changed_importers、user_defined_entry(完整增量扫描时)。 |
ScanStageCache::merge | scan_stage_cache.rs;调用于bundle.rs、hmr_stage.rs | snapshot、module_idx_by_abs_path、module_idx_by_stable_id、barrel_state.resolved_barrel_modules(drain)、modules_with_changed_importers(drain)、快照内tla_*字段;为重新推导的模块刷新插件module_infos。 |
ModuleLoader::revert_partial_scan | module_loader.rs 扫描错误出口 | 把module_id_to_idx/importers/barrel_state/ importer 标记恢复到扫描前状态,并填充pending_rescans。 |
ScanStageCache::set_snapshot | scan_stage_cache.rs;调用于bundle.rs:263及update_defer_sync_data内部 | snapshot+ 重建module_idx_by_abs_path/module_idx_by_stable_id。 |
ScanStageCache::update_defer_sync_data | scan_stage_cache.rs;调用于bundle.rs:270、hmr_stage.rs | 取出并恢复snapshot;defer_sync_scan_data在其中变更每个模块的side_effects。 |
ScanStageCache::create_output | scan_stage_cache.rs;调用于bundle.rs:271 | 变更snapshot.symbol_ref_db(克隆去作用域部分后交换),返回NormalizedScanStageOutput。 |
merge_immutable_fields_for_cache | bundle.rs,调用于bundle.rs:295 | get_snapshot_mut();在 link 阶段后恢复符号表作用域。 |
with_cached_bundle/with_cached_bundle_experimental | impl_bundler_incremental_build.rs / L33 | 在Bundler与Bundle之间移动整个ScanStageCache。 |
ScanStageCache的读取方
| 读取方 | 位置 | 读取内容 |
|---|---|---|
HmrStage | hmr_stage.rs | get_snapshot().module_table、get_snapshot().index_ecma_ast;也使用模块索引映射。HMR 同时是写入方(它调用merge/update_defer_sync_data)。 |
ModuleLoader | module_loader.rs | get_snapshot()(如module_table.modules.get(..)),以及module_id_to_idx、barrel_state、user_defined_entry。 |
defer_sync_scan_data | crates/rolldown/src/module_loader/deferred_scan_data.rs | 读取module_id_to_idx(以&FxHashMap<ModuleId, VisitState>传入);变更快照中每个模块的副作用。 |
merge | scan_stage_cache.rs | 读取module_id_to_idx、user_defined_entry、importers(重推导 importer 集合)。 |
值得注意的是,从 bundle.rs 可以看到,一次构建收尾时还会调用invalidate_js_side_cache回调清空 JS 侧缓存,保证下一次构建的插件上下文数据不会过期——这正是第 5 类缓存(JS 侧缓存)与 Rust 侧缓存体系衔接的闭环。
失败安全与缓存完整性
从源码注释可以提炼出两条贯穿始终的设计原则:
- 快照只增不删、失败可恢复:部分扫描失败时,
ModuleLoader::revert_partial_scan会把module_id_to_idx等字段回滚到扫描前状态,并把需要重试的文件记入pending_rescans;已存在的module_id_to_idx条目具有自愈能力(Seen→Invalidate→Seen),因此只需移除新键。完整的失败语义记录在 internal-docs/bundler-data-lifecycle/implementation.md 的 “Cache integrity on a failed build” 一节。 - 所有路径上归还缓存:
with_cached_bundle刻意不用?提前返回,保证Bundler::cache在任何结果路径上都被恢复;update_defer_sync_data也在所有结果路径上恢复快照,避免下一次 HMR 周期 panic。
相关文档
- internal-docs/cache/design.md —— 缓存完整性契约与未决问题。
- internal-docs/bundler-data-lifecycle/implementation.md —— bundler 级 vs bundle 级数据、
BundleMode、构建失败时的缓存完整性。 - internal-docs/module-id/implementation.md ——
ModuleId设计。 - internal-docs/rust-bundler/implementation.md ——
Bundler结构与构建生命周期。 - internal-docs/watch-mode/implementation.md —— 驱动部分扫描的 watch 模式。
【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考