unrspack_resolver/
tsconfig.rs

1use std::{
2    hash::BuildHasherDefault,
3    path::{Path, PathBuf},
4    sync::Arc,
5};
6
7use indexmap::IndexMap;
8use rustc_hash::FxHasher;
9
10use crate::{TsconfigReferences, path::PathUtil};
11
12pub type CompilerOptionsPathsMap = IndexMap<String, Vec<String>, BuildHasherDefault<FxHasher>>;
13
14/// Abstract representation for the contents of a `tsconfig.json` file, as well
15/// as the location where it was found.
16///
17/// This representation makes no assumptions regarding how the file was
18/// deserialized.
19#[allow(clippy::missing_errors_doc)] // trait impls should be free to return any typesafe error
20pub trait TsConfig: Sized {
21    type Co: CompilerOptions;
22
23    /// Whether this is the caller tsconfig.
24    /// Used for final template variable substitution when all configs are extended and merged.
25    fn root(&self) -> bool;
26
27    /// Returns the path where the `tsconfig.json` was found.
28    ///
29    /// Contains the `tsconfig.json` filename.
30    #[must_use]
31    fn path(&self) -> &Path;
32
33    /// Directory to `tsconfig.json`.
34    ///
35    /// # Panics
36    ///
37    /// * When the `tsconfig.json` path is misconfigured.
38    #[must_use]
39    fn directory(&self) -> &Path;
40
41    /// Returns the compiler options configured in this tsconfig.
42    #[must_use]
43    fn compiler_options(&self) -> &Self::Co;
44
45    /// Returns a mutable reference to the compiler options configured in this
46    /// tsconfig.
47    #[must_use]
48    fn compiler_options_mut(&mut self) -> &mut Self::Co;
49
50    /// Returns any paths to tsconfigs that should be extended by this tsconfig.
51    #[must_use]
52    fn extends(&self) -> impl Iterator<Item = &str>;
53
54    /// Loads the given references into this tsconfig.
55    ///
56    /// Returns whether any references are defined in the tsconfig.
57    fn load_references(&mut self, references: &TsconfigReferences) -> bool;
58
59    /// Returns references to other tsconfig files.
60    #[must_use]
61    fn references(&self) -> impl Iterator<Item = &impl ProjectReference<Tc = Self>>;
62
63    /// Returns mutable references to other tsconfig files.
64    #[must_use]
65    fn references_mut(&mut self) -> impl Iterator<Item = &mut impl ProjectReference<Tc = Self>>;
66
67    /// Returns the base path from which to resolve aliases.
68    ///
69    /// The base path can be configured by the user as part of the
70    /// [CompilerOptions]. If not configured, it returns the directory in which
71    /// the tsconfig itself is found.
72    #[must_use]
73    fn base_path(&self) -> &Path {
74        self.compiler_options().base_url().unwrap_or_else(|| self.directory())
75    }
76
77    /// Expands all template variables in this tsconfig.
78    fn expand_template_variables(&mut self) {
79        if self.root() {
80            let dir = self.directory().to_path_buf();
81            // Substitute template variable in `tsconfig.compilerOptions.paths`
82            if let Some(paths) = &mut self.compiler_options_mut().paths_mut() {
83                for paths in paths.values_mut() {
84                    for path in paths {
85                        Self::substitute_template_variable(&dir, path);
86                    }
87                }
88            }
89        }
90    }
91
92    /// Inherits settings from the given tsconfig into `self`.
93    fn extend_tsconfig(&mut self, tsconfig: &Self) {
94        let compiler_options = self.compiler_options_mut();
95        if compiler_options.paths().is_none() {
96            compiler_options.set_paths_base(compiler_options.base_url().map_or_else(
97                || tsconfig.compiler_options().paths_base().to_path_buf(),
98                Path::to_path_buf,
99            ));
100            compiler_options.set_paths(tsconfig.compiler_options().paths().cloned());
101        }
102        if compiler_options.base_url().is_none() {
103            if let Some(base_url) = tsconfig.compiler_options().base_url() {
104                compiler_options.set_base_url(base_url.to_path_buf());
105            }
106        }
107
108        if compiler_options.experimental_decorators().is_none() {
109            if let Some(experimental_decorators) =
110                tsconfig.compiler_options().experimental_decorators()
111            {
112                compiler_options.set_experimental_decorators(*experimental_decorators);
113            }
114        }
115
116        if compiler_options.jsx().is_none() {
117            if let Some(jsx) = tsconfig.compiler_options().jsx() {
118                compiler_options.set_jsx(jsx.to_string());
119            }
120        }
121
122        if compiler_options.jsx_factory().is_none() {
123            if let Some(jsx_factory) = tsconfig.compiler_options().jsx_factory() {
124                compiler_options.set_jsx_factory(jsx_factory.to_string());
125            }
126        }
127
128        if compiler_options.jsx_fragment_factory().is_none() {
129            if let Some(jsx_fragment_factory) = tsconfig.compiler_options().jsx_fragment_factory() {
130                compiler_options.set_jsx_fragment_factory(jsx_fragment_factory.to_string());
131            }
132        }
133
134        if compiler_options.jsx_import_source().is_none() {
135            if let Some(jsx_import_source) = tsconfig.compiler_options().jsx_import_source() {
136                compiler_options.set_jsx_import_source(jsx_import_source.to_string());
137            }
138        }
139    }
140
141    /// Resolves the given `specifier` within the project configured by this
142    /// tsconfig, relative to the given `path`.
143    ///
144    /// `specifier` can be either a real path or an alias.
145    #[must_use]
146    fn resolve(&self, path: &Path, specifier: &str) -> Vec<PathBuf> {
147        let paths = self.resolve_path_alias(specifier);
148        for tsconfig in self.references().filter_map(ProjectReference::tsconfig) {
149            if path.starts_with(tsconfig.base_path()) {
150                return [tsconfig.resolve_path_alias(specifier), paths].concat();
151            }
152        }
153        paths
154    }
155
156    /// Resolves the given `specifier` within the project configured by this
157    /// tsconfig.
158    ///
159    /// `specifier` is expected to be a path alias.
160    // Copied from parcel
161    // <https://github.com/parcel-bundler/parcel/blob/b6224fd519f95e68d8b93ba90376fd94c8b76e69/packages/utils/node-resolver-rs/src/tsconfig.rs#L93>
162    #[must_use]
163    fn resolve_path_alias(&self, specifier: &str) -> Vec<PathBuf> {
164        if specifier.starts_with(['/', '.']) {
165            return Vec::new();
166        }
167
168        let compiler_options = self.compiler_options();
169        let base_url_iter = compiler_options
170            .base_url()
171            .map_or_else(Vec::new, |base_url| vec![base_url.normalize_with(specifier)]);
172
173        let Some(paths_map) = compiler_options.paths() else {
174            return base_url_iter;
175        };
176
177        let paths = paths_map.get(specifier).map_or_else(
178            || {
179                let mut longest_prefix_length = 0;
180                let mut longest_suffix_length = 0;
181                let mut best_key: Option<&String> = None;
182
183                for key in paths_map.keys() {
184                    if let Some((prefix, suffix)) = key.split_once('*') {
185                        if (best_key.is_none() || prefix.len() > longest_prefix_length)
186                            && specifier.starts_with(prefix)
187                            && specifier.ends_with(suffix)
188                        {
189                            longest_prefix_length = prefix.len();
190                            longest_suffix_length = suffix.len();
191                            best_key.replace(key);
192                        }
193                    }
194                }
195
196                best_key.and_then(|key| paths_map.get(key)).map_or_else(Vec::new, |paths| {
197                    paths
198                        .iter()
199                        .map(|path| {
200                            path.replace(
201                                '*',
202                                &specifier[longest_prefix_length
203                                    ..specifier.len() - longest_suffix_length],
204                            )
205                        })
206                        .collect::<Vec<_>>()
207                })
208            },
209            Clone::clone,
210        );
211
212        paths
213            .into_iter()
214            .map(|p| compiler_options.paths_base().normalize_with(p))
215            .chain(base_url_iter)
216            .collect()
217    }
218
219    /// Template variable `${configDir}` for substitution of config files
220    /// directory path.
221    ///
222    /// NOTE: All tests cases are just a head replacement of `${configDir}`, so
223    ///       we are constrained as such.
224    ///
225    /// See <https://github.com/microsoft/TypeScript/pull/58042>.
226    fn substitute_template_variable(directory: &Path, path: &mut String) {
227        const TEMPLATE_VARIABLE: &str = "${configDir}/";
228        if let Some(stripped_path) = path.strip_prefix(TEMPLATE_VARIABLE) {
229            *path = directory.join(stripped_path).to_string_lossy().to_string();
230        }
231    }
232}
233
234/// Compiler Options.
235///
236/// <https://www.typescriptlang.org/tsconfig#compilerOptions>
237pub trait CompilerOptions {
238    /// Explicit base URL configured by the user.
239    #[must_use]
240    fn base_url(&self) -> Option<&Path>;
241
242    /// Sets the base URL.
243    fn set_base_url(&mut self, base_url: PathBuf);
244
245    /// Path aliases.
246    #[must_use]
247    fn paths(&self) -> Option<&CompilerOptionsPathsMap>;
248
249    /// Returns a mutable reference to the path aliases.
250    #[must_use]
251    fn paths_mut(&mut self) -> Option<&mut CompilerOptionsPathsMap>;
252
253    /// Sets the path aliases.
254    fn set_paths(&mut self, paths: Option<CompilerOptionsPathsMap>);
255
256    /// The actual base from where path aliases are resolved.
257    #[must_use]
258    fn paths_base(&self) -> &Path;
259
260    /// Sets the path base.
261    fn set_paths_base(&mut self, paths_base: PathBuf);
262
263    /// Whether to enable experimental decorators.
264    fn experimental_decorators(&self) -> Option<&bool> {
265        None
266    }
267
268    /// Sets whether to enable experimental decorators.
269    fn set_experimental_decorators(&mut self, _experimental_decorators: bool) {}
270
271    /// JSX.
272    fn jsx(&self) -> Option<&str> {
273        None
274    }
275
276    /// Sets JSX.
277    fn set_jsx(&mut self, _jsx: String) {}
278
279    /// JSX factory.
280    fn jsx_factory(&self) -> Option<&str> {
281        None
282    }
283
284    /// Sets JSX factory.
285    fn set_jsx_factory(&mut self, _jsx_factory: String) {}
286
287    /// JSX fragment factory.
288    fn jsx_fragment_factory(&self) -> Option<&str> {
289        None
290    }
291
292    /// Sets JSX fragment factory.
293    fn set_jsx_fragment_factory(&mut self, _jsx_fragment_factory: String) {}
294
295    /// JSX import source.
296    fn jsx_import_source(&self) -> Option<&str> {
297        None
298    }
299
300    /// Sets JSX import source.
301    fn set_jsx_import_source(&mut self, _jsx_import_source: String) {}
302}
303
304/// Project Reference.
305///
306/// <https://www.typescriptlang.org/docs/handbook/project-references.html>
307pub trait ProjectReference {
308    type Tc: TsConfig;
309
310    /// Returns the path to a directory containing a `tsconfig.json` file, or to
311    /// the config file itself (which may have any name).
312    #[must_use]
313    fn path(&self) -> &Path;
314
315    /// Returns the resolved tsconfig, if one has been set.
316    #[must_use]
317    fn tsconfig(&self) -> Option<Arc<Self::Tc>>;
318
319    /// Sets the resolved tsconfig.
320    fn set_tsconfig(&mut self, tsconfig: Arc<Self::Tc>);
321}