#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
use std::{collections::BTreeMap, error::Error};
use use_ts::{TsModuleResolution, TsStrictness, TsTarget};
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TsConfig {
extends: Option<TsConfigExtends>,
compiler_options: CompilerOptions,
include: Vec<TsConfigInclude>,
exclude: Vec<TsConfigExclude>,
}
impl TsConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_extends(mut self, extends: TsConfigExtends) -> Self {
self.extends = Some(extends);
self
}
#[must_use]
pub fn with_compiler_options(mut self, compiler_options: CompilerOptions) -> Self {
self.compiler_options = compiler_options;
self
}
#[must_use]
pub fn with_include(mut self, include: TsConfigInclude) -> Self {
self.include.push(include);
self
}
#[must_use]
pub fn with_exclude(mut self, exclude: TsConfigExclude) -> Self {
self.exclude.push(exclude);
self
}
#[must_use]
pub const fn extends(&self) -> Option<&TsConfigExtends> {
self.extends.as_ref()
}
#[must_use]
pub const fn compiler_options(&self) -> &CompilerOptions {
&self.compiler_options
}
#[must_use]
pub fn include(&self) -> &[TsConfigInclude] {
&self.include
}
#[must_use]
pub fn exclude(&self) -> &[TsConfigExclude] {
&self.exclude
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CompilerOptions {
target: Option<TsTarget>,
module: Option<String>,
module_resolution: Option<TsModuleResolution>,
strict: Option<bool>,
jsx: Option<String>,
base_url: Option<String>,
paths: BTreeMap<String, Vec<String>>,
}
impl CompilerOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_target(mut self, target: TsTarget) -> Self {
self.target = Some(target);
self
}
#[must_use]
pub fn with_module(mut self, module: &str) -> Self {
self.module = non_empty(module);
self
}
#[must_use]
pub const fn with_module_resolution(mut self, module_resolution: TsModuleResolution) -> Self {
self.module_resolution = Some(module_resolution);
self
}
#[must_use]
pub const fn with_strictness(mut self, strictness: TsStrictness) -> Self {
self.strict = Some(matches!(strictness, TsStrictness::Strict));
self
}
#[must_use]
pub fn with_jsx(mut self, jsx: &str) -> Self {
self.jsx = non_empty(jsx);
self
}
#[must_use]
pub fn with_base_url(mut self, base_url: &str) -> Self {
self.base_url = non_empty(base_url);
self
}
#[must_use]
pub fn with_path_mapping(mut self, key: &str, values: Vec<String>) -> Self {
if let Some(key) = non_empty(key) {
self.paths.insert(key, values);
}
self
}
#[must_use]
pub const fn target(&self) -> Option<TsTarget> {
self.target
}
#[must_use]
pub fn module(&self) -> Option<&str> {
self.module.as_deref()
}
#[must_use]
pub const fn module_resolution(&self) -> Option<TsModuleResolution> {
self.module_resolution
}
#[must_use]
pub const fn strict(&self) -> Option<bool> {
self.strict
}
#[must_use]
pub fn jsx(&self) -> Option<&str> {
self.jsx.as_deref()
}
#[must_use]
pub fn base_url(&self) -> Option<&str> {
self.base_url.as_deref()
}
#[must_use]
pub const fn paths(&self) -> &BTreeMap<String, Vec<String>> {
&self.paths
}
}
macro_rules! string_newtype {
($name:ident) => {
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct $name(String);
impl $name {
pub fn new(input: &str) -> Result<Self, TsConfigTextError> {
let trimmed = input.trim();
if trimmed.is_empty() {
Err(TsConfigTextError::Empty)
} else {
Ok(Self(trimmed.to_string()))
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
};
}
string_newtype!(TsConfigExtends);
string_newtype!(TsConfigInclude);
string_newtype!(TsConfigExclude);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TsConfigTextError {
Empty,
}
impl fmt::Display for TsConfigTextError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("tsconfig metadata text cannot be empty")
}
}
impl Error for TsConfigTextError {}
fn non_empty(input: &str) -> Option<String> {
let trimmed = input.trim();
(!trimmed.is_empty()).then(|| trimmed.to_string())
}
#[cfg(test)]
mod tests {
use super::{CompilerOptions, TsConfig, TsConfigInclude, TsConfigTextError};
use use_ts::{TsModuleResolution, TsStrictness, TsTarget};
#[test]
fn stores_partial_compiler_options() -> Result<(), Box<dyn std::error::Error>> {
let options = CompilerOptions::new()
.with_target("es2024".parse::<TsTarget>()?)
.with_module("esnext")
.with_module_resolution(TsModuleResolution::Bundler)
.with_strictness(TsStrictness::Strict)
.with_jsx("react-jsx")
.with_base_url(".")
.with_path_mapping("@/*", vec![String::from("src/*")]);
assert_eq!(options.module(), Some("esnext"));
assert_eq!(options.strict(), Some(true));
assert_eq!(options.paths().len(), 1);
Ok(())
}
#[test]
fn stores_config_patterns() -> Result<(), TsConfigTextError> {
let config = TsConfig::new().with_include(TsConfigInclude::new("src")?);
assert_eq!(config.include()[0].as_str(), "src");
assert_eq!(TsConfigInclude::new(" "), Err(TsConfigTextError::Empty));
Ok(())
}
}