Skip to content

Commit bc71693

Browse files
committed
fix: incorrect resolution when project reference extends a tsconfig without baseUrl (#882)
1 parent bde9ad6 commit bc71693

File tree

7 files changed

+69
-68
lines changed

7 files changed

+69
-68
lines changed

fixtures/tsconfig/cases/extends-paths-outside/src/index.js

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "../../tsconfig.json"
3+
}

fixtures/tsconfig/cases/references-extend/src/index.ts

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"include": ["**/*.ts"]
4+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"files": [],
4+
"include": [],
5+
"references": [
6+
{
7+
"path": "./tsconfig.a.json"
8+
}
9+
]
10+
}

src/tests/tsconfig_paths.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub fn tsconfig_resolve_impl(tsconfig_discovery: bool) {
1717
let pass = [
1818
(f.clone(), None, "ts-path", f.join("src/foo.js")),
1919
(f.join("nested"), None, "ts-path", f.join("nested/test.js")),
20+
(f.join("cases/extends-paths-outside"), Some("src/index.js"), "ts-path", f.join("src/foo.js")),
2021
(f.join("cases/index"), None, "foo", f.join("node_modules/tsconfig-index/foo.js")),
2122
// This requires reading package.json.tsconfig field
2223
// (f.join("cases/field"), "foo", f.join("node_modules/tsconfig-field/foo.js"))
@@ -26,6 +27,7 @@ pub fn tsconfig_resolve_impl(tsconfig_discovery: bool) {
2627
(f.join("cases/extends-paths"), Some("src"), "@/index", f.join("cases/extends-paths/src/index.js")),
2728
(f.join("cases/extends-multiple"), None, "foo", f.join("cases/extends-multiple/foo.js")),
2829
(f.join("cases/absolute-alias"), None, "/images/foo.js", f.join("cases/absolute-alias/public/images/foo.ts")),
30+
(f.join("cases/references-extend"), Some("src/index.ts"), "ts-path", f.join("src/foo.js")),
2931
];
3032

3133
for (dir, subdir, request, expected) in pass {
@@ -43,7 +45,7 @@ pub fn tsconfig_resolve_impl(tsconfig_discovery: bool) {
4345
});
4446
let path = subdir.map_or_else(|| dir.clone(), |subdir| dir.join(subdir));
4547
let resolved_path = resolver.resolve_file(&path, request).map(|f| f.full_path());
46-
assert_eq!(resolved_path, Ok(expected), "{request} {path:?}");
48+
assert_eq!(resolved_path, Ok(expected), "{request} {path:?} {tsconfig_discovery}");
4749
}
4850

4951
let data = [
@@ -174,7 +176,7 @@ fn empty() {
174176
// <https://github.com/parcel-bundler/parcel/blob/c8f5c97a01f643b4d5c333c02d019ef2618b44a5/packages/utils/node-resolver-rs/src/tsconfig.rs#L193C12-L193C12>
175177
#[test]
176178
fn test_paths() {
177-
let path = Path::new("/foo/tsconfig.json");
179+
let path = Path::new("/foo");
178180
let tsconfig_json = serde_json::json!({
179181
"compilerOptions": {
180182
"paths": {
@@ -188,7 +190,8 @@ fn test_paths() {
188190
}
189191
})
190192
.to_string();
191-
let tsconfig = TsConfig::parse(true, path, tsconfig_json).unwrap().build();
193+
let tsconfig =
194+
TsConfig::parse(true, &path.join("tsconfig.json"), tsconfig_json).unwrap().build();
192195

193196
let data = [
194197
("jquery", vec!["/foo/node_modules/jquery/dist/jquery"]),
@@ -197,13 +200,16 @@ fn test_paths() {
197200
("bar/hi", vec!["/foo/test/hi"]),
198201
("bar/baz/hi", vec!["/foo/baz/hi", "/foo/yo/hi"]),
199202
("@/components/button", vec!["/foo/components/button"]),
200-
("./jquery", vec![]),
201203
("url", vec!["/foo/node_modules/my-url"]),
202204
];
203205

204206
for (specifier, expected) in data {
205207
let paths = tsconfig.resolve_path_alias(specifier);
206-
let expected = expected.into_iter().map(PathBuf::from).collect::<Vec<_>>();
208+
let expected = expected
209+
.into_iter()
210+
.map(PathBuf::from)
211+
.chain(std::iter::once(path.join(specifier)))
212+
.collect::<Vec<_>>();
207213
assert_eq!(paths, expected, "{specifier}");
208214
}
209215
}

src/tsconfig.rs

Lines changed: 41 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const TEMPLATE_VARIABLE: &str = "${configDir}";
2424

2525
const GLOB_ALL_PATTERN: &str = "**/*";
2626

27-
pub type CompilerOptionsPathsMap = IndexMap<String, Vec<String>, BuildHasherDefault<FxHasher>>;
27+
pub type CompilerOptionsPathsMap = IndexMap<String, Vec<PathBuf>, BuildHasherDefault<FxHasher>>;
2828

2929
/// Project Reference
3030
///
@@ -88,6 +88,17 @@ impl TsConfig {
8888
};
8989
tsconfig.root = root;
9090
tsconfig.path = path.to_path_buf();
91+
tsconfig.compiler_options.paths_base =
92+
tsconfig.compiler_options.base_url.as_ref().map_or_else(
93+
|| tsconfig.directory().to_path_buf(),
94+
|base_url| {
95+
if base_url.to_string_lossy().starts_with(TEMPLATE_VARIABLE) {
96+
base_url.clone()
97+
} else {
98+
tsconfig.directory().normalize_with(base_url)
99+
}
100+
},
101+
);
91102
Ok(tsconfig)
92103
}
93104

@@ -149,16 +160,6 @@ impl TsConfig {
149160
!self.references.is_empty()
150161
}
151162

152-
/// Returns the base path from which to resolve aliases.
153-
///
154-
/// The base path can be configured by the user as part of the
155-
/// [CompilerOptions]. If not configured, it returns the directory in which
156-
/// the tsconfig itself is found.
157-
#[must_use]
158-
pub(crate) fn base_path(&self) -> &Path {
159-
self.compiler_options.base_url.as_ref().map_or_else(|| self.directory(), |p| p.as_path())
160-
}
161-
162163
/// Inherits settings from the given tsconfig into `self`.
163164
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
164165
pub(crate) fn extend_tsconfig(&mut self, tsconfig: &Self) {
@@ -180,31 +181,18 @@ impl TsConfig {
180181
self.exclude = Some(exclude.clone());
181182
}
182183

183-
let tsconfig_dir = tsconfig.directory();
184184
let compiler_options = &mut self.compiler_options;
185185

186-
if compiler_options.base_url.is_none()
187-
&& let Some(base_url) = &tsconfig.compiler_options.base_url
188-
{
189-
compiler_options.base_url = Some(if base_url.starts_with(TEMPLATE_VARIABLE) {
190-
base_url.clone()
191-
} else {
192-
tsconfig_dir.join(base_url).normalize()
193-
});
186+
if compiler_options.base_url.is_none() {
187+
compiler_options.base_url.clone_from(&tsconfig.compiler_options.base_url);
188+
if tsconfig.compiler_options.base_url.is_some() {
189+
compiler_options.paths_base.clone_from(&tsconfig.compiler_options.paths_base);
190+
}
194191
}
195-
196192
if compiler_options.paths.is_none() {
197-
let paths_base = compiler_options.base_url.as_ref().map_or_else(
198-
|| tsconfig_dir.to_path_buf(),
199-
|path| {
200-
if path.starts_with(TEMPLATE_VARIABLE) {
201-
path.clone()
202-
} else {
203-
tsconfig_dir.join(path).normalize()
204-
}
205-
},
206-
);
207-
compiler_options.paths_base = paths_base;
193+
if compiler_options.base_url.is_none() && tsconfig.compiler_options.base_url.is_none() {
194+
compiler_options.paths_base.clone_from(&tsconfig.compiler_options.paths_base);
195+
}
208196
compiler_options.paths.clone_from(&tsconfig.compiler_options.paths);
209197
}
210198

@@ -328,30 +316,27 @@ impl TsConfig {
328316
}
329317

330318
if let Some(base_url) = &self.compiler_options.base_url {
331-
let base_url = self.adjust_path(base_url.clone());
332-
self.compiler_options.base_url = Some(base_url);
319+
self.compiler_options.base_url = Some(self.adjust_path(base_url.clone()));
333320
}
334321

335-
if self.compiler_options.paths.is_some() {
336-
// `paths_base` should use config dir if it is not resolved with base url nor extended
337-
// with another tsconfig.
338-
if let Some(base_url) = self.compiler_options.base_url.clone() {
339-
self.compiler_options.paths_base = base_url;
340-
}
341-
342-
if self.compiler_options.paths_base.as_os_str().is_empty() {
343-
self.compiler_options.paths_base.clone_from(&config_dir);
344-
}
322+
if let Some(stripped_path) =
323+
self.compiler_options.paths_base.to_string_lossy().strip_prefix(TEMPLATE_VARIABLE)
324+
{
325+
self.compiler_options.paths_base =
326+
config_dir.join(stripped_path.trim_start_matches('/'));
327+
}
345328

329+
if self.compiler_options.paths.is_some() {
346330
// Substitute template variable in `tsconfig.compilerOptions.paths`.
347331
for paths in self.compiler_options.paths.as_mut().unwrap().values_mut() {
348332
for path in paths {
349-
if let Some(stripped_path) = path.strip_prefix(TEMPLATE_VARIABLE) {
350-
*path = config_dir
351-
.join(stripped_path.trim_start_matches('/'))
352-
.to_string_lossy()
353-
.to_string();
354-
}
333+
*path = if let Some(stripped_path) =
334+
path.to_string_lossy().strip_prefix(TEMPLATE_VARIABLE)
335+
{
336+
config_dir.join(stripped_path.trim_start_matches('/'))
337+
} else {
338+
self.compiler_options.paths_base.normalize_with(&path)
339+
};
355340
}
356341
}
357342
}
@@ -378,7 +363,7 @@ impl TsConfig {
378363
specifier: &str,
379364
) -> Vec<PathBuf> {
380365
for tsconfig in &self.references_resolved {
381-
if path.starts_with(tsconfig.base_path()) {
366+
if path.starts_with(&tsconfig.compiler_options.paths_base) {
382367
return tsconfig.resolve_path_alias(specifier);
383368
}
384369
}
@@ -398,10 +383,7 @@ impl TsConfig {
398383
}
399384

400385
let compiler_options = &self.compiler_options;
401-
let base_url_iter = compiler_options
402-
.base_url
403-
.as_ref()
404-
.map_or_else(Vec::new, |base_url| vec![base_url.normalize_with(specifier)]);
386+
let base_url_iter = vec![compiler_options.paths_base.normalize_with(specifier)];
405387

406388
let Some(paths_map) = &compiler_options.paths else {
407389
return base_url_iter;
@@ -429,23 +411,19 @@ impl TsConfig {
429411
paths
430412
.iter()
431413
.map(|path| {
432-
path.replace(
414+
PathBuf::from(path.to_string_lossy().replace(
433415
'*',
434416
&specifier[longest_prefix_length
435417
..specifier.len() - longest_suffix_length],
436-
)
418+
))
437419
})
438420
.collect::<Vec<_>>()
439421
})
440422
},
441423
Clone::clone,
442424
);
443425

444-
paths
445-
.into_iter()
446-
.map(|p| compiler_options.paths_base.normalize_with(p))
447-
.chain(base_url_iter)
448-
.collect()
426+
paths.into_iter().chain(base_url_iter).collect()
449427
}
450428
}
451429

@@ -460,7 +438,7 @@ pub struct CompilerOptions {
460438
/// Path aliases.
461439
pub paths: Option<CompilerOptionsPathsMap>,
462440

463-
/// The actual base from where path aliases are resolved.
441+
/// The "base_url" at which this tsconfig is defined.
464442
#[serde(skip)]
465443
pub(crate) paths_base: PathBuf,
466444

0 commit comments

Comments
 (0)