use std::{
hash::BuildHasherDefault,
path::{Path, PathBuf},
sync::Arc,
};
use indexmap::IndexMap;
use rustc_hash::FxHasher;
use serde::Deserialize;
use crate::PathUtil;
pub type CompilerOptionsPathsMap = IndexMap<String, Vec<String>, BuildHasherDefault<FxHasher>>;
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum ExtendsField {
Single(String),
Multiple(Vec<String>),
}
const TEMPLATE_VARIABLE: &str = "${configDir}";
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TsConfig {
#[serde(skip)]
root: bool,
#[serde(skip)]
pub(crate) path: PathBuf,
#[serde(default)]
pub extends: Option<ExtendsField>,
#[serde(default)]
pub compiler_options: CompilerOptions,
#[serde(default)]
pub references: Vec<ProjectReference>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompilerOptions {
base_url: Option<PathBuf>,
paths: Option<CompilerOptionsPathsMap>,
#[serde(skip)]
paths_base: PathBuf,
}
#[derive(Debug, Deserialize)]
pub struct ProjectReference {
pub path: PathBuf,
#[serde(skip)]
pub tsconfig: Option<Arc<TsConfig>>,
}
impl TsConfig {
#[cfg_attr(feature="enable_instrument", tracing::instrument(level=tracing::Level::DEBUG, skip_all, fields(path = %path.to_string_lossy())))]
pub fn parse(root: bool, path: &Path, json: &mut str) -> Result<Self, serde_json::Error> {
_ = json_strip_comments::strip(json);
if json.trim().is_empty() {
let mut tsconfig: Self = serde_json::from_str("{}")?;
tsconfig.root = root;
tsconfig.path = path.to_path_buf();
return Ok(tsconfig);
}
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 {
if !base_url.starts_with(TEMPLATE_VARIABLE) {
tsconfig.compiler_options.base_url = Some(directory.normalize_with(base_url));
}
}
if tsconfig.compiler_options.paths.is_some() {
tsconfig.compiler_options.paths_base = tsconfig
.compiler_options
.base_url
.as_ref()
.map_or(directory, Clone::clone);
}
Ok(tsconfig)
}
pub fn build(mut self) -> Self {
if self.root {
let dir = self.directory().to_path_buf();
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);
}
}
}
let mut p = self
.compiler_options
.paths_base
.to_string_lossy()
.to_string();
Self::substitute_template_variable(&dir, &mut p);
self.compiler_options.paths_base = p.into();
if let Some(base_url) = self.compiler_options.base_url.as_mut() {
let mut p = base_url.to_string_lossy().to_string();
Self::substitute_template_variable(&dir, &mut p);
*base_url = p.into();
}
}
self
}
pub fn directory(&self) -> &Path {
debug_assert!(self.path.file_name().is_some());
self.path.parent().unwrap()
}
pub fn extend_tsconfig(&mut self, other_config: &Self) {
let compiler_options = &mut self.compiler_options;
if compiler_options.paths.is_none() {
compiler_options.paths_base = compiler_options.base_url.as_ref().map_or_else(
|| other_config.compiler_options.paths_base.clone(),
Clone::clone,
);
compiler_options
.paths
.clone_from(&other_config.compiler_options.paths);
}
if compiler_options.base_url.is_none() {
compiler_options
.base_url
.clone_from(&other_config.compiler_options.base_url);
}
}
pub fn resolve(&self, path: &Path, specifier: &str) -> Vec<PathBuf> {
for tsconfig in self
.references
.iter()
.filter_map(|reference| reference.tsconfig.as_ref())
{
if path.starts_with(tsconfig.base_path()) {
return tsconfig.resolve_path_alias(specifier);
}
}
self.resolve_path_alias(specifier)
}
pub fn resolve_path_alias(&self, specifier: &str) -> Vec<PathBuf> {
if specifier.starts_with(['/', '.']) {
return vec![];
}
let base_url_iter = self
.compiler_options
.base_url
.as_ref()
.map_or_else(
Vec::new,
|base_url| vec![base_url.normalize_with(specifier)],
);
let Some(paths_map) = &self.compiler_options.paths else {
return base_url_iter;
};
let paths = paths_map.get(specifier).map_or_else(
|| {
let mut longest_prefix_length = 0;
let mut longest_suffix_length = 0;
let mut best_key: Option<&String> = None;
for key in paths_map.keys() {
if let Some((prefix, suffix)) = key.split_once('*') {
if (best_key.is_none() || prefix.len() > longest_prefix_length)
&& specifier.starts_with(prefix)
&& specifier.ends_with(suffix)
{
longest_prefix_length = prefix.len();
longest_suffix_length = suffix.len();
best_key.replace(key);
}
}
}
best_key
.and_then(|key| paths_map.get(key))
.map_or_else(Vec::new, |paths| {
paths
.iter()
.map(|path| {
path.replace(
'*',
&specifier[longest_prefix_length..specifier.len() - longest_suffix_length],
)
})
.collect::<Vec<_>>()
})
},
Clone::clone,
);
paths
.into_iter()
.map(|p| self.compiler_options.paths_base.normalize_with(p))
.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())
}
fn substitute_template_variable(directory: &Path, path: &mut String) {
if let Some(stripped_path) = path.strip_prefix(TEMPLATE_VARIABLE) {
if let Some(unleashed_path) = stripped_path.strip_prefix("/") {
*path = directory.join(unleashed_path).to_string_lossy().to_string();
} else {
*path = directory.join(stripped_path).to_string_lossy().to_string();
}
}
}
}