diff --git a/Cargo.lock b/Cargo.lock index b50a265dc5df..f7c233e3a5e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3963,9 +3963,12 @@ dependencies = [ name = "swc_config" version = "0.1.9" dependencies = [ + "anyhow", "indexmap 2.1.0", "serde", "serde_json", + "sourcemap", + "swc_cached", "swc_config_macro", ] @@ -4534,7 +4537,6 @@ dependencies = [ "serde", "serde_json", "swc_atoms", - "swc_cached", "swc_common", "swc_config", "swc_ecma_ast", diff --git a/crates/swc/src/lib.rs b/crates/swc/src/lib.rs index 0c3c7016564c..ab3069a50e59 100644 --- a/crates/swc/src/lib.rs +++ b/crates/swc/src/lib.rs @@ -765,14 +765,8 @@ impl Compiler { .source_map .as_ref() .map(|obj| -> Result<_, Error> { - let orig = obj - .content - .as_ref() - .map(|s| sourcemap::SourceMap::from_slice(s.as_bytes())); - let orig = match orig { - Some(v) => Some(v?), - None => None, - }; + let orig = obj.content.as_ref().map(|s| s.to_sourcemap()).transpose()?; + Ok((SourceMapsConfig::Bool(true), orig)) }) .unwrap_as_option(|v| { diff --git a/crates/swc/tests/minify/issue-7475/issue-8372/1/config.json b/crates/swc/tests/minify/issue-7475/issue-8372/1/config.json new file mode 100644 index 000000000000..678d1aae652a --- /dev/null +++ b/crates/swc/tests/minify/issue-7475/issue-8372/1/config.json @@ -0,0 +1,23 @@ +{ + "compress": { + "negate_iife": false, + "sequences": 30 + }, + "mangle": { + "safari10": true + }, + "output": { + "semicolons": false + }, + "sourceMap": { + "filename": "input.js", + "url": "input.map", + "content": { + "version": 3, + "sources": ["input-preprocess.js"], + "sourcesContent": ["const a = {aób: 'ó'}\n\nconsole.log(a)"], + "names": ["console", "log", "aób"], + "mappings": "AAEAA,QAAQC,GAAG,CAFD;IAACC,UAAK;AAAG" + } + } +} diff --git a/crates/swc/tests/minify/issue-7475/issue-8372/1/input.js b/crates/swc/tests/minify/issue-7475/issue-8372/1/input.js new file mode 100644 index 000000000000..7d39d5e7d6e7 --- /dev/null +++ b/crates/swc/tests/minify/issue-7475/issue-8372/1/input.js @@ -0,0 +1,3 @@ +const a = { aób: 'ó' } + +console.log(a) \ No newline at end of file diff --git a/crates/swc/tests/minify/issue-7475/issue-8372/1/output.js b/crates/swc/tests/minify/issue-7475/issue-8372/1/output.js new file mode 100644 index 000000000000..9855a4b10a46 --- /dev/null +++ b/crates/swc/tests/minify/issue-7475/issue-8372/1/output.js @@ -0,0 +1 @@ +let a={aób:"\xf3"};console.log(a); diff --git a/crates/swc/tests/minify/issue-7475/issue-8372/1/output.map b/crates/swc/tests/minify/issue-7475/issue-8372/1/output.map new file mode 100644 index 000000000000..345aa72c2aac --- /dev/null +++ b/crates/swc/tests/minify/issue-7475/issue-8372/1/output.map @@ -0,0 +1,16 @@ +{ + "mappings": "AAEAA,IAAAA,EAAQC,CAFEC,IAAA,MAAA,EAASF,QAAAC,GAAA,CAAAE", + "names": [ + "console", + "log", + "aób", + "a" + ], + "sources": [ + "input-preprocess.js" + ], + "sourcesContent": [ + "const a = {aób: 'ó'}\n\nconsole.log(a)" + ], + "version": 3 +} diff --git a/crates/swc/tests/minify/issue-7475/issue-8372/2/config.json b/crates/swc/tests/minify/issue-7475/issue-8372/2/config.json new file mode 100644 index 000000000000..c01df265f570 --- /dev/null +++ b/crates/swc/tests/minify/issue-7475/issue-8372/2/config.json @@ -0,0 +1,12 @@ +{ + "compress": { + "negate_iife": false, + "sequences": 30 + }, + "mangle": { + "safari10": true + }, + "output": { + "semicolons": false + } +} diff --git a/crates/swc/tests/minify/issue-7475/issue-8372/2/input.js b/crates/swc/tests/minify/issue-7475/issue-8372/2/input.js new file mode 100644 index 000000000000..7d39d5e7d6e7 --- /dev/null +++ b/crates/swc/tests/minify/issue-7475/issue-8372/2/input.js @@ -0,0 +1,3 @@ +const a = { aób: 'ó' } + +console.log(a) \ No newline at end of file diff --git a/crates/swc/tests/minify/issue-7475/issue-8372/2/output.js b/crates/swc/tests/minify/issue-7475/issue-8372/2/output.js new file mode 100644 index 000000000000..9855a4b10a46 --- /dev/null +++ b/crates/swc/tests/minify/issue-7475/issue-8372/2/output.js @@ -0,0 +1 @@ +let a={aób:"\xf3"};console.log(a); diff --git a/crates/swc/tests/minify/issue-7475/issue-8372/2/output.map b/crates/swc/tests/minify/issue-7475/issue-8372/2/output.map new file mode 100644 index 000000000000..9a47e00da8e2 --- /dev/null +++ b/crates/swc/tests/minify/issue-7475/issue-8372/2/output.map @@ -0,0 +1,16 @@ +{ + "mappings": "AAAA,IAAMA,EAAI,CAAEC,IAAK,MAAI,EAErBC,QAAQC,GAAG,CAACH", + "names": [ + "a", + "aób", + "console", + "log" + ], + "sources": [ + "$DIR/tests/minify/issue-7475/issue-8372/2/input.js" + ], + "sourcesContent": [ + "const a = { aób: 'ó' }\n\nconsole.log(a)" + ], + "version": 3 +} diff --git a/crates/swc/tests/projects.rs b/crates/swc/tests/projects.rs index e49497cf378c..7b1b8629fead 100644 --- a/crates/swc/tests/projects.rs +++ b/crates/swc/tests/projects.rs @@ -1128,10 +1128,13 @@ fn minify(input_js: PathBuf) { let c = Compiler::new(cm); let fm = c.cm.load_file(&input_js).unwrap(); - let mut config: JsMinifyOptions = - serde_json::from_str(&std::fs::read_to_string(&config_json_path).unwrap()).unwrap(); + let config_str = std::fs::read_to_string(&config_json_path).unwrap(); + let mut config: JsMinifyOptions = serde_json::from_str(&config_str).unwrap(); + + if config.source_map.inner().is_none() { + config.source_map = BoolOrDataConfig::from_bool(true); + } - config.source_map = BoolOrDataConfig::from_bool(true); let output = c.minify(fm, &handler, &config).unwrap(); NormalizedOutput::from(output.code) diff --git a/crates/swc_cached/src/lib.rs b/crates/swc_cached/src/lib.rs index ab8e0890692a..7c3d71241766 100644 --- a/crates/swc_cached/src/lib.rs +++ b/crates/swc_cached/src/lib.rs @@ -4,3 +4,4 @@ #![deny(warnings)] pub mod regex; +pub use anyhow::Error; diff --git a/crates/swc_cached/src/regex.rs b/crates/swc_cached/src/regex.rs index ca61e3aa109e..18b19f28f1e1 100644 --- a/crates/swc_cached/src/regex.rs +++ b/crates/swc_cached/src/regex.rs @@ -1,6 +1,6 @@ //! Regex cache -use std::{ops::Deref, sync::Arc}; +use std::{ops::Deref, str::FromStr, sync::Arc}; pub use anyhow::Error; use anyhow::{Context, Result}; @@ -77,3 +77,11 @@ impl From<&'_ str> for CachedRegex { Self::new(s).unwrap() } } + +impl FromStr for CachedRegex { + type Err = Error; + + fn from_str(s: &str) -> Result { + Self::new(s) + } +} diff --git a/crates/swc_common/src/source_map.rs b/crates/swc_common/src/source_map.rs index f5f968767073..282bb859f05d 100644 --- a/crates/swc_common/src/source_map.rs +++ b/crates/swc_common/src/source_map.rs @@ -1269,11 +1269,13 @@ impl SourceMap { if config.skip(&f.name) { continue; } - src_id = builder.add_source(&config.file_name_to_source(&f.name)); + if orig.is_none() { + src_id = builder.add_source(&config.file_name_to_source(&f.name)); - inline_sources_content = config.inline_sources_content(&f.name); - if inline_sources_content && orig.is_none() { - builder.set_source_contents(src_id, Some(&f.src)); + inline_sources_content = config.inline_sources_content(&f.name); + if inline_sources_content && orig.is_none() { + builder.set_source_contents(src_id, Some(&f.src)); + } } ch_state = ByteToCharPosState::default(); diff --git a/crates/swc_config/Cargo.toml b/crates/swc_config/Cargo.toml index ef96061afd62..856840cf5911 100644 --- a/crates/swc_config/Cargo.toml +++ b/crates/swc_config/Cargo.toml @@ -9,10 +9,13 @@ repository = "https://github.com/swc-project/swc.git" version = "0.1.9" [dependencies] +anyhow = "1" indexmap = "2.0.0" serde = { version = "1", features = ["derive"] } serde_json = "1" +sourcemap = { version = "6.4", optional = true } +swc_cached = { version = "0.3.18", path = "../swc_cached" } swc_config_macro = { version = "0.1.3", path = "../swc_config_macro" } [lib] diff --git a/crates/swc_config/src/config_types/bool_or_data.rs b/crates/swc_config/src/config_types/bool_or_data.rs index 2c56d0e7da56..33c3de25ee3b 100644 --- a/crates/swc_config/src/config_types/bool_or_data.rs +++ b/crates/swc_config/src/config_types/bool_or_data.rs @@ -69,6 +69,14 @@ impl BoolOrDataConfig { pub fn into_inner(self) -> Option> { self.0 } + + pub fn inner(&self) -> Option> { + match &self.0 { + Some(BoolOr::Data(v)) => Some(BoolOr::Data(v)), + Some(BoolOr::Bool(b)) => Some(BoolOr::Bool(*b)), + None => None, + } + } } impl From for BoolOrDataConfig { diff --git a/crates/swc_config/src/lib.rs b/crates/swc_config/src/lib.rs index cc3c57455746..b46409f4d255 100644 --- a/crates/swc_config/src/lib.rs +++ b/crates/swc_config/src/lib.rs @@ -4,3 +4,10 @@ mod macros; pub mod config_types; pub mod merge; +#[cfg(feature = "sourcemap")] +mod source_map; + +pub use swc_cached::{regex::CachedRegex, Error}; + +#[cfg(feature = "sourcemap")] +pub use crate::source_map::*; diff --git a/crates/swc_config/src/source_map.rs b/crates/swc_config/src/source_map.rs new file mode 100644 index 000000000000..824924510d0e --- /dev/null +++ b/crates/swc_config/src/source_map.rs @@ -0,0 +1,118 @@ +use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; +use sourcemap::{vlq::parse_vlq_segment, RawToken, SourceMap}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SourceMapContent { + Json(String), + #[serde(rename_all = "camelCase")] + Parsed { + #[serde(default)] + sources: Vec, + #[serde(default)] + names: Vec, + #[serde(default)] + mappings: String, + #[serde(default)] + file: Option, + #[serde(default)] + source_root: Option, + #[serde(default)] + sources_content: Option>>, + }, +} + +impl SourceMapContent { + pub fn to_sourcemap(&self) -> Result { + match self { + SourceMapContent::Json(s) => { + SourceMap::from_slice(s.as_bytes()).context("failed to parse sourcemap") + } + SourceMapContent::Parsed { + sources, + names, + mappings, + file, + source_root, + sources_content, + } => { + let mut dst_col; + let mut src_id = 0; + let mut src_line = 0; + let mut src_col = 0; + let mut name_id = 0; + + let allocation_size = mappings.matches(&[',', ';'][..]).count() + 10; + let mut tokens = Vec::with_capacity(allocation_size); + + let mut nums = Vec::with_capacity(6); + + for (dst_line, line) in mappings.split(';').enumerate() { + if line.is_empty() { + continue; + } + + dst_col = 0; + + for segment in line.split(',') { + if segment.is_empty() { + continue; + } + + nums.clear(); + nums = parse_vlq_segment(segment)?; + dst_col = (i64::from(dst_col) + nums[0]) as u32; + + let mut src = !0; + let mut name = !0; + + if nums.len() > 1 { + if nums.len() != 4 && nums.len() != 5 { + bail!( + "invalid vlq segment size; expected 4 or 5, got {}", + nums.len() + ); + } + src_id = (i64::from(src_id) + nums[1]) as u32; + if src_id >= sources.len() as u32 { + bail!("invalid source reference: {}", src_id); + } + + src = src_id; + src_line = (i64::from(src_line) + nums[2]) as u32; + src_col = (i64::from(src_col) + nums[3]) as u32; + + if nums.len() > 4 { + name_id = (i64::from(name_id) + nums[4]) as u32; + if name_id >= names.len() as u32 { + bail!("invalid name reference: {}", name_id); + } + name = name_id; + } + } + + tokens.push(RawToken { + dst_line: dst_line as u32, + dst_col, + src_line, + src_col, + src_id: src, + name_id: name, + }); + } + } + + let mut map = SourceMap::new( + file.clone(), + tokens, + names.clone(), + sources.clone(), + sources_content.clone(), + ); + map.set_source_root(source_root.clone()); + Ok(map) + } + } + } +} diff --git a/crates/swc_ecma_minifier/Cargo.toml b/crates/swc_ecma_minifier/Cargo.toml index d397ec93d074..f94cedd27522 100644 --- a/crates/swc_ecma_minifier/Cargo.toml +++ b/crates/swc_ecma_minifier/Cargo.toml @@ -51,9 +51,10 @@ serde_json = "1.0.61" tracing = "0.1.37" swc_atoms = { version = "0.6.5", path = "../swc_atoms" } -swc_cached = { version = "0.3.18", path = "../swc_cached" } swc_common = { version = "0.33.15", path = "../swc_common" } -swc_config = { version = "0.1.9", path = "../swc_config" } +swc_config = { version = "0.1.9", path = "../swc_config", features = [ + "sourcemap", +] } swc_ecma_ast = { version = "0.111.1", path = "../swc_ecma_ast", features = [ "serde", ] } diff --git a/crates/swc_ecma_minifier/src/js.rs b/crates/swc_ecma_minifier/src/js.rs index 1db01f61304f..cb49c72fb174 100644 --- a/crates/swc_ecma_minifier/src/js.rs +++ b/crates/swc_ecma_minifier/src/js.rs @@ -1,7 +1,7 @@ //! NOT A PUBLIC API use serde::{Deserialize, Serialize}; -use swc_config::config_types::BoolOrDataConfig; +use swc_config::{config_types::BoolOrDataConfig, SourceMapContent}; use crate::option::{ terser::{TerserCompressorOptions, TerserEcmaVersion}, @@ -59,6 +59,8 @@ fn true_by_default() -> bool { true } +/// `sourceMap` of `minify()`.` +/// /// `jsc.minify.sourceMap` #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] @@ -73,7 +75,7 @@ pub struct TerserSourceMapOption { pub root: Option, #[serde(default)] - pub content: Option, + pub content: Option, } /// Parser options for `minify()`, which should have the same API as terser. diff --git a/crates/swc_ecma_minifier/src/option/mod.rs b/crates/swc_ecma_minifier/src/option/mod.rs index 8940c188e3a5..a315df2dfbc4 100644 --- a/crates/swc_ecma_minifier/src/option/mod.rs +++ b/crates/swc_ecma_minifier/src/option/mod.rs @@ -2,9 +2,8 @@ use serde::{Deserialize, Serialize}; use swc_atoms::JsWord; -use swc_cached::regex::CachedRegex; use swc_common::{collections::AHashMap, Mark}; -use swc_config::merge::Merge; +use swc_config::{merge::Merge, CachedRegex}; use swc_ecma_ast::{EsVersion, Expr}; /// Implement default using serde.