Skip to content

Commit

Permalink
feat(resolver): configurable tsconfig project references (#965)
Browse files Browse the repository at this point in the history
closes #942
  • Loading branch information
Boshen authored Oct 8, 2023
1 parent 7e84369 commit 5dbccaa
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 38 deletions.
47 changes: 35 additions & 12 deletions crates/oxc_resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -921,8 +924,9 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
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");
Expand All @@ -934,13 +938,17 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
Ok(None)
}

fn load_tsconfig(&self, path: &Path) -> Result<Arc<TsConfig>, ResolveError> {
fn load_tsconfig(
&self,
path: &Path,
references: &TsconfigReferences,
) -> Result<Arc<TsConfig>, 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),
Expand All @@ -966,15 +974,30 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
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(())
})
Expand Down
38 changes: 34 additions & 4 deletions crates/oxc_resolver/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub struct ResolveOptions {
/// Path to TypeScript configuration file.
///
/// Default `None`
pub tsconfig: Option<PathBuf>,
pub tsconfig: Option<TsconfigOptions>,

/// 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.
Expand Down Expand Up @@ -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<PathBuf>),
}

impl Default for ResolveOptions {
fn default() -> Self {
Self {
Expand Down Expand Up @@ -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]
Expand All @@ -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()],
Expand All @@ -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);
}
}
29 changes: 23 additions & 6 deletions crates/oxc_resolver/src/tests/tsconfig_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
//!
//! Fixtures copied from <https://github.com/parcel-bundler/parcel/tree/v2/packages/utils/node-resolver-core/test/fixture/tsconfig>.
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,
};

// <https://github.com/parcel-bundler/parcel/blob/b6224fd519f95e68d8b93ba90376fd94c8b76e69/packages/utils/node-resolver-rs/src/lib.rs#L2303>
#[test]
fn tsconfig() {
Expand All @@ -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());
Expand All @@ -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 {
Expand All @@ -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()
});

Expand Down Expand Up @@ -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 {
Expand Down
73 changes: 69 additions & 4 deletions crates/oxc_resolver/src/tests/tsconfig_project_references.rs
Original file line number Diff line number Diff line change
@@ -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()
});

Expand All @@ -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
Expand All @@ -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:?}");
}
}
19 changes: 7 additions & 12 deletions crates/oxc_resolver/src/tsconfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ pub struct TsConfig {
path: PathBuf,

#[serde(default, deserialize_with = "deserialize_extends")]
extends: Vec<String>,
pub extends: Vec<String>,

#[serde(default)]
references: Vec<ProjectReference>,
pub references: Vec<ProjectReference>,

#[serde(default)]
compiler_options: CompilerOptions,
pub compiler_options: CompilerOptions,
}

/// Project Reference
Expand Down Expand Up @@ -92,14 +92,6 @@ impl TsConfig {
self.path.parent().unwrap()
}

pub fn extends(&self) -> &Vec<String> {
&self.extends
}

pub fn references_mut(&mut self) -> &mut Vec<ProjectReference> {
self.references.as_mut()
}

fn base_path(&self) -> &Path {
self.compiler_options
.base_url
Expand All @@ -120,7 +112,10 @@ impl TsConfig {

pub fn resolve(&self, path: &Path, specifier: &str) -> Vec<PathBuf> {
if path.starts_with(self.base_path()) {
return self.resolve_path_alias(specifier);
let paths = self.resolve_path_alias(specifier);
if !paths.is_empty() {
return paths;
}
}
for reference in &self.references {
if let Some(tsconfig) = &reference.tsconfig {
Expand Down

0 comments on commit 5dbccaa

Please sign in to comment.