Skip to content
Open
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
2 changes: 2 additions & 0 deletions fixtures/tsconfig/cases/extends-not-found/a.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { b } from './b';
export const a = b;
1 change: 1 addition & 0 deletions fixtures/tsconfig/cases/extends-not-found/b.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const b = 1;
2 changes: 2 additions & 0 deletions fixtures/tsconfig/cases/extends-package-not-found/a.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { b } from './b';
export const a = b;
1 change: 1 addition & 0 deletions fixtures/tsconfig/cases/extends-package-not-found/b.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const b = 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "extends": "@acme/not-installed/tsconfig.json", "compilerOptions": { "baseUrl": ".", "paths": { "@app/*": ["./src/*"] } } }
5 changes: 5 additions & 0 deletions src/tests/tsconfck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ fn parse_invalid() {
let dir = super::fixture_root().join("tsconfck").join("parse").join("invalid");
let resolver = Resolver::default();
for path in walk(&dir).into_iter().filter(|path| path.file_name().unwrap() == "tsconfig.json") {
// A missing `extends` target is non-fatal in oxc (TS6053), matching
// tsc/tsgo — unlike tsconfck, which classifies it as invalid.
if path.parent().is_some_and(|p| p.ends_with("extends-not-found")) {
continue;
}
let tsconfig = resolver.resolve_tsconfig(&path);
assert!(tsconfig.is_err(), "{} {tsconfig:?}", path.display());
}
Expand Down
51 changes: 41 additions & 10 deletions src/tests/tsconfig_extends.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,11 @@ fn test_extend_tsconfig_no_override_existing() {
assert!(compiler_options.base_url.is_some());
}

/// When a tsconfig's `extends` target does not exist,
/// `resolve_tsconfig` should return `TsconfigNotFound`.
/// A missing `extends` target is non-fatal: `tsc` reports TS6053 and keeps the
/// options that parsed, so `resolve_tsconfig` succeeds rather than failing every
/// resolution under the config.
#[test]
fn test_extend_tsconfig_not_found() {
use crate::ResolveError;

let f = super::fixture_root().join("tsconfig/cases/extends-not-found");

let resolver = Resolver::new(ResolveOptions {
Expand All @@ -215,10 +214,7 @@ fn test_extend_tsconfig_not_found() {
});

let result = resolver.resolve_tsconfig(&f);
assert!(
matches!(&result, Err(ResolveError::TsconfigNotFound(_))),
"expected TsconfigNotFound for missing extends target, got {result:?}",
);
assert!(result.is_ok(), "a missing `extends` target must be non-fatal, got {result:?}");
}

/// When a tsconfig's `references` target does not exist,
Expand Down Expand Up @@ -385,10 +381,11 @@ fn test_extend_imports() {
let resolution = resolver.resolve_tsconfig(&f).expect("resolved");
assert_eq!(resolution.compiler_options.target, Some("ES2015".to_string()));

// An undefined `#` import in `extends` is non-fatal — the config still loads.
let result = resolver.resolve_tsconfig(f.join("tsconfig-missing.json"));
assert!(
matches!(&result, Err(crate::ResolveError::TsconfigNotFound(_))),
"expected TsconfigNotFound for an undefined `#` import, got {result:?}",
result.is_ok(),
"an undefined `#` import in `extends` must be non-fatal, got {result:?}"
);
}

Expand Down Expand Up @@ -423,3 +420,37 @@ fn test_extend_tsconfig_via_symlink_relative() {
let f = super::fixture_root().join("tsconfig/cases/extends-symlink");
assert_extends_symlink_resolves_to_canonical(&f.join("project/tsconfig.relative.json"));
}

/// A plain relative import must still resolve when the discovered tsconfig
/// `extends` a **package** that isn't installed (common in monorepos / before
/// dependencies are installed). `tsc`/`tsgo` report TS6053 and keep resolving;
/// previously oxc returned no result for every specifier under the config.
#[test]
fn test_extend_package_not_found_still_resolves() {
let f = super::fixture_root().join("tsconfig/cases/extends-package-not-found");

let resolver = Resolver::new(ResolveOptions {
tsconfig: Some(TsconfigDiscovery::Auto),
extensions: vec![".ts".into()],
..ResolveOptions::default()
});

let resolved = resolver.resolve_file(f.join("a.ts"), "./b").map(|r| r.full_path());
assert_eq!(resolved, Ok(f.join("b.ts")));
}

/// Same for a missing **relative** `extends` target (e.g. a generated
/// `./.nuxt/tsconfig.json` before `nuxt prepare`).
#[test]
fn test_extend_relative_not_found_still_resolves() {
let f = super::fixture_root().join("tsconfig/cases/extends-not-found");

let resolver = Resolver::new(ResolveOptions {
tsconfig: Some(TsconfigDiscovery::Auto),
extensions: vec![".ts".into()],
..ResolveOptions::default()
});

let resolved = resolver.resolve_file(f.join("a.ts"), "./b").map(|r| r.full_path());
assert_eq!(resolved, Ok(f.join("b.ts")));
}
6 changes: 3 additions & 3 deletions src/tests/tsconfig_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ pub fn tsconfig_resolve_impl(tsconfig_discovery: bool) {
Err(ResolveError::NotFound("ts-path".to_string())),
),
(
// A missing `extends` target is non-fatal: the config loads, so the
// alias it would have provided is just a normal `NotFound`.
f.join("cases/extends-not-found"),
"ts-path",
f.join("cases").join("extends-not-found").join("tsconfig.json"),
Err(ResolveError::TsconfigNotFound(
f.join("cases").join("extends-not-found").join("not-found"),
)),
Err(ResolveError::NotFound("ts-path".to_string())),
),
// no `base_url` <https://github.com/microsoft/TypeScript/issues/62207>
(
Expand Down
27 changes: 20 additions & 7 deletions src/tsconfig_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,20 +207,33 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
// `extend_tsconfig` only fills `None` fields, we iterate in reverse
// so that the last base sets fields first and earlier bases can no
// longer override them — net effect: later wins.
let extended_tsconfig_paths = tsconfig
.extends()
.map(|specifier| self.get_extended_tsconfig_path(&directory, tsconfig, specifier))
.collect::<Result<Vec<_>, _>>()?;
// A missing `extends` target is non-fatal: `tsc` reports TS6053 and
// keeps the options that did parse, so skip the missing base rather
// than failing every resolution under this config.
let mut extended_tsconfig_paths = Vec::new();
for specifier in tsconfig.extends() {
match self.get_extended_tsconfig_path(&directory, tsconfig, specifier) {
Ok(path) => extended_tsconfig_paths.push(path),
Err(ResolveError::TsconfigNotFound(_)) => {}
Err(err) => return Err(err),
}
}
if !extended_tsconfig_paths.is_empty() {
ctx.with_extended_file(tsconfig.path().to_owned(), |ctx| {
for extended_tsconfig_path in extended_tsconfig_paths.into_iter().rev() {
let extended_tsconfig = self.load_tsconfig(
match self.load_tsconfig(
/* root */ false,
&extended_tsconfig_path,
TsconfigReferences::Disabled,
ctx,
)?;
tsconfig.extend_tsconfig(&extended_tsconfig);
) {
Ok(extended_tsconfig) => tsconfig.extend_tsconfig(&extended_tsconfig),
// A relative `extends` target that doesn't exist is
// non-fatal too (TS6053); an existing-but-unreadable or
// malformed base still errors (`IOError` / `Json`).
Err(ResolveError::TsconfigNotFound(_)) => {}
Err(err) => return Err(err),
}
}
Result::Ok::<(), ResolveError>(())
})?;
Expand Down