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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Rust port of [enhanced-resolve].
* support extending tsconfig defined in `tsconfig.extends`
* support paths alias defined in `tsconfig.compilerOptions.paths`
* support project references defined `tsconfig.references`
* support [template variable ${configDir} for substitution of config files directory path](https://github.com/microsoft/TypeScript/pull/58042)
* supports in-memory file system via the `FileSystem` trait
* contains `tracing` instrumentation

Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig_template_variable.json"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"paths": {
"foo": ["${configDir}/foo.js"]
}
}
}
3 changes: 3 additions & 0 deletions fixtures/tsconfig/cases/project_references/app/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
},
{
"path": "../project_c/tsconfig.json"
},
{
"path": "../../paths_template_variable/tsconfig2.json"
}
]
}
7 changes: 7 additions & 0 deletions fixtures/tsconfig/tsconfig_template_variable.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"foo": ["${configDir}/foo.js"]
}
}
}
7 changes: 4 additions & 3 deletions src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ impl<Fs: FileSystem> Cache<Fs> {

pub fn tsconfig<F: FnOnce(&mut TsConfig) -> Result<(), ResolveError>>(
&self,
root: bool,
path: &Path,
callback: F, // callback for modifying tsconfig with `extends`
) -> Result<Arc<TsConfig>, ResolveError> {
Expand All @@ -74,13 +75,13 @@ impl<Fs: FileSystem> Cache<Fs> {
let mut tsconfig_string = self
.fs
.read_to_string(&tsconfig_path)
.map_err(|_| ResolveError::TsconfigNotFound(tsconfig_path.to_path_buf()))?;
.map_err(|_| ResolveError::TsconfigNotFound(path.to_path_buf()))?;
let mut tsconfig =
TsConfig::parse(&tsconfig_path, &mut tsconfig_string).map_err(|error| {
TsConfig::parse(root, &tsconfig_path, &mut tsconfig_string).map_err(|error| {
ResolveError::from_serde_json_error(tsconfig_path.to_path_buf(), &error)
})?;
callback(&mut tsconfig)?;
let tsconfig = Arc::new(tsconfig);
let tsconfig = Arc::new(tsconfig.build());
self.tsconfigs.insert(path.to_path_buf(), Arc::clone(&tsconfig));
Ok(tsconfig)
}
Expand Down
23 changes: 17 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1013,8 +1013,11 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
let Some(tsconfig_options) = &self.options.tsconfig else {
return Ok(None);
};
let tsconfig =
self.load_tsconfig(&tsconfig_options.config_file, &tsconfig_options.references)?;
let tsconfig = self.load_tsconfig(
/* root */ true,
&tsconfig_options.config_file,
&tsconfig_options.references,
)?;
let paths = tsconfig.resolve(cached_path.path(), specifier);
for path in paths {
let cached_path = self.cache.value(&path);
Expand All @@ -1027,10 +1030,11 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {

fn load_tsconfig(
&self,
root: bool,
path: &Path,
references: &TsconfigReferences,
) -> Result<Arc<TsConfig>, ResolveError> {
self.cache.tsconfig(path, |tsconfig| {
self.cache.tsconfig(root, path, |tsconfig| {
let directory = self.cache.value(tsconfig.directory());
tracing::trace!(tsconfig = ?tsconfig, "load_tsconfig");

Expand All @@ -1046,8 +1050,11 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
.collect::<Result<Vec<PathBuf>, ResolveError>>()?,
};
for extended_tsconfig_path in extended_tsconfig_paths {
let extended_tsconfig =
self.load_tsconfig(&extended_tsconfig_path, &TsconfigReferences::Disabled)?;
let extended_tsconfig = self.load_tsconfig(
/* root */ false,
&extended_tsconfig_path,
&TsconfigReferences::Disabled,
)?;
tsconfig.extend_tsconfig(&extended_tsconfig);
}
}
Expand All @@ -1069,7 +1076,11 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
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(()))?;
let tsconfig = self.cache.tsconfig(
/* root */ true,
&reference_tsconfig_path,
|_| Ok(()),
)?;
reference.tsconfig.replace(tsconfig);
}
}
Expand Down
33 changes: 30 additions & 3 deletions src/tests/tsconfig_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ fn test_paths() {
}
})
.to_string();
let tsconfig = TsConfig::parse(path, &mut tsconfig_json).unwrap();
let tsconfig = TsConfig::parse(true, path, &mut tsconfig_json).unwrap();

let data = [
("jquery", vec!["/foo/node_modules/jquery/dist/jquery"]),
Expand Down Expand Up @@ -119,7 +119,7 @@ fn test_base_url() {
}
})
.to_string();
let tsconfig = TsConfig::parse(path, &mut tsconfig_json).unwrap();
let tsconfig = TsConfig::parse(true, path, &mut tsconfig_json).unwrap();

let data = [
("foo", vec!["/foo/src/foo"]),
Expand Down Expand Up @@ -150,7 +150,7 @@ fn test_paths_and_base_url() {
}
})
.to_string();
let tsconfig = TsConfig::parse(path, &mut tsconfig_json).unwrap();
let tsconfig = TsConfig::parse(true, path, &mut tsconfig_json).unwrap();

let data = [
("test", vec!["/foo/src/generated/test", "/foo/src/test"]),
Expand All @@ -168,6 +168,33 @@ fn test_paths_and_base_url() {
}
}

// Template variable ${configDir} for substitution of config files directory path
// https://github.com/microsoft/TypeScript/pull/58042
#[test]
fn test_template_variable() {
let f = super::fixture_root().join("tsconfig");
let f2 = f.join("cases").join("paths_template_variable");

#[rustfmt::skip]
let pass = [
(f2.clone(), "tsconfig1.json", "foo", f2.join("foo.js")),
(f2.clone(), "tsconfig2.json", "foo", f2.join("foo.js")),
(f.clone(), "tsconfig_template_variable.json", "foo", f.join("foo.js")),
];

for (dir, tsconfig, request, expected) in pass {
let resolver = Resolver::new(ResolveOptions {
tsconfig: Some(TsconfigOptions {
config_file: dir.join(tsconfig),
references: TsconfigReferences::Auto,
}),
..ResolveOptions::default()
});
let resolved_path = resolver.resolve(&dir, request).map(|f| f.full_path());
assert_eq!(resolved_path, Ok(expected), "{request} {tsconfig} {dir:?}");
}
}

#[cfg(not(target_os = "windows"))] // MemoryFS's path separator is always `/` so the test will not pass in windows.
mod windows_test {
use std::path::{Path, PathBuf};
Expand Down
5 changes: 5 additions & 0 deletions src/tests/tsconfig_project_references.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ fn auto() {
// 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")),
// Template variable
{
let dir = f.parent().unwrap().join("paths_template_variable");
(dir.clone(), "foo", dir.join("foo.js"))
}
];

for (path, request, expected) in pass {
Expand Down
56 changes: 45 additions & 11 deletions src/tsconfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ use std::{
sync::Arc,
};

use crate::PathUtil;
use serde::Deserialize;
use typescript_tsconfig_json::{CompilerOptionsPathsMap, ExtendsField};

use crate::PathUtil;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TsConfig {
/// Whether this is the caller tsconfig.
/// Used for final template variable substitution when all configs are extended and merged.
#[serde(skip)]
root: bool,

/// Path to `tsconfig.json`. Contains the `tsconfig.json` filename.
#[serde(skip)]
path: PathBuf,
Expand Down Expand Up @@ -56,9 +62,10 @@ pub struct ProjectReference {
}

impl TsConfig {
pub fn parse(path: &Path, json: &mut str) -> Result<Self, serde_json::Error> {
pub fn parse(root: bool, path: &Path, json: &mut str) -> Result<Self, serde_json::Error> {
_ = json_strip_comments::strip(json);
let mut tsconfig: Self = serde_json::from_str(json)?;
tsconfig.root = root;
tsconfig.path = path.to_path_buf();
let directory = tsconfig.directory().to_path_buf();
if let Some(base_url) = tsconfig.compiler_options.base_url {
Expand All @@ -71,23 +78,31 @@ impl TsConfig {
Ok(tsconfig)
}

/// Directory to `package.json`
pub fn build(mut self) -> Self {
if self.root {
let dir = self.directory().to_path_buf();
// Substitute template variable in `tsconfig.compilerOptions.paths`
if let Some(paths) = &mut self.compiler_options.paths {
for paths in paths.values_mut() {
for path in paths {
Self::substitute_template_variable(&dir, path);
}
}
}
}
self
}

/// Directory to `tsconfig.json`
///
/// # Panics
///
/// * When the package.json path is misconfigured.
/// * When the `tsconfig.json` path is misconfigured.
pub fn directory(&self) -> &Path {
debug_assert!(self.path.file_name().is_some());
self.path.parent().unwrap()
}

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.paths.is_none() {
Expand Down Expand Up @@ -175,4 +190,23 @@ impl TsConfig {
.chain(base_url_iter)
.collect()
}

fn base_path(&self) -> &Path {
self.compiler_options
.base_url
.as_ref()
.map_or_else(|| self.directory(), |path| path.as_ref())
}

/// Template variable `${configDir}` for substitution of config files directory path
///
/// NOTE: All tests cases are just a head replacement of `${configDir}`, so we are constrained as such.
///
/// See <https://github.com/microsoft/TypeScript/pull/58042>
fn substitute_template_variable(directory: &Path, path: &mut String) {
const TEMPLATE_VARIABLE: &str = "${configDir}/";
if let Some(stripped_path) = path.strip_prefix(TEMPLATE_VARIABLE) {
*path = directory.join(stripped_path).to_string_lossy().to_string();
}
}
}