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