Rspack 源码解析(三十):SourceMapDevToolPlugin 如何生成并关联 source map

本篇是 Rspack 源码解析系列第三十篇。前面从模块讲到 chunk、HTML、静态复制。本文进入 rspack_plugin_devtool,看 source map 如何从已有产物里提取、如何确定 sources 数组、如何生成 .map 文件或内联 data URL,又如何追加 sourceMappingURL 注释。

前言:source map 是「产物的影子」

source map 不改变产物内容,只提供「生成代码 -> 源码」的映射。它有两个形态:

text 复制代码
外部文件:main.js.map + main.js 末尾 //# sourceMappingURL=main.js.map
内联:main.js 末尾 //# sourceMappingURL=data:application/json;base64,...

SourceMapDevToolPlugin 就是生成这个「影子」,并把它关联回产物。

源码入口:process_assets(DEV_TOOLING 阶段)

crates/rspack_plugin_devtool/src/source_map_dev_tool_plugin.rs:

rust 复制代码
#[plugin_hook(CompilationProcessAssets for SourceMapDevToolPlugin, stage = Compilation::PROCESS_ASSETS_STAGE_DEV_TOOLING)]
async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
  // 建立 file -> chunk 映射
  let mut file_to_chunk: HashMap<&str, &Chunk> = HashMap::default();
  for chunk in compilation.build_chunk_graph_artifact.chunk_by_ukey.values() {
    for file in chunk.files() {
      file_to_chunk.insert(file, chunk);
    }
    for file in chunk.auxiliary_files() {
      file_to_chunk.insert(file, chunk);
    }
  }

  let compilation_assets = compilation.assets().iter()
    .filter(|(_filename, asset)| asset.info.related.source_map.is_none());
  let mapped_assets = self.mapped_assets_cache.use_cache(compilation_assets, |assets| {
    self.map_assets(compilation, &file_to_chunk, &output_path, assets)
  }).await?;
  // ...
}

PROCESS_ASSETS_STAGE_DEV_TOOLING 是一个靠后的阶段,说明 source map 生成发生在其他产物基本就绪之后。

过滤:已经有关联 source map 的跳过

rust 复制代码
.filter(|(_filename, asset)| asset.info.related.source_map.is_none())

asset.info.related.source_map 记录「这个 asset 是否已经有关联的 source map」。如果已经有(例如 loader 生成的),就不重复处理。

collect_tasks:从产物提取 source map

map_assets 先 collect_tasks:

rust 复制代码
let tls: ThreadLocal<ObjectPool> = ThreadLocal::new();
let tasks = compilation_assets.into_par_iter()
  .filter_map(|(asset_filename, asset)| {
    // test/include/exclude 匹配
    // ...
    asset.get_source().and_then(|source| {
      let object_pool = tls.get_or(ObjectPool::default);
      let source_map = source.map(object_pool, &map_options)?;
      // 从 source_map.sources() 提取 SourceReference
      // ...
    })
  })
  .collect::<Result<Vec<_>>>()?;

这里用 rayon 的 into_par_iter 并行处理所有 assets,ThreadLocal<ObjectPool> 让每个线程有独立的对象池,避免锁竞争。

SourceReference:区分模块引用和源码引用

提取出的每个 source 名要判断是「模块」还是「普通源码」:

rust 复制代码
let source_references = source_map.sources().iter().map(|source_name| {
  if let Some(stripped) = source_name.strip_prefix("webpack://") {
    let source_name = make_paths_absolute(compilation.options.context.as_str(), stripped);
    let identifier = ModuleIdentifier::from(source_name.as_str());
    match compilation.get_module_graph().module_by_identifier(&identifier) {
      Some(module) => SourceReference::Module(module.identifier()),
      None => SourceReference::Source(Arc::from(source_name)),
    }
  } else {
    SourceReference::Source(Arc::from(source_name.clone()))
  }
}).collect::<Vec<_>>();

webpack:// 前缀是模块的命名空间,能反查回模块,用于后续的 module_filename_template 计算。非 webpack:// 的(如 node_modules 里的第三方源码)是普通 Source。

finalize_source_maps:确定 sources 数组

finalize_source_maps 负责把每个 source reference 映射到最终的 source 名字。module_filename_template 默认是 webpack://[namespace]/[resourcePath]:

rust 复制代码
let reference_to_source_name_mapping = match &self.module_filename_template {
  ModuleFilenameTemplate::String(template) => {
    // 对每个 source_reference 并行计算最终名字
  }
}

这里也有 SCHEMA_SOURCE_REGEXP 的过滤:

rust 复制代码
static SCHEMA_SOURCE_REGEXP: LazyLock<Regex> =
  LazyLock::new(|| Regex::new(r"^(data|https?):").expect("failed to compile SCHEMA_SOURCE_REGEXP"));

data: 或 http(s): 开头的 source 不映射成文件路径,保持原样。

map_assets:两种输出形态

map_assets 的核心分支是「有 filename 还是没 filename」:

rust 复制代码
if let Some(source_map_filename_config) = &plugin.source_map_filename {
  // 外部 .map 文件
  // 1. 计算 source map 文件名(含 contenthash)
  // 2. 计算 sourceMappingURL
  // 3. 给产物追加 sourceMappingURL 注释
  // 4. 生成独立的 .map 文件
} else {
  // 内联 data URL
  let base64 = base64::encode_to_string(source_map_json.as_bytes());
  // 追加 //# sourceMappingURL=data:application/json;charset=utf-8;base64,...
}

外部 .map:contenthash 与文件名

外部 .map 文件的 contenthash 是 source map JSON 的 hash:

rust 复制代码
let mut hasher = RspackHash::from(&compilation.options.output);
hasher.write(source_map_json.as_bytes());
let digest = hasher.digest(&compilation.options.output.hash_digest);
// ...
let data = PathData::default().filename(&filename)
  .chunk_id_optional(...).chunk_hash_optional(...).content_hash_optional(Some(digest.encoded()));
let source_map_filename = compilation.get_asset_path(source_map_filename_config, data).await?;

sourceMappingURL 注释

默认追加的注释是:

rust 复制代码
Some(SourceMappingUrlComment::String("\n//# sourceMappingURL=[url]".to_string()))

CSS 需要特殊处理:// 注释在 CSS 里不合法,要换成 /* */:

rust 复制代码
let css_extension_detected = CSS_EXTENSION_DETECT_REGEXP.is_match(&asset_filename);
let current_source_mapping_url_comment = match &plugin.source_mapping_url_comment {
  Some(SourceMappingUrlComment::String(s)) => {
    let s = if css_extension_detected {
      URL_FORMATTING_REGEXP.replace_all(s, "\n/*$1*/")
    } else { Cow::from(s) };
    Some(SourceMappingUrlCommentRef::String(s))
  }
  // ...
};

CSS_EXTENSION_DETECT_REGEXP 是 \.css($|\?),URL_FORMATTING_REGEXP 是 ^\n\/\/(.*)$。CSS 的 //# sourceMappingURL=... 会变成 /*# sourceMappingURL=... */。

计算相对 URL

外部 .map 的 URL 需要相对路径:

rust 复制代码
let source_map_url = if let Some(public_path) = &plugin.public_path {
  format!("{public_path}{source_map_filename}")
} else {
  // 计算 .map 相对当前产物文件的路径
  source_map_path.relative(file_path.parent().unwrap()).to_string_lossy().to_string()
};

然后用 [url] 占位符替换:

rust 复制代码
let current_source_mapping_url_comment = current_source_mapping_url_comment
  .cow_replace("[url]", &source_map_url)
  .into_owned();

debug_ids

debug_ids 选项会生成 //# debugId=...:

rust 复制代码
let debug_id = plugin.debug_ids.then(|| {
  let debug_id = generate_debug_id(&asset_filename, &source.buffer());
  source_map.set_debug_id(Some(debug_id.clone()));
  debug_id
});
// ...
let debug_id_comment = debug_id.map(|id| format!("\n//# debugId={id}")).unwrap_or_default();

debug_id 用于把产物和它的 source map 关联起来,是调试工具(如 Chrome DevTools)识别 source map 的一种方式。

关联回 asset

生成 .map 后,要把它记录到产物和 chunk:

rust 复制代码
if let Some((source_map_filename, source_map_asset)) = source_map {
  compilation.emit_asset(source_map_filename.clone(), source_map_asset);
  // 加到 chunk 的 auxiliary_files
  chunk.add_auxiliary_file(source_map_filename.clone());
}

source map 是 chunk 的 auxiliary file------和第 24 篇 asset 文件、第 26 篇 CSS 的归类一致。

mapped_assets_cache

MappedAssetsCache 用于增量缓存:

rust 复制代码
self.mapped_assets_cache.use_cache(compilation_assets, |assets| {
  self.map_assets(compilation, &file_to_chunk, &output_path, assets)
}).await?

只有 assets 变化时才重新生成 source map,避免每次构建都重复计算------这是增量构建性能的关键。

Rust 角度:并行提取 + 统一关联

SourceMapDevToolPlugin 的两个阶段分工清晰:

text 复制代码
collect_tasks(并行):从每个产物提取已有 source map
finalize_source_maps(并行):计算 sources 数组
map_assets(并行):生成 .map 或内联,追加注释

全流程用 rayon 的 into_par_iter 和自定义的 rspack_futures::scope 并发,因为不同产物之间互不依赖。ThreadLocal<ObjectPool> 避免了共享对象池的锁竞争。

这一篇应该带走什么

  1. source map 在 PROCESS_ASSETS_STAGE_DEV_TOOLING 阶段生成;
  2. asset.info.related.source_map 记录关联,已关联的跳过;
  3. webpack:// 前缀可反查模块,用于 module_filename_template;
  4. 外部 .map 的 contenthash 是 JSON hash,内联用 base64 data URL;
  5. CSS 的 sourceMappingURL 注释要换成 /* */。

写在最后

source map 是「产物 -> 源码」的映射。接下来回到「模块替换」这个更早期的阶段------Hot Module Replacement。它通过比对两次构建的 module hash,生成只含「变化模块」的 hot update 文件。下一篇进入 rspack_plugin_hmr,看 HMR 如何做增量更新。

相关推荐
console.log('npc')1 小时前
从0到1实现任务评价统计功能:React + TypeScript 实战指南(小白注释版)
前端·react.js·typescript
IT_陈寒1 小时前
为什么你应该学习React?
前端·人工智能·后端
ZoeLandia1 小时前
React04:事件处理与受控表单
开发语言·前端·react.js·typescript
亿元程序员1 小时前
用AI做了一款微信小游戏,却发现上传不了
前端
Ai-_Man1 小时前
我需要批量导出Poe左边栏所有对话,而不是一条具体的对话内容,你们AI导出鸭可以吗?
开发语言·前端·javascript·人工智能·小程序
java1234_小锋1 小时前
【技术专题】HTML5 网页技术 - Web Storage 本地存储 & 文件 API & 拖放 API
前端·html·html5
恋猫de小郭1 小时前
Flutter 官方表示:AI 时代代码越来越容易写,为什么还要一套代码库?
android·前端·flutter
软件派1 小时前
Chrome 插件开发实战指南
前端·chrome
ly76891 小时前
性能剖析与压测方法论:JMH、async-profiler 与分层瓶颈定位的可复现实验
服务器·前端·数据库·压测·jmh·性能剖析·async-profiler