Skip to main content

use_tsconfig/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::{collections::BTreeMap, error::Error};
6use use_ts::{TsModuleResolution, TsStrictness, TsTarget};
7
8/// Partial `tsconfig.json` metadata.
9#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
10#[derive(Clone, Debug, Default, Eq, PartialEq)]
11pub struct TsConfig {
12    extends: Option<TsConfigExtends>,
13    compiler_options: CompilerOptions,
14    include: Vec<TsConfigInclude>,
15    exclude: Vec<TsConfigExclude>,
16}
17
18impl TsConfig {
19    /// Creates empty `tsconfig` metadata.
20    #[must_use]
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    /// Sets the `extends` metadata.
26    #[must_use]
27    pub fn with_extends(mut self, extends: TsConfigExtends) -> Self {
28        self.extends = Some(extends);
29        self
30    }
31
32    /// Sets compiler options metadata.
33    #[must_use]
34    pub fn with_compiler_options(mut self, compiler_options: CompilerOptions) -> Self {
35        self.compiler_options = compiler_options;
36        self
37    }
38
39    /// Adds an include pattern.
40    #[must_use]
41    pub fn with_include(mut self, include: TsConfigInclude) -> Self {
42        self.include.push(include);
43        self
44    }
45
46    /// Adds an exclude pattern.
47    #[must_use]
48    pub fn with_exclude(mut self, exclude: TsConfigExclude) -> Self {
49        self.exclude.push(exclude);
50        self
51    }
52
53    /// Returns `extends` metadata.
54    #[must_use]
55    pub const fn extends(&self) -> Option<&TsConfigExtends> {
56        self.extends.as_ref()
57    }
58
59    /// Returns compiler options metadata.
60    #[must_use]
61    pub const fn compiler_options(&self) -> &CompilerOptions {
62        &self.compiler_options
63    }
64
65    /// Returns include patterns.
66    #[must_use]
67    pub fn include(&self) -> &[TsConfigInclude] {
68        &self.include
69    }
70
71    /// Returns exclude patterns.
72    #[must_use]
73    pub fn exclude(&self) -> &[TsConfigExclude] {
74        &self.exclude
75    }
76}
77
78/// Partial compiler options metadata.
79#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
80#[derive(Clone, Debug, Default, Eq, PartialEq)]
81pub struct CompilerOptions {
82    target: Option<TsTarget>,
83    module: Option<String>,
84    module_resolution: Option<TsModuleResolution>,
85    strict: Option<bool>,
86    jsx: Option<String>,
87    base_url: Option<String>,
88    paths: BTreeMap<String, Vec<String>>,
89}
90
91impl CompilerOptions {
92    /// Creates empty compiler options metadata.
93    #[must_use]
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    /// Sets the target option.
99    #[must_use]
100    pub const fn with_target(mut self, target: TsTarget) -> Self {
101        self.target = Some(target);
102        self
103    }
104
105    /// Sets the module option label.
106    #[must_use]
107    pub fn with_module(mut self, module: &str) -> Self {
108        self.module = non_empty(module);
109        self
110    }
111
112    /// Sets the module resolution option.
113    #[must_use]
114    pub const fn with_module_resolution(mut self, module_resolution: TsModuleResolution) -> Self {
115        self.module_resolution = Some(module_resolution);
116        self
117    }
118
119    /// Sets strictness metadata.
120    #[must_use]
121    pub const fn with_strictness(mut self, strictness: TsStrictness) -> Self {
122        self.strict = Some(matches!(strictness, TsStrictness::Strict));
123        self
124    }
125
126    /// Sets the JSX option label.
127    #[must_use]
128    pub fn with_jsx(mut self, jsx: &str) -> Self {
129        self.jsx = non_empty(jsx);
130        self
131    }
132
133    /// Sets the base URL option.
134    #[must_use]
135    pub fn with_base_url(mut self, base_url: &str) -> Self {
136        self.base_url = non_empty(base_url);
137        self
138    }
139
140    /// Adds a paths mapping.
141    #[must_use]
142    pub fn with_path_mapping(mut self, key: &str, values: Vec<String>) -> Self {
143        if let Some(key) = non_empty(key) {
144            self.paths.insert(key, values);
145        }
146        self
147    }
148
149    /// Returns the target option.
150    #[must_use]
151    pub const fn target(&self) -> Option<TsTarget> {
152        self.target
153    }
154
155    /// Returns the module option label.
156    #[must_use]
157    pub fn module(&self) -> Option<&str> {
158        self.module.as_deref()
159    }
160
161    /// Returns the module resolution option.
162    #[must_use]
163    pub const fn module_resolution(&self) -> Option<TsModuleResolution> {
164        self.module_resolution
165    }
166
167    /// Returns the strict option value.
168    #[must_use]
169    pub const fn strict(&self) -> Option<bool> {
170        self.strict
171    }
172
173    /// Returns the JSX option label.
174    #[must_use]
175    pub fn jsx(&self) -> Option<&str> {
176        self.jsx.as_deref()
177    }
178
179    /// Returns the base URL option.
180    #[must_use]
181    pub fn base_url(&self) -> Option<&str> {
182        self.base_url.as_deref()
183    }
184
185    /// Returns path mappings.
186    #[must_use]
187    pub const fn paths(&self) -> &BTreeMap<String, Vec<String>> {
188        &self.paths
189    }
190}
191
192macro_rules! string_newtype {
193    ($name:ident) => {
194        #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
195        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
196        pub struct $name(String);
197
198        impl $name {
199            /// Creates non-empty tsconfig metadata text.
200            ///
201            /// # Errors
202            ///
203            /// Returns [`TsConfigTextError::Empty`] when `input` is empty after trimming.
204            pub fn new(input: &str) -> Result<Self, TsConfigTextError> {
205                let trimmed = input.trim();
206                if trimmed.is_empty() {
207                    Err(TsConfigTextError::Empty)
208                } else {
209                    Ok(Self(trimmed.to_string()))
210                }
211            }
212
213            /// Returns the stored text.
214            #[must_use]
215            pub fn as_str(&self) -> &str {
216                &self.0
217            }
218        }
219
220        impl fmt::Display for $name {
221            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
222                formatter.write_str(self.as_str())
223            }
224        }
225    };
226}
227
228string_newtype!(TsConfigExtends);
229string_newtype!(TsConfigInclude);
230string_newtype!(TsConfigExclude);
231
232/// Error returned when tsconfig text metadata is empty.
233#[derive(Clone, Copy, Debug, Eq, PartialEq)]
234pub enum TsConfigTextError {
235    Empty,
236}
237
238impl fmt::Display for TsConfigTextError {
239    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
240        formatter.write_str("tsconfig metadata text cannot be empty")
241    }
242}
243
244impl Error for TsConfigTextError {}
245
246fn non_empty(input: &str) -> Option<String> {
247    let trimmed = input.trim();
248    (!trimmed.is_empty()).then(|| trimmed.to_string())
249}
250
251#[cfg(test)]
252mod tests {
253    use super::{CompilerOptions, TsConfig, TsConfigInclude, TsConfigTextError};
254    use use_ts::{TsModuleResolution, TsStrictness, TsTarget};
255
256    #[test]
257    fn stores_partial_compiler_options() -> Result<(), Box<dyn std::error::Error>> {
258        let options = CompilerOptions::new()
259            .with_target("es2024".parse::<TsTarget>()?)
260            .with_module("esnext")
261            .with_module_resolution(TsModuleResolution::Bundler)
262            .with_strictness(TsStrictness::Strict)
263            .with_jsx("react-jsx")
264            .with_base_url(".")
265            .with_path_mapping("@/*", vec![String::from("src/*")]);
266
267        assert_eq!(options.module(), Some("esnext"));
268        assert_eq!(options.strict(), Some(true));
269        assert_eq!(options.paths().len(), 1);
270        Ok(())
271    }
272
273    #[test]
274    fn stores_config_patterns() -> Result<(), TsConfigTextError> {
275        let config = TsConfig::new().with_include(TsConfigInclude::new("src")?);
276        assert_eq!(config.include()[0].as_str(), "src");
277        assert_eq!(TsConfigInclude::new(" "), Err(TsConfigTextError::Empty));
278        Ok(())
279    }
280}