use std::{
fmt::Debug,
hash::BuildHasherDefault,
path::{Path, PathBuf},
sync::Arc,
};
use indexmap::IndexMap;
use rustc_hash::FxHasher;
use serde::Deserialize;
use crate::{TsconfigReferences, path::PathUtil};
const TEMPLATE_VARIABLE: &str = "${configDir}";
pub type CompilerOptionsPathsMap = IndexMap<String, Vec<String>, BuildHasherDefault<FxHasher>>;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TsConfig {
#[serde(skip)]
pub root: bool,
#[serde(skip)]
pub path: PathBuf,
#[serde(default)]
pub files: Option<Vec<String>>,
#[serde(default)]
pub include: Option<Vec<String>>,
#[serde(default)]
pub exclude: Option<Vec<String>>,
#[serde(default)]
pub extends: Option<ExtendsField>,
#[serde(default)]
pub compiler_options: CompilerOptions,
#[serde(default)]
pub references: Vec<ProjectReference>,
}
impl TsConfig {
#[must_use]
pub fn root(&self) -> bool {
self.root
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn directory(&self) -> &Path {
debug_assert!(self.path.file_name().is_some());
self.path.parent().unwrap()
}
#[must_use]
pub fn compiler_options(&self) -> &CompilerOptions {
&self.compiler_options
}
#[must_use]
pub fn compiler_options_mut(&mut self) -> &mut CompilerOptions {
&mut self.compiler_options
}
pub fn extends(&self) -> impl Iterator<Item = &str> {
let specifiers = match &self.extends {
Some(ExtendsField::Single(specifier)) => {
vec![specifier.as_str()]
}
Some(ExtendsField::Multiple(specifiers)) => {
specifiers.iter().map(String::as_str).collect()
}
None => Vec::new(),
};
specifiers.into_iter()
}
pub(crate) fn load_references(&mut self, references: &TsconfigReferences) -> bool {
match references {
TsconfigReferences::Disabled => {
self.references.drain(..);
}
TsconfigReferences::Auto => {}
TsconfigReferences::Paths(paths) => {
self.references = paths
.iter()
.map(|path| ProjectReference { path: path.clone(), tsconfig: None })
.collect();
}
}
!self.references.is_empty()
}
pub(crate) fn references(&self) -> impl Iterator<Item = &ProjectReference> {
self.references.iter()
}
pub(crate) fn references_mut(&mut self) -> impl Iterator<Item = &mut ProjectReference> {
self.references.iter_mut()
}
#[must_use]
pub(crate) fn base_path(&self) -> &Path {
self.compiler_options().base_url().unwrap_or_else(|| self.directory())
}
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
pub(crate) fn extend_tsconfig(&mut self, tsconfig: &Self) {
if self.files.is_none() {
if let Some(files) = &tsconfig.files {
self.files = Some(files.clone());
}
}
if self.include.is_none() {
if let Some(include) = &tsconfig.include {
self.include = Some(include.clone());
}
}
if self.exclude.is_none() {
if let Some(exclude) = &tsconfig.exclude {
self.exclude = Some(exclude.clone());
}
}
let tsconfig_dir = tsconfig.directory();
let compiler_options = self.compiler_options_mut();
if compiler_options.base_url().is_none() {
if let Some(base_url) = tsconfig.compiler_options().base_url() {
compiler_options.set_base_url(if base_url.starts_with(TEMPLATE_VARIABLE) {
base_url.to_path_buf()
} else {
tsconfig_dir.join(base_url).normalize()
});
}
}
if compiler_options.paths().is_none() {
let paths_base = compiler_options.base_url().map_or_else(
|| tsconfig_dir.to_path_buf(),
|path| {
if path.starts_with(TEMPLATE_VARIABLE) {
path.to_path_buf()
} else {
tsconfig_dir.join(path).normalize()
}
},
);
compiler_options.set_paths_base(paths_base);
compiler_options.set_paths(tsconfig.compiler_options().paths().cloned());
}
if compiler_options.experimental_decorators().is_none() {
if let Some(experimental_decorators) =
tsconfig.compiler_options().experimental_decorators()
{
compiler_options.set_experimental_decorators(*experimental_decorators);
}
}
if compiler_options.emit_decorator_metadata.is_none() {
if let Some(emit_decorator_metadata) =
tsconfig.compiler_options().emit_decorator_metadata()
{
compiler_options.set_emit_decorator_metadata(*emit_decorator_metadata);
}
}
if compiler_options.use_define_for_class_fields.is_none() {
if let Some(use_define_for_class_fields) =
tsconfig.compiler_options().use_define_for_class_fields()
{
compiler_options.set_use_define_for_class_fields(*use_define_for_class_fields);
}
}
if compiler_options.rewrite_relative_import_extensions.is_none() {
if let Some(rewrite_relative_import_extensions) =
tsconfig.compiler_options().rewrite_relative_import_extensions()
{
compiler_options
.set_rewrite_relative_import_extensions(*rewrite_relative_import_extensions);
}
}
if compiler_options.jsx().is_none() {
if let Some(jsx) = tsconfig.compiler_options().jsx() {
compiler_options.set_jsx(jsx.to_string());
}
}
if compiler_options.jsx_factory().is_none() {
if let Some(jsx_factory) = tsconfig.compiler_options().jsx_factory() {
compiler_options.set_jsx_factory(jsx_factory.to_string());
}
}
if compiler_options.jsx_fragment_factory().is_none() {
if let Some(jsx_fragment_factory) = tsconfig.compiler_options().jsx_fragment_factory() {
compiler_options.set_jsx_fragment_factory(jsx_fragment_factory.to_string());
}
}
if compiler_options.jsx_import_source().is_none() {
if let Some(jsx_import_source) = tsconfig.compiler_options().jsx_import_source() {
compiler_options.set_jsx_import_source(jsx_import_source.to_string());
}
}
if compiler_options.verbatim_module_syntax().is_none() {
if let Some(verbatim_module_syntax) =
tsconfig.compiler_options().verbatim_module_syntax()
{
compiler_options.set_verbatim_module_syntax(*verbatim_module_syntax);
}
}
if compiler_options.preserve_value_imports().is_none() {
if let Some(preserve_value_imports) =
tsconfig.compiler_options().preserve_value_imports()
{
compiler_options.set_preserve_value_imports(*preserve_value_imports);
}
}
if compiler_options.imports_not_used_as_values().is_none() {
if let Some(imports_not_used_as_values) =
tsconfig.compiler_options().imports_not_used_as_values()
{
compiler_options
.set_imports_not_used_as_values(imports_not_used_as_values.to_string());
}
}
if compiler_options.target().is_none() {
if let Some(target) = tsconfig.compiler_options().target() {
compiler_options.set_target(target.to_string());
}
}
if compiler_options.module().is_none() {
if let Some(module) = tsconfig.compiler_options().module() {
compiler_options.set_module(module.to_string());
}
}
if compiler_options.allow_js().is_none() {
if let Some(allow_js) = tsconfig.compiler_options().allow_js() {
compiler_options.set_allow_js(*allow_js);
}
}
}
#[must_use]
pub(crate) fn build(mut self) -> Self {
if !self.root() {
return self;
}
let config_dir = self.directory().to_path_buf();
if let Some(base_url) = self.compiler_options().base_url() {
let base_url = base_url.to_string_lossy().strip_prefix(TEMPLATE_VARIABLE).map_or_else(
|| config_dir.normalize_with(base_url),
|stripped_path| config_dir.join(stripped_path.trim_start_matches('/')),
);
self.compiler_options_mut().set_base_url(base_url);
}
if self.compiler_options().paths().is_some() {
if let Some(base_url) = self.compiler_options().base_url().map(Path::to_path_buf) {
self.compiler_options_mut().set_paths_base(base_url);
}
if self.compiler_options().paths_base().as_os_str().is_empty() {
self.compiler_options_mut().set_paths_base(config_dir.clone());
}
for paths in self.compiler_options_mut().paths_mut().unwrap().values_mut() {
for path in paths {
Self::substitute_template_variable(&config_dir, path);
}
}
}
self
}
pub(crate) fn substitute_template_variable(directory: &Path, path: &mut String) {
if let Some(stripped_path) = path.strip_prefix(TEMPLATE_VARIABLE) {
*path =
directory.join(stripped_path.trim_start_matches('/')).to_string_lossy().to_string();
}
}
#[must_use]
pub(crate) fn resolve(&self, path: &Path, specifier: &str) -> Vec<PathBuf> {
let paths = self.resolve_path_alias(specifier);
for tsconfig in self.references().filter_map(ProjectReference::tsconfig) {
if path.starts_with(tsconfig.base_path()) {
return [tsconfig.resolve_path_alias(specifier), paths].concat();
}
}
paths
}
#[must_use]
pub(crate) fn resolve_path_alias(&self, specifier: &str) -> Vec<PathBuf> {
if specifier.starts_with('.') {
return Vec::new();
}
let compiler_options = self.compiler_options();
let base_url_iter = compiler_options
.base_url()
.map_or_else(Vec::new, |base_url| vec![base_url.normalize_with(specifier)]);
let Some(paths_map) = 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| compiler_options.paths_base().normalize_with(p))
.chain(base_url_iter)
.collect()
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompilerOptions {
pub base_url: Option<PathBuf>,
pub paths: Option<CompilerOptionsPathsMap>,
#[serde(skip)]
pub(crate) paths_base: PathBuf,
pub experimental_decorators: Option<bool>,
pub emit_decorator_metadata: Option<bool>,
pub use_define_for_class_fields: Option<bool>,
pub rewrite_relative_import_extensions: Option<bool>,
pub jsx: Option<String>,
pub jsx_factory: Option<String>,
pub jsx_fragment_factory: Option<String>,
pub jsx_import_source: Option<String>,
pub verbatim_module_syntax: Option<bool>,
pub preserve_value_imports: Option<bool>,
pub imports_not_used_as_values: Option<String>,
pub target: Option<String>,
pub module: Option<String>,
pub allow_js: Option<bool>,
}
impl CompilerOptions {
#[must_use]
fn base_url(&self) -> Option<&Path> {
self.base_url.as_deref()
}
fn set_base_url(&mut self, base_url: PathBuf) {
self.base_url = Some(base_url);
}
#[must_use]
fn paths(&self) -> Option<&CompilerOptionsPathsMap> {
self.paths.as_ref()
}
#[must_use]
fn paths_mut(&mut self) -> Option<&mut CompilerOptionsPathsMap> {
self.paths.as_mut()
}
fn set_paths(&mut self, paths: Option<CompilerOptionsPathsMap>) {
self.paths = paths;
}
#[must_use]
fn paths_base(&self) -> &Path {
&self.paths_base
}
fn set_paths_base(&mut self, paths_base: PathBuf) {
self.paths_base = paths_base;
}
fn experimental_decorators(&self) -> Option<&bool> {
self.experimental_decorators.as_ref()
}
fn set_experimental_decorators(&mut self, experimental_decorators: bool) {
self.experimental_decorators = Some(experimental_decorators);
}
fn emit_decorator_metadata(&self) -> Option<&bool> {
self.emit_decorator_metadata.as_ref()
}
fn set_emit_decorator_metadata(&mut self, emit_decorator_metadata: bool) {
self.emit_decorator_metadata = Some(emit_decorator_metadata);
}
fn use_define_for_class_fields(&self) -> Option<&bool> {
self.use_define_for_class_fields.as_ref()
}
fn set_use_define_for_class_fields(&mut self, use_define_for_class_fields: bool) {
self.use_define_for_class_fields = Some(use_define_for_class_fields);
}
fn rewrite_relative_import_extensions(&self) -> Option<&bool> {
self.rewrite_relative_import_extensions.as_ref()
}
fn set_rewrite_relative_import_extensions(&mut self, rewrite_relative_import_extensions: bool) {
self.rewrite_relative_import_extensions = Some(rewrite_relative_import_extensions);
}
fn jsx(&self) -> Option<&str> {
self.jsx.as_deref()
}
fn set_jsx(&mut self, jsx: String) {
self.jsx = Some(jsx);
}
fn jsx_factory(&self) -> Option<&str> {
self.jsx_factory.as_deref()
}
fn set_jsx_factory(&mut self, jsx_factory: String) {
self.jsx_factory = Some(jsx_factory);
}
fn jsx_fragment_factory(&self) -> Option<&str> {
self.jsx_fragment_factory.as_deref()
}
fn set_jsx_fragment_factory(&mut self, jsx_fragment_factory: String) {
self.jsx_fragment_factory = Some(jsx_fragment_factory);
}
fn jsx_import_source(&self) -> Option<&str> {
self.jsx_import_source.as_deref()
}
fn set_jsx_import_source(&mut self, jsx_import_source: String) {
self.jsx_import_source = Some(jsx_import_source);
}
fn verbatim_module_syntax(&self) -> Option<&bool> {
self.verbatim_module_syntax.as_ref()
}
fn set_verbatim_module_syntax(&mut self, verbatim_module_syntax: bool) {
self.verbatim_module_syntax = Some(verbatim_module_syntax);
}
fn preserve_value_imports(&self) -> Option<&bool> {
self.preserve_value_imports.as_ref()
}
fn set_preserve_value_imports(&mut self, preserve_value_imports: bool) {
self.preserve_value_imports = Some(preserve_value_imports);
}
fn imports_not_used_as_values(&self) -> Option<&str> {
self.imports_not_used_as_values.as_deref()
}
fn set_imports_not_used_as_values(&mut self, imports_not_used_as_values: String) {
self.imports_not_used_as_values = Some(imports_not_used_as_values);
}
fn target(&self) -> Option<&str> {
self.target.as_deref()
}
fn set_target(&mut self, target: String) {
self.target = Some(target);
}
fn module(&self) -> Option<&str> {
self.module.as_deref()
}
fn set_module(&mut self, module: String) {
self.module = Some(module);
}
fn allow_js(&self) -> Option<&bool> {
self.allow_js.as_ref()
}
fn set_allow_js(&mut self, allow_js: bool) {
self.allow_js = Some(allow_js);
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(untagged)]
pub enum ExtendsField {
Single(String),
Multiple(Vec<String>),
}
#[derive(Debug, Deserialize)]
pub struct ProjectReference {
pub path: PathBuf,
#[serde(skip)]
pub tsconfig: Option<Arc<TsConfig>>,
}
impl ProjectReference {
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn tsconfig(&self) -> Option<Arc<TsConfig>> {
self.tsconfig.clone()
}
pub fn set_tsconfig(&mut self, tsconfig: Arc<TsConfig>) {
self.tsconfig.replace(tsconfig);
}
}
impl TsConfig {
pub fn parse(root: bool, path: &Path, json: &mut str) -> Result<Self, serde_json::Error> {
let json = trim_start_matches_mut(json, '\u{feff}'); _ = json_strip_comments::strip(json);
let mut tsconfig: Self =
serde_json::from_str(if json.trim().is_empty() { "{}" } else { json })?;
tsconfig.root = root;
tsconfig.path = path.to_path_buf();
Ok(tsconfig)
}
}
fn trim_start_matches_mut(s: &mut str, pat: char) -> &mut str {
if s.starts_with(pat) {
&mut s[pat.len_utf8()..]
} else {
s
}
}