From de9d59eecfdc6cabb53dafc4b6bdf0d1829c2d65 Mon Sep 17 00:00:00 2001 From: Boshen Date: Sat, 7 Oct 2023 17:26:21 +0800 Subject: [PATCH] feat(resolver): configurable tsconfig project references closes #942 --- crates/oxc_resolver/src/lib.rs | 47 +++++++++--- crates/oxc_resolver/src/options.rs | 38 +++++++++- .../oxc_resolver/src/tests/tsconfig_paths.rs | 29 ++++++-- .../src/tests/tsconfig_project_references.rs | 73 ++++++++++++++++++- crates/oxc_resolver/src/tsconfig.rs | 14 +--- 5 files changed, 164 insertions(+), 37 deletions(-) diff --git a/crates/oxc_resolver/src/lib.rs b/crates/oxc_resolver/src/lib.rs index ae03b68354b986..b8ee292800de4b 100644 --- a/crates/oxc_resolver/src/lib.rs +++ b/crates/oxc_resolver/src/lib.rs @@ -49,12 +49,15 @@ use crate::{ package_json::{ExportsField, ExportsKey, MatchObject}, path::PathUtil, specifier::Specifier, - tsconfig::TsConfig, + tsconfig::{ProjectReference, TsConfig}, }; pub use crate::{ error::{JSONError, ResolveError, SpecifierError}, file_system::{FileMetadata, FileSystem}, - options::{Alias, AliasValue, EnforceExtension, ResolveOptions, Restriction}, + options::{ + Alias, AliasValue, EnforceExtension, ResolveOptions, Restriction, TsconfigOptions, + TsconfigReferences, + }, package_json::PackageJson, resolution::Resolution, }; @@ -921,8 +924,9 @@ impl ResolverGeneric { specifier: &str, ctx: &mut ResolveContext, ) -> ResolveState { - let Some(tsconfig_path) = &self.options.tsconfig else { return Ok(None) }; - let tsconfig = self.load_tsconfig(tsconfig_path)?; + let Some(tsconfig_options) = &self.options.tsconfig else { return Ok(None) }; + let tsconfig = + self.load_tsconfig(&tsconfig_options.config_file, &tsconfig_options.references)?; let paths = tsconfig.resolve(cached_path.path(), specifier); for path in paths { tracing::trace!(path = ?cached_path, tsconfig_path = ?path, "load_tsconfig_paths"); @@ -934,13 +938,17 @@ impl ResolverGeneric { Ok(None) } - fn load_tsconfig(&self, path: &Path) -> Result, ResolveError> { + fn load_tsconfig( + &self, + path: &Path, + references: &TsconfigReferences, + ) -> Result, ResolveError> { self.cache.tsconfig(path, |tsconfig| { let directory = self.cache.value(tsconfig.directory()); tracing::trace!(tsconfig = ?tsconfig, "load_tsconfig"); // Extend tsconfig let mut extended_tsconfig_paths = vec![]; - for tsconfig_extend_specifier in tsconfig.extends() { + for tsconfig_extend_specifier in &tsconfig.extends { let extended_tsconfig_path = match tsconfig_extend_specifier.as_bytes().first() { None => return Err(ResolveError::Specifier(SpecifierError::Empty)), Some(b'/') => PathBuf::from(tsconfig_extend_specifier), @@ -966,15 +974,30 @@ impl ResolverGeneric { extended_tsconfig_paths.push(extended_tsconfig_path); } for extended_tsconfig_path in extended_tsconfig_paths { - let extended_tsconfig = self.load_tsconfig(&extended_tsconfig_path)?; + let extended_tsconfig = + self.load_tsconfig(&extended_tsconfig_path, &TsconfigReferences::Disabled)?; tsconfig.extend_tsconfig(&extended_tsconfig); } // Load project references - let directory = tsconfig.directory().to_path_buf(); - for reference in tsconfig.references_mut() { - let reference_tsconfig_path = directory.normalize_with(&reference.path); - let tsconfig = self.cache.tsconfig(&reference_tsconfig_path, |_| Ok(()))?; - reference.tsconfig.replace(tsconfig); + match references { + TsconfigReferences::Disabled => { + tsconfig.references.drain(..); + } + TsconfigReferences::Auto => {} + TsconfigReferences::Paths(paths) => { + tsconfig.references = paths + .iter() + .map(|path| ProjectReference { path: path.clone(), tsconfig: None }) + .collect(); + } + } + if !tsconfig.references.is_empty() { + let directory = tsconfig.directory().to_path_buf(); + for reference in &mut tsconfig.references { + let reference_tsconfig_path = directory.normalize_with(&reference.path); + let tsconfig = self.cache.tsconfig(&reference_tsconfig_path, |_| Ok(()))?; + reference.tsconfig.replace(tsconfig); + } } Ok(()) }) diff --git a/crates/oxc_resolver/src/options.rs b/crates/oxc_resolver/src/options.rs index ee524f93f19370..eecc74d006ca33 100644 --- a/crates/oxc_resolver/src/options.rs +++ b/crates/oxc_resolver/src/options.rs @@ -11,7 +11,7 @@ pub struct ResolveOptions { /// Path to TypeScript configuration file. /// /// Default `None` - pub tsconfig: Option, + pub tsconfig: Option, /// Create aliases to import or require certain modules more easily. /// A trailing $ can also be added to the given object's keys to signify an exact match. @@ -171,6 +171,30 @@ pub enum Restriction { RegExp(String), } +/// Tsconfig Options +/// +/// Derived from [tsconfig-paths-webpack-plugin](https://github.com/dividab/tsconfig-paths-webpack-plugin#options) +#[derive(Debug, Clone)] +pub struct TsconfigOptions { + /// Allows you to specify where to find the TypeScript configuration file. + /// You may provide + /// * a relative path to the configuration file. It will be resolved relative to cwd. + /// * an absolute path to the configuration file. + pub config_file: PathBuf, + + /// Support for Typescript Project References. + pub references: TsconfigReferences, +} + +#[derive(Debug, Clone)] +pub enum TsconfigReferences { + Disabled, + /// Use the `references` field from tsconfig read from `config_file`. + Auto, + /// Manually provided relative or absolute path. + Paths(Vec), +} + impl Default for ResolveOptions { fn default() -> Self { Self { @@ -283,7 +307,10 @@ impl fmt::Display for ResolveOptions { #[cfg(test)] mod test { - use super::{AliasValue, EnforceExtension, ResolveOptions, Restriction}; + use super::{ + AliasValue, EnforceExtension, ResolveOptions, Restriction, TsconfigOptions, + TsconfigReferences, + }; use std::path::PathBuf; #[test] @@ -304,7 +331,10 @@ mod test { #[test] fn display() { let options = ResolveOptions { - tsconfig: Some(PathBuf::from("tsconfig.json")), + tsconfig: Some(TsconfigOptions { + config_file: PathBuf::from("tsconfig.json"), + references: TsconfigReferences::Auto, + }), alias: vec![("a".into(), vec![AliasValue::Ignore])], alias_fields: vec![vec!["browser".into()]], condition_names: vec!["require".into()], @@ -322,7 +352,7 @@ mod test { ..ResolveOptions::default() }; - let expected = r#"tsconfig:"tsconfig.json",alias:[("a", [Ignore])],alias_fields:[["browser"]],condition_names:["require"],enforce_extension:Enabled,exports_fields:[["exports"]],extension_alias:[(".js", [".ts"])],extensions:[".js", ".json", ".node"],fallback:[("fallback", [Ignore])],fully_specified:true,main_fields:["main"],main_files:["index"],modules:["node_modules"],resolve_to_context:true,prefer_relative:true,prefer_absolute:true,restrictions:[Path("restrictions")],roots:["roots"],symlinks:true,builtin_modules:true,"#; + let expected = r#"tsconfig:TsconfigOptions { config_file: "tsconfig.json", references: Auto },alias:[("a", [Ignore])],alias_fields:[["browser"]],condition_names:["require"],enforce_extension:Enabled,exports_fields:[["exports"]],extension_alias:[(".js", [".ts"])],extensions:[".js", ".json", ".node"],fallback:[("fallback", [Ignore])],fully_specified:true,main_fields:["main"],main_files:["index"],modules:["node_modules"],resolve_to_context:true,prefer_relative:true,prefer_absolute:true,restrictions:[Path("restrictions")],roots:["roots"],symlinks:true,builtin_modules:true,"#; assert_eq!(format!("{options}"), expected); } } diff --git a/crates/oxc_resolver/src/tests/tsconfig_paths.rs b/crates/oxc_resolver/src/tests/tsconfig_paths.rs index 1c6c502838cc16..7fc28816d82931 100644 --- a/crates/oxc_resolver/src/tests/tsconfig_paths.rs +++ b/crates/oxc_resolver/src/tests/tsconfig_paths.rs @@ -2,10 +2,15 @@ //! //! Fixtures copied from . -use super::memory_fs::MemoryFS; -use crate::{ResolveError, ResolveOptions, Resolver, ResolverGeneric, TsConfig}; use std::path::{Path, PathBuf}; +use super::memory_fs::MemoryFS; + +use crate::{ + ResolveError, ResolveOptions, Resolver, ResolverGeneric, TsConfig, TsconfigOptions, + TsconfigReferences, +}; + // #[test] fn tsconfig() { @@ -24,7 +29,10 @@ fn tsconfig() { for (path, request, expected) in pass { let resolver = Resolver::new(ResolveOptions { - tsconfig: Some(path.join("tsconfig.json")), + tsconfig: Some(TsconfigOptions { + config_file: path.join("tsconfig.json"), + references: TsconfigReferences::Auto, + }), ..ResolveOptions::default() }); let resolved_path = resolver.resolve(&path, request).map(|f| f.full_path()); @@ -37,7 +45,10 @@ fn tsconfig() { ]; let resolver = Resolver::new(ResolveOptions { - tsconfig: Some(f.join("tsconfig.json")), + tsconfig: Some(TsconfigOptions { + config_file: f.join("tsconfig.json"), + references: TsconfigReferences::Auto, + }), ..ResolveOptions::default() }); for (path, request, expected) in data { @@ -51,7 +62,10 @@ fn json_with_comments() { let f = super::fixture_root().join("parcel/tsconfig/trailing-comma"); let resolver = Resolver::new(ResolveOptions { - tsconfig: Some(f.join("tsconfig.json")), + tsconfig: Some(TsconfigOptions { + config_file: f.join("tsconfig.json"), + references: TsconfigReferences::Auto, + }), ..ResolveOptions::default() }); @@ -208,7 +222,10 @@ impl OneTest { let mut options = ResolveOptions { extensions: self.extensions.clone(), - tsconfig: Some(root.join("tsconfig.json")), + tsconfig: Some(TsconfigOptions { + config_file: root.join("tsconfig.json"), + references: TsconfigReferences::Auto, + }), ..ResolveOptions::default() }; if let Some(main_fields) = &self.main_fields { diff --git a/crates/oxc_resolver/src/tests/tsconfig_project_references.rs b/crates/oxc_resolver/src/tests/tsconfig_project_references.rs index aa0f880701768f..0fcea37e52f828 100644 --- a/crates/oxc_resolver/src/tests/tsconfig_project_references.rs +++ b/crates/oxc_resolver/src/tests/tsconfig_project_references.rs @@ -1,13 +1,16 @@ //! Tests for tsconfig project references -use crate::{ResolveOptions, Resolver}; +use crate::{ResolveError, ResolveOptions, Resolver, TsconfigOptions, TsconfigReferences}; #[test] -fn test() { +fn auto() { let f = super::fixture_root().join("tsconfig_project_references"); let resolver = Resolver::new(ResolveOptions { - tsconfig: Some(f.join("app")), + tsconfig: Some(TsconfigOptions { + config_file: f.join("app"), + references: TsconfigReferences::Auto, + }), ..ResolveOptions::default() }); @@ -16,7 +19,7 @@ fn test() { // Test normal paths alias (f.join("app"), "@/index.ts", f.join("app/aliased/index.ts")), (f.join("app"), "@/../index.ts", f.join("app/index.ts")), - // // Test project reference + // Test project reference (f.join("project_a"), "@/index.ts", f.join("project_a/aliased/index.ts")), (f.join("project_b/src"), "@/index.ts", f.join("project_b/src/aliased/index.ts")), // Does not have paths alias @@ -29,3 +32,65 @@ fn test() { assert_eq!(resolved_path, Ok(expected), "{request} {path:?}"); } } + +#[test] +fn disabled() { + let f = super::fixture_root().join("tsconfig_project_references"); + + let resolver = Resolver::new(ResolveOptions { + tsconfig: Some(TsconfigOptions { + config_file: f.join("app"), + references: TsconfigReferences::Disabled, + }), + ..ResolveOptions::default() + }); + + #[rustfmt::skip] + let pass = [ + // Test normal paths alias + (f.join("app"), "@/index.ts", Ok(f.join("app/aliased/index.ts"))), + (f.join("app"), "@/../index.ts", Ok(f.join("app/index.ts"))), + // Test project reference + (f.join("project_a"), "@/index.ts", Err(ResolveError::NotFound(f.join("project_a")))), + (f.join("project_b/src"), "@/index.ts", Err(ResolveError::NotFound(f.join("project_b/src")))), + // Does not have paths alias + (f.join("project_a"), "./index.ts", Ok(f.join("project_a/index.ts"))), + (f.join("project_c"), "./index.ts", Ok(f.join("project_c/index.ts"))), + ]; + + for (path, request, expected) in pass { + let resolved_path = resolver.resolve(&path, request).map(|f| f.full_path()); + assert_eq!(resolved_path, expected, "{request} {path:?}"); + } +} + +#[test] +fn manual() { + let f = super::fixture_root().join("tsconfig_project_references"); + + let resolver = Resolver::new(ResolveOptions { + tsconfig: Some(TsconfigOptions { + config_file: f.join("app"), + references: TsconfigReferences::Paths(vec!["../project_a/conf.json".into()]), + }), + ..ResolveOptions::default() + }); + + #[rustfmt::skip] + let pass = [ + // Test normal paths alias + (f.join("app"), "@/index.ts", Ok(f.join("app/aliased/index.ts"))), + (f.join("app"), "@/../index.ts", Ok(f.join("app/index.ts"))), + // Test project reference + (f.join("project_a"), "@/index.ts", Ok(f.join("project_a/aliased/index.ts"))), + (f.join("project_b/src"), "@/index.ts", Err(ResolveError::NotFound(f.join("project_b/src")))), + // Does not have paths alias + (f.join("project_a"), "./index.ts", Ok(f.join("project_a/index.ts"))), + (f.join("project_c"), "./index.ts", Ok(f.join("project_c/index.ts"))), + ]; + + for (path, request, expected) in pass { + let resolved_path = resolver.resolve(&path, request).map(|f| f.full_path()); + assert_eq!(resolved_path, expected, "{request} {path:?}"); + } +} diff --git a/crates/oxc_resolver/src/tsconfig.rs b/crates/oxc_resolver/src/tsconfig.rs index 7d38b30d945a97..1cd1927120b65c 100644 --- a/crates/oxc_resolver/src/tsconfig.rs +++ b/crates/oxc_resolver/src/tsconfig.rs @@ -19,13 +19,13 @@ pub struct TsConfig { path: PathBuf, #[serde(default, deserialize_with = "deserialize_extends")] - extends: Vec, + pub extends: Vec, #[serde(default)] - references: Vec, + pub references: Vec, #[serde(default)] - compiler_options: CompilerOptions, + pub compiler_options: CompilerOptions, } /// Project Reference @@ -92,14 +92,6 @@ impl TsConfig { self.path.parent().unwrap() } - pub fn extends(&self) -> &Vec { - &self.extends - } - - pub fn references_mut(&mut self) -> &mut Vec { - self.references.as_mut() - } - fn base_path(&self) -> &Path { self.compiler_options .base_url