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#[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 #[must_use]
21 pub fn new() -> Self {
22 Self::default()
23 }
24
25 #[must_use]
27 pub fn with_extends(mut self, extends: TsConfigExtends) -> Self {
28 self.extends = Some(extends);
29 self
30 }
31
32 #[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 #[must_use]
41 pub fn with_include(mut self, include: TsConfigInclude) -> Self {
42 self.include.push(include);
43 self
44 }
45
46 #[must_use]
48 pub fn with_exclude(mut self, exclude: TsConfigExclude) -> Self {
49 self.exclude.push(exclude);
50 self
51 }
52
53 #[must_use]
55 pub const fn extends(&self) -> Option<&TsConfigExtends> {
56 self.extends.as_ref()
57 }
58
59 #[must_use]
61 pub const fn compiler_options(&self) -> &CompilerOptions {
62 &self.compiler_options
63 }
64
65 #[must_use]
67 pub fn include(&self) -> &[TsConfigInclude] {
68 &self.include
69 }
70
71 #[must_use]
73 pub fn exclude(&self) -> &[TsConfigExclude] {
74 &self.exclude
75 }
76}
77
78#[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 #[must_use]
94 pub fn new() -> Self {
95 Self::default()
96 }
97
98 #[must_use]
100 pub const fn with_target(mut self, target: TsTarget) -> Self {
101 self.target = Some(target);
102 self
103 }
104
105 #[must_use]
107 pub fn with_module(mut self, module: &str) -> Self {
108 self.module = non_empty(module);
109 self
110 }
111
112 #[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 #[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 #[must_use]
128 pub fn with_jsx(mut self, jsx: &str) -> Self {
129 self.jsx = non_empty(jsx);
130 self
131 }
132
133 #[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 #[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 #[must_use]
151 pub const fn target(&self) -> Option<TsTarget> {
152 self.target
153 }
154
155 #[must_use]
157 pub fn module(&self) -> Option<&str> {
158 self.module.as_deref()
159 }
160
161 #[must_use]
163 pub const fn module_resolution(&self) -> Option<TsModuleResolution> {
164 self.module_resolution
165 }
166
167 #[must_use]
169 pub const fn strict(&self) -> Option<bool> {
170 self.strict
171 }
172
173 #[must_use]
175 pub fn jsx(&self) -> Option<&str> {
176 self.jsx.as_deref()
177 }
178
179 #[must_use]
181 pub fn base_url(&self) -> Option<&str> {
182 self.base_url.as_deref()
183 }
184
185 #[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 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 #[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#[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}