Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions examples/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

use std::path::PathBuf;

use oxc_resolver::{AliasValue, ResolveOptions, Resolver, TsconfigOptions, TsconfigReferences};
use oxc_resolver::{
AliasValue, ResolveOptions, Resolver, TsconfigDiscovery, TsconfigOptions, TsconfigReferences,
};
use pico_args::Arguments;

fn main() {
Expand Down Expand Up @@ -30,10 +32,15 @@ fn main() {
condition_names: vec!["node".into(), "import".into()],
// CJS
// condition_names: vec!["node".into(), "require".into()],
tsconfig: tsconfig_path.map(|config_file| TsconfigOptions {
config_file,
references: TsconfigReferences::Auto,
}),
tsconfig: Some(tsconfig_path.map_or_else(
|| TsconfigDiscovery::Auto,
|config_file| {
TsconfigDiscovery::Manual(TsconfigOptions {
config_file,
references: TsconfigReferences::Auto,
})
},
)),
..ResolveOptions::default()
};

Expand All @@ -45,7 +52,7 @@ fn main() {
println!("Resolution: {}", resolution.full_path().to_string_lossy());
println!("Module Type: {:?}", resolution.module_type());
println!(
"package json: {:?}",
"package.json: {:?}",
resolution.package_json().map(|p| p.path.to_string_lossy())
);
}
Expand Down
4 changes: 2 additions & 2 deletions napi/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ export declare const enum ModuleType {
*/
export interface NapiResolveOptions {
/**
* Path to TypeScript configuration file.
* Discover tsconfig automatically or use the specified tsconfig.json path.
*
* Default `None`
*/
tsconfig?: TsconfigOptions
tsconfig?: 'auto' | TsconfigOptions
/**
* Alias for [ResolveOptions::alias] and [ResolveOptions::fallback].
*
Expand Down
9 changes: 6 additions & 3 deletions napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use std::{
sync::Arc,
};

use napi::{Task, bindgen_prelude::AsyncTask};
use napi::{Either, Task, bindgen_prelude::AsyncTask};
use napi_derive::napi;
use oxc_resolver::{ResolveError, ResolveOptions, Resolver};
use oxc_resolver::{ResolveError, ResolveOptions, Resolver, TsconfigDiscovery, TsconfigOptions};

use self::options::{NapiResolveOptions, StrOrStrList};

Expand Down Expand Up @@ -190,7 +190,10 @@ impl ResolverFactory {
// merging options
ResolveOptions {
cwd: None,
tsconfig: op.tsconfig.map(|tsconfig| tsconfig.into()),
tsconfig: op.tsconfig.map(|value| match value {
Either::A(_) => TsconfigDiscovery::Auto,
Either::B(options) => TsconfigDiscovery::Manual(TsconfigOptions::from(options)),
}),
alias: op
.alias
.map(|alias| {
Expand Down
5 changes: 3 additions & 2 deletions napi/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ use napi_derive::napi;
#[derive(Debug, Clone)]
#[napi(object)]
pub struct NapiResolveOptions {
/// Path to TypeScript configuration file.
/// Discover tsconfig automatically or use the specified tsconfig.json path.
///
/// Default `None`
pub tsconfig: Option<TsconfigOptions>,
#[napi(ts_type = "'auto' | TsconfigOptions")]
pub tsconfig: Option<Either<String, TsconfigOptions>>,

/// Alias for [ResolveOptions::alias] and [ResolveOptions::fallback].
///
Expand Down
4 changes: 3 additions & 1 deletion src/cache/cached_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use once_cell::sync::OnceCell as OnceLock;
use super::cache_impl::Cache;
use super::thread_local::SCRATCH_PATH;
use crate::{
FileMetadata, FileSystem, PackageJson, ResolveError, ResolveOptions,
FileMetadata, FileSystem, PackageJson, ResolveError, ResolveOptions, TsConfig,
context::ResolveContext as Ctx,
};

Expand All @@ -31,6 +31,7 @@ pub struct CachedPathImpl {
pub canonicalizing: AtomicU64,
pub node_modules: OnceLock<Option<Weak<CachedPathImpl>>>,
pub package_json: OnceLock<Option<Arc<PackageJson>>>,
pub tsconfig: OnceLock<Option<Arc<TsConfig>>>,
}

impl CachedPathImpl {
Expand All @@ -52,6 +53,7 @@ impl CachedPathImpl {
canonicalizing: AtomicU64::new(0),
node_modules: OnceLock::new(),
package_json: OnceLock::new(),
tsconfig: OnceLock::new(),
}
}
}
Expand Down
85 changes: 72 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ pub use crate::{
error::{JSONError, ResolveError, SpecifierError},
file_system::{FileMetadata, FileSystem, FileSystemOs},
options::{
Alias, AliasValue, EnforceExtension, ResolveOptions, Restriction, TsconfigOptions,
TsconfigReferences,
Alias, AliasValue, EnforceExtension, ResolveOptions, Restriction, TsconfigDiscovery,
TsconfigOptions, TsconfigReferences,
},
package_json::{
ImportsExportsArray, ImportsExportsEntry, ImportsExportsKind, ImportsExportsMap,
Expand Down Expand Up @@ -281,6 +281,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
debug_assert!(path.starts_with(package_json.directory()));
}
let module_type = self.esm_file_format(&cached_path, ctx)?;

Ok(Resolution {
path,
query: ctx.query.take(),
Expand Down Expand Up @@ -1456,21 +1457,78 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
if cached_path.inside_node_modules() {
return Ok(None);
}
let Some(tsconfig_options) = &self.options.tsconfig else {
return Ok(None);
let tsconfig = match &self.options.tsconfig {
None => return Ok(None),
Some(TsconfigDiscovery::Manual(tsconfig_options)) => {
let tsconfig = self.load_tsconfig(
/* root */ true,
&tsconfig_options.config_file,
&tsconfig_options.references,
&mut TsconfigResolveContext::default(),
)?;
// Cache the loaded tsconfig in the path's directory
let tsconfig_dir = self.cache.value(tsconfig.directory());
_ = tsconfig_dir.tsconfig.get_or_init(|| Some(Arc::clone(&tsconfig)));
tsconfig
}
Some(TsconfigDiscovery::Auto) => {
let Some(tsconfig) = self.find_tsconfig(cached_path, ctx)? else {
return Ok(None);
};
tsconfig
}
};
let tsconfig = self.load_tsconfig(
/* root */ true,
&tsconfig_options.config_file,
&tsconfig_options.references,
&mut TsconfigResolveContext::default(),
)?;

let paths = tsconfig.resolve(cached_path.path(), specifier);
for path in paths {
let cached_path = self.cache.value(&path);
if let Some(path) = self.load_as_file_or_directory(&cached_path, ".", ctx)? {
return Ok(Some(path));
let resolved_path = self.cache.value(&path);
if let Some(resolution) = self.load_as_file_or_directory(&resolved_path, ".", ctx)? {
// Cache the tsconfig in the resolved path
_ = resolved_path.tsconfig.get_or_init(|| Some(Arc::clone(&tsconfig)));
return Ok(Some(resolution));
}
}
Ok(None)
}

/// Find tsconfig.json of a path by traversing parent directories.
///
/// # Errors
///
/// * [ResolveError::Json]
pub(crate) fn find_tsconfig(
&self,
cached_path: &CachedPath,
ctx: &mut Ctx,
) -> Result<Option<Arc<TsConfig>>, ResolveError> {
// Don't discover tsconfig for paths inside node_modules
if cached_path.inside_node_modules() {
return Ok(None);
}

let mut cache_value = cached_path.clone();
// Go up directories when the querying path is not a directory
while !self.cache.is_dir(&cache_value, ctx) {
if let Some(cv) = cache_value.parent() {
cache_value = cv;
} else {
break;
}
}
let mut cache_value = Some(cache_value);
while let Some(cv) = cache_value {
if let Some(tsconfig) = cv.tsconfig.get_or_try_init(|| {
let tsconfig_path = cv.path.join("tsconfig.json");
let tsconfig_path = self.cache.value(&tsconfig_path);
if self.cache.is_file(&tsconfig_path, ctx) {
self.resolve_tsconfig(tsconfig_path.path()).map(Some)
} else {
Ok(None)
}
})? {
return Ok(Some(Arc::clone(tsconfig)));
}
cache_value = cv.parent();
}
Ok(None)
}
Expand All @@ -1487,6 +1545,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
Some(b'.') => Ok(tsconfig.directory().normalize_with(specifier)),
_ => self
.clone_with_options(ResolveOptions {
tsconfig: None,
extensions: vec![".json".into()],
main_files: vec!["tsconfig.json".into()],
#[cfg(feature = "yarn_pnp")]
Expand Down
20 changes: 13 additions & 7 deletions src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ pub struct ResolveOptions {
/// Current working directory, used for testing purposes.
pub cwd: Option<PathBuf>,

/// Path to TypeScript configuration file.
/// Discover tsconfig automatically or use the specified tsconfig.json path.
///
/// Default `None`
pub tsconfig: Option<TsconfigOptions>,
pub tsconfig: Option<TsconfigDiscovery>,

/// Create aliases to import or require certain modules more easily.
///
Expand Down Expand Up @@ -472,6 +472,12 @@ impl std::fmt::Debug for Restriction {
}
}

#[derive(Debug, Clone)]
pub enum TsconfigDiscovery {
Auto,
Manual(TsconfigOptions),
}

/// Tsconfig Options for [ResolveOptions::tsconfig]
///
/// Derived from [tsconfig-paths-webpack-plugin](https://github.com/dividab/tsconfig-paths-webpack-plugin#options)
Expand Down Expand Up @@ -612,8 +618,8 @@ mod test {
use std::path::PathBuf;

use super::{
AliasValue, EnforceExtension, ResolveOptions, Restriction, TsconfigOptions,
TsconfigReferences,
AliasValue, EnforceExtension, ResolveOptions, Restriction, TsconfigDiscovery,
TsconfigOptions, TsconfigReferences,
};

#[test]
Expand All @@ -634,10 +640,10 @@ mod test {
#[test]
fn display() {
let options = ResolveOptions {
tsconfig: Some(TsconfigOptions {
tsconfig: Some(TsconfigDiscovery::Manual(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 @@ -657,7 +663,7 @@ mod test {
..ResolveOptions::default()
};

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"]],imports_fields:[["imports"]],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,allow_package_exports_in_directory_resolve:true,"#;
let expected = r#"tsconfig:Manual(TsconfigOptions { config_file: "tsconfig.json", references: Auto }),alias:[("a", [Ignore])],alias_fields:[["browser"]],condition_names:["require"],enforce_extension:Enabled,exports_fields:[["exports"]],imports_fields:[["imports"]],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,allow_package_exports_in_directory_resolve:true,"#;
assert_eq!(format!("{options}"), expected);

let options = ResolveOptions {
Expand Down
1 change: 1 addition & 0 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ mod roots;
mod scoped_packages;
mod simple;
mod symlink;
mod tsconfig_discovery;
mod tsconfig_extends;
mod tsconfig_paths;
mod tsconfig_project_references;
Expand Down
8 changes: 8 additions & 0 deletions src/tests/tsconfig_discovery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//! Tests for tsconfig discovery
//!
//! Tests that tsconfig.json can be auto-discovered when no explicit tsconfig option is provided.

#[test]
fn tsconfig_discovery() {
super::tsconfig_paths::tsconfig_resolve_impl(/* tsconfig_discovery */ true);
}
Loading