diff --git a/crates/node_binding/binding.d.ts b/crates/node_binding/binding.d.ts index 37ae507b5d08..d4971877985d 100644 --- a/crates/node_binding/binding.d.ts +++ b/crates/node_binding/binding.d.ts @@ -1975,6 +1975,7 @@ export interface RawSourceMapDevToolPluginOptions { test?: string | RegExp | (string | RegExp)[] include?: string | RegExp | (string | RegExp)[] exclude?: string | RegExp | (string | RegExp)[] + debugIds?: boolean } export interface RawSplitChunkSizes { diff --git a/crates/rspack_binding_values/src/raw_options/raw_devtool.rs b/crates/rspack_binding_values/src/raw_options/raw_devtool.rs index c54ee21bc7f5..c2812143b6d0 100644 --- a/crates/rspack_binding_values/src/raw_options/raw_devtool.rs +++ b/crates/rspack_binding_values/src/raw_options/raw_devtool.rs @@ -118,6 +118,7 @@ pub struct RawSourceMapDevToolPluginOptions { pub include: Option, #[napi(ts_type = "string | RegExp | (string | RegExp)[]")] pub exclude: Option, + pub debug_ids: Option, } impl From for SourceMapDevToolPluginOptions { @@ -153,6 +154,7 @@ impl From for SourceMapDevToolPluginOptions { test: opts.test.map(into_asset_conditions), include: opts.include.map(into_asset_conditions), exclude: opts.exclude.map(into_asset_conditions), + debug_ids: opts.debug_ids.unwrap_or(false), } } } diff --git a/crates/rspack_plugin_devtool/src/source_map_dev_tool_plugin.rs b/crates/rspack_plugin_devtool/src/source_map_dev_tool_plugin.rs index 8267140511e7..c36b670b82af 100644 --- a/crates/rspack_plugin_devtool/src/source_map_dev_tool_plugin.rs +++ b/crates/rspack_plugin_devtool/src/source_map_dev_tool_plugin.rs @@ -80,6 +80,36 @@ pub struct SourceMapDevToolPluginOptions { pub test: Option, pub include: Option, pub exclude: Option, + pub debug_ids: bool, +} + +fn create_debug_id(filename: &str, source: &[u8]) -> String { + // We need two 64 bit hashes to give us the 128 bits required for a uuid + // The first 64 bit hash is built from the source + let mut hasher = RspackHash::new(&rspack_hash::HashFunction::Xxhash64); + hasher.write(source); + let hash1 = hasher.finish().to_le_bytes(); + + // The second 64 bit hash is built from the filename and the source hash + let mut hasher = RspackHash::new(&rspack_hash::HashFunction::Xxhash64); + hasher.write(filename.as_bytes()); + hasher.write(&hash1); + let hash2 = hasher.finish().to_le_bytes(); + + let mut bytes = [hash1, hash2].concat(); + + // Build the uuid from the 16 bytes + let mut uuid = String::with_capacity(36); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + for (i, byte) in bytes.iter().enumerate() { + if i == 4 || i == 6 || i == 8 || i == 10 { + uuid.push('-'); + } + uuid.push_str(&format!("{byte:02x}")); + } + uuid } enum SourceMappingUrlComment { @@ -119,6 +149,7 @@ pub struct SourceMapDevToolPlugin { test: Option, include: Option, exclude: Option, + debug_ids: bool, mapped_assets_cache: MappedAssetsCache, } @@ -183,6 +214,7 @@ impl SourceMapDevToolPlugin { options.test, options.include, options.exclude, + options.debug_ids, MappedAssetsCache::new(), ) } @@ -369,9 +401,17 @@ impl SourceMapDevToolPlugin { .into_iter() .map(|(source_filename, source, source_map)| { async { - let source_map_json = match source_map { - Some(map) => Some(map.to_json().into_diagnostic()?), - None => None, + let (source_map_json, debug_id) = match source_map { + Some(mut map) => { + let debug_id = self.debug_ids.then(|| { + let debug_id = create_debug_id(&source_filename, &source.buffer()); + map.set_debug_id(Some(debug_id.clone())); + debug_id + }); + + (Some(map.to_json().into_diagnostic()?), debug_id) + } + None => (None, None), }; let mut asset = compilation @@ -473,15 +513,19 @@ impl SourceMapDevToolPlugin { .always_ok() } }; + let current_source_mapping_url_comment = current_source_mapping_url_comment + .cow_replace("[url]", &source_map_url) + .into_owned(); + + let debug_id_comment = debug_id + .map(|id| format!("\n//# debugId={id}")) + .unwrap_or_default(); + asset.source = Some( ConcatSource::new([ source.clone(), - RawStringSource::from( - current_source_mapping_url_comment - .cow_replace("[url]", &source_map_url) - .into_owned(), - ) - .boxed(), + RawStringSource::from(debug_id_comment).boxed(), + RawStringSource::from(current_source_mapping_url_comment).boxed(), ]) .boxed(), ); diff --git a/packages/rspack/src/rspackOptionsApply.ts b/packages/rspack/src/rspackOptionsApply.ts index f73602203201..ce54c7626756 100644 --- a/packages/rspack/src/rspackOptionsApply.ts +++ b/packages/rspack/src/rspackOptionsApply.ts @@ -188,6 +188,7 @@ export class RspackOptionsApply { const cheap = options.devtool.includes("cheap"); const moduleMaps = options.devtool.includes("module"); const noSources = options.devtool.includes("nosources"); + const debug_ids = options.devtool.includes("debugids"); const Plugin = evalWrapped ? EvalSourceMapDevToolPlugin : SourceMapDevToolPlugin; @@ -200,7 +201,8 @@ export class RspackOptionsApply { module: moduleMaps ? true : !cheap, columns: !cheap, noSources: noSources, - namespace: options.output.devtoolNamespace + namespace: options.output.devtoolNamespace, + debugIds: debug_ids, }).apply(compiler); } else if (options.devtool.includes("eval")) { new EvalDevToolModulePlugin({ diff --git a/tests/webpack-test/configCases/source-map/source-map-debugids/index.js b/tests/webpack-test/configCases/source-map/source-map-debugids/index.js new file mode 100644 index 000000000000..7945ce188e39 --- /dev/null +++ b/tests/webpack-test/configCases/source-map/source-map-debugids/index.js @@ -0,0 +1,12 @@ +const fs = require("fs"); + +it("source should include debug id that matches debugId key in sourcemap", function() { + const source = fs.readFileSync(__filename, "utf-8"); + const sourceMap = fs.readFileSync(__filename + ".map", "utf-8"); + const map = JSON.parse(sourceMap); + expect(map.debugId).toBeDefined(); + expect( + /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i.test(map.debugId) + ).toBe(true); + expect(source).toContain(`//# debugId=${map.debugId}`); +}); diff --git a/tests/webpack-test/configCases/source-map/source-map-debugids/webpack.config.js b/tests/webpack-test/configCases/source-map/source-map-debugids/webpack.config.js new file mode 100644 index 000000000000..467ccfd15ea6 --- /dev/null +++ b/tests/webpack-test/configCases/source-map/source-map-debugids/webpack.config.js @@ -0,0 +1,4 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + devtool: "source-map-debugids" +};