swc_ecma_loader/resolvers/
tsc.rs

1use std::{
2    cmp::Ordering,
3    path::{Component, Path, PathBuf},
4};
5
6use anyhow::{bail, Context, Error};
7use swc_common::FileName;
8use tracing::{debug, info, trace, warn, Level};
9
10use crate::resolve::{Resolution, Resolve};
11
12#[derive(Debug)]
13enum Pattern {
14    Wildcard {
15        prefix: String,
16    },
17    /// No wildcard.
18    Exact(String),
19}
20
21/// Support for `paths` of `tsconfig.json`.
22///
23/// See https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping
24#[derive(Debug)]
25pub struct TsConfigResolver<R>
26where
27    R: Resolve,
28{
29    inner: R,
30    base_url: PathBuf,
31    base_url_filename: FileName,
32    paths: Vec<(Pattern, Vec<String>)>,
33}
34
35impl<R> TsConfigResolver<R>
36where
37    R: Resolve,
38{
39    ///
40    /// # Parameters
41    ///
42    /// ## base_url
43    ///
44    /// See https://www.typescriptlang.org/tsconfig#baseUrl
45    ///
46    /// The typescript documentation says `This must be specified if "paths"
47    /// is.`.
48    ///
49    /// ## `paths`
50    ///
51    /// Pass `paths` map from `tsconfig.json`.
52    ///
53    /// See https://www.typescriptlang.org/tsconfig#paths
54    ///
55    /// Note that this is not a hashmap because value is not used as a hash map.
56    pub fn new(inner: R, base_url: PathBuf, paths: Vec<(String, Vec<String>)>) -> Self {
57        if cfg!(debug_assertions) {
58            info!(
59                base_url = tracing::field::display(base_url.display()),
60                "jsc.paths"
61            );
62        }
63
64        let mut paths: Vec<(Pattern, Vec<String>)> = paths
65            .into_iter()
66            .map(|(from, to)| {
67                assert!(
68                    !to.is_empty(),
69                    "value of `paths.{from}` should not be an empty array",
70                );
71
72                let pos = from.as_bytes().iter().position(|&c| c == b'*');
73                let pat = if from.contains('*') {
74                    if from.as_bytes().iter().rposition(|&c| c == b'*') != pos {
75                        panic!("`paths.{from}` should have only one wildcard")
76                    }
77
78                    Pattern::Wildcard {
79                        prefix: from[..pos.unwrap()].to_string(),
80                    }
81                } else {
82                    assert_eq!(
83                        to.len(),
84                        1,
85                        "value of `paths.{from}` should be an array with one element because the \
86                         src path does not contains * (wildcard)",
87                    );
88
89                    Pattern::Exact(from)
90                };
91
92                (pat, to)
93            })
94            .collect();
95
96        paths.sort_by(|(a, _), (b, _)| match (a, b) {
97            (Pattern::Wildcard { .. }, Pattern::Exact(_)) => Ordering::Greater,
98            (Pattern::Exact(_), Pattern::Wildcard { .. }) => Ordering::Less,
99            (Pattern::Exact(_), Pattern::Exact(_)) => Ordering::Equal,
100            (Pattern::Wildcard { prefix: prefix_a }, Pattern::Wildcard { prefix: prefix_b }) => {
101                prefix_a.len().cmp(&prefix_b.len()).reverse()
102            }
103        });
104
105        Self {
106            inner,
107            base_url_filename: FileName::Real(base_url.clone()),
108            base_url,
109            paths,
110        }
111    }
112
113    fn invoke_inner_resolver(
114        &self,
115        base: &FileName,
116        module_specifier: &str,
117    ) -> Result<Resolution, Error> {
118        let res = self.inner.resolve(base, module_specifier).with_context(|| {
119            format!(
120                "failed to resolve `{module_specifier}` from `{base}` using inner \
121                 resolver\nbase_url={}",
122                self.base_url_filename
123            )
124        });
125
126        match res {
127            Ok(resolved) => {
128                info!(
129                    "Resolved `{}` as `{}` from `{}`",
130                    module_specifier, resolved.filename, base
131                );
132
133                let is_base_in_node_modules = if let FileName::Real(v) = base {
134                    v.components().any(|c| match c {
135                        Component::Normal(v) => v == "node_modules",
136                        _ => false,
137                    })
138                } else {
139                    false
140                };
141                let is_target_in_node_modules = if let FileName::Real(v) = &resolved.filename {
142                    v.components().any(|c| match c {
143                        Component::Normal(v) => v == "node_modules",
144                        _ => false,
145                    })
146                } else {
147                    false
148                };
149
150                // If node_modules is in path, we should return module specifier.
151                if !is_base_in_node_modules && is_target_in_node_modules {
152                    return Ok(Resolution {
153                        filename: FileName::Real(module_specifier.into()),
154                        ..resolved
155                    });
156                }
157
158                Ok(resolved)
159            }
160
161            Err(err) => {
162                warn!("{:?}", err);
163                Err(err)
164            }
165        }
166    }
167}
168
169impl<R> Resolve for TsConfigResolver<R>
170where
171    R: Resolve,
172{
173    fn resolve(&self, base: &FileName, module_specifier: &str) -> Result<Resolution, Error> {
174        let _tracing = if cfg!(debug_assertions) {
175            Some(
176                tracing::span!(
177                    Level::ERROR,
178                    "TsConfigResolver::resolve",
179                    base_url = tracing::field::display(self.base_url.display()),
180                    base = tracing::field::display(base),
181                    src = tracing::field::display(module_specifier),
182                )
183                .entered(),
184            )
185        } else {
186            None
187        };
188
189        if module_specifier.starts_with('.')
190            && (module_specifier == ".."
191                || module_specifier.starts_with("./")
192                || module_specifier.starts_with("../"))
193        {
194            return self
195                .invoke_inner_resolver(base, module_specifier)
196                .context("not processed by tsc resolver because it's relative import");
197        }
198
199        if let FileName::Real(v) = base {
200            if v.components().any(|c| match c {
201                Component::Normal(v) => v == "node_modules",
202                _ => false,
203            }) {
204                return self.invoke_inner_resolver(base, module_specifier).context(
205                    "not processed by tsc resolver because base module is in node_modules",
206                );
207            }
208        }
209
210        info!("Checking `jsc.paths`");
211
212        // https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping
213        for (from, to) in &self.paths {
214            match from {
215                Pattern::Wildcard { prefix } => {
216                    debug!("Checking `{}` in `jsc.paths`", prefix);
217
218                    let extra = module_specifier.strip_prefix(prefix);
219                    let extra = match extra {
220                        Some(v) => v,
221                        None => {
222                            if cfg!(debug_assertions) {
223                                trace!("skip because src doesn't start with prefix");
224                            }
225                            continue;
226                        }
227                    };
228
229                    if cfg!(debug_assertions) {
230                        debug!("Extra: `{}`", extra);
231                    }
232
233                    let mut errors = Vec::new();
234                    for target in to {
235                        let replaced = target.replace('*', extra);
236
237                        let _tracing = if cfg!(debug_assertions) {
238                            Some(
239                                tracing::span!(
240                                    Level::ERROR,
241                                    "TsConfigResolver::resolve::jsc.paths",
242                                    replaced = tracing::field::display(&replaced),
243                                )
244                                .entered(),
245                            )
246                        } else {
247                            None
248                        };
249
250                        let relative = format!("./{replaced}");
251
252                        let res = self
253                            .invoke_inner_resolver(base, module_specifier)
254                            .or_else(|_| {
255                                self.invoke_inner_resolver(&self.base_url_filename, &relative)
256                            })
257                            .or_else(|_| {
258                                self.invoke_inner_resolver(&self.base_url_filename, &replaced)
259                            });
260
261                        errors.push(match res {
262                            Ok(resolved) => return Ok(resolved),
263                            Err(err) => err,
264                        });
265
266                        if to.len() == 1 && !prefix.is_empty() {
267                            info!(
268                                "Using `{}` for `{}` because the length of the jsc.paths entry is \
269                                 1",
270                                replaced, module_specifier
271                            );
272                            return Ok(Resolution {
273                                slug: Some(
274                                    replaced
275                                        .split([std::path::MAIN_SEPARATOR, '/'])
276                                        .next_back()
277                                        .unwrap()
278                                        .into(),
279                                ),
280                                filename: FileName::Real(replaced.into()),
281                            });
282                        }
283                    }
284
285                    bail!(
286                        "`{}` matched `{}` (from tsconfig.paths) but failed to resolve:\n{:?}",
287                        module_specifier,
288                        prefix,
289                        errors
290                    )
291                }
292                Pattern::Exact(from) => {
293                    // Should be exactly matched
294                    if module_specifier != from {
295                        continue;
296                    }
297
298                    let tp = Path::new(&to[0]);
299                    let slug = to[0]
300                        .split([std::path::MAIN_SEPARATOR, '/'])
301                        .next_back()
302                        .filter(|&slug| slug != "index.ts" && slug != "index.tsx")
303                        .map(|v| v.rsplit_once('.').map(|v| v.0).unwrap_or(v))
304                        .map(From::from);
305
306                    if tp.is_absolute() {
307                        return Ok(Resolution {
308                            filename: FileName::Real(tp.into()),
309                            slug,
310                        });
311                    }
312
313                    if let Ok(res) = self
314                        .invoke_inner_resolver(&self.base_url_filename, &format!("./{}", &to[0]))
315                    {
316                        return Ok(Resolution { slug, ..res });
317                    }
318
319                    return Ok(Resolution {
320                        filename: FileName::Real(self.base_url.join(&to[0])),
321                        slug,
322                    });
323                }
324            }
325        }
326
327        let path = Path::new(module_specifier);
328        if matches!(path.components().next(), Some(Component::Normal(_))) {
329            let path = self.base_url.join(module_specifier);
330
331            // https://www.typescriptlang.org/docs/handbook/modules/reference.html#baseurl
332            if let Ok(v) = self.invoke_inner_resolver(base, &path.to_string_lossy()) {
333                return Ok(v);
334            }
335        }
336
337        self.invoke_inner_resolver(base, module_specifier)
338    }
339}