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 01/12] 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") + } + ] + } +}; From 40bd02a82dcca4a83cbec4ae05f1c9e266cb367e Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Sat, 11 Oct 2025 16:33:04 +0800 Subject: [PATCH 02/12] fix: should not error on module without scheme --- crates/rspack_loader_runner/src/runner.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/rspack_loader_runner/src/runner.rs b/crates/rspack_loader_runner/src/runner.rs index c9f37464affe..a9c424bf34c5 100644 --- a/crates/rspack_loader_runner/src/runner.rs +++ b/crates/rspack_loader_runner/src/runner.rs @@ -46,6 +46,11 @@ async fn process_resource( let resource_data = &loader_context.resource_data; let scheme = resource_data.get_scheme(); + + if scheme.is_none() { + return Ok(()); + } + let resource = resource_data.resource(); Err(error!( r#"Reading from "{resource}" is not handled by plugins (Unhandled scheme). From 277c47315626af50da8dadb0cb466b0f344643f6 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Sat, 11 Oct 2025 18:20:34 +0800 Subject: [PATCH 03/12] feat: implement `node_is_absolute()` --- crates/rspack_util/src/node_path.rs | 126 ++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/crates/rspack_util/src/node_path.rs b/crates/rspack_util/src/node_path.rs index 17c97fc93177..117fd0850669 100644 --- a/crates/rspack_util/src/node_path.rs +++ b/crates/rspack_util/src/node_path.rs @@ -114,6 +114,12 @@ pub trait NodePath { fn node_normalize_posix(&self) -> Utf8PathBuf; fn node_normalize_win32(&self) -> Utf8PathBuf; + + fn node_is_absolute(&self) -> bool; + + fn node_is_absolute_posix(&self) -> bool; + + fn node_is_absolute_win32(&self) -> bool; } impl NodePath for Utf8Path { @@ -160,6 +166,21 @@ impl NodePath for Utf8Path { fn node_normalize_win32(&self) -> Utf8PathBuf { self.to_path_buf().node_normalize_win32() } + + #[inline] + fn node_is_absolute(&self) -> bool { + self.to_path_buf().node_is_absolute() + } + + #[inline] + fn node_is_absolute_posix(&self) -> bool { + self.to_path_buf().node_is_absolute_posix() + } + + #[inline] + fn node_is_absolute_win32(&self) -> bool { + self.to_path_buf().node_is_absolute_win32() + } } impl NodePath for Utf8PathBuf { @@ -471,6 +492,45 @@ impl NodePath for Utf8PathBuf { _ => Utf8PathBuf::from(tail), } } + + #[inline] + fn node_is_absolute(&self) -> bool { + if cfg!(windows) { + self.node_is_absolute_win32() + } else { + self.node_is_absolute_posix() + } + } + + fn node_is_absolute_posix(&self) -> bool { + // POSIX absolute path starts with '/' + self.as_str().starts_with('/') + } + + fn node_is_absolute_win32(&self) -> bool { + let path = self.as_str(); + + // Windows absolute path logic matching Node.js: + // 1. UNC path: \\server\share + if path.len() >= 2 && path.starts_with("\\") { + return true; + } + + // 2. Drive letter path: C:\path or C:/path + if path.len() >= 3 { + let bytes = path.as_bytes(); + if is_windows_device_root(&bytes[0]) && bytes[1] == b':' && is_path_separator(&bytes[2]) { + return true; + } + } + + // 3. Device path: \\?\ or \\.\ + if path.len() >= 4 && (path.starts_with("\\\\?\\") || path.starts_with("\\\\.\\")) { + return true; + } + + false + } } #[cfg(test)] @@ -1005,4 +1065,70 @@ mod test { assert_eq!(path.node_normalize_win32().as_str(), *expected); }); } + + #[test] + fn test_node_is_absolute_posix() { + // POSIX absolute paths + assert!(Utf8Path::new("/").node_is_absolute_posix()); + assert!(Utf8Path::new("/foo").node_is_absolute_posix()); + assert!(Utf8Path::new("/foo/bar").node_is_absolute_posix()); + assert!(Utf8Path::new("/foo/bar/").node_is_absolute_posix()); + + // POSIX relative paths + assert!(!Utf8Path::new("foo").node_is_absolute_posix()); + assert!(!Utf8Path::new("foo/bar").node_is_absolute_posix()); + assert!(!Utf8Path::new("./foo").node_is_absolute_posix()); + assert!(!Utf8Path::new("../foo").node_is_absolute_posix()); + assert!(!Utf8Path::new("").node_is_absolute_posix()); + + // Windows-style paths should return false in POSIX mode + assert!(!Utf8Path::new("C:\\foo").node_is_absolute_posix()); + assert!(!Utf8Path::new("C:/foo").node_is_absolute_posix()); + assert!(!Utf8Path::new("\\\\server\\share").node_is_absolute_posix()); + } + + #[test] + fn test_node_is_absolute_win32() { + // Windows absolute paths + assert!(Utf8Path::new("C:\\foo").node_is_absolute_win32()); + assert!(Utf8Path::new("C:/foo").node_is_absolute_win32()); + assert!(Utf8Path::new("C:\\").node_is_absolute_win32()); + assert!(Utf8Path::new("C:/").node_is_absolute_win32()); + assert!(Utf8Path::new("\\\\server\\share").node_is_absolute_win32()); + assert!(Utf8Path::new("\\\\server\\share\\foo").node_is_absolute_win32()); + assert!(Utf8Path::new("\\\\?\\C:\\foo").node_is_absolute_win32()); + assert!(Utf8Path::new("\\\\.\\C:\\foo").node_is_absolute_win32()); + assert!(Utf8Path::new("\\\\server").node_is_absolute_win32()); + assert!(Utf8Path::new("\\foo").node_is_absolute_win32()); + + // Windows relative paths + assert!(!Utf8Path::new("foo").node_is_absolute_win32()); + assert!(!Utf8Path::new("foo\\bar").node_is_absolute_win32()); + assert!(!Utf8Path::new("./foo").node_is_absolute_win32()); + assert!(!Utf8Path::new("../foo").node_is_absolute_win32()); + assert!(!Utf8Path::new("C:foo").node_is_absolute_win32()); // Drive-relative + assert!(!Utf8Path::new("C:").node_is_absolute_win32()); // Drive-relative + assert!(!Utf8Path::new("").node_is_absolute_win32()); + + // POSIX-style paths should return false in Windows mode + assert!(!Utf8Path::new("/foo").node_is_absolute_win32()); + assert!(!Utf8Path::new("/").node_is_absolute_win32()); + } + + #[test] + fn test_node_is_absolute_cross_platform() { + // Test the cross-platform version + if cfg!(windows) { + // On Windows, should use win32 logic + assert!(Utf8Path::new("C:\\foo").node_is_absolute()); + assert!(Utf8Path::new("\\\\server\\share").node_is_absolute()); + assert!(!Utf8Path::new("/foo").node_is_absolute()); + assert!(!Utf8Path::new("foo").node_is_absolute()); + } else { + // On POSIX systems, should use posix logic + assert!(Utf8Path::new("/foo").node_is_absolute()); + assert!(!Utf8Path::new("C:\\foo").node_is_absolute()); + assert!(!Utf8Path::new("foo").node_is_absolute()); + } + } } From 4624c50498a477ffe6f291d48b5b9dba89e33772 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Sat, 11 Oct 2025 18:20:55 +0800 Subject: [PATCH 04/12] fix: use `Utf8Path` and `node_is_absolute` --- .../src/utils/extract_source_map.rs | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/crates/rspack_core/src/utils/extract_source_map.rs b/crates/rspack_core/src/utils/extract_source_map.rs index efd92cf85732..92b896023913 100644 --- a/crates/rspack_core/src/utils/extract_source_map.rs +++ b/crates/rspack_core/src/utils/extract_source_map.rs @@ -3,21 +3,16 @@ Author Natsu @xiaoxiaojx */ -use std::{ - borrow::Cow, - collections::HashSet, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::{borrow::Cow, collections::HashSet, 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_paths::{AssertUtf8, Utf8Path, Utf8PathBuf}; use rspack_sources::SourceMap; -use rspack_util::base64; +use rspack_util::{base64, node_path::NodePath}; /// Source map extractor result #[derive(Debug, Clone)] @@ -76,7 +71,11 @@ pub fn get_source_mapping_url(code: &str) -> SourceMappingURL { /// Check if value is a URL fn is_url(value: &str) -> bool { - VALID_PROTOCOL_PATTERN.is_match(value) && !PathBuf::from(value).is_absolute() + VALID_PROTOCOL_PATTERN.is_match(value) && !Utf8Path::new(value).node_is_absolute_win32() +} + +fn is_absolute(path: &Utf8Path) -> bool { + path.node_is_absolute_posix() || path.node_is_absolute_win32() } /// Decode data URI @@ -112,14 +111,14 @@ fn fetch_from_data_url(source_url: &str) -> Result { } } -/// Get absolute path for source file -fn get_absolute_path(context: &Path, request: &str, source_root: Option<&str>) -> PathBuf { +/// Get absolute path for source file using Node.js logic +fn get_absolute_path(context: &Utf8Path, request: &str, source_root: Option<&str>) -> Utf8PathBuf { if let Some(source_root) = source_root { - let source_root_path = Path::new(source_root); - if source_root_path.is_absolute() { + let source_root_path = Utf8Path::new(source_root); + if is_absolute(source_root_path) { return source_root_path.join(request); } - return context.join(source_root_path).join(request); + return context.join(source_root).join(request); } context.join(request) @@ -170,7 +169,7 @@ async fn fetch_paths_from_filesystem( /// Fetch source content from URL async fn fetch_from_url( fs: &Arc, - context: &Path, + context: &Utf8Path, url: &str, source_root: Option<&str>, skip_reading: bool, @@ -210,9 +209,8 @@ async fn fetch_from_url( } // 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 is_absolute(Utf8Path::new(url)) { + let source_url = url.to_string(); if !skip_reading { let mut possible_requests = Vec::with_capacity(2); @@ -220,7 +218,7 @@ async fn fetch_from_url( 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()); + possible_requests.push(absolute_path.to_string()); } return fetch_paths_from_filesystem(fs, &possible_requests, String::new()).await; @@ -231,7 +229,7 @@ async fn fetch_from_url( // 4. Relative path let source_url = get_absolute_path(context, url, source_root); - let source_url_str = source_url.to_string_lossy().to_string(); + let source_url_str = source_url.to_string(); if !skip_reading { let (_, content) = fetch_from_filesystem(fs, &source_url_str).await?; @@ -260,7 +258,7 @@ pub async fn extract_source_map( }); } - let base_context = Path::new(resource_path) + let base_context = Utf8Path::new(resource_path) .parent() .ok_or_else(|| "Invalid resource path".to_string())?; @@ -289,7 +287,7 @@ pub async fn extract_source_map( .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) + Utf8Path::new(&source_url).parent().unwrap_or(base_context) } else { base_context }; From 76af6f050d9a71d5c889f45e4527aa3fd6fb21f9 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Sat, 11 Oct 2025 18:23:05 +0800 Subject: [PATCH 05/12] docs: add `Rule.extractSourceMap` --- website/docs/en/config/module.mdx | 22 ++++++++++++++++++++++ website/docs/zh/config/module.mdx | 22 ++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/website/docs/en/config/module.mdx b/website/docs/en/config/module.mdx index 168267c65a28..2d514e3e8eb1 100644 --- a/website/docs/en/config/module.mdx +++ b/website/docs/en/config/module.mdx @@ -2680,3 +2680,25 @@ export default { }, }; ``` + +### Rule.extractSourceMap + + + +- **Type**: `boolean` +- **Default:** `false` + +Extracts existing source map data from files (from their `//# sourceMappingURL` comment), useful for preserving the source maps of third-party libraries. + +```js title="rspack.config.mjs" +export default { + module: { + rules: [ + { + test: /\.m?js$/, + extractSourceMap: true, + }, + ], + }, +}; +``` diff --git a/website/docs/zh/config/module.mdx b/website/docs/zh/config/module.mdx index 19175f658fbd..b073ed92ff22 100644 --- a/website/docs/zh/config/module.mdx +++ b/website/docs/zh/config/module.mdx @@ -2677,3 +2677,25 @@ export default { }, }; ``` + +### Rule.extractSourceMap + + + +- **类型**: `boolean` +- **默认值:** `false` + +从文件中的 `//# sourceMappingURL` 注释中提取现有的 source-map 数据,对于保留第三方库的 source-map 非常有用。 + +```js title="rspack.config.mjs" +export default { + module: { + rules: [ + { + test: /\.m?js$/, + extractSourceMap: true, + }, + ], + }, +}; +``` From 391b5ae4b69d3f1918c99ca8fd432bbe07be57ba Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Sat, 11 Oct 2025 23:24:56 +0800 Subject: [PATCH 06/12] test: fix snapshot test --- .../__snapshots__/Config.test.js.snap | 81 ------------------- .../extract-source-map-css/index.js | 6 +- .../source-map/extract-source-map/extract1.js | 10 +-- .../source-map/extract-source-map/extract2.js | 7 +- .../source-map/extract-source-map/extract3.js | 7 +- 5 files changed, 18 insertions(+), 93 deletions(-) delete mode 100644 tests/rspack-test/__snapshots__/Config.test.js.snap diff --git a/tests/rspack-test/__snapshots__/Config.test.js.snap b/tests/rspack-test/__snapshots__/Config.test.js.snap deleted file mode 100644 index 5f7789c1ec35..000000000000 --- a/tests/rspack-test/__snapshots__/Config.test.js.snap +++ /dev/null @@ -1,81 +0,0 @@ -// 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/index.js b/tests/rspack-test/configCases/source-map/extract-source-map-css/index.js index ca49230c1620..069a4d462e80 100644 --- 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 @@ -2,13 +2,13 @@ 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(link.sheet.css).toContain(".row"); + expect(link.sheet.css).toContain(".col-inner"); + expect(link.sheet.css).toContain("/*# sourceMappingURL=bundle0.css.map*/"); } - expect(css).toMatchSnapshot(); done(); }); 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 index 23d27bf61b16..58ddc1f9410f 100644 --- a/tests/rspack-test/configCases/source-map/extract-source-map/extract1.js +++ b/tests/rspack-test/configCases/source-map/extract-source-map/extract1.js @@ -1,14 +1,14 @@ "use strict"; const fs = require("fs"); -const path = require("path"); require("./test1"); -require("./no-source-map") +require("./no-source-map"); it("should extract source map - 1", () => { - const fileData = fs.readFileSync(path.resolve(__dirname, "bundle1.js.map")).toString("utf-8"); + const fileData = fs.readFileSync(__filename + ".map").toString("utf-8"); const { sources } = JSON.parse(fileData); - expect(sources).toMatchSnapshot(); - expect(1).toBe(1) + expect(sources).toContain("webpack:///./extract1.js"); + expect(sources).toContain("webpack:///./charset-inline-source-map.txt"); + expect(sources).toContain("webpack:///./no-source-map.js"); }); 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 index 48efe1b5ba6b..05218b771f1b 100644 --- a/tests/rspack-test/configCases/source-map/extract-source-map/extract2.js +++ b/tests/rspack-test/configCases/source-map/extract-source-map/extract2.js @@ -6,7 +6,10 @@ 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 fileData = fs + .readFileSync(path.resolve(__dirname, "bundle1.js.map")) + .toString("utf-8"); const { sources } = JSON.parse(fileData); - expect(sources).toMatchSnapshot(); + expect(sources).toContain("webpack:///./external-source-map.txt"); + expect(sources).toContain("webpack:///./extract2.js"); }); 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 index 60fcac2eaaff..c4a123ccf39b 100644 --- a/tests/rspack-test/configCases/source-map/extract-source-map/extract3.js +++ b/tests/rspack-test/configCases/source-map/extract-source-map/extract3.js @@ -6,7 +6,10 @@ 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 fileData = fs + .readFileSync(path.resolve(__dirname, "bundle2.js.map")) + .toString("utf-8"); const { sources } = JSON.parse(fileData); - expect(sources).toMatchSnapshot(); + expect(sources).toContain("webpack:///./external-source-map.txt"); + expect(sources).toContain("webpack:///./extract3.js"); }); From e1c03032588c8c10a5c321681213286b0fe5e91c Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Sun, 12 Oct 2025 19:09:14 +0800 Subject: [PATCH 07/12] test: add virtual modules --- .../index.js | 57 +++++++++++++++++++ .../rspack.config.js | 35 ++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map-virtual-modules/index.js create mode 100644 tests/rspack-test/configCases/source-map/extract-source-map-virtual-modules/rspack.config.js diff --git a/tests/rspack-test/configCases/source-map/extract-source-map-virtual-modules/index.js b/tests/rspack-test/configCases/source-map/extract-source-map-virtual-modules/index.js new file mode 100644 index 000000000000..11c7ea0407b4 --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map-virtual-modules/index.js @@ -0,0 +1,57 @@ +const fs = require("fs"); + +require("./virtual-module-with-sourcemap"); +require("./virtual-module-without-sourcemap"); +require("./lib/components/virtual-component"); + +it("should extract source map from virtual modules", () => { + const fileData = fs.readFileSync(__filename + ".map").toString("utf-8"); + const { sources } = JSON.parse(fileData); + + // Should include main file and virtual modules without inline sourcemap + expect(sources).toContain("webpack:///./index.js"); + expect(sources).toContain("webpack:///./virtual-module-without-sourcemap.js"); + expect(sources).toContain("webpack:///./virtual-test.txt"); + expect(sources).toContain("webpack:///./src/components/virtual-component.js"); + + // Should not include virtual modules with inline sourcemap as they're filtered by extractSourceMap + expect(sources).not.toContain("webpack:///./virtual-module-with-sourcemap.js"); +}); + +it("should handle virtual modules with extractSourceMap correctly", () => { + const fileData = fs.readFileSync(__filename + ".map").toString("utf-8"); + const { sourcesContent } = JSON.parse(fileData); + + // Verify virtual module content is processed correctly + const hasVirtualModuleContent = sourcesContent.some(content => + content && content.includes("virtual module content") + ); + expect(hasVirtualModuleContent).toBe(true); + + // Verify extractSourceMap handles virtual modules correctly + // Modules with inline sourcemap should be filtered, those without should be kept + const hasModuleWithoutSourceMap = sourcesContent.some(content => + content && content.includes("const b = 2;") + ); + expect(hasModuleWithoutSourceMap).toBe(true); +}); + +it("should handle virtual modules with different source map references", () => { + const fileData = fs.readFileSync(__filename + ".map").toString("utf-8"); + const { sourcesContent } = JSON.parse(fileData); + + // Verify virtual modules with external .js.map references are processed correctly + const hasMapReferenceContent = sourcesContent.some(content => + content && content.includes("module with map reference") + ); + expect(hasMapReferenceContent).toBe(true); + + // Verify virtual modules with inline data URL source maps are processed correctly + const hasAnotherModuleContent = sourcesContent.some(content => + content && content.includes("another module") + ); + expect(hasAnotherModuleContent).toBe(true); + + // Note: extractSourceMap extracts inline source maps but may not remove all sourceMappingURL comments + // The key behavior is that modules with inline source maps are filtered from the source map sources +}); \ No newline at end of file diff --git a/tests/rspack-test/configCases/source-map/extract-source-map-virtual-modules/rspack.config.js b/tests/rspack-test/configCases/source-map/extract-source-map-virtual-modules/rspack.config.js new file mode 100644 index 000000000000..8b85cfa37472 --- /dev/null +++ b/tests/rspack-test/configCases/source-map/extract-source-map-virtual-modules/rspack.config.js @@ -0,0 +1,35 @@ +const { + experiments: { VirtualModulesPlugin } +} = require("@rspack/core"); + +/** @type {import("@rspack/core").Configuration} */ +module.exports = { + target: "node", + entry: { + main: "./index.js" + }, + devtool: "source-map", + module: { + rules: [ + { + extractSourceMap: true + } + ] + }, + plugins: [ + new VirtualModulesPlugin({ + // Regular virtual modules + "virtual-module-with-sourcemap.js": `const a = 1; +// @ sourceMappingURL = data:application/source-map;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmlydHVhbC1tb2R1bGUtd2l0aC1zb3VyY2VtYXAuanMiLCJzb3VyY2VzIjpbInZpcnR1YWwtdGVzdC50eHQiXSwic291cmNlc0NvbnRlbnQiOlsidmlydHVhbCBtb2R1bGUgd2l0aCBzb3VyY2VtYXAiXSwibWFwcGluZ3MiOiJBQUFBIn0= +// comment`, + "virtual-module-without-sourcemap.js": `const b = 2; +// regular comment without source map`, + "virtual-test.txt": "virtual module content", + + // Virtual modules in subdirectories + "lib/components/virtual-component.js": `export const Component = "virtual component"; +// @sourceMappingURL=virtual-component.js.map`, + "lib/components/virtual-component.js.map": `{"version":3,"file":"lib/components/virtual-component.js","sources":["../../src/components/virtual-component.js"],"sourcesContent":["export const Component: string = 'virtual component'"],"mappings":"AAAA"}` + }) + ] +}; \ No newline at end of file From cf798f416e7a23c35549da166d65738675bb20b7 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Sun, 12 Oct 2025 19:09:30 +0800 Subject: [PATCH 08/12] fix: simplify content reading --- .../src/utils/extract_source_map.rs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/rspack_core/src/utils/extract_source_map.rs b/crates/rspack_core/src/utils/extract_source_map.rs index 92b896023913..5681181d3689 100644 --- a/crates/rspack_core/src/utils/extract_source_map.rs +++ b/crates/rspack_core/src/utils/extract_source_map.rs @@ -265,26 +265,26 @@ pub async fn extract_source_map( 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(")]}"); + let content = match source_content.as_deref() { + Some(c) => c.trim_start_matches(")]}"), + 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) + }, + }); + } + }; // 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 mut source_map = + SourceMap::from_json(content).map_err(|e| format!("Failed to parse source map: {e}"))?; let context = if !source_url.is_empty() { Utf8Path::new(&source_url).parent().unwrap_or(base_context) From 487dbfa8dc81faeb32d312cefc031c01dbea4528 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Mon, 13 Oct 2025 17:27:47 +0800 Subject: [PATCH 09/12] fix: should add file_dependencies --- .../rspack_core/src/loader/rspack_loader.rs | 20 +++++++++++++------ crates/rspack_loader_runner/src/plugin.rs | 3 ++- crates/rspack_loader_runner/src/runner.rs | 3 ++- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/crates/rspack_core/src/loader/rspack_loader.rs b/crates/rspack_core/src/loader/rspack_loader.rs index 1df22dd3dd88..8ddb99a1b03e 100644 --- a/crates/rspack_core/src/loader/rspack_loader.rs +++ b/crates/rspack_core/src/loader/rspack_loader.rs @@ -4,6 +4,7 @@ use rspack_error::{Diagnostic, Result}; use rspack_fs::ReadableFileSystem; use rspack_loader_runner::{Content, LoaderContext, LoaderRunnerPlugin, ResourceData}; use rspack_sources::SourceMap; +use rustc_hash::FxHashSet as HashSet; use crate::{RunnerContext, SharedPluginDriver, utils::extract_source_map}; @@ -34,7 +35,7 @@ impl LoaderRunnerPlugin for RspackLoaderRunnerPlugin { &self, resource_data: &ResourceData, fs: Arc, - ) -> Result)>> { + ) -> Result, HashSet)>> { // First try the plugin's read_resource hook let result = self .plugin_driver @@ -55,15 +56,21 @@ impl LoaderRunnerPlugin for RspackLoaderRunnerPlugin { match extract_result { Ok(extract_result) => { - // Return the content with source map extracted - // The source map will be available through the loader context + // Convert file dependencies to FxHashSet for consistency + let file_deps = extract_result + .file_dependencies + .map(|deps| deps.into_iter().collect::>()) + .unwrap_or_default(); + + // Return the content with source map extracted and file dependencies return Ok(Some(( Content::String(extract_result.source), extract_result.source_map, + file_deps, ))); } Err(e) => { - // If extraction fails, return original content + // If extraction fails, return original content with empty dependencies // Log the error as a warning self .plugin_driver @@ -71,11 +78,12 @@ impl LoaderRunnerPlugin for RspackLoaderRunnerPlugin { .lock() .expect("should get lock") .push(Diagnostic::warn("extractSourceMap".into(), e)); - return Ok(Some((content, None))); + return Ok(Some((content, None, HashSet::default()))); } } } - return Ok(Some((content, None))); + // Return original content with empty dependencies when extract_source_map is disabled + return Ok(Some((content, None, HashSet::default()))); } // If no plugin handled it, return None so the default logic can handle it diff --git a/crates/rspack_loader_runner/src/plugin.rs b/crates/rspack_loader_runner/src/plugin.rs index 447f162cbc41..bf538a31d4da 100644 --- a/crates/rspack_loader_runner/src/plugin.rs +++ b/crates/rspack_loader_runner/src/plugin.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use rspack_error::Result; use rspack_fs::ReadableFileSystem; use rspack_sources::SourceMap; +use rustc_hash::FxHashSet as HashSet; use crate::{ LoaderContext, @@ -33,5 +34,5 @@ pub trait LoaderRunnerPlugin: Send + Sync { &self, resource_data: &ResourceData, fs: Arc, - ) -> Result)>>; + ) -> Result, HashSet)>>; } diff --git a/crates/rspack_loader_runner/src/runner.rs b/crates/rspack_loader_runner/src/runner.rs index a9c424bf34c5..c1600afc613d 100644 --- a/crates/rspack_loader_runner/src/runner.rs +++ b/crates/rspack_loader_runner/src/runner.rs @@ -35,12 +35,13 @@ async fn process_resource( fs: Arc, ) -> Result<()> { if let Some(plugin) = &loader_context.plugin - && let Some((content, source_map)) = plugin + && let Some((content, source_map, file_dependencies)) = plugin .process_resource(&loader_context.resource_data, fs) .await? { loader_context.content = Some(content); loader_context.source_map = source_map; + loader_context.file_dependencies.extend(file_dependencies); return Ok(()); } From 68347b8bf9b36e9cc6aaaa601c29d131a4d5c35f Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Mon, 13 Oct 2025 17:48:41 +0800 Subject: [PATCH 10/12] fix: rust test --- crates/rspack_loader_runner/src/runner.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/rspack_loader_runner/src/runner.rs b/crates/rspack_loader_runner/src/runner.rs index c1600afc613d..eace35f71b15 100644 --- a/crates/rspack_loader_runner/src/runner.rs +++ b/crates/rspack_loader_runner/src/runner.rs @@ -252,6 +252,7 @@ mod test { use rspack_error::Result; use rspack_fs::{NativeFileSystem, ReadableFileSystem}; use rspack_sources::SourceMap; + use rustc_hash::FxHashSet as HashSet; use super::{Loader, LoaderContext, ResourceData, run_loaders}; use crate::{AdditionalData, content::Content, plugin::LoaderRunnerPlugin}; @@ -274,8 +275,8 @@ mod test { &self, _resource_data: &ResourceData, _fs: Arc, - ) -> Result)>> { - Ok(Some((Content::Buffer(vec![]), None))) + ) -> Result, HashSet)>> { + Ok(Some((Content::Buffer(vec![]), None, Default::default()))) } } From 28911414f853fb1259c95956d9bfb7561024034b Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:04:28 +0800 Subject: [PATCH 11/12] fix: trim `'` --- crates/rspack_core/src/utils/extract_source_map.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rspack_core/src/utils/extract_source_map.rs b/crates/rspack_core/src/utils/extract_source_map.rs index 5681181d3689..9ca78728def9 100644 --- a/crates/rspack_core/src/utils/extract_source_map.rs +++ b/crates/rspack_core/src/utils/extract_source_map.rs @@ -266,7 +266,7 @@ pub async fn extract_source_map( fetch_from_url(&fs, base_context, &source_mapping_url, None, false).await?; let content = match source_content.as_deref() { - Some(c) => c.trim_start_matches(")]}"), + Some(c) => c.trim_start_matches(")]}'"), None => { return Ok(ExtractSourceMapResult { source: input.to_string(), From 79b32510fbfa73e44456c5986b3f97c9a1124a93 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:39:29 +0800 Subject: [PATCH 12/12] fix: `node_is_absolute_win32` --- crates/rspack_util/src/node_path.rs | 112 ++++++++++------------------ 1 file changed, 38 insertions(+), 74 deletions(-) diff --git a/crates/rspack_util/src/node_path.rs b/crates/rspack_util/src/node_path.rs index 117fd0850669..91c4fa4f43f3 100644 --- a/crates/rspack_util/src/node_path.rs +++ b/crates/rspack_util/src/node_path.rs @@ -510,22 +510,22 @@ impl NodePath for Utf8PathBuf { fn node_is_absolute_win32(&self) -> bool { let path = self.as_str(); - // Windows absolute path logic matching Node.js: - // 1. UNC path: \\server\share - if path.len() >= 2 && path.starts_with("\\") { - return true; + if path.is_empty() { + return false; } - // 2. Drive letter path: C:\path or C:/path - if path.len() >= 3 { - let bytes = path.as_bytes(); - if is_windows_device_root(&bytes[0]) && bytes[1] == b':' && is_path_separator(&bytes[2]) { - return true; - } + let bytes = path.as_bytes(); + + // Mirrors Node.js `path.win32.isAbsolute`: first separator is absolute. + if is_path_separator(&bytes[0]) { + return true; } - // 3. Device path: \\?\ or \\.\ - if path.len() >= 4 && (path.starts_with("\\\\?\\") || path.starts_with("\\\\.\\")) { + if path.len() > 2 + && is_windows_device_root(&bytes[0]) + && bytes[1] == b':' + && is_path_separator(&bytes[2]) + { return true; } @@ -1066,69 +1066,33 @@ mod test { }); } + // Test cases from https://github.com/nodejs/node/blob/5cf3c3e24c7257a0c6192ed8ef71efec8ddac22b/test/parallel/test-path-isabsolute.js #[test] - fn test_node_is_absolute_posix() { - // POSIX absolute paths - assert!(Utf8Path::new("/").node_is_absolute_posix()); - assert!(Utf8Path::new("/foo").node_is_absolute_posix()); - assert!(Utf8Path::new("/foo/bar").node_is_absolute_posix()); - assert!(Utf8Path::new("/foo/bar/").node_is_absolute_posix()); - - // POSIX relative paths - assert!(!Utf8Path::new("foo").node_is_absolute_posix()); - assert!(!Utf8Path::new("foo/bar").node_is_absolute_posix()); - assert!(!Utf8Path::new("./foo").node_is_absolute_posix()); - assert!(!Utf8Path::new("../foo").node_is_absolute_posix()); - assert!(!Utf8Path::new("").node_is_absolute_posix()); - - // Windows-style paths should return false in POSIX mode - assert!(!Utf8Path::new("C:\\foo").node_is_absolute_posix()); - assert!(!Utf8Path::new("C:/foo").node_is_absolute_posix()); - assert!(!Utf8Path::new("\\\\server\\share").node_is_absolute_posix()); - } - - #[test] - fn test_node_is_absolute_win32() { - // Windows absolute paths - assert!(Utf8Path::new("C:\\foo").node_is_absolute_win32()); - assert!(Utf8Path::new("C:/foo").node_is_absolute_win32()); - assert!(Utf8Path::new("C:\\").node_is_absolute_win32()); - assert!(Utf8Path::new("C:/").node_is_absolute_win32()); - assert!(Utf8Path::new("\\\\server\\share").node_is_absolute_win32()); - assert!(Utf8Path::new("\\\\server\\share\\foo").node_is_absolute_win32()); - assert!(Utf8Path::new("\\\\?\\C:\\foo").node_is_absolute_win32()); - assert!(Utf8Path::new("\\\\.\\C:\\foo").node_is_absolute_win32()); + fn test_node_is_absolute() { + // win32 cases + assert!(Utf8Path::new("/").node_is_absolute_win32()); + assert!(Utf8Path::new("//").node_is_absolute_win32()); + assert!(Utf8Path::new("//server").node_is_absolute_win32()); + assert!(Utf8Path::new("//server/file").node_is_absolute_win32()); + assert!(Utf8Path::new("\\\\server\\file").node_is_absolute_win32()); assert!(Utf8Path::new("\\\\server").node_is_absolute_win32()); - assert!(Utf8Path::new("\\foo").node_is_absolute_win32()); - - // Windows relative paths - assert!(!Utf8Path::new("foo").node_is_absolute_win32()); - assert!(!Utf8Path::new("foo\\bar").node_is_absolute_win32()); - assert!(!Utf8Path::new("./foo").node_is_absolute_win32()); - assert!(!Utf8Path::new("../foo").node_is_absolute_win32()); - assert!(!Utf8Path::new("C:foo").node_is_absolute_win32()); // Drive-relative - assert!(!Utf8Path::new("C:").node_is_absolute_win32()); // Drive-relative - assert!(!Utf8Path::new("").node_is_absolute_win32()); - - // POSIX-style paths should return false in Windows mode - assert!(!Utf8Path::new("/foo").node_is_absolute_win32()); - assert!(!Utf8Path::new("/").node_is_absolute_win32()); - } - - #[test] - fn test_node_is_absolute_cross_platform() { - // Test the cross-platform version - if cfg!(windows) { - // On Windows, should use win32 logic - assert!(Utf8Path::new("C:\\foo").node_is_absolute()); - assert!(Utf8Path::new("\\\\server\\share").node_is_absolute()); - assert!(!Utf8Path::new("/foo").node_is_absolute()); - assert!(!Utf8Path::new("foo").node_is_absolute()); - } else { - // On POSIX systems, should use posix logic - assert!(Utf8Path::new("/foo").node_is_absolute()); - assert!(!Utf8Path::new("C:\\foo").node_is_absolute()); - assert!(!Utf8Path::new("foo").node_is_absolute()); - } + assert!(Utf8Path::new("\\\\").node_is_absolute_win32()); + assert!(!Utf8Path::new("c").node_is_absolute_win32()); + assert!(!Utf8Path::new("c:").node_is_absolute_win32()); + assert!(Utf8Path::new("c:\\").node_is_absolute_win32()); + assert!(Utf8Path::new("c:/").node_is_absolute_win32()); + assert!(Utf8Path::new("c://").node_is_absolute_win32()); + assert!(Utf8Path::new("C:/Users/").node_is_absolute_win32()); + assert!(Utf8Path::new("C:\\Users\\").node_is_absolute_win32()); + assert!(!Utf8Path::new("C:cwd/another").node_is_absolute_win32()); + assert!(!Utf8Path::new("C:cwd\\another").node_is_absolute_win32()); + assert!(!Utf8Path::new("directory/directory").node_is_absolute_win32()); + assert!(!Utf8Path::new("directory\\directory").node_is_absolute_win32()); + + // posix cases + assert!(Utf8Path::new("/home/foo").node_is_absolute_posix()); + assert!(Utf8Path::new("/home/foo/..").node_is_absolute_posix()); + assert!(!Utf8Path::new("bar/").node_is_absolute_posix()); + assert!(!Utf8Path::new("./baz").node_is_absolute_posix()); } }