diff --git a/fixtures/tsconfig/cases/extends-imports/default.tsconfig.json b/fixtures/tsconfig/cases/extends-imports/default.tsconfig.json new file mode 100644 index 000000000..90c5f25ad --- /dev/null +++ b/fixtures/tsconfig/cases/extends-imports/default.tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "target": "ES2022" + } +} diff --git a/fixtures/tsconfig/cases/extends-imports/node.tsconfig.json b/fixtures/tsconfig/cases/extends-imports/node.tsconfig.json new file mode 100644 index 000000000..560b59325 --- /dev/null +++ b/fixtures/tsconfig/cases/extends-imports/node.tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "target": "ES2015" + } +} diff --git a/fixtures/tsconfig/cases/extends-imports/package.json b/fixtures/tsconfig/cases/extends-imports/package.json new file mode 100644 index 000000000..81befc2ad --- /dev/null +++ b/fixtures/tsconfig/cases/extends-imports/package.json @@ -0,0 +1,10 @@ +{ + "name": "extends-imports", + "imports": { + "#string": "./string.tsconfig.json", + "#conditional": { + "node": "./node.tsconfig.json", + "default": "./default.tsconfig.json" + } + } +} diff --git a/fixtures/tsconfig/cases/extends-imports/string.tsconfig.json b/fixtures/tsconfig/cases/extends-imports/string.tsconfig.json new file mode 100644 index 000000000..bd4d6867c --- /dev/null +++ b/fixtures/tsconfig/cases/extends-imports/string.tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "target": "ES2020" + } +} diff --git a/fixtures/tsconfig/cases/extends-imports/tsconfig-missing.json b/fixtures/tsconfig/cases/extends-imports/tsconfig-missing.json new file mode 100644 index 000000000..02188fe68 --- /dev/null +++ b/fixtures/tsconfig/cases/extends-imports/tsconfig-missing.json @@ -0,0 +1,3 @@ +{ + "extends": "#missing" +} diff --git a/fixtures/tsconfig/cases/extends-imports/tsconfig-string.json b/fixtures/tsconfig/cases/extends-imports/tsconfig-string.json new file mode 100644 index 000000000..be354864c --- /dev/null +++ b/fixtures/tsconfig/cases/extends-imports/tsconfig-string.json @@ -0,0 +1,3 @@ +{ + "extends": "#string" +} diff --git a/fixtures/tsconfig/cases/extends-imports/tsconfig.json b/fixtures/tsconfig/cases/extends-imports/tsconfig.json new file mode 100644 index 000000000..f7c633b10 --- /dev/null +++ b/fixtures/tsconfig/cases/extends-imports/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "#conditional" +} diff --git a/src/tests/tsconfig_extends.rs b/src/tests/tsconfig_extends.rs index 2d4541219..240c8a752 100644 --- a/src/tests/tsconfig_extends.rs +++ b/src/tests/tsconfig_extends.rs @@ -367,6 +367,31 @@ fn test_extend_package() { } } +#[test] +fn test_extend_imports() { + let f = super::fixture_root().join("tsconfig/cases/extends-imports"); + + let resolver = Resolver::new(ResolveOptions { + tsconfig: Some(TsconfigDiscovery::Manual(TsconfigOptions { + config_file: f.join("tsconfig.json"), + references: TsconfigReferences::Auto, + })), + ..ResolveOptions::default() + }); + + let resolution = resolver.resolve_tsconfig(f.join("tsconfig-string.json")).expect("resolved"); + assert_eq!(resolution.compiler_options.target, Some("ES2020".to_string())); + + let resolution = resolver.resolve_tsconfig(&f).expect("resolved"); + assert_eq!(resolution.compiler_options.target, Some("ES2015".to_string())); + + 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:?}", + ); +} + fn assert_extends_symlink_resolves_to_canonical(config_file: &Path) { let f = super::fixture_root().join("tsconfig/cases/extends-symlink"); let resolver = Resolver::new(ResolveOptions { diff --git a/src/tsconfig_resolver.rs b/src/tsconfig_resolver.rs index 81d5ae699..c4016372f 100644 --- a/src/tsconfig_resolver.rs +++ b/src/tsconfig_resolver.rs @@ -396,6 +396,25 @@ impl ResolverGeneric { Ok(None) } + /// Lookup configuration shared by tsconfig `extends` targets that resolve + /// through `node_modules`: bare package specifiers (via the package + /// `exports` field) and `#`-prefixed subpath imports (via the package + /// `imports` field). Both use the same conditions so that, for example, + /// `extends: "pkg"` and `extends: "#pkg"` agree on which condition wins. + fn tsconfig_extends_resolver(&self) -> Self { + self.clone_with_options(ResolveOptions { + tsconfig: None, + condition_names: vec!["node".into(), "import".into()], + extensions: vec![".json".into()], + main_files: vec!["tsconfig".into()], + #[cfg(feature = "yarn_pnp")] + yarn_pnp: self.options.yarn_pnp, + #[cfg(feature = "yarn_pnp")] + cwd: self.options.cwd.clone(), + ..ResolveOptions::default() + }) + } + fn get_extended_tsconfig_path( &self, directory: &CachedPath, @@ -406,18 +425,25 @@ impl ResolverGeneric { None => Err(ResolveError::Specifier(SpecifierError::Empty(specifier.to_string()))), Some(b'/') => Ok(PathBuf::from(specifier)), Some(b'.') => Ok(tsconfig.directory().normalize_with(specifier)), - _ => self - .clone_with_options(ResolveOptions { - tsconfig: None, - condition_names: vec!["node".into(), "import".into()], - extensions: vec![".json".into()], - main_files: vec!["tsconfig".into()], - #[cfg(feature = "yarn_pnp")] - yarn_pnp: self.options.yarn_pnp, - #[cfg(feature = "yarn_pnp")] - cwd: self.options.cwd.clone(), - ..ResolveOptions::default() + // Node.js subpath imports, e.g. `extends: "#config"`, resolved + // through the nearest `package.json` `imports` field — the same + // path the resolver takes for `require("#config")`. + Some(b'#') => self + .tsconfig_extends_resolver() + .load_package_imports(directory, specifier, Some(tsconfig), &mut Ctx::default()) + .map_err(|err| match err { + ResolveError::PackageImportNotDefined(..) | ResolveError::NotFound(..) => { + ResolveError::TsconfigNotFound(PathBuf::from(specifier)) + } + _ => err, }) + .and_then(|resolved| { + resolved + .map(|p| p.path().to_path_buf()) + .ok_or_else(|| ResolveError::TsconfigNotFound(PathBuf::from(specifier))) + }), + _ => self + .tsconfig_extends_resolver() .load_package_self_or_node_modules(directory, specifier, None, &mut Ctx::default()) .map(|p| p.to_path_buf()) .map_err(|err| match err {