From dc7becc91ac59332504d091460fd9ea501de2c12 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Mon, 6 Oct 2025 11:26:06 +0800 Subject: [PATCH] feat: implement `extractSourceMap` option --- Cargo.lock | 1 + crates/node_binding/napi-binding.d.ts | 2 + .../snapshots/defaults__default_options.snap | 28 + .../src/raw_options/raw_module/mod.rs | 4 + crates/rspack_core/Cargo.toml | 1 + .../rspack_core/src/loader/rspack_loader.rs | 55 +- crates/rspack_core/src/normal_module.rs | 9 +- .../rspack_core/src/normal_module_factory.rs | 13 + crates/rspack_core/src/options/module.rs | 2 + .../src/utils/extract_source_map.rs | 488 ++++++++++++++++++ crates/rspack_core/src/utils/mod.rs | 2 + crates/rspack_loader_runner/src/plugin.rs | 10 +- crates/rspack_loader_runner/src/runner.rs | 34 +- crates/rspack_plugin_schemes/src/data_uri.rs | 9 +- crates/rspack_plugin_schemes/src/file_uri.rs | 35 +- .../rspack_plugin_schemes/src/http_uri/mod.rs | 8 +- packages/rspack/etc/core.api.md | 1 + packages/rspack/src/config/adapter.ts | 3 +- packages/rspack/src/config/types.ts | 3 + packages/rspack/src/schema/config.ts | 3 +- .../__snapshots__/Config.test.js.snap | 81 +++ .../source-map/extract-source-map-css/app.css | 22 + .../extract-source-map-css/app.css.map | 1 + .../extract-source-map-css/index.js | 14 + .../extract-source-map-css/test.config.js | 10 + .../extract-source-map-css/webpack.config.js | 20 + .../source-map/extract-source-map/extract1.js | 14 + .../source-map/extract-source-map/extract2.js | 12 + .../source-map/extract-source-map/extract3.js | 12 + .../extract-source-map/infrastructure-log.js | 5 + .../extract-source-map/no-source-map.js | 2 + .../extract-source-map/remove-comment.js | 11 + .../source-map/extract-source-map/test1.js | 3 + .../source-map/extract-source-map/test2.js | 3 + .../source-map/extract-source-map/test2.map | 1 + .../source-map/extract-source-map/test3.js | 3 + .../source-map/extract-source-map/warnings.js | 3 + .../extract-source-map/webpack.config.js | 52 ++ .../source-map/extract-source-map2/a.js | 3 + .../source-map/extract-source-map2/a.map | 1 + .../extract-source-map2/babel-loader.js | 27 + .../external-source-map.txt | 1 + .../source-map/extract-source-map2/index.js | 11 + .../extract-source-map2/test.filter.js | 5 + .../extract-source-map2/webpack.config.js | 17 + 45 files changed, 1007 insertions(+), 38 deletions(-) create mode 100644 crates/rspack_core/src/utils/extract_source_map.rs create mode 100644 tests/rspack-test/__snapshots__/Config.test.js.snap create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map-css/app.css create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map-css/app.css.map create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map-css/index.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map-css/test.config.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map-css/webpack.config.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map/extract1.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map/extract2.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map/extract3.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map/infrastructure-log.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map/no-source-map.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map/remove-comment.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map/test1.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map/test2.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map/test2.map create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map/test3.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map/warnings.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map/webpack.config.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map2/a.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map2/a.map create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map2/babel-loader.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map2/external-source-map.txt create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map2/index.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map2/test.filter.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map2/webpack.config.js diff --git a/Cargo.lock b/Cargo.lock index 7e7585c88b9f..e9da66f4e483 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3795,6 +3795,7 @@ dependencies = [ "swc_node_comments", "tokio", "tracing", + "urlencoding", "ustr-fxhash", "winnow", ] diff --git a/crates/node_binding/napi-binding.d.ts b/crates/node_binding/napi-binding.d.ts index b44e242e1de7..ee93f86b13e1 100644 --- a/crates/node_binding/napi-binding.d.ts +++ b/crates/node_binding/napi-binding.d.ts @@ -2489,6 +2489,8 @@ export interface RawModuleRule { rules?: Array /** Specifies the category of the loader. No value means normal loader. */ enforce?: 'pre' | 'post' + /** Whether to extract source maps from the module. */ + extractSourceMap?: boolean } /** diff --git a/crates/rspack/tests/snapshots/defaults__default_options.snap b/crates/rspack/tests/snapshots/defaults__default_options.snap index 8ea70e83b6ea..b75c1eb0d372 100644 --- a/crates/rspack/tests/snapshots/defaults__default_options.snap +++ b/crates/rspack/tests/snapshots/defaults__default_options.snap @@ -736,7 +736,9 @@ CompilerOptions { generator: None, resolve: None, enforce: Normal, + extract_source_map: None, }, + extract_source_map: None, }, ModuleRule { rspack_resource: None, @@ -768,7 +770,9 @@ CompilerOptions { generator: None, resolve: None, enforce: Normal, + extract_source_map: None, }, + extract_source_map: None, }, ModuleRule { rspack_resource: None, @@ -805,7 +809,9 @@ CompilerOptions { generator: None, resolve: None, enforce: Normal, + extract_source_map: None, }, + extract_source_map: None, }, ModuleRule { rspack_resource: None, @@ -895,7 +901,9 @@ CompilerOptions { }, ), enforce: Normal, + extract_source_map: None, }, + extract_source_map: None, }, ModuleRule { rspack_resource: None, @@ -994,7 +1002,9 @@ CompilerOptions { }, ), enforce: Normal, + extract_source_map: None, }, + extract_source_map: None, }, ModuleRule { rspack_resource: None, @@ -1026,7 +1036,9 @@ CompilerOptions { generator: None, resolve: None, enforce: Normal, + extract_source_map: None, }, + extract_source_map: None, }, ModuleRule { rspack_resource: None, @@ -1067,7 +1079,9 @@ CompilerOptions { generator: None, resolve: None, enforce: Normal, + extract_source_map: None, }, + extract_source_map: None, }, ModuleRule { rspack_resource: None, @@ -1171,7 +1185,9 @@ CompilerOptions { }, ), enforce: Normal, + extract_source_map: None, }, + extract_source_map: None, }, ModuleRule { rspack_resource: None, @@ -1227,7 +1243,9 @@ CompilerOptions { generator: None, resolve: None, enforce: Normal, + extract_source_map: None, }, + extract_source_map: None, }, ModuleRule { rspack_resource: None, @@ -1257,7 +1275,9 @@ CompilerOptions { generator: None, resolve: None, enforce: Normal, + extract_source_map: None, }, + extract_source_map: None, }, ], ), @@ -1271,7 +1291,9 @@ CompilerOptions { generator: None, resolve: None, enforce: Normal, + extract_source_map: None, }, + extract_source_map: None, }, ModuleRule { rspack_resource: None, @@ -1310,7 +1332,9 @@ CompilerOptions { generator: None, resolve: None, enforce: Normal, + extract_source_map: None, }, + extract_source_map: None, }, ], ), @@ -1323,7 +1347,9 @@ CompilerOptions { generator: None, resolve: None, enforce: Normal, + extract_source_map: None, }, + extract_source_map: None, }, ModuleRule { rspack_resource: None, @@ -1353,7 +1379,9 @@ CompilerOptions { generator: None, resolve: None, enforce: Normal, + extract_source_map: None, }, + extract_source_map: None, }, ], parser: Some( diff --git a/crates/rspack_binding_api/src/raw_options/raw_module/mod.rs b/crates/rspack_binding_api/src/raw_options/raw_module/mod.rs index ee055f6cd745..8f44b38e8a23 100644 --- a/crates/rspack_binding_api/src/raw_options/raw_module/mod.rs +++ b/crates/rspack_binding_api/src/raw_options/raw_module/mod.rs @@ -176,6 +176,8 @@ pub struct RawModuleRule { /// Specifies the category of the loader. No value means normal loader. #[napi(ts_type = "'pre' | 'post'")] pub enforce: Option, + /// Whether to extract source maps from the module. + pub extract_source_map: Option, } #[derive(Debug, Default)] @@ -970,7 +972,9 @@ impl TryFrom for ModuleRule { resolve: value.resolve.map(|raw| raw.try_into()).transpose()?, side_effects: value.side_effects, enforce, + extract_source_map: value.extract_source_map, }, + extract_source_map: value.extract_source_map, }) } } diff --git a/crates/rspack_core/Cargo.toml b/crates/rspack_core/Cargo.toml index cc44952a2723..81232c2b0564 100644 --- a/crates/rspack_core/Cargo.toml +++ b/crates/rspack_core/Cargo.toml @@ -77,6 +77,7 @@ swc_core = { workspace = true, features = [ swc_node_comments = { workspace = true } tokio = { workspace = true, features = ["rt", "macros"] } tracing = { workspace = true } +urlencoding = { workspace = true } ustr = { workspace = true } winnow = { workspace = true } diff --git a/crates/rspack_core/src/loader/rspack_loader.rs b/crates/rspack_core/src/loader/rspack_loader.rs index 30ee59eb5642..1df22dd3dd88 100644 --- a/crates/rspack_core/src/loader/rspack_loader.rs +++ b/crates/rspack_core/src/loader/rspack_loader.rs @@ -1,13 +1,16 @@ -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; -use rspack_error::Result; +use rspack_error::{Diagnostic, Result}; +use rspack_fs::ReadableFileSystem; use rspack_loader_runner::{Content, LoaderContext, LoaderRunnerPlugin, ResourceData}; +use rspack_sources::SourceMap; -use crate::{RunnerContext, SharedPluginDriver}; +use crate::{RunnerContext, SharedPluginDriver, utils::extract_source_map}; pub struct RspackLoaderRunnerPlugin { pub plugin_driver: SharedPluginDriver, pub current_loader: Mutex>, + pub extract_source_map: Option, } #[async_trait::async_trait] @@ -27,17 +30,55 @@ impl LoaderRunnerPlugin for RspackLoaderRunnerPlugin { .await } - async fn process_resource(&self, resource_data: &ResourceData) -> Result> { + async fn process_resource( + &self, + resource_data: &ResourceData, + fs: Arc, + ) -> Result)>> { + // First try the plugin's read_resource hook let result = self .plugin_driver .normal_module_hooks .read_resource - .call(resource_data) + .call(resource_data, &fs) .await?; - if result.is_some() { - return Ok(result); + + if let Some(content) = result { + if let Some(true) = self.extract_source_map { + // Try to extract source map from the content + let extract_result = match &content { + Content::String(s) => extract_source_map(fs, s, resource_data.resource()).await, + Content::Buffer(b) => { + extract_source_map(fs, &String::from_utf8_lossy(b), resource_data.resource()).await + } + }; + + match extract_result { + Ok(extract_result) => { + // Return the content with source map extracted + // The source map will be available through the loader context + return Ok(Some(( + Content::String(extract_result.source), + extract_result.source_map, + ))); + } + Err(e) => { + // If extraction fails, return original content + // Log the error as a warning + self + .plugin_driver + .diagnostics + .lock() + .expect("should get lock") + .push(Diagnostic::warn("extractSourceMap".into(), e)); + return Ok(Some((content, None))); + } + } + } + return Ok(Some((content, None))); } + // If no plugin handled it, return None so the default logic can handle it Ok(None) } diff --git a/crates/rspack_core/src/normal_module.rs b/crates/rspack_core/src/normal_module.rs index 67d24b4c3904..7611b552f305 100644 --- a/crates/rspack_core/src/normal_module.rs +++ b/crates/rspack_core/src/normal_module.rs @@ -16,6 +16,7 @@ use rspack_cacheable::{ }; use rspack_collections::{Identifiable, IdentifierMap, IdentifierSet}; use rspack_error::{Diagnosable, Diagnostic, Result, error}; +use rspack_fs::ReadableFileSystem; use rspack_hash::{RspackHash, RspackHashDigest}; use rspack_hook::define_hook; use rspack_loader_runner::{AdditionalData, Content, LoaderContext, ResourceData, run_loaders}; @@ -77,7 +78,7 @@ impl ModuleIssuer { } } -define_hook!(NormalModuleReadResource: SeriesBail(resource_data: &ResourceData) -> Content,tracing=false); +define_hook!(NormalModuleReadResource: SeriesBail(resource_data: &ResourceData, fs: &Arc) -> Content,tracing=false); define_hook!(NormalModuleLoader: Series(loader_context: &mut LoaderContext),tracing=false); define_hook!(NormalModuleLoaderShouldYield: SeriesBail(loader_context: &LoaderContext) -> bool,tracing=false); define_hook!(NormalModuleLoaderStartYielding: Series(loader_context: &mut LoaderContext),tracing=false); @@ -134,6 +135,8 @@ pub struct NormalModule { parser_options: Option, /// Generator options derived from [Rule.generator] generator_options: Option, + /// enable/disable extracting source map + extract_source_map: Option, #[allow(unused)] debug_id: usize, @@ -182,6 +185,7 @@ impl NormalModule { resolve_options: Option>, loaders: Vec, context: Option, + extract_source_map: Option, ) -> Self { let module_type = module_type.into(); let id = Self::create_id(&module_type, layer.as_ref(), &request); @@ -204,6 +208,7 @@ impl NormalModule { loaders, source: None, debug_id: DEBUG_ID.fetch_add(1, Ordering::Relaxed), + extract_source_map, cached_source_sizes: DashMap::default(), diagnostics: Default::default(), @@ -399,6 +404,7 @@ impl Module for NormalModule { let plugin = Arc::new(RspackLoaderRunnerPlugin { plugin_driver: build_context.plugin_driver.clone(), current_loader: Default::default(), + extract_source_map: self.extract_source_map, }); let (mut loader_result, err) = run_loaders( @@ -453,6 +459,7 @@ impl Module for NormalModule { optimization_bailouts: vec![], }); }; + build_context .plugin_driver .normal_module_hooks diff --git a/crates/rspack_core/src/normal_module_factory.rs b/crates/rspack_core/src/normal_module_factory.rs index 4e33fbffdeec..5e39ef4b0f0c 100644 --- a/crates/rspack_core/src/normal_module_factory.rs +++ b/crates/rspack_core/src/normal_module_factory.rs @@ -498,6 +498,7 @@ impl NormalModuleFactory { resolved_generator_options, ); let resolved_side_effects = self.calculate_side_effects(&resolved_module_rules); + let resolved_extract_source_map = self.calculate_extract_source_map(&resolved_module_rules); let mut resolved_parser_and_generator = self .plugin_driver .registered_parser_and_generator_builder @@ -573,6 +574,7 @@ impl NormalModuleFactory { resolved_resolve_options, loaders, create_data.context.clone().map(|x| x.into()), + resolved_extract_source_map, ) .boxed() }; @@ -636,6 +638,17 @@ impl NormalModuleFactory { side_effect_res } + fn calculate_extract_source_map(&self, module_rules: &[&ModuleRuleEffect]) -> Option { + let mut extract_source_map_res = None; + // extract_source_map from module rule has higher priority + for rule in module_rules.iter() { + if rule.extract_source_map.is_some() { + extract_source_map_res = rule.extract_source_map; + } + } + extract_source_map_res + } + fn calculate_parser_and_generator_options( &self, module_rules: &[&ModuleRuleEffect], diff --git a/crates/rspack_core/src/options/module.rs b/crates/rspack_core/src/options/module.rs index 476272562e74..3e9965955581 100644 --- a/crates/rspack_core/src/options/module.rs +++ b/crates/rspack_core/src/options/module.rs @@ -1017,6 +1017,7 @@ pub struct ModuleRule { pub one_of: Option>, pub rules: Option>, pub effect: ModuleRuleEffect, + pub extract_source_map: Option, } #[derive(Debug, Default)] @@ -1030,6 +1031,7 @@ pub struct ModuleRuleEffect { pub generator: Option, pub resolve: Option, pub enforce: ModuleRuleEnforce, + pub extract_source_map: Option, } pub enum ModuleRuleUse { diff --git a/crates/rspack_core/src/utils/extract_source_map.rs b/crates/rspack_core/src/utils/extract_source_map.rs new file mode 100644 index 000000000000..efd92cf85732 --- /dev/null +++ b/crates/rspack_core/src/utils/extract_source_map.rs @@ -0,0 +1,488 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Natsu @xiaoxiaojx +*/ + +use std::{ + borrow::Cow, + collections::HashSet, + path::{Path, PathBuf}, + sync::Arc, +}; + +use cow_utils::CowUtils; +use futures::stream::{FuturesOrdered, StreamExt}; +use once_cell::sync::Lazy; +use regex::Regex; +use rspack_fs::ReadableFileSystem; +use rspack_paths::AssertUtf8; +use rspack_sources::SourceMap; +use rspack_util::base64; + +/// Source map extractor result +#[derive(Debug, Clone)] +pub struct ExtractSourceMapResult { + pub source: String, + pub source_map: Option, + pub file_dependencies: Option>, +} + +/// Source mapping URL information +#[derive(Debug, Clone)] +pub struct SourceMappingURL { + pub source_mapping_url: String, + pub replacement_string: String, +} + +static VALID_PROTOCOL_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").expect("Invalid regex pattern")); +static SOURCE_MAPPING_URL_REGEX: Lazy = Lazy::new(|| { + Regex::new(r#"(?:/\*(?:\s*\r?\n(?://)?)?(?:\s*[#@]\s*sourceMappingURL\s*=\s*([^\s'"]*)\s*)\s*\*/|//(?:\s*[#@]\s*sourceMappingURL\s*=\s*([^\s'"]*)\s*))\s*"#).expect("Invalid regex pattern") +}); +static URI_REGEX: Lazy = Lazy::new(|| { + Regex::new(r"^data:([^;,]+)?((?:;[^;,]+)*?)(?:;(base64)?)?,(.*)$").expect("Invalid regex pattern") +}); + +/// Extract source mapping URL from code comments +pub fn get_source_mapping_url(code: &str) -> SourceMappingURL { + // Use captures_iter to find the last match, avoiding split and collect overhead + let mut match_result = None; + let mut replacement_string = String::new(); + + // Find the last match from the end + if let Some(captures) = SOURCE_MAPPING_URL_REGEX.captures_iter(code).last() { + match_result = captures + .get(1) + .or_else(|| captures.get(2)) + .map(|m| m.as_str()); + + // Get the complete match string for replacement + replacement_string = captures.get(0).map_or("", |m| m.as_str()).to_string(); + } + + let source_mapping_url = match_result.unwrap_or("").to_string(); + + SourceMappingURL { + source_mapping_url: if !source_mapping_url.is_empty() { + urlencoding::decode(&source_mapping_url) + .unwrap_or(Cow::Borrowed(&source_mapping_url)) + .to_string() + } else { + source_mapping_url + }, + replacement_string, + } +} + +/// Check if value is a URL +fn is_url(value: &str) -> bool { + VALID_PROTOCOL_PATTERN.is_match(value) && !PathBuf::from(value).is_absolute() +} + +/// Decode data URI +fn decode_data_uri(uri: &str) -> Option { + // data URL scheme: "data:text/javascript;charset=utf-8;base64,some-string" + // http://www.ietf.org/rfc/rfc2397.txt + let captures = URI_REGEX.captures(uri)?; + let is_base64 = captures.get(3).is_some(); + let body = captures.get(4)?.as_str(); + + if is_base64 { + return base64::decode_to_vec(body) + .ok() + .and_then(|bytes| String::from_utf8(bytes).ok()); + } + + // CSS allows to use `data:image/svg+xml;utf8,` + // so we return original body if we can't `decodeURIComponent` + match urlencoding::decode(body) { + Ok(decoded) => Some(decoded.to_string()), + Err(_) => Some(body.to_string()), + } +} + +/// Fetch source content from data URL +fn fetch_from_data_url(source_url: &str) -> Result { + if let Some(content) = decode_data_uri(source_url) { + Ok(content) + } else { + Err(format!( + "Failed to parse source map from \"data\" URL: {source_url}" + )) + } +} + +/// Get absolute path for source file +fn get_absolute_path(context: &Path, request: &str, source_root: Option<&str>) -> PathBuf { + if let Some(source_root) = source_root { + let source_root_path = Path::new(source_root); + if source_root_path.is_absolute() { + return source_root_path.join(request); + } + return context.join(source_root_path).join(request); + } + + context.join(request) +} + +/// Fetch source content from file system +async fn fetch_from_filesystem( + fs: &Arc, + source_url: &str, +) -> Result<(String, Option), String> { + if is_url(source_url) { + return Ok((source_url.to_string(), None)); + } + + let path = PathBuf::from(source_url); + fs.read_to_string(&path.assert_utf8()) + .await + .map(|content| (source_url.to_string(), Some(content))) + .map_err(|err| format!("Failed to parse source map from '{source_url}' file: {err}")) +} + +/// Fetch from multiple possible file paths +async fn fetch_paths_from_filesystem( + fs: &Arc, + possible_requests: &[String], + mut errors_accumulator: String, +) -> Result<(String, Option), String> { + if possible_requests.is_empty() { + return Err(errors_accumulator); + } + + // Use iteration instead of recursion to avoid Box::pin and dynamic dispatch overhead + for (i, request) in possible_requests.iter().enumerate() { + match fetch_from_filesystem(fs, request).await { + Ok(result) => return Ok(result), + Err(error) => { + if i > 0 { + errors_accumulator.push_str("\n\n"); + } + errors_accumulator.push_str(&error); + } + } + } + + Err(errors_accumulator) +} + +/// Fetch source content from URL +async fn fetch_from_url( + fs: &Arc, + context: &Path, + url: &str, + source_root: Option<&str>, + skip_reading: bool, +) -> Result<(String, Option), String> { + // 1. It's an absolute url and it is not `windows` path like `C:\dir\file` + if is_url(url) { + if url.starts_with("data:") { + if skip_reading { + return Ok((String::new(), None)); + } + + let source_content = fetch_from_data_url(url)?; + return Ok((String::new(), Some(source_content))); + } + + if skip_reading { + return Ok((url.to_string(), None)); + } + + if url.starts_with("file:") { + // Handle file:// URLs + let path_from_url = url.strip_prefix("file://").unwrap_or(url); + let source_url = PathBuf::from(path_from_url).to_string_lossy().into_owned(); + return fetch_from_filesystem(fs, &source_url).await; + } + + return Err(format!( + "Failed to parse source map: '{url}' URL is not supported" + )); + } + + // 2. It's a scheme-relative + if url.starts_with("//") { + return Err(format!( + "Failed to parse source map: '{url}' URL is not supported" + )); + } + + // 3. Absolute path + let url_path = PathBuf::from(url); + if url_path.is_absolute() { + let source_url = url_path.to_string_lossy().into_owned(); + + if !skip_reading { + let mut possible_requests = Vec::with_capacity(2); + possible_requests.push(source_url.clone()); + + if let Some(stripped) = url.strip_prefix('/') { + let absolute_path = get_absolute_path(context, stripped, source_root); + possible_requests.push(absolute_path.to_string_lossy().into_owned()); + } + + return fetch_paths_from_filesystem(fs, &possible_requests, String::new()).await; + } + + return Ok((source_url, None)); + } + + // 4. Relative path + let source_url = get_absolute_path(context, url, source_root); + let source_url_str = source_url.to_string_lossy().to_string(); + + if !skip_reading { + let (_, content) = fetch_from_filesystem(fs, &source_url_str).await?; + return Ok((source_url_str, content)); + } + + Ok((source_url_str, None)) +} + +/// Extract source map from code content +pub async fn extract_source_map( + fs: Arc, + input: &str, + resource_path: &str, +) -> Result { + let SourceMappingURL { + source_mapping_url, + replacement_string, + } = get_source_mapping_url(input); + + if source_mapping_url.is_empty() { + return Ok(ExtractSourceMapResult { + source: input.to_string(), + source_map: None, + file_dependencies: None, + }); + } + + let base_context = Path::new(resource_path) + .parent() + .ok_or_else(|| "Invalid resource path".to_string())?; + + let (source_url, source_content) = + fetch_from_url(&fs, base_context, &source_mapping_url, None, false).await?; + + if source_content.is_none() { + return Ok(ExtractSourceMapResult { + source: input.to_string(), + source_map: None, + file_dependencies: if source_url.is_empty() { + None + } else { + let mut set = HashSet::new(); + set.insert(PathBuf::from(source_url)); + Some(set) + }, + }); + } + + let content = source_content.expect("Source content should be available"); + let cleaned_content = content.trim_start_matches(")]}"); + + // Create SourceMap directly from JSON + let mut source_map = SourceMap::from_json(cleaned_content) + .map_err(|e| format!("Failed to parse source map: {e}"))?; + + let context = if !source_url.is_empty() { + Path::new(&source_url).parent().unwrap_or(base_context) + } else { + base_context + }; + + let mut resolved_sources = Vec::new(); + let mut file_dependencies = if source_url.is_empty() { + None + } else { + let mut set = HashSet::new(); + set.insert(PathBuf::from(&source_url)); + Some(set) + }; + + // Get sources from SourceMap and take ownership + let sources = source_map.sources().to_vec(); + let source_root = source_map.source_root().map(|s| s.to_string()); + + // Pre-collect all source content to avoid borrowing issues + let source_contents: Vec> = (0..sources.len()) + .map(|i| source_map.get_source_content(i).map(|s| s.to_string())) + .collect(); + + // Process sources in parallel using FuturesOrdered to maintain order + let mut futures = FuturesOrdered::new(); + + // Use zip to consume both vectors without extra cloning + for (source, original_content) in sources.into_iter().zip(source_contents.into_iter()) { + let skip_reading = original_content.is_some(); + let source_root = source_root.clone(); + let context = context.to_path_buf(); + + let fs = fs.clone(); + futures.push_back(async move { + let result = + fetch_from_url(&fs, &context, &source, source_root.as_deref(), skip_reading).await; + (original_content, skip_reading, result) + }); + } + + // Collect results in order + while let Some((original_content, skip_reading, result)) = futures.next().await { + let (source_url_result, source_content_result) = result?; + + let final_content = if skip_reading { + original_content + } else { + source_content_result + }; + + if !skip_reading && !source_url_result.is_empty() && !is_url(&source_url_result) { + if let Some(ref mut deps) = file_dependencies { + deps.insert(PathBuf::from(&source_url_result)); + } else { + let mut set = HashSet::new(); + set.insert(PathBuf::from(&source_url_result)); + file_dependencies = Some(set); + } + } + + resolved_sources.push((source_url_result, final_content)); + } + + // Build the final SourceMap using setter methods - consume resolved_sources to avoid cloning + let (sources_vec, sources_content_vec): (Vec, Vec) = resolved_sources + .into_iter() + .map(|(url, content)| (url, content.unwrap_or_default())) + .unzip(); + + source_map.set_sources(sources_vec); + source_map.set_sources_content(sources_content_vec); + + // Remove source_root as per original logic + source_map.set_source_root(None::); + + // Optimize string replacement to avoid unnecessary cloning + let new_source = if replacement_string.is_empty() { + input.to_string() + } else { + input.cow_replace(&replacement_string, "").into_owned() + }; + + Ok(ExtractSourceMapResult { + source: new_source, + source_map: Some(source_map), + file_dependencies, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_source_mapping_url() { + // Test cases based on expected results from extractSourceMap.unittest.js.snap + let test_cases = vec![ + ( + "/*#sourceMappingURL=absolute-sourceRoot-source-map.map*/", + "absolute-sourceRoot-source-map.map", + ), + ( + "/* #sourceMappingURL=absolute-sourceRoot-source-map.map */", + "absolute-sourceRoot-source-map.map", + ), + ( + "//#sourceMappingURL=absolute-sourceRoot-source-map.map", + "absolute-sourceRoot-source-map.map", + ), + ( + "//@sourceMappingURL=absolute-sourceRoot-source-map.map", + "absolute-sourceRoot-source-map.map", + ), + ( + " // #sourceMappingURL=absolute-sourceRoot-source-map.map", + "absolute-sourceRoot-source-map.map", + ), + ( + " // # sourceMappingURL = absolute-sourceRoot-source-map.map ", + "absolute-sourceRoot-source-map.map", + ), + ( + "// #sourceMappingURL = http://hello.com/external-source-map2.map", + "http://hello.com/external-source-map2.map", + ), + ( + "// #sourceMappingURL = //hello.com/external-source-map2.map", + "//hello.com/external-source-map2.map", + ), + ( + "// @sourceMappingURL=data:application/source-map;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5saW5lLXNvdXJjZS1tYXAuanMiLCJzb3VyY2VzIjpbImlubGluZS1zb3VyY2UtbWFwLnR4dCJdLCJzb3VyY2VzQ29udGVudCI6WyJ3aXRoIFNvdXJjZU1hcCJdLCJtYXBwaW5ncyI6IkFBQUEifQ==", + "data:application/source-map;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5saW5lLXNvdXJjZS1tYXAuanMiLCJzb3VyY2VzIjpbImlubGluZS1zb3VyY2UtbWFwLnR4dCJdLCJzb3VyY2VzQ29udGVudCI6WyJ3aXRoIFNvdXJjZU1hcCJdLCJtYXBwaW5ncyI6IkFBQUEifQ==", + ), + ( + r#" + with SourceMap + + // #sourceMappingURL = /sample-source-map.map + // comment + "#, + "/sample-source-map.map", + ), + ( + r#" + with SourceMap + // #sourceMappingURL = /sample-source-map-1.map + // #sourceMappingURL = /sample-source-map-2.map + // #sourceMappingURL = /sample-source-map-last.map + // comment + "#, + "/sample-source-map-last.map", + ), + // JavaScript code snippet with variable reference, expected to return empty string + ( + r#"" + /*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+" */";"#, + "", + ), + // JavaScript code snippet, expected to truncate at first variable reference + ( + r#"// # sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+"'"#, + "data:application/json;base64,", + ), + // JavaScript code snippet with variable reference, expected to return empty string + ( + r#"anInvalidDirective = " +/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+" */";"#, + "", + ), + ]; + + for (input, expected) in test_cases { + let result = get_source_mapping_url(input); + assert_eq!( + result.source_mapping_url, expected, + "Failed for input: {input}" + ); + } + } + + #[test] + fn test_get_source_mapping_url_empty_cases() { + // Test cases without sourceMappingURL + let cases = vec![ + "const foo = 'bar';", + "// This is a regular comment", + "/* Multi-line\n comment\n without sourceMappingURL */", + "", + ]; + + for case in cases { + let result = get_source_mapping_url(case); + assert!(result.source_mapping_url.is_empty()); + assert!(result.replacement_string.is_empty()); + } + } +} diff --git a/crates/rspack_core/src/utils/mod.rs b/crates/rspack_core/src/utils/mod.rs index c72b38f9bdbc..db9918ff3a04 100644 --- a/crates/rspack_core/src/utils/mod.rs +++ b/crates/rspack_core/src/utils/mod.rs @@ -10,6 +10,7 @@ mod comment; mod compile_boolean_matcher; mod concatenated_module_visitor; mod concatenation_scope; +mod extract_source_map; mod extract_url_and_global; mod fast_actions; mod file_counter; @@ -37,6 +38,7 @@ pub use memory_gc::MemoryGCStorage; pub use self::{ comment::*, + extract_source_map::*, extract_url_and_global::*, fast_actions::*, file_counter::{FileCounter, ResourceId}, diff --git a/crates/rspack_loader_runner/src/plugin.rs b/crates/rspack_loader_runner/src/plugin.rs index dd7e0ef251a0..447f162cbc41 100644 --- a/crates/rspack_loader_runner/src/plugin.rs +++ b/crates/rspack_loader_runner/src/plugin.rs @@ -1,4 +1,8 @@ +use std::sync::Arc; + use rspack_error::Result; +use rspack_fs::ReadableFileSystem; +use rspack_sources::SourceMap; use crate::{ LoaderContext, @@ -25,5 +29,9 @@ pub trait LoaderRunnerPlugin: Send + Sync { Ok(()) } - async fn process_resource(&self, resource_data: &ResourceData) -> Result>; + async fn process_resource( + &self, + resource_data: &ResourceData, + fs: Arc, + ) -> Result)>>; } diff --git a/crates/rspack_loader_runner/src/runner.rs b/crates/rspack_loader_runner/src/runner.rs index 41a5590d9b12..c9f37464affe 100644 --- a/crates/rspack_loader_runner/src/runner.rs +++ b/crates/rspack_loader_runner/src/runner.rs @@ -4,7 +4,6 @@ use rspack_error::{Diagnostic, Error, Result, error}; use rspack_fs::ReadableFileSystem; use rspack_sources::SourceMap; use rustc_hash::FxHashSet as HashSet; -use tokio::task::spawn_blocking; use tracing::{Instrument, info_span}; use crate::{ @@ -36,31 +35,17 @@ async fn process_resource( fs: Arc, ) -> Result<()> { if let Some(plugin) = &loader_context.plugin - && let Some(processed_resource) = plugin - .process_resource(&loader_context.resource_data) + && let Some((content, source_map)) = plugin + .process_resource(&loader_context.resource_data, fs) .await? { - loader_context.content = Some(processed_resource); + loader_context.content = Some(content); + loader_context.source_map = source_map; return Ok(()); } let resource_data = &loader_context.resource_data; let scheme = resource_data.get_scheme(); - if scheme.is_none() { - if let Some(resource_path) = resource_data.path() - && !resource_path.as_str().is_empty() - { - let resource_path_owned = resource_path.to_owned(); - // use spawn_blocking to avoid block, see https://docs.rs/tokio/latest/src/tokio/fs/read.rs.html#48 - let result = spawn_blocking(move || fs.read_sync(resource_path_owned.as_path())) - .await - .map_err(|e| error!("{e}, spawn task failed"))?; - let result = result.map_err(|e| error!("{e}, failed to read {resource_path}"))?; - loader_context.content = Some(Content::from(result)); - } - return Ok(()); - } - let resource = resource_data.resource(); Err(error!( r#"Reading from "{resource}" is not handled by plugins (Unhandled scheme). @@ -259,7 +244,8 @@ mod test { use rspack_cacheable::{cacheable, cacheable_dyn}; use rspack_collections::Identifier; use rspack_error::Result; - use rspack_fs::NativeFileSystem; + use rspack_fs::{NativeFileSystem, ReadableFileSystem}; + use rspack_sources::SourceMap; use super::{Loader, LoaderContext, ResourceData, run_loaders}; use crate::{AdditionalData, content::Content, plugin::LoaderRunnerPlugin}; @@ -278,8 +264,12 @@ mod test { Ok(()) } - async fn process_resource(&self, _resource_data: &ResourceData) -> Result> { - Ok(Some(Content::Buffer(vec![]))) + async fn process_resource( + &self, + _resource_data: &ResourceData, + _fs: Arc, + ) -> Result)>> { + Ok(Some((Content::Buffer(vec![]), None))) } } diff --git a/crates/rspack_plugin_schemes/src/data_uri.rs b/crates/rspack_plugin_schemes/src/data_uri.rs index 5c9498e517e1..f07e06015dfd 100644 --- a/crates/rspack_plugin_schemes/src/data_uri.rs +++ b/crates/rspack_plugin_schemes/src/data_uri.rs @@ -1,4 +1,4 @@ -use std::sync::LazyLock; +use std::sync::{Arc, LazyLock}; use regex::Regex; use rspack_core::{ @@ -6,6 +6,7 @@ use rspack_core::{ Plugin, ResourceData, Scheme, }; use rspack_error::Result; +use rspack_fs::ReadableFileSystem; use rspack_hook::{plugin, plugin_hook}; use rspack_util::base64; @@ -57,7 +58,11 @@ async fn resolve_for_scheme( } #[plugin_hook(NormalModuleReadResource for DataUriPlugin,tracing=false)] -async fn read_resource(&self, resource_data: &ResourceData) -> Result> { +async fn read_resource( + &self, + resource_data: &ResourceData, + _fs: &Arc, +) -> Result> { if resource_data.get_scheme().is_data() && let Some(captures) = URI_REGEX.captures(resource_data.resource()) { diff --git a/crates/rspack_plugin_schemes/src/file_uri.rs b/crates/rspack_plugin_schemes/src/file_uri.rs index 556d873bfc99..e396d08b52ca 100644 --- a/crates/rspack_plugin_schemes/src/file_uri.rs +++ b/crates/rspack_plugin_schemes/src/file_uri.rs @@ -1,9 +1,14 @@ +use std::sync::Arc; + use rspack_core::{ - ModuleFactoryCreateData, NormalModuleFactoryResolveForScheme, Plugin, ResourceData, Scheme, + Content, ModuleFactoryCreateData, NormalModuleFactoryResolveForScheme, NormalModuleReadResource, + Plugin, ResourceData, Scheme, }; use rspack_error::{Result, ToStringResultToRspackResultExt, error}; +use rspack_fs::ReadableFileSystem; use rspack_hook::{plugin, plugin_hook}; use rspack_paths::AssertUtf8; +use tokio::task::spawn_blocking; use url::Url; #[plugin] @@ -37,6 +42,30 @@ async fn normal_module_factory_resolve_for_scheme( Ok(None) } +#[plugin_hook(NormalModuleReadResource for FileUriPlugin,tracing=false)] +async fn read_resource( + &self, + resource_data: &ResourceData, + fs: &Arc, +) -> Result> { + let scheme = resource_data.get_scheme(); + if scheme.is_none() + && let Some(resource_path) = resource_data.path() + && !resource_path.as_str().is_empty() + { + let resource_path_owned = resource_path.to_owned(); + let fs = fs.clone(); + // use spawn_blocking to avoid block, see https://docs.rs/tokio/latest/src/tokio/fs/read.rs.html#48 + let result = spawn_blocking(move || fs.read_sync(resource_path_owned.as_path())) + .await + .map_err(|e| error!("{e}, spawn task failed"))?; + let result = result.map_err(|e| error!("{e}, failed to read {resource_path}"))?; + return Ok(Some(Content::from(result))); + } + + Ok(None) +} + impl Plugin for FileUriPlugin { fn name(&self) -> &'static str { "rspack.FileUriPlugin" @@ -47,6 +76,10 @@ impl Plugin for FileUriPlugin { .normal_module_factory_hooks .resolve_for_scheme .tap(normal_module_factory_resolve_for_scheme::new(self)); + ctx + .normal_module_hooks + .read_resource + .tap(read_resource::new(self)); Ok(()) } } diff --git a/crates/rspack_plugin_schemes/src/http_uri/mod.rs b/crates/rspack_plugin_schemes/src/http_uri/mod.rs index bffdebdc6d7c..1f060ce9be7f 100644 --- a/crates/rspack_plugin_schemes/src/http_uri/mod.rs +++ b/crates/rspack_plugin_schemes/src/http_uri/mod.rs @@ -12,7 +12,7 @@ use rspack_core::{ NormalModuleFactoryResolveInScheme, NormalModuleReadResource, Plugin, ResourceData, Scheme, }; use rspack_error::{AnyhowResultToRspackResultExt, Result, error}; -use rspack_fs::WritableFileSystem; +use rspack_fs::{ReadableFileSystem, WritableFileSystem}; use rspack_hook::{plugin, plugin_hook}; use rspack_util::asset_condition::{AssetCondition, AssetConditions}; use url::Url; @@ -169,7 +169,11 @@ async fn resolve_in_scheme( } #[plugin_hook(NormalModuleReadResource for HttpUriPlugin)] -async fn read_resource(&self, resource_data: &ResourceData) -> Result> { +async fn read_resource( + &self, + resource_data: &ResourceData, + _fs: &Arc, +) -> Result> { if (resource_data.get_scheme().is_http() || resource_data.get_scheme().is_https()) && EXTERNAL_HTTP_REQUEST.is_match(resource_data.resource()) { diff --git a/packages/rspack/etc/core.api.md b/packages/rspack/etc/core.api.md index e021f2fb1060..d04366ad6d29 100644 --- a/packages/rspack/etc/core.api.md +++ b/packages/rspack/etc/core.api.md @@ -6838,6 +6838,7 @@ export type RuleSetRule = { enforce?: "pre" | "post"; oneOf?: (RuleSetRule | Falsy)[]; rules?: (RuleSetRule | Falsy)[]; + extractSourceMap?: boolean; }; // @public diff --git a/packages/rspack/src/config/adapter.ts b/packages/rspack/src/config/adapter.ts index 5bc760c286a8..a91bd05c7232 100644 --- a/packages/rspack/src/config/adapter.ts +++ b/packages/rspack/src/config/adapter.ts @@ -368,7 +368,8 @@ const getRawModuleRule = ( ) ) : undefined, - enforce: rule.enforce + enforce: rule.enforce, + extractSourceMap: rule.extractSourceMap }; // Function calls may contain side-effects when interoperating with single-threaded environment. diff --git a/packages/rspack/src/config/types.ts b/packages/rspack/src/config/types.ts index 191cc3dc5049..0d38fc430011 100644 --- a/packages/rspack/src/config/types.ts +++ b/packages/rspack/src/config/types.ts @@ -939,6 +939,9 @@ export type RuleSetRule = { /** A kind of Nested Rule, an array of Rules that is also used when the parent Rule matches. */ rules?: (RuleSetRule | Falsy)[]; + + /** Whether to extract source maps from the module. */ + extractSourceMap?: boolean; }; /** A list of rules. */ diff --git a/packages/rspack/src/schema/config.ts b/packages/rspack/src/schema/config.ts index 85a38d46be4b..7dfb80eaf900 100644 --- a/packages/rspack/src/schema/config.ts +++ b/packages/rspack/src/schema/config.ts @@ -569,7 +569,8 @@ export const getRspackOptionsSchema = memoize(() => { generator: z.record(z.string(), z.any()), resolve: resolveOptions, sideEffects: z.boolean(), - enforce: z.literal("pre").or(z.literal("post")) + enforce: z.literal("pre").or(z.literal("post")), + extractSourceMap: z.boolean() }) .partial() satisfies z.ZodType; diff --git a/tests/rspack-test/__snapshots__/Config.test.js.snap b/tests/rspack-test/__snapshots__/Config.test.js.snap new file mode 100644 index 000000000000..5f7789c1ec35 --- /dev/null +++ b/tests/rspack-test/__snapshots__/Config.test.js.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`config configCases/source-map/extract-source-map should pass 1`] = ` +Array [ + webpack:///./external-source-map.txt, + webpack:///./extract2.js, +] +`; + +exports[`config configCases/source-map/extract-source-map should pass 2`] = ` +Array [ + webpack:///./external-source-map.txt, + webpack:///./extract3.js, +] +`; + +exports[`config configCases/source-map/extract-source-map should pass 3`] = ` +Array [ + webpack:///./external-source-map.txt, + webpack:///./extract3.js, +] +`; + +exports[`config configCases/source-map/extract-source-map-css should pass 1`] = ` +Array [ + /*!*****************!*\\ + !*** ./app.css ***! + \\*****************/ +* { + box-sizing: border-box; } + + .row { + display: flex; + margin-right: -15px; + margin-left: -15px; } + + .col-inner { + display: flex; + align-items: center; + justify-content: center; + color: #fff; + height: 50px; + background: coral; } + + .col-s3 { + flex: 0 1 25%; + padding: 0 15px; } + + + + +/*# sourceMappingURL=bundle0.css.map*/, + /*!*****************!*\\ + !*** ./app.css ***! + \\*****************/ +* { + box-sizing: border-box; } + + .row { + display: flex; + margin-right: -15px; + margin-left: -15px; } + + .col-inner { + display: flex; + align-items: center; + justify-content: center; + color: #fff; + height: 50px; + background: coral; } + + .col-s3 { + flex: 0 1 25%; + padding: 0 15px; } + + + + +/*# sourceMappingURL=bundle0.css.map*/, +] +`; diff --git a/tests/rspack-test/configCases/source-map/extract-source-map-css/app.css b/tests/rspack-test/configCases/source-map/extract-source-map-css/app.css new file mode 100644 index 000000000000..3ed2e44f3f83 --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map-css/app.css @@ -0,0 +1,22 @@ +* { + box-sizing: border-box; } + + .row { + display: flex; + margin-right: -15px; + margin-left: -15px; } + + .col-inner { + display: flex; + align-items: center; + justify-content: center; + color: #fff; + height: 50px; + background: coral; } + + .col-s3 { + flex: 0 1 25%; + padding: 0 15px; } + + + /*# sourceMappingURL=app.css.map*/ \ No newline at end of file diff --git a/tests/rspack-test/configCases/source-map/extract-source-map-css/app.css.map b/tests/rspack-test/configCases/source-map/extract-source-map-css/app.css.map new file mode 100644 index 000000000000..70f9234afa0e --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map-css/app.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///./src/app/app.scss"],"names":[],"mappings":"AAAA;EACE,sBAAsB;;AAGxB;EACE,aAAa;EACb,mBAAmB;EACnB,kBAAkB;;AAIlB;EACE,aAAa;EACb,mBAAmB;EACnB,uBAAuB;EACvB,WAAW;EACX,YAAY;EACZ,iBAAiB;;AAInB;EACE,aAAa;EACb,eAAe","file":"app.css","sourcesContent":["* {\n box-sizing: border-box;\n}\n\n.row {\n display: flex;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.col {\n &-inner {\n display: flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n height: 50px;\n background: coral;\n //background: red;\n }\n\n &-s3 {\n flex: 0 1 25%;\n padding: 0 15px;\n }\n}\n"],"sourceRoot":""} \ No newline at end of file diff --git a/tests/rspack-test/configCases/source-map/extract-source-map-css/index.js b/tests/rspack-test/configCases/source-map/extract-source-map-css/index.js new file mode 100644 index 000000000000..ca49230c1620 --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map-css/index.js @@ -0,0 +1,14 @@ +import "./app.css"; + +it("should compile", done => { + const links = document.getElementsByTagName("link"); + const css = []; + + // Skip first because import it by default + for (const link of links.slice(1)) { + css.push(link.sheet.css); + } + + expect(css).toMatchSnapshot(); + done(); +}); diff --git a/tests/rspack-test/configCases/source-map/extract-source-map-css/test.config.js b/tests/rspack-test/configCases/source-map/extract-source-map-css/test.config.js new file mode 100644 index 000000000000..eaabc0c0c551 --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map-css/test.config.js @@ -0,0 +1,10 @@ +"use strict"; + +module.exports = { + moduleScope(scope) { + const link = scope.window.document.createElement("link"); + link.rel = "stylesheet"; + link.href = "bundle0.css"; + scope.window.document.head.appendChild(link); + } +}; diff --git a/tests/rspack-test/configCases/source-map/extract-source-map-css/webpack.config.js b/tests/rspack-test/configCases/source-map/extract-source-map-css/webpack.config.js new file mode 100644 index 000000000000..25941bf174d9 --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map-css/webpack.config.js @@ -0,0 +1,20 @@ +"use strict"; + +/** @type {import("../../../../").Configuration} */ +module.exports = { + target: "web", + mode: "development", + devtool: "source-map", + module: { + rules: [ + { + test: /\.css$/i, + type: "css", + extractSourceMap: true + } + ] + }, + experiments: { + css: true + } +}; diff --git a/tests/rspack-test/configCases/source-map/extract-source-map/extract1.js b/tests/rspack-test/configCases/source-map/extract-source-map/extract1.js new file mode 100644 index 000000000000..23d27bf61b16 --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map/extract1.js @@ -0,0 +1,14 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +require("./test1"); +require("./no-source-map") + +it("should extract source map - 1", () => { + const fileData = fs.readFileSync(path.resolve(__dirname, "bundle1.js.map")).toString("utf-8"); + const { sources } = JSON.parse(fileData); + expect(sources).toMatchSnapshot(); + expect(1).toBe(1) +}); diff --git a/tests/rspack-test/configCases/source-map/extract-source-map/extract2.js b/tests/rspack-test/configCases/source-map/extract-source-map/extract2.js new file mode 100644 index 000000000000..48efe1b5ba6b --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map/extract2.js @@ -0,0 +1,12 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +require("./test2"); + +it("should extract source map - 2", () => { + const fileData = fs.readFileSync(path.resolve(__dirname, "bundle2.js.map")).toString("utf-8"); + const { sources } = JSON.parse(fileData); + expect(sources).toMatchSnapshot(); +}); diff --git a/tests/rspack-test/configCases/source-map/extract-source-map/extract3.js b/tests/rspack-test/configCases/source-map/extract-source-map/extract3.js new file mode 100644 index 000000000000..60fcac2eaaff --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map/extract3.js @@ -0,0 +1,12 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +require("./test3"); + +it("should extract source map - 3", () => { + const fileData = fs.readFileSync(path.resolve(__dirname, "bundle2.js.map")).toString("utf-8"); + const { sources } = JSON.parse(fileData); + expect(sources).toMatchSnapshot(); +}); diff --git a/tests/rspack-test/configCases/source-map/extract-source-map/infrastructure-log.js b/tests/rspack-test/configCases/source-map/extract-source-map/infrastructure-log.js new file mode 100644 index 000000000000..f42d7b98c60a --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map/infrastructure-log.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = [ + /^Pack got invalid because of write to: Compilation\/modules.+no-source-map\.js$/ +]; diff --git a/tests/rspack-test/configCases/source-map/extract-source-map/no-source-map.js b/tests/rspack-test/configCases/source-map/extract-source-map/no-source-map.js new file mode 100644 index 000000000000..928ca443101d --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map/no-source-map.js @@ -0,0 +1,2 @@ +const a = 1; +//#sourceMappingURL=no-source-map.map diff --git a/tests/rspack-test/configCases/source-map/extract-source-map/remove-comment.js b/tests/rspack-test/configCases/source-map/extract-source-map/remove-comment.js new file mode 100644 index 000000000000..722127255480 --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map/remove-comment.js @@ -0,0 +1,11 @@ +"use strict"; + +const fs = require("fs"); + +require("./test1"); + +it("should remove sourceMap comment", () => { + expect( + fs.readFileSync(__filename).toString("utf-8") + ).not.toMatch(/\/\/\s*@\s*sourceMappingURL/); +}); diff --git a/tests/rspack-test/configCases/source-map/extract-source-map/test1.js b/tests/rspack-test/configCases/source-map/extract-source-map/test1.js new file mode 100644 index 000000000000..3f444aabc406 --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map/test1.js @@ -0,0 +1,3 @@ +const a = 1; +// @ sourceMappingURL = data:application/source-map;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2hhcnNldC1pbmxpbmUtc291cmNlLW1hcC5qcyIsInNvdXJjZXMiOlsiY2hhcnNldC1pbmxpbmUtc291cmNlLW1hcC50eHQiXSwic291cmNlc0NvbnRlbnQiOlsid2l0aCBTb3VyY2VNYXAiXSwibWFwcGluZ3MiOiJBQUFBIn0= +// comment diff --git a/tests/rspack-test/configCases/source-map/extract-source-map/test2.js b/tests/rspack-test/configCases/source-map/extract-source-map/test2.js new file mode 100644 index 000000000000..bf3d6993857a --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map/test2.js @@ -0,0 +1,3 @@ +const a = 1; +// comment +//#sourceMappingURL=test2.map diff --git a/tests/rspack-test/configCases/source-map/extract-source-map/test2.map b/tests/rspack-test/configCases/source-map/extract-source-map/test2.map new file mode 100644 index 000000000000..b68b9aa1a61e --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map/test2.map @@ -0,0 +1 @@ +{"version":3,"file":"test2.js","sources":["external-source-map.txt"],"sourcesContent":["with SourceMap"],"mappings":"AAAA"} diff --git a/tests/rspack-test/configCases/source-map/extract-source-map/test3.js b/tests/rspack-test/configCases/source-map/extract-source-map/test3.js new file mode 100644 index 000000000000..fa35249a038a --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map/test3.js @@ -0,0 +1,3 @@ +const a = 1; +// comment +//#sourceMappingURL=/test2.map diff --git a/tests/rspack-test/configCases/source-map/extract-source-map/warnings.js b/tests/rspack-test/configCases/source-map/extract-source-map/warnings.js new file mode 100644 index 000000000000..d882cdc5bdd6 --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map/warnings.js @@ -0,0 +1,3 @@ +"use strict"; + +module.exports = [[/Failed to parse source map/]]; diff --git a/tests/rspack-test/configCases/source-map/extract-source-map/webpack.config.js b/tests/rspack-test/configCases/source-map/extract-source-map/webpack.config.js new file mode 100644 index 000000000000..42b4fc485ade --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map/webpack.config.js @@ -0,0 +1,52 @@ +"use strict"; + +/** @type {import("../../../../").Configuration[]} */ +module.exports = [ + { + target: "node", + entry: "./extract1", + devtool: "source-map", + module: { + rules: [ + { + extractSourceMap: true + } + ] + } + }, + { + target: "node", + entry: "./extract2", + devtool: "source-map", + module: { + rules: [ + { + extractSourceMap: true + } + ] + } + }, + { + target: "node", + entry: "./extract3", + devtool: "source-map", + module: { + rules: [ + { + extractSourceMap: true + } + ] + } + }, + { + entry: "./remove-comment", + devtool: "source-map", + module: { + rules: [ + { + extractSourceMap: true + } + ] + } + } +]; diff --git a/tests/rspack-test/configCases/source-map/extract-source-map2/a.js b/tests/rspack-test/configCases/source-map/extract-source-map2/a.js new file mode 100644 index 000000000000..37f2d9b56a0b --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map2/a.js @@ -0,0 +1,3 @@ +const a = 1; +// comment +//#sourceMappingURL=/a.map diff --git a/tests/rspack-test/configCases/source-map/extract-source-map2/a.map b/tests/rspack-test/configCases/source-map/extract-source-map2/a.map new file mode 100644 index 000000000000..08277368a163 --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map2/a.map @@ -0,0 +1 @@ +{"version":3,"file":"test2.js","sources":["external-source-map.txt"],"sourcesContent":[],"mappings":"AAAA"} diff --git a/tests/rspack-test/configCases/source-map/extract-source-map2/babel-loader.js b/tests/rspack-test/configCases/source-map/extract-source-map2/babel-loader.js new file mode 100644 index 000000000000..87ef85dfe9e3 --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map2/babel-loader.js @@ -0,0 +1,27 @@ +/** @typedef {import("webpack").LoaderContext} LoaderContext */ + +const assert = require("assert") + +/** + * @this {LoaderContext} + * @param {string} source The source code to process + * @param {import("webpack-sources").RawSourceMap} sourceMap The source map to process + * @returns {void} + */ +module.exports = function(source, sourceMap) { + const callback = this.async(); + const resourcePath = this.resourcePath; + + if (resourcePath.endsWith("a.js")) { + assert(sourceMap && sourceMap.version && sourceMap.mappings, "should have source map when extract source map"); + } + + try { + const withoutConst = source.replace(/const/g, "var"); + + callback(null, withoutConst, sourceMap); + } catch (err) { + callback(/** @type {Error} */ (err)); + } +}; + \ No newline at end of file diff --git a/tests/rspack-test/configCases/source-map/extract-source-map2/external-source-map.txt b/tests/rspack-test/configCases/source-map/extract-source-map2/external-source-map.txt new file mode 100644 index 000000000000..5a18cd2fbf65 --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map2/external-source-map.txt @@ -0,0 +1 @@ +source diff --git a/tests/rspack-test/configCases/source-map/extract-source-map2/index.js b/tests/rspack-test/configCases/source-map/extract-source-map2/index.js new file mode 100644 index 000000000000..f7bd5806b69d --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map2/index.js @@ -0,0 +1,11 @@ +const fs = require("fs"); +const path = require("path"); + +require("./a"); + +it("should extract source map", () => { + const fileData = fs.readFileSync(path.resolve(__dirname, "bundle0.js.map")).toString("utf-8"); + const { sources, sourcesContent } = JSON.parse(fileData); + expect(sources.includes("webpack:///./external-source-map.txt")).toBe(true); + expect(sourcesContent.map(s => s.trim())).toContain("source"); +}); diff --git a/tests/rspack-test/configCases/source-map/extract-source-map2/test.filter.js b/tests/rspack-test/configCases/source-map/extract-source-map2/test.filter.js new file mode 100644 index 000000000000..54399a5a2a72 --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map2/test.filter.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = function filter() { + return process.platform !== "win32"; +}; diff --git a/tests/rspack-test/configCases/source-map/extract-source-map2/webpack.config.js b/tests/rspack-test/configCases/source-map/extract-source-map2/webpack.config.js new file mode 100644 index 000000000000..0f76860e83ad --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map2/webpack.config.js @@ -0,0 +1,17 @@ +"use strict"; + +/** @type {import("../../../../").Configuration} */ +module.exports = { + target: "node", + entry: "./index", + devtool: "source-map", + module: { + rules: [ + { + test: /\.js$/, + extractSourceMap: true, + loader: require.resolve("./babel-loader") + } + ] + } +};