Repository: oxc-project/oxc-resolver
Pull request #463 (2025-04-23 09:11:01)

fix: handle package.json and tsconfig.json with BOM
Node supports `package.json` with UTF-8 BOM and TypeScript supports `tsconfig.json` with UTF-8 BOM.

An error was happening for these cases.


Patch:
diff --git a/fixtures/misc/package-json-with-bom/index.js b/fixtures/misc/package-json-with-bom/index.js
new file mode 100644
index 0000000..e69de29
diff --git a/fixtures/misc/package-json-with-bom/package.json b/fixtures/misc/package-json-with-bom/package.json
new file mode 100644
index 0000000..ff60db7
--- /dev/null
+++ b/fixtures/misc/package-json-with-bom/package.json
@@ -0,0 +1,7 @@
+﻿{
+  "name": "@oxc-resolver/test-package-json-with-bom",
+  "private": true,
+  "version": "0.0.0",
+  "main": "index.js",
+  "type": "module"
+}
diff --git a/fixtures/tsconfig/cases/with-bom/bar.js b/fixtures/tsconfig/cases/with-bom/bar.js
new file mode 100644
index 0000000..e69de29
diff --git a/fixtures/tsconfig/cases/with-bom/tsconfig.json b/fixtures/tsconfig/cases/with-bom/tsconfig.json
new file mode 100644
index 0000000..a644308
--- /dev/null
+++ b/fixtures/tsconfig/cases/with-bom/tsconfig.json
@@ -0,0 +1,7 @@
+﻿{
+  "compilerOptions": {
+    "paths": {
+      "foo": ["bar.js"]
+    }
+  }
+}
diff --git a/src/package_json_serde.rs b/src/package_json_serde.rs
index 729d37c..1e9772c 100644
--- a/src/package_json_serde.rs
+++ b/src/package_json_serde.rs
@@ -134,6 +134,7 @@ impl PackageJsonSerde {
         realpath: PathBuf,
         json: &str,
     ) -> Result<Self, serde_json::Error> {
+        let json = json.trim_start_matches("\u{feff}"); // strip bom
         let mut raw_json: JSONValue = serde_json::from_str(json)?;
         let mut package_json = Self::default();
 
diff --git a/src/tsconfig_serde.rs b/src/tsconfig_serde.rs
index 4bb0bc8..93e54cf 100644
--- a/src/tsconfig_serde.rs
+++ b/src/tsconfig_serde.rs
@@ -269,6 +269,7 @@ impl TsConfigSerde {
     ///
     /// * Any error that can be returned by `serde_json::from_str()`.
     pub fn parse(root: bool, path: &Path, json: &mut str) -> Result<Self, serde_json::Error> {
+        let json = trim_start_matches_mut(json, '\u{feff}'); // strip bom
         _ = json_strip_comments::strip(json);
         let mut tsconfig: Self = serde_json::from_str(json)?;
         tsconfig.root = root;
@@ -276,3 +277,12 @@ impl TsConfigSerde {
         Ok(tsconfig)
     }
 }
+
+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
+    }
+}


Test patch:
diff --git a/src/tests/tsconfig_paths.rs b/src/tests/tsconfig_paths.rs
index a79d580..98479df 100644
--- a/src/tests/tsconfig_paths.rs
+++ b/src/tests/tsconfig_paths.rs
@@ -91,6 +91,22 @@ fn json_with_comments() {
     assert_eq!(resolved_path, Ok(f.join("bar.js")));
 }
 
+#[test]
+fn with_bom() {
+    let f = super::fixture_root().join("tsconfig/cases/with-bom");
+
+    let resolver = Resolver::new(ResolveOptions {
+        tsconfig: Some(TsconfigOptions {
+            config_file: f.join("tsconfig.json"),
+            references: TsconfigReferences::Auto,
+        }),
+        ..ResolveOptions::default()
+    });
+
+    let resolved_path = resolver.resolve(&f, "foo").map(|f| f.full_path());
+    assert_eq!(resolved_path, Ok(f.join("bar.js")));
+}
+
 #[test]
 fn broken() {
     let f = super::fixture_root().join("tsconfig");
diff --git a/tests/resolve_test.rs b/tests/resolve_test.rs
index 50f8f89..eece3ef 100644
--- a/tests/resolve_test.rs
+++ b/tests/resolve_test.rs
@@ -235,3 +235,15 @@ fn windows_symlinked_longfilename() {
         Resolver::new(ResolveOptions::default()).resolve(&path, specifier).map(|r| r.full_path());
     assert_eq!(resolution, Ok(module_path));
 }
+
+#[test]
+fn package_json_with_bom() {
+    let dir = dir();
+    let dir = dir.join("fixtures/misc");
+    assert_eq!(
+        Resolver::new(ResolveOptions::default())
+            .resolve(&dir, "./package-json-with-bom")
+            .map(oxc_resolver::Resolution::into_path_buf),
+        Ok(dir.join("package-json-with-bom/index.js"))
+    );
+}