Repository: oxc-project/oxc-resolver (rust)

Issue:
tsconfig `extends` field can be an array
need to revert https://github.com/oxc-project/oxc-resolver/pull/80, see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#supporting-multiple-configuration-files-in-extends

This was buried so deep in the docs that I couldn't find it until someone brought it up.

Pull request description:
feat: support tsconfig#extends array
closes #101

Patch:
diff --git a/fixtures/parcel/tsconfig/extends-multiple/foo.js b/fixtures/parcel/tsconfig/extends-multiple/foo.js
new file mode 100644
index 0000000..e69de29
diff --git a/fixtures/parcel/tsconfig/extends-multiple/tsconfig.json b/fixtures/parcel/tsconfig/extends-multiple/tsconfig.json
new file mode 100644
index 0000000..aa2ae78
--- /dev/null
+++ b/fixtures/parcel/tsconfig/extends-multiple/tsconfig.json
@@ -0,0 +1,6 @@
+{
+  "extends": ["../extends-extension", "../extends-paths"],
+  "compilerOptions": {
+    "baseUrl": ".",
+  },
+}
diff --git a/src/lib.rs b/src/lib.rs
index 551b9dd..dca1738 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1002,40 +1002,21 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
             tracing::trace!(tsconfig = ?tsconfig, "load_tsconfig");
 
             // Extend tsconfig
-            // 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(
-                            tsconfig_extend_specifier.to_string(),
-                        )))
+            if let Some(extends) = &tsconfig.data.extends {
+                let extended_tsconfig_paths = match extends {
+                    ExtendsField::Single(s) => {
+                        vec![self.get_extended_tsconfig_path(&directory, tsconfig, s)?]
                     }
-                    Some(b'/') => PathBuf::from(tsconfig_extend_specifier),
-                    Some(b'.') => tsconfig.directory().normalize_with(tsconfig_extend_specifier),
-                    _ => self
-                        .clone_with_options(ResolveOptions {
-                            description_files: vec![],
-                            extensions: vec![".json".into()],
-                            main_files: vec!["tsconfig.json".into()],
-                            ..ResolveOptions::default()
-                        })
-                        .load_package_self_or_node_modules(
-                            &directory,
-                            tsconfig_extend_specifier,
-                            &mut Ctx::default(),
-                        )
-                        .map_err(|err| match err {
-                            ResolveError::NotFound(_) => ResolveError::TsconfigNotFound(
-                                PathBuf::from(tsconfig_extend_specifier),
-                            ),
-                            _ => err,
-                        })?
-                        .to_path_buf(),
+                    ExtendsField::Multiple(specifiers) => specifiers
+                        .iter()
+                        .map(|s| self.get_extended_tsconfig_path(&directory, tsconfig, s))
+                        .collect::<Result<Vec<PathBuf>, ResolveError>>()?,
                 };
-
-                let extended_tsconfig =
-                    self.load_tsconfig(&extended_tsconfig_path, &TsconfigReferences::Disabled)?;
-                tsconfig.extend_tsconfig(&extended_tsconfig);
+                for extended_tsconfig_path in extended_tsconfig_paths {
+                    let extended_tsconfig =
+                        self.load_tsconfig(&extended_tsconfig_path, &TsconfigReferences::Disabled)?;
+                    tsconfig.extend_tsconfig(&extended_tsconfig);
+                }
             }
 
             // Load project references
@@ -1063,6 +1044,34 @@ impl<Fs: FileSystem + Default> ResolverGeneric<Fs> {
         })
     }
 
+    fn get_extended_tsconfig_path(
+        &self,
+        directory: &CachedPath,
+        tsconfig: &TsConfig,
+        specifier: &str,
+    ) -> Result<PathBuf, ResolveError> {
+        match specifier.as_bytes().first() {
+            None => Err(ResolveError::Specifier(SpecifierError::Empty(specifier.to_string()))),
+            Some(b'/') => Ok(PathBuf::from(specifier)),
+            Some(b'.') => Ok(tsconfig.directory().normalize_with(specifier)),
+            _ => self
+                .clone_with_options(ResolveOptions {
+                    description_files: vec![],
+                    extensions: vec![".json".into()],
+                    main_files: vec!["tsconfig.json".into()],
+                    ..ResolveOptions::default()
+                })
+                .load_package_self_or_node_modules(directory, specifier, &mut Ctx::default())
+                .map(|p| p.to_path_buf())
+                .map_err(|err| match err {
+                    ResolveError::NotFound(_) => {
+                        ResolveError::TsconfigNotFound(PathBuf::from(specifier))
+                    }
+                    _ => err,
+                }),
+        }
+    }
+
     /// PACKAGE_RESOLVE(packageSpecifier, parentURL)
     fn package_resolve(
         &self,


Test patch:
diff --git a/src/tests/tsconfig_paths.rs b/src/tests/tsconfig_paths.rs
index 240e337..3511848 100644
--- a/src/tests/tsconfig_paths.rs
+++ b/src/tests/tsconfig_paths.rs
@@ -22,6 +22,7 @@ fn tsconfig() {
         (f.join("tsconfig/extends-extension"), None, "foo", f.join("tsconfig/extends-extension/foo.js")),
         (f.join("tsconfig/extends-extensionless"), None, "foo", f.join("node_modules/tsconfig-field/foo.js")),
         (f.join("tsconfig/extends-paths"), Some("src"), "@/index", f.join("tsconfig/extends-paths/src/index.js")),
+        (f.join("tsconfig/extends-multiple"), None, "foo", f.join("tsconfig/extends-multiple/foo.js")),
     ];
 
     for (dir, subdir, request, expected) in pass {