diff --git a/Cargo.lock b/Cargo.lock index fc520936..17d82355 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -721,9 +721,9 @@ dependencies = [ [[package]] name = "json-strip-comments" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4135b29c84322dbc3327272084360785665452213a576a991b3ac2f63148e82" +checksum = "25376d12b2f6ae53f986f86e2a808a56af03d72284ae24fc35a2e290d09ee3c3" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 1facff86..5b1184c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,7 @@ name = "resolver" [dependencies] cfg-if = "1" indexmap = { version = "2", features = ["serde"] } -json-strip-comments = "3" +json-strip-comments = "3.1" once_cell = "1" # Use `std::sync::OnceLock::get_or_try_init` when it is stable. papaya = "0.2" parking_lot = "0.12" diff --git a/src/cache/cache_impl.rs b/src/cache/cache_impl.rs index ad2e35d5..174cfade 100644 --- a/src/cache/cache_impl.rs +++ b/src/cache/cache_impl.rs @@ -227,12 +227,12 @@ impl Cache { os_string.push(".json"); Cow::Owned(PathBuf::from(os_string)) }; - let mut tsconfig_string = self + let tsconfig_string = self .fs .read_to_string(&tsconfig_path) .map_err(|_| ResolveError::TsconfigNotFound(path.to_path_buf()))?; let mut tsconfig = - TsConfig::parse(root, &tsconfig_path, &mut tsconfig_string).map_err(|error| { + TsConfig::parse(root, &tsconfig_path, tsconfig_string).map_err(|error| { ResolveError::from_serde_json_error(tsconfig_path.to_path_buf(), &error) })?; callback(&mut tsconfig)?; diff --git a/src/lib.rs b/src/lib.rs index 1589aec9..3b341372 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1926,3 +1926,13 @@ fn resolve_file_protocol(specifier: &str) -> Result, ResolveError> Ok(Cow::Borrowed(specifier)) } } + +/// Strip BOM in place by replacing with spaces (no reallocation) +/// UTF-8 BOM is 3 bytes: 0xEF, 0xBB, 0xBF +pub(crate) fn replace_bom_with_whitespace(s: &mut [u8]) { + if s.starts_with(b"\xEF\xBB\xBF") { + s[0] = b' '; + s[1] = b' '; + s[2] = b' '; + } +} diff --git a/src/package_json/serde.rs b/src/package_json/serde.rs index 73291c89..1be0ca1a 100644 --- a/src/package_json/serde.rs +++ b/src/package_json/serde.rs @@ -9,8 +9,9 @@ use std::{ use serde_json::Value; +use crate::{FileSystem, JSONError, ResolveError, path::PathUtil, replace_bom_with_whitespace}; + use super::{ImportsExportsKind, PackageType, SideEffects}; -use crate::{FileSystem, JSONError, ResolveError, path::PathUtil}; /// Serde implementation for the deserialized `package.json`. /// @@ -226,20 +227,15 @@ impl PackageJson { realpath: PathBuf, json: Vec, ) -> Result { - // Strip BOM - UTF-8 BOM is 3 bytes: 0xEF, 0xBB, 0xBF - let json_bytes = if json.starts_with(b"\xEF\xBB\xBF") { &json[3..] } else { &json[..] }; - - // Check if empty after BOM stripping - super::check_if_empty(json_bytes, &path)?; - - // Parse JSON directly from bytes - let value = serde_json::from_slice::(json_bytes).map_err(|error| JSONError { + let mut json = json; + replace_bom_with_whitespace(&mut json); + super::check_if_empty(&json, &path)?; + let value = serde_json::from_slice::(&json).map_err(|error| JSONError { path: path.clone(), message: error.to_string(), line: error.line(), column: error.column(), })?; - Ok(Self { path, realpath, value }) } diff --git a/src/package_json/simd.rs b/src/package_json/simd.rs index 373c643d..1c5f6d96 100644 --- a/src/package_json/simd.rs +++ b/src/package_json/simd.rs @@ -11,7 +11,7 @@ use self_cell::MutBorrow; use simd_json::{BorrowedValue, prelude::*}; use super::{ImportsExportsKind, PackageType, SideEffects}; -use crate::{FileSystem, JSONError, ResolveError, path::PathUtil}; +use crate::{FileSystem, JSONError, ResolveError, path::PathUtil, replace_bom_with_whitespace}; // Use simd_json's Object type which handles the hasher correctly based on features type BorrowedObject<'a> = simd_json::value::borrowed::Object<'a>; @@ -260,19 +260,14 @@ impl PackageJson { realpath: PathBuf, json: Vec, ) -> Result { - // Strip BOM in place by replacing with spaces (no reallocation) - let mut json_bytes = json; - if json_bytes.starts_with(b"\xEF\xBB\xBF") { - json_bytes[0] = b' '; - json_bytes[1] = b' '; - json_bytes[2] = b' '; - } + let mut json = json; + replace_bom_with_whitespace(&mut json); // Check if empty after BOM stripping - super::check_if_empty(&json_bytes, &path)?; + super::check_if_empty(&json, &path)?; // Create the self-cell with the JSON bytes and parsed BorrowedValue - let cell = PackageJsonCell::try_new(MutBorrow::new(json_bytes), |bytes| { + let cell = PackageJsonCell::try_new(MutBorrow::new(json), |bytes| { // Use MutBorrow to safely get mutable access for simd_json parsing simd_json::to_borrowed_value(bytes.borrow_mut()) }) diff --git a/src/tests/tsconfig_extends.rs b/src/tests/tsconfig_extends.rs index 89e7d8c7..f12ede69 100644 --- a/src/tests/tsconfig_extends.rs +++ b/src/tests/tsconfig_extends.rs @@ -162,7 +162,7 @@ fn test_extend_tsconfig_no_override_existing() { let parent_path = Path::new("/parent/tsconfig.json"); let child_path = Path::new("/child/tsconfig.json"); - let mut parent_config = serde_json::json!({ + let parent_config = serde_json::json!({ "compilerOptions": { "baseUrl": "./src", "jsx": "react-jsx", @@ -171,15 +171,15 @@ fn test_extend_tsconfig_no_override_existing() { }) .to_string(); - let mut child_config = serde_json::json!({ + let child_config = serde_json::json!({ "compilerOptions": { "jsx": "preserve" // This should NOT be overridden } }) .to_string(); - let parent_tsconfig = TsConfig::parse(true, parent_path, &mut parent_config).unwrap().build(); - let mut child_tsconfig = TsConfig::parse(true, child_path, &mut child_config).unwrap(); + let parent_tsconfig = TsConfig::parse(true, parent_path, parent_config).unwrap().build(); + let mut child_tsconfig = TsConfig::parse(true, child_path, child_config).unwrap(); // Perform the extension child_tsconfig.extend_tsconfig(&parent_tsconfig); diff --git a/src/tests/tsconfig_paths.rs b/src/tests/tsconfig_paths.rs index 68ac2a9e..3443e0c3 100644 --- a/src/tests/tsconfig_paths.rs +++ b/src/tests/tsconfig_paths.rs @@ -175,7 +175,7 @@ fn empty() { #[test] fn test_paths() { let path = Path::new("/foo/tsconfig.json"); - let mut tsconfig_json = serde_json::json!({ + let tsconfig_json = serde_json::json!({ "compilerOptions": { "paths": { "jquery": ["node_modules/jquery/dist/jquery"], @@ -188,7 +188,7 @@ fn test_paths() { } }) .to_string(); - let tsconfig = TsConfig::parse(true, path, &mut tsconfig_json).unwrap().build(); + let tsconfig = TsConfig::parse(true, path, tsconfig_json).unwrap().build(); let data = [ ("jquery", vec!["/foo/node_modules/jquery/dist/jquery"]), @@ -212,13 +212,13 @@ fn test_paths() { #[test] fn test_base_url() { let path = Path::new("/foo/tsconfig.json"); - let mut tsconfig_json = serde_json::json!({ + let tsconfig_json = serde_json::json!({ "compilerOptions": { "baseUrl": "./src" } }) .to_string(); - let tsconfig = TsConfig::parse(true, path, &mut tsconfig_json).unwrap().build(); + let tsconfig = TsConfig::parse(true, path, tsconfig_json).unwrap().build(); let data = [ ("foo", vec!["/foo/src/foo"]), @@ -237,7 +237,7 @@ fn test_base_url() { #[test] fn test_paths_and_base_url() { let path = Path::new("/foo/tsconfig.json"); - let mut tsconfig_json = serde_json::json!({ + let tsconfig_json = serde_json::json!({ "compilerOptions": { "baseUrl": "./src", "paths": { @@ -249,7 +249,7 @@ fn test_paths_and_base_url() { } }) .to_string(); - let tsconfig = TsConfig::parse(true, path, &mut tsconfig_json).unwrap().build(); + let tsconfig = TsConfig::parse(true, path, tsconfig_json).unwrap().build(); let data = [ ("test", vec!["/foo/src/generated/test", "/foo/src/test"]), diff --git a/src/tsconfig.rs b/src/tsconfig.rs index 4f776437..da449283 100644 --- a/src/tsconfig.rs +++ b/src/tsconfig.rs @@ -9,7 +9,7 @@ use indexmap::IndexMap; use rustc_hash::FxHasher; use serde::Deserialize; -use crate::{TsconfigReferences, path::PathUtil}; +use crate::{TsconfigReferences, path::PathUtil, replace_bom_with_whitespace}; const TEMPLATE_VARIABLE: &str = "${configDir}"; @@ -23,7 +23,7 @@ pub struct ProjectReference { pub path: PathBuf, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TsConfig { /// Whether this is the caller tsconfig. @@ -66,11 +66,15 @@ impl TsConfig { /// # Errors /// /// * Any error that can be returned by `serde_json::from_str()`. - pub fn parse(root: bool, path: &Path, json: &mut str) -> Result { - let json = trim_start_matches_mut(json, '\u{feff}'); // strip bom - _ = json_strip_comments::strip(json); - let mut tsconfig: Self = - serde_json::from_str(if json.trim().is_empty() { "{}" } else { json })?; + pub fn parse(root: bool, path: &Path, json: String) -> Result { + let mut json = json.into_bytes(); + replace_bom_with_whitespace(&mut json); + _ = json_strip_comments::strip_slice(&mut json); + let mut tsconfig: Self = if json.iter().all(u8::is_ascii_whitespace) { + Self::default() + } else { + serde_json::from_slice(&json)? + }; tsconfig.root = root; tsconfig.path = path.to_path_buf(); Ok(tsconfig) @@ -488,12 +492,3 @@ pub enum ExtendsField { Single(String), Multiple(Vec), } - -fn trim_start_matches_mut(s: &mut str, pat: char) -> &mut str { - if s.starts_with(pat) { - // trim the prefix - &mut s[pat.len_utf8()..] - } else { - s - } -}