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.{}` 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 !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 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 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 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}