Skip to content

Commit b2e2094

Browse files
authored
feat: substitute path that starts with ${configDir}/ in tsconfig.compilerOptions.paths (#136)
closes #129 NOTE: All tests cases are just a head replacement of `${configDir}`, so we are constrained as such. Reference: microsoft/TypeScript#58042
1 parent 8dc2a26 commit b2e2094

File tree

11 files changed

+123
-23
lines changed

11 files changed

+123
-23
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Rust port of [enhanced-resolve].
2727
* support extending tsconfig defined in `tsconfig.extends`
2828
* support paths alias defined in `tsconfig.compilerOptions.paths`
2929
* support project references defined `tsconfig.references`
30+
* support [template variable ${configDir} for substitution of config files directory path](https://github.com/microsoft/TypeScript/pull/58042)
3031
* supports in-memory file system via the `FileSystem` trait
3132
* contains `tracing` instrumentation
3233

fixtures/tsconfig/cases/paths_template_variable/foo.js

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "../../tsconfig_template_variable.json"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"compilerOptions": {
3+
"composite": true,
4+
"paths": {
5+
"foo": ["${configDir}/foo.js"]
6+
}
7+
}
8+
}

fixtures/tsconfig/cases/project_references/app/tsconfig.json

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
},
1616
{
1717
"path": "../project_c/tsconfig.json"
18+
},
19+
{
20+
"path": "../../paths_template_variable/tsconfig2.json"
1821
}
1922
]
2023
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"compilerOptions": {
3+
"paths": {
4+
"foo": ["${configDir}/foo.js"]
5+
}
6+
}
7+
}

src/cache.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ impl<Fs: FileSystem> Cache<Fs> {
5555

5656
pub fn tsconfig<F: FnOnce(&mut TsConfig) -> Result<(), ResolveError>>(
5757
&self,
58+
root: bool,
5859
path: &Path,
5960
callback: F, // callback for modifying tsconfig with `extends`
6061
) -> Result<Arc<TsConfig>, ResolveError> {
@@ -74,13 +75,13 @@ impl<Fs: FileSystem> Cache<Fs> {
7475
let mut tsconfig_string = self
7576
.fs
7677
.read_to_string(&tsconfig_path)
77-
.map_err(|_| ResolveError::TsconfigNotFound(tsconfig_path.to_path_buf()))?;
78+
.map_err(|_| ResolveError::TsconfigNotFound(path.to_path_buf()))?;
7879
let mut tsconfig =
79-
TsConfig::parse(&tsconfig_path, &mut tsconfig_string).map_err(|error| {
80+
TsConfig::parse(root, &tsconfig_path, &mut tsconfig_string).map_err(|error| {
8081
ResolveError::from_serde_json_error(tsconfig_path.to_path_buf(), &error)
8182
})?;
8283
callback(&mut tsconfig)?;
83-
let tsconfig = Arc::new(tsconfig);
84+
let tsconfig = Arc::new(tsconfig.build());
8485
self.tsconfigs.insert(path.to_path_buf(), Arc::clone(&tsconfig));
8586
Ok(tsconfig)
8687
}

src/lib.rs

+17-6
Original file line numberDiff line numberDiff line change
@@ -1013,8 +1013,11 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
10131013
let Some(tsconfig_options) = &self.options.tsconfig else {
10141014
return Ok(None);
10151015
};
1016-
let tsconfig =
1017-
self.load_tsconfig(&tsconfig_options.config_file, &tsconfig_options.references)?;
1016+
let tsconfig = self.load_tsconfig(
1017+
/* root */ true,
1018+
&tsconfig_options.config_file,
1019+
&tsconfig_options.references,
1020+
)?;
10181021
let paths = tsconfig.resolve(cached_path.path(), specifier);
10191022
for path in paths {
10201023
let cached_path = self.cache.value(&path);
@@ -1027,10 +1030,11 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
10271030

10281031
fn load_tsconfig(
10291032
&self,
1033+
root: bool,
10301034
path: &Path,
10311035
references: &TsconfigReferences,
10321036
) -> Result<Arc<TsConfig>, ResolveError> {
1033-
self.cache.tsconfig(path, |tsconfig| {
1037+
self.cache.tsconfig(root, path, |tsconfig| {
10341038
let directory = self.cache.value(tsconfig.directory());
10351039
tracing::trace!(tsconfig = ?tsconfig, "load_tsconfig");
10361040

@@ -1046,8 +1050,11 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
10461050
.collect::<Result<Vec<PathBuf>, ResolveError>>()?,
10471051
};
10481052
for extended_tsconfig_path in extended_tsconfig_paths {
1049-
let extended_tsconfig =
1050-
self.load_tsconfig(&extended_tsconfig_path, &TsconfigReferences::Disabled)?;
1053+
let extended_tsconfig = self.load_tsconfig(
1054+
/* root */ false,
1055+
&extended_tsconfig_path,
1056+
&TsconfigReferences::Disabled,
1057+
)?;
10511058
tsconfig.extend_tsconfig(&extended_tsconfig);
10521059
}
10531060
}
@@ -1069,7 +1076,11 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
10691076
let directory = tsconfig.directory().to_path_buf();
10701077
for reference in &mut tsconfig.references {
10711078
let reference_tsconfig_path = directory.normalize_with(&reference.path);
1072-
let tsconfig = self.cache.tsconfig(&reference_tsconfig_path, |_| Ok(()))?;
1079+
let tsconfig = self.cache.tsconfig(
1080+
/* root */ true,
1081+
&reference_tsconfig_path,
1082+
|_| Ok(()),
1083+
)?;
10731084
reference.tsconfig.replace(tsconfig);
10741085
}
10751086
}

src/tests/tsconfig_paths.rs

+30-3
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ fn test_paths() {
8989
}
9090
})
9191
.to_string();
92-
let tsconfig = TsConfig::parse(path, &mut tsconfig_json).unwrap();
92+
let tsconfig = TsConfig::parse(true, path, &mut tsconfig_json).unwrap();
9393

9494
let data = [
9595
("jquery", vec!["/foo/node_modules/jquery/dist/jquery"]),
@@ -119,7 +119,7 @@ fn test_base_url() {
119119
}
120120
})
121121
.to_string();
122-
let tsconfig = TsConfig::parse(path, &mut tsconfig_json).unwrap();
122+
let tsconfig = TsConfig::parse(true, path, &mut tsconfig_json).unwrap();
123123

124124
let data = [
125125
("foo", vec!["/foo/src/foo"]),
@@ -150,7 +150,7 @@ fn test_paths_and_base_url() {
150150
}
151151
})
152152
.to_string();
153-
let tsconfig = TsConfig::parse(path, &mut tsconfig_json).unwrap();
153+
let tsconfig = TsConfig::parse(true, path, &mut tsconfig_json).unwrap();
154154

155155
let data = [
156156
("test", vec!["/foo/src/generated/test", "/foo/src/test"]),
@@ -168,6 +168,33 @@ fn test_paths_and_base_url() {
168168
}
169169
}
170170

171+
// Template variable ${configDir} for substitution of config files directory path
172+
// https://github.com/microsoft/TypeScript/pull/58042
173+
#[test]
174+
fn test_template_variable() {
175+
let f = super::fixture_root().join("tsconfig");
176+
let f2 = f.join("cases").join("paths_template_variable");
177+
178+
#[rustfmt::skip]
179+
let pass = [
180+
(f2.clone(), "tsconfig1.json", "foo", f2.join("foo.js")),
181+
(f2.clone(), "tsconfig2.json", "foo", f2.join("foo.js")),
182+
(f.clone(), "tsconfig_template_variable.json", "foo", f.join("foo.js")),
183+
];
184+
185+
for (dir, tsconfig, request, expected) in pass {
186+
let resolver = Resolver::new(ResolveOptions {
187+
tsconfig: Some(TsconfigOptions {
188+
config_file: dir.join(tsconfig),
189+
references: TsconfigReferences::Auto,
190+
}),
191+
..ResolveOptions::default()
192+
});
193+
let resolved_path = resolver.resolve(&dir, request).map(|f| f.full_path());
194+
assert_eq!(resolved_path, Ok(expected), "{request} {tsconfig} {dir:?}");
195+
}
196+
}
197+
171198
#[cfg(not(target_os = "windows"))] // MemoryFS's path separator is always `/` so the test will not pass in windows.
172199
mod windows_test {
173200
use std::path::{Path, PathBuf};

src/tests/tsconfig_project_references.rs

+5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ fn auto() {
2525
// Does not have paths alias
2626
(f.join("project_a"), "./index.ts", f.join("project_a/index.ts")),
2727
(f.join("project_c"), "./index.ts", f.join("project_c/index.ts")),
28+
// Template variable
29+
{
30+
let dir = f.parent().unwrap().join("paths_template_variable");
31+
(dir.clone(), "foo", dir.join("foo.js"))
32+
}
2833
];
2934

3035
for (path, request, expected) in pass {

src/tsconfig.rs

+45-11
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ use std::{
33
sync::Arc,
44
};
55

6-
use crate::PathUtil;
76
use serde::Deserialize;
87
use typescript_tsconfig_json::{CompilerOptionsPathsMap, ExtendsField};
98

9+
use crate::PathUtil;
10+
1011
#[derive(Debug, Deserialize)]
1112
#[serde(rename_all = "camelCase")]
1213
pub struct TsConfig {
14+
/// Whether this is the caller tsconfig.
15+
/// Used for final template variable substitution when all configs are extended and merged.
16+
#[serde(skip)]
17+
root: bool,
18+
1319
/// Path to `tsconfig.json`. Contains the `tsconfig.json` filename.
1420
#[serde(skip)]
1521
path: PathBuf,
@@ -56,9 +62,10 @@ pub struct ProjectReference {
5662
}
5763

5864
impl TsConfig {
59-
pub fn parse(path: &Path, json: &mut str) -> Result<Self, serde_json::Error> {
65+
pub fn parse(root: bool, path: &Path, json: &mut str) -> Result<Self, serde_json::Error> {
6066
_ = json_strip_comments::strip(json);
6167
let mut tsconfig: Self = serde_json::from_str(json)?;
68+
tsconfig.root = root;
6269
tsconfig.path = path.to_path_buf();
6370
let directory = tsconfig.directory().to_path_buf();
6471
if let Some(base_url) = tsconfig.compiler_options.base_url {
@@ -71,23 +78,31 @@ impl TsConfig {
7178
Ok(tsconfig)
7279
}
7380

74-
/// Directory to `package.json`
81+
pub fn build(mut self) -> Self {
82+
if self.root {
83+
let dir = self.directory().to_path_buf();
84+
// Substitute template variable in `tsconfig.compilerOptions.paths`
85+
if let Some(paths) = &mut self.compiler_options.paths {
86+
for paths in paths.values_mut() {
87+
for path in paths {
88+
Self::substitute_template_variable(&dir, path);
89+
}
90+
}
91+
}
92+
}
93+
self
94+
}
95+
96+
/// Directory to `tsconfig.json`
7597
///
7698
/// # Panics
7799
///
78-
/// * When the package.json path is misconfigured.
100+
/// * When the `tsconfig.json` path is misconfigured.
79101
pub fn directory(&self) -> &Path {
80102
debug_assert!(self.path.file_name().is_some());
81103
self.path.parent().unwrap()
82104
}
83105

84-
fn base_path(&self) -> &Path {
85-
self.compiler_options
86-
.base_url
87-
.as_ref()
88-
.map_or_else(|| self.directory(), |path| path.as_ref())
89-
}
90-
91106
pub fn extend_tsconfig(&mut self, tsconfig: &Self) {
92107
let compiler_options = &mut self.compiler_options;
93108
if compiler_options.paths.is_none() {
@@ -175,4 +190,23 @@ impl TsConfig {
175190
.chain(base_url_iter)
176191
.collect()
177192
}
193+
194+
fn base_path(&self) -> &Path {
195+
self.compiler_options
196+
.base_url
197+
.as_ref()
198+
.map_or_else(|| self.directory(), |path| path.as_ref())
199+
}
200+
201+
/// Template variable `${configDir}` for substitution of config files directory path
202+
///
203+
/// NOTE: All tests cases are just a head replacement of `${configDir}`, so we are constrained as such.
204+
///
205+
/// See <https://github.com/microsoft/TypeScript/pull/58042>
206+
fn substitute_template_variable(directory: &Path, path: &mut String) {
207+
const TEMPLATE_VARIABLE: &str = "${configDir}/";
208+
if let Some(stripped_path) = path.strip_prefix(TEMPLATE_VARIABLE) {
209+
*path = directory.join(stripped_path).to_string_lossy().to_string();
210+
}
211+
}
178212
}

0 commit comments

Comments
 (0)