Skip to content

Commit

Permalink
feat: substitute path that starts with ${configDir}/ in tsconfig.co…
Browse files Browse the repository at this point in the history
…mpilerOptions.paths

closes #129

NOTE: All tests cases are just a head replacement of `${configDir}`, so we are constrained as such.

Reference: microsoft/TypeScript#58042
  • Loading branch information
Boshen committed Apr 23, 2024
1 parent 8dc2a26 commit 1a26fae
Show file tree
Hide file tree
Showing 11 changed files with 123 additions and 23 deletions.
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();
}
}
}

0 comments on commit 1a26fae

Please sign in to comment.