mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(resolver): configurable tsconfig project references (#965)
closes #942
This commit is contained in:
parent
7e84369cad
commit
5dbccaa711
5 changed files with 168 additions and 38 deletions
|
|
@ -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<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");
|
||||
|
|
@ -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),
|
||||
|
|
@ -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(())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue