Repository: oxc-project/oxc-resolver
Pull request #99 (2024-02-29 23:24:28)

Migrate to package.json/tsconfig.json crates
This PR migrates to the https://crates.io/crates/nodejs_package_json and https://crates.io/crates/typescript_tsconfig_json crates, that'll be used by Rolldown and other tools in the future. Better to standardize on common shapes, instead of duplicating over and over again.

For actually changes, will leave them as comments in the PR.

codspeed-hq[bot]: ## [CodSpeed Performance Report](https://codspeed.io/oxc-project/oxc-resolver/branches/milesj:jsts)

### Merging #99 will **not alter performance**

<sub>:warning: No base runs were found</sub>

<sub>Falling back to comparing <code>milesj:jsts</code> (9cb68d6) with <code>main</code> (224efee)</sub>



### Summary

`✅ 2` untouched benchmarks





milesj: @Boshen @ematipico Made some changes. I have some more ideas to take this further if need be.
Boshen: I'll test this in Rspack before merging.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 8b3520e..f832144 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -105,6 +105,12 @@ version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
 
+[[package]]
+name = "clean-path"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aaa6b4b263a5d737e9bf6b7c09b72c41a5480aec4d7219af827f6564e950b6a5"
+
 [[package]]
 name = "codspeed"
 version = "2.4.0"
@@ -426,6 +432,20 @@ dependencies = [
  "libloading",
 ]
 
+[[package]]
+name = "nodejs_package_json"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd96b70ab81020680a99e7e4afd0bdf08c11e969e36d89c0db8ec44dd7a94392"
+dependencies = [
+ "indexmap",
+ "rustc-hash",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
 [[package]]
 name = "normalize-path"
 version = "0.2.1"
@@ -471,8 +491,8 @@ dependencies = [
  "criterion",
  "dashmap",
  "dunce",
- "indexmap",
  "json-strip-comments",
+ "nodejs_package_json",
  "normalize-path",
  "once_cell",
  "rayon",
@@ -481,6 +501,7 @@ dependencies = [
  "serde_json",
  "thiserror",
  "tracing",
+ "typescript_tsconfig_json",
  "vfs",
 ]
 
@@ -621,9 +642,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
 [[package]]
 name = "semver"
-version = "1.0.20"
+version = "1.0.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
+checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "serde"
@@ -735,6 +759,19 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "typescript_tsconfig_json"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "102a24e13fa23fa7fc6fcfa8749da4ffbd4a3fc346d82baf4fbadda4bf01ae97"
+dependencies = [
+ "clean-path",
+ "indexmap",
+ "rustc-hash",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.12"
diff --git a/Cargo.toml b/Cargo.toml
index cfc3023..583a6fd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -78,11 +78,12 @@ serde_json = { version = "1.0.114", features = [
   "preserve_order",
 ] } # preserve_order: package_json.exports requires order such as `["require", "import", "default"]`
 rustc-hash = { version = "1.1.0", default-features = false, features = ["std"] }
-indexmap = { version = "2.2.5", features = ["serde"] } # serde for Deserialize from package.json
 dunce = "1.0.4" # Normalize Windows paths to the most compatible format, avoiding UNC where possible
 once_cell = "1.19.0" # Use `std::sync::OnceLock::get_or_try_init` when it is stable.
 thiserror = { version = "1.0.57" }
 json-strip-comments = { version = "1.0.2" }
+typescript_tsconfig_json = { version = "0.1.1" }
+nodejs_package_json = { version = "0.1.1" }
 
 codspeed-criterion-compat = { version = "2.4.0", default-features = false, optional = true }
 
diff --git a/napi/src/lib.rs b/napi/src/lib.rs
index cad1b72..fd0d33a 100644
--- a/napi/src/lib.rs
+++ b/napi/src/lib.rs
@@ -157,6 +157,7 @@ impl ResolverFactory {
                 .unwrap_or(default.roots),
             symlinks: op.symlinks.unwrap_or(default.symlinks),
             builtin_modules: op.builtin_modules.unwrap_or(default.builtin_modules),
+            remove_unused_fields: op.remove_unused_fields.unwrap_or(default.remove_unused_fields),
         }
     }
 }
diff --git a/napi/src/options.rs b/napi/src/options.rs
index f1ecb14..9fe4fce 100644
--- a/napi/src/options.rs
+++ b/napi/src/options.rs
@@ -141,6 +141,10 @@ pub struct NapiResolveOptions {
     ///
     /// Default `false`
     pub builtin_modules: Option<bool>,
+
+    /// When enabled, will attempt to reduce memory usage by clearing data and fields
+    /// that are not relevant to the current process, and should not be held in memory.
+    pub remove_unused_fields: Option<bool>,
 }
 
 #[napi]
diff --git a/src/lib.rs b/src/lib.rs
index 033d8a3..551b9dd 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -45,6 +45,7 @@ use std::{
     path::{Component, Path, PathBuf},
     sync::Arc,
 };
+use typescript_tsconfig_json::ExtendsField;
 
 pub use crate::{
     builtins::NODEJS_BUILTINS,
@@ -61,11 +62,11 @@ use crate::{
     cache::{Cache, CachedPath},
     context::ResolveContext as Ctx,
     file_system::FileSystemOs,
-    package_json::{ExportsField, ExportsKey, MatchObject},
     path::{PathUtil, SLASH_START},
     specifier::Specifier,
     tsconfig::{ProjectReference, TsConfig},
 };
+use nodejs_package_json::{ImportExportField, ImportExportKey, ImportExportMap};
 
 type ResolveResult = Result<Option<CachedPath>, ResolveError>;
 
@@ -752,6 +753,7 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
         if !package_json.exports.is_empty() {
             // 4. If the SCOPE/package.json "name" is not the first segment of X, return.
             if let Some(subpath) = package_json
+                .data
                 .name
                 .as_ref()
                 .and_then(|package_name| Self::strip_package_name(specifier, package_name))
@@ -1000,7 +1002,8 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
             tracing::trace!(tsconfig = ?tsconfig, "load_tsconfig");
 
             // Extend tsconfig
-            if let Some(tsconfig_extend_specifier) = &tsconfig.extends {
+            // TODO multiple extends
+            if let Some(ExtendsField::Single(tsconfig_extend_specifier)) = &tsconfig.data.extends {
                 let extended_tsconfig_path = match tsconfig_extend_specifier.as_bytes().first() {
                     None => {
                         return Err(ResolveError::Specifier(SpecifierError::Empty(
@@ -1131,17 +1134,18 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
         &self,
         package_url: &Path,
         subpath: &str,
-        exports: &ExportsField,
+        exports: &ImportExportField,
         conditions: &[String],
         ctx: &mut Ctx,
     ) -> ResolveResult {
         // 1. If exports is an Object with both a key starting with "." and a key not starting with ".", throw an Invalid Package Configuration error.
-        if let ExportsField::Map(map) = exports {
+        if let ImportExportField::Map(map) = exports {
             let mut has_dot = false;
             let mut without_dot = false;
             for key in map.keys() {
-                has_dot = has_dot || matches!(key, ExportsKey::Main | ExportsKey::Pattern(_));
-                without_dot = without_dot || matches!(key, ExportsKey::CustomCondition(_));
+                has_dot =
+                    has_dot || matches!(key, ImportExportKey::Main | ImportExportKey::Pattern(_));
+                without_dot = without_dot || matches!(key, ImportExportKey::CustomCondition(_));
                 if has_dot && without_dot {
                     return Err(ResolveError::InvalidPackageConfig(
                         package_url.join("package.json"),
@@ -1165,18 +1169,18 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
             }
             // 1. Let mainExport be undefined.
             let main_export = match exports {
-                ExportsField::None => None,
+                ImportExportField::None => None,
                 // 2. If exports is a String or Array, or an Object containing no keys starting with ".", then
-                ExportsField::String(_) | ExportsField::Array(_) => {
+                ImportExportField::String(_) | ImportExportField::Array(_) => {
                     // 1. Set mainExport to exports.
                     Some(exports)
                 }
                 // 3. Otherwise if exports is an Object containing a "." property, then
-                ExportsField::Map(map) => {
+                ImportExportField::Map(map) => {
                     // 1. Set mainExport to exports["."].
-                    map.get(&ExportsKey::Main).map_or_else(
+                    map.get(&ImportExportKey::Main).map_or_else(
                         || {
-                            if map.keys().any(|key| matches!(key, ExportsKey::Pattern(_))) {
+                            if map.keys().any(|key| matches!(key, ImportExportKey::Pattern(_))) {
                                 None
                             } else {
                                 Some(exports)
@@ -1205,7 +1209,7 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
             }
         }
         // 3. Otherwise, if exports is an Object and all keys of exports start with ".", then
-        if let ExportsField::Map(exports) = exports {
+        if let ImportExportField::Map(exports) = exports {
             // 1. Let matchKey be the string "./" concatenated with subpath.
             // Note: `package_imports_exports_resolve` does not require the leading dot.
             let match_key = &subpath;
@@ -1273,7 +1277,7 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
     fn package_imports_exports_resolve(
         &self,
         match_key: &str,
-        match_obj: &MatchObject,
+        match_obj: &ImportExportMap,
         package_url: &Path,
         is_imports: bool,
         conditions: &[String],
@@ -1287,7 +1291,7 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
         // 1. If matchKey is a key of matchObj and does not contain "*", then
         if !match_key.contains('*') {
             // 1. Let target be the value of matchObj[matchKey].
-            if let Some(target) = match_obj.get(&ExportsKey::Pattern(match_key.to_string())) {
+            if let Some(target) = match_obj.get(&ImportExportKey::Pattern(match_key.to_string())) {
                 // 2. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, null, isImports, conditions).
                 return self.package_target_resolve(
                     package_url,
@@ -1307,7 +1311,7 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
         // 2. Let expansionKeys be the list of keys of matchObj containing only a single "*", sorted by the sorting function PATTERN_KEY_COMPARE which orders in descending order of specificity.
         // 3. For each key expansionKey in expansionKeys, do
         for (expansion_key, target) in match_obj {
-            if let ExportsKey::Pattern(expansion_key) = expansion_key {
+            if let ImportExportKey::Pattern(expansion_key) = expansion_key {
                 // 1. Let patternBase be the substring of expansionKey up to but excluding the first "*" character.
                 if let Some((pattern_base, pattern_trailer)) = expansion_key.split_once('*') {
                     // 2. If matchKey starts with but is not equal to patternBase, then
@@ -1360,7 +1364,7 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
         &self,
         package_url: &Path,
         target_key: &str,
-        target: &ExportsField,
+        target: &ImportExportField,
         pattern_match: Option<&str>,
         is_imports: bool,
         conditions: &[String],
@@ -1393,9 +1397,9 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
         }
 
         match target {
-            ExportsField::None => {}
+            ImportExportField::None => {}
             // 1. If target is a String, then
-            ExportsField::String(target) => {
+            ImportExportField::String(target) => {
                 // 1. If target does not start with "./", then
                 if !target.starts_with("./") {
                     // 1. If isImports is false, or if target starts with "../" or "/", or if target is a valid URL, then
@@ -1436,14 +1440,14 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
                 return Ok(Some(value));
             }
             // 2. Otherwise, if target is a non-null Object, then
-            ExportsField::Map(target) => {
+            ImportExportField::Map(target) => {
                 // 1. If exports contains any index property keys, as defined in ECMA-262 6.1.7 Array Index, throw an Invalid Package Configuration error.
                 // 2. For each property p of target, in object insertion order as,
                 for (i, (key, target_value)) in target.iter().enumerate() {
                     // https://nodejs.org/api/packages.html#conditional-exports
                     // "default" - the generic fallback that always matches. Can be a CommonJS or ES module file. This condition should always come last.
                     // Note: node.js does not throw this but enhanced-resolve does.
-                    let is_default = matches!(key, ExportsKey::CustomCondition(condition) if condition == "default");
+                    let is_default = matches!(key, ImportExportKey::CustomCondition(condition) if condition == "default");
                     if i < target.len() - 1 && is_default {
                         return Err(ResolveError::InvalidPackageConfigDefault(
                             package_url.join("package.json"),
@@ -1452,7 +1456,7 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
 
                     // 1. If p equals "default" or conditions contains an entry for p, then
                     if is_default
-                        || matches!(key, ExportsKey::CustomCondition(condition) if conditions.contains(condition))
+                        || matches!(key, ImportExportKey::CustomCondition(condition) if conditions.contains(condition))
                     {
                         // 1. Let targetValue be the value of the p property in target.
                         // 2. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions).
@@ -1476,7 +1480,7 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
                 return Ok(None);
             }
             // 3. Otherwise, if target is an Array, then
-            ExportsField::Array(targets) => {
+            ImportExportField::Array(targets) => {
                 // 1. If _target.length is zero, return null.
                 if targets.is_empty() {
                     // Note: return PackagePathNotExported has the same effect as return because there are no matches.
diff --git a/src/options.rs b/src/options.rs
index 2c4ef7b..704c88c 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -133,6 +133,10 @@ pub struct ResolveOptions {
     ///
     /// Default `false`
     pub builtin_modules: bool,
+
+    /// When enabled, will attempt to reduce memory usage by clearing data and fields
+    /// that are not relevant to the current process, and should not be held in memory.
+    pub remove_unused_fields: bool,
 }
 
 impl ResolveOptions {
@@ -373,6 +377,7 @@ impl Default for ResolveOptions {
             roots: vec![],
             symlinks: true,
             builtin_modules: false,
+            remove_unused_fields: true,
         }
     }
 }
diff --git a/src/package_json.rs b/src/package_json.rs
index 7fbf359..feb04fb 100644
--- a/src/package_json.rs
+++ b/src/package_json.rs
@@ -2,38 +2,35 @@
 //!
 //! Code related to export field are copied from [Parcel's resolver](https://github.com/parcel-bundler/parcel/blob/v2/packages/utils/node-resolver-rs/src/package_json.rs)
 use std::{
-    hash::BuildHasherDefault,
     path::{Path, PathBuf},
     sync::Arc,
 };
 
-use indexmap::IndexMap;
-use rustc_hash::{FxHashMap, FxHasher};
-use serde::{Deserialize, Deserializer};
+use nodejs_package_json::{
+    BrowserField, ImportExportField, ImportExportMap, PackageJson as BasePackageJson,
+};
+use rustc_hash::FxHashMap;
+use serde::Deserialize;
+use serde_json::Value;
 
 use crate::{path::PathUtil, ResolveError, ResolveOptions};
 
-type FxIndexMap<K, V> = IndexMap<K, V, BuildHasherDefault<FxHasher>>;
-
 /// Deserialized package.json
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Default, Deserialize)]
 pub struct PackageJson {
     /// Path to `package.json`. Contains the `package.json` filename.
     #[serde(skip)]
-    pub(crate) path: PathBuf,
+    pub path: PathBuf,
 
     /// Realpath to `package.json`. Contains the `package.json` filename.
     #[serde(skip)]
-    pub(crate) realpath: PathBuf,
+    pub realpath: PathBuf,
 
     #[serde(skip)]
     pub(crate) raw_json: Arc<serde_json::Value>,
 
-    /// The "name" field defines your package's name.
-    /// The "name" field can be used in addition to the "exports" field to self-reference a package using its name.
-    ///
-    /// <https://nodejs.org/api/packages.html#name>
-    pub(crate) name: Option<String>,
+    /// The deserialized `package.json`.
+    pub data: BasePackageJson,
 
     /// The "main" field defines the entry point of a package when imported by name via a node_modules lookup. Its value is a path.
     /// When a package has an "exports" field, this will take precedence over the "main" field when importing the package by name.
@@ -42,78 +39,26 @@ pub struct PackageJson {
     ///
     /// <https://nodejs.org/api/packages.html#main>
     #[serde(skip)]
-    pub(crate) main_fields: Vec<String>,
+    pub main_fields: Vec<String>,
 
     /// The "exports" field allows defining the entry points of a package when imported by name loaded either via a node_modules lookup or a self-reference to its own name.
     ///
     /// <https://nodejs.org/api/packages.html#exports>
     #[serde(skip)]
-    pub(crate) exports: Vec<ExportsField>,
+    pub exports: Vec<ImportExportField>,
 
     /// In addition to the "exports" field, there is a package "imports" field to create private mappings that only apply to import specifiers from within the package itself.
     ///
     /// <https://nodejs.org/api/packages.html#subpath-imports>
     #[serde(default)]
-    pub(crate) imports: Box<MatchObject>,
+    pub imports: Box<ImportExportMap>,
 
     /// The "browser" field is provided by a module author as a hint to javascript bundlers or component tools when packaging modules for client side use.
     /// Multiple values are configured by [ResolveOptions::alias_fields].
     ///
     /// <https://github.com/defunctzombie/package-browser-field-spec>
     #[serde(skip)]
-    pub(crate) browser_fields: Vec<BrowserField>,
-}
-
-/// `matchObj` defined in `PACKAGE_IMPORTS_EXPORTS_RESOLVE`
-/// This is an IndexMap provided by the `preserve_order` feature.
-pub type MatchObject = FxIndexMap<ExportsKey, ExportsField>;
-
-#[derive(Debug, Default, Deserialize)]
-#[serde(untagged)]
-pub enum ExportsField {
-    #[default]
-    None, // For `undefined` or `null` value.
-    String(String),
-    Array(Vec<ExportsField>),
-    Map(MatchObject),
-}
-
-#[derive(Debug, Eq, PartialEq, Hash)]
-pub enum ExportsKey {
-    Main,
-    Pattern(String),
-    CustomCondition(String),
-}
-
-impl From<&str> for ExportsKey {
-    fn from(key: &str) -> Self {
-        if key == "." {
-            Self::Main
-        } else if key.starts_with("./") {
-            Self::Pattern(key.trim_start_matches('.').to_string())
-        } else if key.starts_with('#') {
-            Self::Pattern(key.to_string())
-        } else {
-            Self::CustomCondition(key.to_string())
-        }
-    }
-}
-
-impl<'de> Deserialize<'de> for ExportsKey {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        let s: &'de str = Deserialize::deserialize(deserializer)?;
-        Ok(Self::from(s))
-    }
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(untagged)]
-pub enum BrowserField {
-    String(String),
-    Map(FxHashMap<PathBuf, serde_json::Value>),
+    pub browser_fields: Vec<BrowserField>,
 }
 
 impl PackageJson {
@@ -125,85 +70,137 @@ impl PackageJson {
         json: &str,
         options: &ResolveOptions,
     ) -> Result<Self, serde_json::Error> {
-        let mut package_json_value: serde_json::Value = serde_json::from_str(json)?;
-        let mut package_json: Self = Self::deserialize(&package_json_value)?;
+        let mut raw_json: Value = serde_json::from_str(json)?;
+        let mut data: BasePackageJson = serde_json::from_value(raw_json.clone())?;
+
+        let mut package_json = Self::default();
         package_json.main_fields.reserve_exact(options.main_fields.len());
         package_json.browser_fields.reserve_exact(options.alias_fields.len());
         package_json.exports.reserve_exact(options.exports_fields.len());
 
-        if let Some(package_json_value) = package_json_value.as_object_mut() {
-            // Dynamically create `main_fields`.
-            for main_field_key in &options.main_fields {
-                // Using `get` + `clone` instead of remove here
-                // because `main_fields` may contain `browser`, which is also used in `browser_fields.
-                if let Some(serde_json::Value::String(value)) =
-                    package_json_value.get(main_field_key)
-                {
-                    package_json.main_fields.push(value.clone());
+        // Dynamically create `main_fields`.
+        for main_field_key in &options.main_fields {
+            match main_field_key.as_str() {
+                "main" => {
+                    if let Some(main_field) = &data.main {
+                        package_json.main_fields.push(main_field.to_string_lossy().to_string());
+                    }
                 }
-            }
+                "module" => {
+                    if let Some(module_field) = &data.module {
+                        package_json.main_fields.push(module_field.to_string_lossy().to_string());
+                    }
+                }
+                "browser" => {
+                    if let Some(BrowserField::String(browser_field)) = &data.browser {
+                        package_json.main_fields.push(browser_field.to_owned());
+                    }
+                }
+                field => {
+                    // Using `get` + `clone` instead of remove here
+                    // because `main_fields` may contain `browser`, which is also used in `browser_fields.
+                    if let Some(Value::String(value)) = data.other_fields.get(field) {
+                        package_json.main_fields.push(value.clone());
+                    }
+                }
+            };
         }
 
         // Dynamically create `browser_fields`.
         let dir = path.parent().unwrap();
         for object_path in &options.alias_fields {
-            if let Some(browser_field) = Self::get_value_by_path(&package_json_value, object_path) {
-                let mut browser_field = BrowserField::deserialize(browser_field)?;
-                // Normalize all relative paths to make browser_field a constant value lookup
-                if let BrowserField::Map(map) = &mut browser_field {
-                    let keys = map.keys().cloned().collect::<Vec<_>>();
-                    for key in keys {
-                        // Normalize the key if it looks like a file "foo.js"
-                        if key.extension().is_some() {
-                            map.insert(dir.normalize_with(&key), map[&key].clone());
-                        }
-                        // Normalize the key if it is relative path "./relative"
-                        if key.starts_with(".") {
-                            if let Some(value) = map.remove(&key) {
-                                map.insert(dir.normalize_with(&key), value);
-                            }
+            let mut browser_field = if object_path.len() == 1 && object_path[0] == "browser" {
+                if let Some(field) = &data.browser {
+                    field.to_owned()
+                } else {
+                    continue;
+                }
+            } else if let Some(field) = Self::get_value_by_path(&data.other_fields, object_path) {
+                BrowserField::deserialize(field)?
+            } else {
+                continue;
+            };
+
+            // Normalize all relative paths to make browser_field a constant value lookup
+            if let BrowserField::Map(map) = &mut browser_field {
+                let keys = map.keys().cloned().collect::<Vec<_>>();
+                for key in keys {
+                    // Normalize the key if it looks like a file "foo.js"
+                    if key.extension().is_some() {
+                        map.insert(dir.normalize_with(&key), map[&key].clone());
+                    }
+                    // Normalize the key if it is relative path "./relative"
+                    if key.starts_with(".") {
+                        if let Some(value) = map.remove(&key) {
+                            map.insert(dir.normalize_with(&key), value);
                         }
                     }
                 }
-                package_json.browser_fields.push(browser_field);
             }
+            package_json.browser_fields.push(browser_field);
         }
 
         // Dynamically create `exports`.
         for object_path in &options.exports_fields {
-            if let Some(exports) = Self::get_value_by_path(&package_json_value, object_path) {
-                let exports = ExportsField::deserialize(exports)?;
-                package_json.exports.push(exports);
+            if object_path.len() == 1 && object_path[0] == "exports" {
+                if let Some(exports) = &data.exports {
+                    package_json.exports.push(exports.to_owned());
+                }
+            } else if let Some(exports) = Self::get_value_by_path(&data.other_fields, object_path) {
+                package_json.exports.push(ImportExportField::deserialize(exports)?);
             }
         }
 
+        // Bubble up `imports`
+        if let Some(imports) = &data.imports {
+            package_json.imports = Box::new(imports.to_owned());
+        }
+
         package_json.path = path;
         package_json.realpath = realpath;
 
-        // Remove large fields that useless for pragmatic use, see [raw_json] API
-        if let Some(package_json_value) = package_json_value.as_object_mut() {
-            package_json_value.remove("description");
-            package_json_value.remove("keywords");
-            package_json_value.remove("scripts");
-            package_json_value.remove("dependencies");
-            package_json_value.remove("devDependencies");
-            package_json_value.remove("peerDependencies");
-            package_json_value.remove("optionalDependencies");
+        // Remove large fields that are useless for pragmatic use
+        if options.remove_unused_fields {
+            data.scripts = None;
+            data.dependencies = None;
+            data.dependencies_meta = None;
+            data.dev_dependencies = None;
+            data.peer_dependencies = None;
+            data.peer_dependencies_meta = None;
+            data.bundle_dependencies = None;
+            data.optional_dependencies = None;
+            data.imports = None;
+            data.exports = None;
+            data.browser = None;
+
+            if let Some(raw_json) = raw_json.as_object_mut() {
+                raw_json.remove("description");
+                raw_json.remove("keywords");
+                raw_json.remove("scripts");
+                raw_json.remove("dependencies");
+                raw_json.remove("devDependencies");
+                raw_json.remove("peerDependencies");
+                raw_json.remove("optionalDependencies");
+            }
         }
 
-        package_json.raw_json = Arc::new(package_json_value);
+        package_json.data = data;
+        package_json.raw_json = Arc::new(raw_json);
+
         Ok(package_json)
     }
 
     fn get_value_by_path<'a>(
-        package_json: &'a serde_json::Value,
+        fields: &'a FxHashMap<String, serde_json::Value>,
         path: &[String],
     ) -> Option<&'a serde_json::Value> {
         if path.is_empty() {
             return None;
         }
-        let mut value = package_json;
-        for key in path {
+        let Some(mut value) = fields.get(&path[0]) else {
+            return None;
+        };
+        for key in path.iter().skip(1) {
             if let Some(inner_value) = value.as_object().and_then(|o| o.get(key)) {
                 value = inner_value;
             } else {
diff --git a/src/tsconfig.rs b/src/tsconfig.rs
index c16a908..fc1b0f2 100644
--- a/src/tsconfig.rs
+++ b/src/tsconfig.rs
@@ -1,15 +1,11 @@
 use std::{
-    hash::BuildHasherDefault,
     path::{Path, PathBuf},
     sync::Arc,
 };
 
 use crate::PathUtil;
-use indexmap::IndexMap;
-use rustc_hash::FxHasher;
 use serde::Deserialize;
-
-type FxIndexMap<K, V> = IndexMap<K, V, BuildHasherDefault<FxHasher>>;
+use typescript_tsconfig_json::{CompilerOptions, TsConfigJson};
 
 #[derive(Debug, Deserialize)]
 #[serde(rename_all = "camelCase")]
@@ -18,16 +14,16 @@ pub struct TsConfig {
     #[serde(skip)]
     path: PathBuf,
 
-    /// The value of extends must be a string containing a path to another configuration file to inherit from.
-    /// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#configuration-inheritance
-    #[serde(default)]
-    pub extends: Option<String>,
+    // Base url for compiler option paths.
+    #[serde(skip)]
+    paths_base: PathBuf,
 
-    #[serde(default)]
-    pub references: Vec<ProjectReference>,
+    /// The deserialized `tsconfig.json`.
+    pub data: TsConfigJson,
 
+    /// Bubbled up project references with a reference to their tsconfig.
     #[serde(default)]
-    pub compiler_options: CompilerOptions,
+    pub references: Vec<ProjectReference>,
 }
 
 /// Project Reference
@@ -43,28 +39,35 @@ pub struct ProjectReference {
     pub tsconfig: Option<Arc<TsConfig>>,
 }
 
-#[derive(Debug, Default, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct CompilerOptions {
-    base_url: Option<PathBuf>,
-    paths: Option<FxIndexMap<String, Vec<String>>>,
-    #[serde(skip)]
-    paths_base: PathBuf,
-}
-
 impl TsConfig {
     pub fn parse(path: &Path, json: &mut str) -> Result<Self, serde_json::Error> {
         _ = json_strip_comments::strip(json);
-        let mut tsconfig: Self = serde_json::from_str(json)?;
-        tsconfig.path = path.to_path_buf();
+
+        let data: TsConfigJson = serde_json::from_str(json)?;
+
+        let mut tsconfig = Self {
+            path: path.to_path_buf(),
+            paths_base: PathBuf::new(),
+            references: data.references.as_ref().map_or_else(Vec::new, |refs| {
+                refs.iter()
+                    .map(|pr| ProjectReference { path: pr.path.clone(), tsconfig: None })
+                    .collect()
+            }),
+            data,
+        };
         let directory = tsconfig.directory().to_path_buf();
-        if let Some(base_url) = tsconfig.compiler_options.base_url {
-            tsconfig.compiler_options.base_url = Some(directory.normalize_with(base_url));
-        }
-        if tsconfig.compiler_options.paths.is_some() {
-            tsconfig.compiler_options.paths_base =
-                tsconfig.compiler_options.base_url.as_ref().map_or(directory, Clone::clone);
+
+        if let Some(compiler_options) = &mut tsconfig.data.compiler_options {
+            if let Some(base_url) = &compiler_options.base_url {
+                compiler_options.base_url = Some(directory.normalize_with(base_url));
+            }
+
+            if compiler_options.paths.is_some() {
+                tsconfig.paths_base =
+                    compiler_options.base_url.as_ref().map_or(directory, Clone::clone);
+            }
         }
+
         Ok(tsconfig)
     }
 
@@ -79,23 +82,29 @@ impl TsConfig {
     }
 
     fn base_path(&self) -> &Path {
-        self.compiler_options
-            .base_url
+        self.data
+            .compiler_options
             .as_ref()
+            .and_then(|opt| opt.base_url.as_ref())
             .map_or_else(|| self.directory(), |path| path.as_ref())
     }
 
     pub fn extend_tsconfig(&mut self, tsconfig: &Self) {
-        let compiler_options = &mut self.compiler_options;
-        if compiler_options.paths.is_none() {
-            compiler_options.paths_base = compiler_options
-                .base_url
-                .as_ref()
-                .map_or_else(|| tsconfig.compiler_options.paths_base.clone(), Clone::clone);
-            compiler_options.paths = tsconfig.compiler_options.paths.clone();
-        }
-        if compiler_options.base_url.is_none() {
-            compiler_options.base_url = tsconfig.compiler_options.base_url.clone();
+        if let Some(their_options) = &tsconfig.data.compiler_options {
+            let my_options = self.data.compiler_options.get_or_insert(CompilerOptions::default());
+
+            if my_options.paths.is_none() {
+                self.paths_base = my_options
+                    .base_url
+                    .as_ref()
+                    .map_or_else(|| tsconfig.paths_base.clone(), Clone::clone);
+
+                my_options.paths = their_options.paths.clone();
+            }
+
+            if my_options.base_url.is_none() {
+                my_options.base_url = their_options.base_url.clone();
+            }
         }
     }
 
@@ -122,12 +131,15 @@ impl TsConfig {
         }
 
         let base_url_iter = self
+            .data
             .compiler_options
-            .base_url
             .as_ref()
+            .and_then(|opt| opt.base_url.as_ref())
             .map_or_else(Vec::new, |base_url| vec![base_url.normalize_with(specifier)]);
 
-        let Some(paths_map) = &self.compiler_options.paths else {
+        let Some(paths_map) =
+            self.data.compiler_options.as_ref().and_then(|opt| opt.paths.as_ref())
+        else {
             return base_url_iter;
         };
 
@@ -166,10 +178,6 @@ impl TsConfig {
             Clone::clone,
         );
 
-        paths
-            .into_iter()
-            .map(|p| self.compiler_options.paths_base.normalize_with(p))
-            .chain(base_url_iter)
-            .collect()
+        paths.into_iter().map(|p| self.paths_base.normalize_with(p)).chain(base_url_iter).collect()
     }
 }


Test patch:
diff --git a/src/tests/exports_field.rs b/src/tests/exports_field.rs
index 63a3705..7e4d081 100644
--- a/src/tests/exports_field.rs
+++ b/src/tests/exports_field.rs
@@ -2,7 +2,7 @@
 //!
 //! The huge exports field test cases are at the bottom of this file.
 
-use crate::{Ctx, ExportsField, PathUtil, ResolveError, ResolveOptions, Resolver};
+use crate::{Ctx, ImportExportField, PathUtil, ResolveError, ResolveOptions, Resolver};
 use serde_json::json;
 use std::path::Path;
 
@@ -276,13 +276,13 @@ fn extension_alias_throw_error() {
 struct TestCase {
     name: &'static str,
     expect: Option<Vec<&'static str>>,
-    exports_field: ExportsField,
+    exports_field: ImportExportField,
     request: &'static str,
     condition_names: Vec<&'static str>,
 }
 
 #[allow(clippy::needless_pass_by_value)]
-fn exports_field(value: serde_json::Value) -> ExportsField {
+fn exports_field(value: serde_json::Value) -> ImportExportField {
     let s = serde_json::to_string(&value).unwrap();
     serde_json::from_str(&s).unwrap()
 }
diff --git a/src/tests/imports_field.rs b/src/tests/imports_field.rs
index ab431d8..99cb1b0 100644
--- a/src/tests/imports_field.rs
+++ b/src/tests/imports_field.rs
@@ -4,7 +4,7 @@
 
 use serde_json::json;
 
-use crate::{Ctx, MatchObject, PathUtil, ResolveError, ResolveOptions, Resolver};
+use crate::{Ctx, ImportExportMap, PathUtil, ResolveError, ResolveOptions, Resolver};
 use std::path::Path;
 
 #[test]
@@ -68,13 +68,13 @@ fn test() {
 struct TestCase {
     name: &'static str,
     expect: Option<Vec<&'static str>>,
-    imports_field: MatchObject,
+    imports_field: ImportExportMap,
     request: &'static str,
     condition_names: Vec<&'static str>,
 }
 
 #[allow(clippy::needless_pass_by_value)]
-fn imports_field(value: serde_json::Value) -> MatchObject {
+fn imports_field(value: serde_json::Value) -> ImportExportMap {
     let s = serde_json::to_string(&value).unwrap();
     serde_json::from_str(&s).unwrap()
 }
diff --git a/tests/integration_test.rs b/tests/integration_test.rs
index 213f71a..2130304 100644
--- a/tests/integration_test.rs
+++ b/tests/integration_test.rs
@@ -35,7 +35,11 @@ fn eq() {
 #[test]
 fn package_json() {
     let resolution = resolve("./tests/package.json");
-    assert!(resolution.package_json().is_some_and(|json| json.raw_json().is_object()));
+    assert!(resolution.package_json().is_some_and(|json| json
+        .data
+        .name
+        .as_ref()
+        .is_some_and(|name| name == "name")));
 }
 
 #[test]
diff --git a/tests/package.json b/tests/package.json
index 2c63c08..f099adc 100644
--- a/tests/package.json
+++ b/tests/package.json
@@ -1,2 +1,3 @@
 {
+  "name": "name"
 }