From 5bbad73db59ea88c88e573b095e506898a46412f Mon Sep 17 00:00:00 2001 From: Boshen Date: Thu, 7 Sep 2023 14:01:48 +0800 Subject: [PATCH] feat(resolver): tsconfig project references (#862) closes #751 --- crates/oxc_resolver/README.md | 3 + crates/oxc_resolver/src/cache.rs | 15 +++-- crates/oxc_resolver/src/lib.rs | 62 ++++++++++++------- crates/oxc_resolver/src/tests/mod.rs | 1 + .../src/tests/tsconfig_project_references.rs | 32 ++++++++++ crates/oxc_resolver/src/tsconfig.rs | 42 +++++++++++++ .../app/aliased/index.ts | 0 .../tsconfig_project_references/app/index.ts | 0 .../app/tsconfig.json | 19 ++++++ .../project_a/aliased/index.ts | 0 .../project_a/conf.json | 8 +++ .../project_a/index.ts | 0 .../project_b/src/aliased/index.ts | 0 .../project_b/tsconfig.json | 9 +++ .../project_c/index.ts | 0 .../project_c/tsconfig.json | 5 ++ 16 files changed, 167 insertions(+), 29 deletions(-) create mode 100644 crates/oxc_resolver/src/tests/tsconfig_project_references.rs create mode 100644 crates/oxc_resolver/tests/tsconfig_project_references/app/aliased/index.ts create mode 100644 crates/oxc_resolver/tests/tsconfig_project_references/app/index.ts create mode 100644 crates/oxc_resolver/tests/tsconfig_project_references/app/tsconfig.json create mode 100644 crates/oxc_resolver/tests/tsconfig_project_references/project_a/aliased/index.ts create mode 100644 crates/oxc_resolver/tests/tsconfig_project_references/project_a/conf.json create mode 100644 crates/oxc_resolver/tests/tsconfig_project_references/project_a/index.ts create mode 100644 crates/oxc_resolver/tests/tsconfig_project_references/project_b/src/aliased/index.ts create mode 100644 crates/oxc_resolver/tests/tsconfig_project_references/project_b/tsconfig.json create mode 100644 crates/oxc_resolver/tests/tsconfig_project_references/project_c/index.ts create mode 100644 crates/oxc_resolver/tests/tsconfig_project_references/project_c/tsconfig.json diff --git a/crates/oxc_resolver/README.md b/crates/oxc_resolver/README.md index 72de1b6e9..caa4421c3 100644 --- a/crates/oxc_resolver/README.md +++ b/crates/oxc_resolver/README.md @@ -2,6 +2,9 @@ * [enhanced-resolve](https://github.com/webpack/enhanced-resolve) configurations * built-in [tsconfig-paths-webpack-plugin](https://github.com/dividab/tsconfig-paths-webpack-plugin) + * support extending tsconfig defined in `tsconfig.extends` + * support paths alias defined in `tsconfig.compilerOptions.paths` + * support project references defined `tsconfig.references` #### Resolver Options diff --git a/crates/oxc_resolver/src/cache.rs b/crates/oxc_resolver/src/cache.rs index 93706bffb..e4c0ac2c7 100644 --- a/crates/oxc_resolver/src/cache.rs +++ b/crates/oxc_resolver/src/cache.rs @@ -1,6 +1,6 @@ use once_cell::sync::OnceCell as OnceLock; use std::{ - borrow::Borrow, + borrow::{Borrow, Cow}, convert::AsRef, hash::{BuildHasherDefault, Hash, Hasher}, ops::Deref, @@ -45,18 +45,23 @@ impl Cache { pub fn tsconfig( &self, - tsconfig_path: &Path, + tsconfig_path: &CachedPath, callback: impl FnOnce(&mut TsConfig) -> Result<(), ResolveError>, // callback for modifying tsconfig with `extends` ) -> Result, ResolveError> { self.tsconfigs - .entry(tsconfig_path.to_path_buf()) + .entry(tsconfig_path.path().to_path_buf()) .or_try_insert_with(|| { + let tsconfig_path = if tsconfig_path.is_dir(&self.fs) { + Cow::Owned(tsconfig_path.path().join("tsconfig.json")) + } else { + Cow::Borrowed(tsconfig_path.path()) + }; let mut tsconfig_string = self .fs - .read_to_string(tsconfig_path) + .read_to_string(&tsconfig_path) .map_err(|_| ResolveError::NotFound(tsconfig_path.to_path_buf()))?; let mut tsconfig = - TsConfig::parse(tsconfig_path, &mut tsconfig_string).map_err(|error| { + TsConfig::parse(&tsconfig_path, &mut tsconfig_string).map_err(|error| { ResolveError::from_serde_json_error(tsconfig_path.to_path_buf(), &error) })?; callback(&mut tsconfig)?; diff --git a/crates/oxc_resolver/src/lib.rs b/crates/oxc_resolver/src/lib.rs index 0912f2d36..e2d1559f2 100644 --- a/crates/oxc_resolver/src/lib.rs +++ b/crates/oxc_resolver/src/lib.rs @@ -224,7 +224,9 @@ impl ResolverGeneric { } // tsconfig-paths - if let Some(path) = self.load_tsconfig_paths(specifier, &mut ResolveContext::default())? { + if let Some(path) = + self.load_tsconfig_paths(cached_path, specifier, &mut ResolveContext::default())? + { return Ok(path); } @@ -890,11 +892,16 @@ impl ResolverGeneric { Err(ResolveError::ExtensionAlias) } - fn load_tsconfig_paths(&self, specifier: &str, ctx: &mut ResolveContext) -> ResolveState { + fn load_tsconfig_paths( + &self, + cached_path: &CachedPath, + specifier: &str, + ctx: &mut ResolveContext, + ) -> ResolveState { let Some(tsconfig_path) = &self.options.tsconfig else { return Ok(None) }; let tsconfig_path = self.cache.value(tsconfig_path); let tsconfig = self.load_tsconfig(&tsconfig_path)?; - let paths = tsconfig.resolve_path_alias(specifier); + let paths = tsconfig.resolve(cached_path.path(), specifier); for path in paths { let cached_path = self.cache.value(&path); if let Ok(path) = self.require_relative(&cached_path, ".", ctx) { @@ -905,28 +912,35 @@ impl ResolverGeneric { } fn load_tsconfig(&self, cached_path: &CachedPath) -> Result, ResolveError> { - debug_assert!(cached_path.path().extension().is_some_and(|ext| ext == "json")); - self.cache.tsconfig(cached_path.path(), |tsconfig| { - if tsconfig.extends().is_empty() { - return Ok(()); + self.cache.tsconfig(cached_path, |tsconfig| { + // Extend tsconfig + if !tsconfig.extends().is_empty() { + let resolver = self.clone_with_options(ResolveOptions { + extensions: vec![".json".into()], + main_files: vec!["tsconfig".into()], + ..ResolveOptions::default() + }); + let mut extended_tsconfigs = vec![]; + for tsconfig_extend_specifier in tsconfig.extends() { + let extended_tsconfig_path = resolver.require( + cached_path.parent().unwrap(), + tsconfig_extend_specifier, + &mut ResolveContext::default(), + )?; + let extended_tsconfig = self.load_tsconfig(&extended_tsconfig_path)?; + extended_tsconfigs.push(extended_tsconfig); + } + for extended_tsconfig in extended_tsconfigs { + tsconfig.extend_tsconfig(&extended_tsconfig); + } } - let resolver = self.clone_with_options(ResolveOptions { - extensions: vec![".json".into()], - main_files: vec!["tsconfig".into()], - ..ResolveOptions::default() - }); - let mut extended_tsconfigs = vec![]; - for tsconfig_extend_specifier in tsconfig.extends() { - let extended_tsconfig_path = resolver.require( - cached_path.parent().unwrap(), - tsconfig_extend_specifier, - &mut ResolveContext::default(), - )?; - let extended_tsconfig = self.load_tsconfig(&extended_tsconfig_path)?; - extended_tsconfigs.push(extended_tsconfig); - } - for extended_tsconfig in extended_tsconfigs { - 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 = + self.cache.value(&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/tests/mod.rs b/crates/oxc_resolver/src/tests/mod.rs index e08983285..32c885752 100644 --- a/crates/oxc_resolver/src/tests/mod.rs +++ b/crates/oxc_resolver/src/tests/mod.rs @@ -17,6 +17,7 @@ mod scoped_packages; mod simple; mod symlink; mod tsconfig_paths; +mod tsconfig_project_references; use crate::Resolver; use std::{env, path::PathBuf, sync::Arc, thread}; diff --git a/crates/oxc_resolver/src/tests/tsconfig_project_references.rs b/crates/oxc_resolver/src/tests/tsconfig_project_references.rs new file mode 100644 index 000000000..02853016c --- /dev/null +++ b/crates/oxc_resolver/src/tests/tsconfig_project_references.rs @@ -0,0 +1,32 @@ +//! Tests for tsconfig project references + +use crate::{ResolveOptions, Resolver}; +use std::env; + +#[test] +fn test() { + let f = env::current_dir().unwrap().join("tests/tsconfig_project_references"); + + let resolver = Resolver::new(ResolveOptions { + tsconfig: Some(f.join("app")), + ..ResolveOptions::default() + }); + + #[rustfmt::skip] + let pass = [ + // 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 + (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 + (f.join("project_a"), "./index.ts", f.join("project_a/index.ts")), + (f.join("project_c"), "./index.ts", 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, Ok(expected), "{request} {path:?}"); + } +} diff --git a/crates/oxc_resolver/src/tsconfig.rs b/crates/oxc_resolver/src/tsconfig.rs index 8ab7cb5b7..7d38b30d9 100644 --- a/crates/oxc_resolver/src/tsconfig.rs +++ b/crates/oxc_resolver/src/tsconfig.rs @@ -1,6 +1,7 @@ use std::{ hash::BuildHasherDefault, path::{Path, PathBuf}, + sync::Arc, }; use crate::{json_comments::strip_comments_in_place, PathUtil}; @@ -20,10 +21,26 @@ pub struct TsConfig { #[serde(default, deserialize_with = "deserialize_extends")] extends: Vec, + #[serde(default)] + references: Vec, + #[serde(default)] compiler_options: CompilerOptions, } +/// Project Reference +/// +#[derive(Debug, Deserialize)] +pub struct ProjectReference { + /// The path property of each reference can point to a directory containing a tsconfig.json file, + /// or to the config file itself (which may have any name). + pub path: PathBuf, + + /// Reference to the resolved tsconfig + #[serde(skip)] + pub tsconfig: Option>, +} + #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CompilerOptions { @@ -79,6 +96,17 @@ impl TsConfig { &self.extends } + pub fn references_mut(&mut self) -> &mut Vec { + self.references.as_mut() + } + + fn base_path(&self) -> &Path { + self.compiler_options + .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.base_url.is_none() { @@ -90,6 +118,20 @@ impl TsConfig { } } + pub fn resolve(&self, path: &Path, specifier: &str) -> Vec { + if path.starts_with(self.base_path()) { + return self.resolve_path_alias(specifier); + } + for reference in &self.references { + if let Some(tsconfig) = &reference.tsconfig { + if path.starts_with(tsconfig.base_path()) { + return tsconfig.resolve_path_alias(specifier); + } + } + } + vec![] + } + // Copied from parcel // pub fn resolve_path_alias(&self, specifier: &str) -> Vec { diff --git a/crates/oxc_resolver/tests/tsconfig_project_references/app/aliased/index.ts b/crates/oxc_resolver/tests/tsconfig_project_references/app/aliased/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/crates/oxc_resolver/tests/tsconfig_project_references/app/index.ts b/crates/oxc_resolver/tests/tsconfig_project_references/app/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/crates/oxc_resolver/tests/tsconfig_project_references/app/tsconfig.json b/crates/oxc_resolver/tests/tsconfig_project_references/app/tsconfig.json new file mode 100644 index 000000000..1cef7a813 --- /dev/null +++ b/crates/oxc_resolver/tests/tsconfig_project_references/app/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@/*": ["./aliased/*"] + } + }, + "references": [ + { + "path": "../project_a/conf.json" + }, + { + "path": "../project_b" + }, + { + "path": "../project_c/tsconfig.json" + } + ] +} diff --git a/crates/oxc_resolver/tests/tsconfig_project_references/project_a/aliased/index.ts b/crates/oxc_resolver/tests/tsconfig_project_references/project_a/aliased/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/crates/oxc_resolver/tests/tsconfig_project_references/project_a/conf.json b/crates/oxc_resolver/tests/tsconfig_project_references/project_a/conf.json new file mode 100644 index 000000000..4d389d7ab --- /dev/null +++ b/crates/oxc_resolver/tests/tsconfig_project_references/project_a/conf.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": true, + "paths": { + "@/*": ["./aliased/*"] + } + } +} diff --git a/crates/oxc_resolver/tests/tsconfig_project_references/project_a/index.ts b/crates/oxc_resolver/tests/tsconfig_project_references/project_a/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/crates/oxc_resolver/tests/tsconfig_project_references/project_b/src/aliased/index.ts b/crates/oxc_resolver/tests/tsconfig_project_references/project_b/src/aliased/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/crates/oxc_resolver/tests/tsconfig_project_references/project_b/tsconfig.json b/crates/oxc_resolver/tests/tsconfig_project_references/project_b/tsconfig.json new file mode 100644 index 000000000..4ab05cd87 --- /dev/null +++ b/crates/oxc_resolver/tests/tsconfig_project_references/project_b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "baseUrl": "./src", + "paths": { + "@/*": ["./aliased/*"] + } + } +} diff --git a/crates/oxc_resolver/tests/tsconfig_project_references/project_c/index.ts b/crates/oxc_resolver/tests/tsconfig_project_references/project_c/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/crates/oxc_resolver/tests/tsconfig_project_references/project_c/tsconfig.json b/crates/oxc_resolver/tests/tsconfig_project_references/project_c/tsconfig.json new file mode 100644 index 000000000..fe520939c --- /dev/null +++ b/crates/oxc_resolver/tests/tsconfig_project_references/project_c/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "composite": true + } +}