swc_ecma_loader/resolvers/
tsc.rs1use 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 Exact(String),
19}
20
21#[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 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 !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 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 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 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}