swc_ecma_transforms_module/
path.rs1use std::{
2 borrow::Cow,
3 env::current_dir,
4 fs::canonicalize,
5 io,
6 path::{Component, Path, PathBuf},
7 sync::Arc,
8};
9
10use anyhow::{anyhow, Context, Error};
11use path_clean::PathClean;
12use pathdiff::diff_paths;
13use swc_atoms::Atom;
14use swc_common::{FileName, Mark, Span, SyntaxContext, DUMMY_SP};
15use swc_ecma_ast::*;
16use swc_ecma_loader::resolve::{Resolution, Resolve};
17use swc_ecma_utils::{quote_ident, ExprFactory};
18use tracing::{debug, info, warn, Level};
19
20#[derive(Default)]
21pub enum Resolver {
22 Real {
23 base: FileName,
24 resolver: Arc<dyn ImportResolver>,
25 },
26 #[default]
27 Default,
28}
29
30impl Resolver {
31 pub(crate) fn resolve(&self, src: Atom) -> Atom {
32 match self {
33 Self::Real { resolver, base } => resolver
34 .resolve_import(base, &src)
35 .with_context(|| format!("failed to resolve import `{}`", src))
36 .unwrap(),
37 Self::Default => src,
38 }
39 }
40
41 pub(crate) fn make_require_call(
42 &self,
43 unresolved_mark: Mark,
44 src: Atom,
45 src_span: Span,
46 ) -> Expr {
47 let src = self.resolve(src);
48
49 CallExpr {
50 span: DUMMY_SP,
51 callee: quote_ident!(
52 SyntaxContext::empty().apply_mark(unresolved_mark),
53 "require"
54 )
55 .as_callee(),
56 args: vec![Lit::Str(Str {
57 span: src_span,
58 raw: None,
59 value: src,
60 })
61 .as_arg()],
62 ..Default::default()
63 }
64 .into()
65 }
66}
67
68pub trait ImportResolver {
69 fn resolve_import(&self, base: &FileName, module_specifier: &str) -> Result<Atom, Error>;
73}
74
75#[derive(Debug, Clone, Copy, Default)]
77pub struct NoopImportResolver;
78
79impl ImportResolver for NoopImportResolver {
80 fn resolve_import(&self, _: &FileName, module_specifier: &str) -> Result<Atom, Error> {
81 Ok(module_specifier.into())
82 }
83}
84
85#[derive(Debug, Clone, Default)]
91pub struct NodeImportResolver<R>
92where
93 R: Resolve,
94{
95 resolver: R,
96 config: Config,
97}
98
99#[derive(Debug, Clone)]
100pub struct Config {
101 pub base_dir: Option<PathBuf>,
102 pub resolve_fully: bool,
103 pub file_extension: String,
104}
105
106impl Default for Config {
107 fn default() -> Config {
108 Config {
109 file_extension: crate::util::Config::default_js_ext(),
110 resolve_fully: bool::default(),
111 base_dir: Option::default(),
112 }
113 }
114}
115
116impl<R> NodeImportResolver<R>
117where
118 R: Resolve,
119{
120 pub fn with_config(resolver: R, config: Config) -> Self {
121 #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"))))]
122 if let Some(base_dir) = &config.base_dir {
123 assert!(
124 base_dir.is_absolute(),
125 "base_dir(`{}`) must be absolute. Please ensure that `jsc.baseUrl` is specified \
126 correctly. This cannot be deduced by SWC itself because SWC is a transpiler and \
127 it does not try to resolve project details. In other words, SWC does not know \
128 which directory should be used as a base directory. It can be deduced if \
129 `.swcrc` is used, but if not, there are many candidates. e.g. the directory \
130 containing `package.json`, or the current working directory. Because of that, \
131 the caller (typically the developer of the JavaScript package) should specify \
132 it. If you see this error, please report an issue to the package author.",
133 base_dir.display()
134 );
135 }
136
137 Self { resolver, config }
138 }
139}
140
141impl<R> NodeImportResolver<R>
142where
143 R: Resolve,
144{
145 fn to_specifier(&self, mut target_path: PathBuf, orig_filename: Option<&str>) -> Atom {
146 debug!(
147 "Creating a specifier for `{}` with original filename `{:?}`",
148 target_path.display(),
149 orig_filename
150 );
151
152 if let Some(orig_filename) = orig_filename {
153 let is_resolved_as_index = if let Some(stem) = target_path.file_stem() {
154 stem == "index"
155 } else {
156 false
157 };
158
159 let is_resolved_as_non_js = if let Some(ext) = target_path.extension() {
160 ext.to_string_lossy() != self.config.file_extension
161 } else {
162 false
163 };
164
165 let is_resolved_as_js = if let Some(ext) = target_path.extension() {
166 ext.to_string_lossy() == self.config.file_extension
167 } else {
168 false
169 };
170
171 let is_exact = if let Some(filename) = target_path.file_name() {
172 filename == orig_filename
173 } else {
174 false
175 };
176
177 let file_stem_matches = if let Some(stem) = target_path.file_stem() {
178 stem == orig_filename
179 } else {
180 false
181 };
182
183 if self.config.resolve_fully && is_resolved_as_js {
184 } else if orig_filename == "index" {
185 if self.config.resolve_fully {
189 target_path.set_file_name(format!("index.{}", self.config.file_extension));
190 } else {
191 target_path.set_file_name("index");
192 }
193 } else if is_resolved_as_index
194 && is_resolved_as_js
195 && orig_filename != format!("index.{}", self.config.file_extension)
196 {
197 target_path.pop();
201 } else if is_resolved_as_non_js && self.config.resolve_fully && file_stem_matches {
202 target_path.set_extension(self.config.file_extension.clone());
203 } else if !is_resolved_as_js && !is_resolved_as_index && !is_exact {
204 target_path.set_file_name(orig_filename);
205 } else if is_resolved_as_non_js && is_exact {
206 if let Some(ext) = Path::new(orig_filename).extension() {
207 target_path.set_extension(ext);
208 } else {
209 target_path.set_extension(self.config.file_extension.clone());
210 }
211 } else if self.config.resolve_fully && is_resolved_as_non_js {
212 target_path.set_extension(self.config.file_extension.clone());
213 } else if is_resolved_as_non_js && is_resolved_as_index {
214 if orig_filename == "index" {
215 target_path.set_extension("");
216 } else {
217 target_path.pop();
218 }
219 }
220 } else {
221 target_path.set_extension("");
222 }
223
224 if cfg!(target_os = "windows") {
225 target_path.display().to_string().replace('\\', "/").into()
226 } else {
227 target_path.display().to_string().into()
228 }
229 }
230
231 fn try_resolve_import(&self, base: &FileName, module_specifier: &str) -> Result<Atom, Error> {
232 let _tracing = if cfg!(debug_assertions) {
233 Some(
234 tracing::span!(
235 Level::ERROR,
236 "resolve_import",
237 base = tracing::field::display(base),
238 module_specifier = tracing::field::display(module_specifier),
239 )
240 .entered(),
241 )
242 } else {
243 None
244 };
245
246 let orig_slug = module_specifier.split('/').last();
247
248 let target = self.resolver.resolve(base, module_specifier);
249 let mut target = match target {
250 Ok(v) => v,
251 Err(err) => {
252 warn!("import rewriter: failed to resolve: {}", err);
253 return Ok(module_specifier.into());
254 }
255 };
256
257 if let FileName::Real(resolved) = &target.filename {
261 if let Ok(orig) = canonicalize(resolved) {
262 target.filename = FileName::Real(orig);
263 }
264 }
265
266 let Resolution {
267 filename: target,
268 slug,
269 } = target;
270 let slug = slug.as_deref().or(orig_slug);
271
272 info!("Resolved as {target:?} with slug = {slug:?}");
273
274 let mut target = match target {
275 FileName::Real(v) => v,
276 FileName::Custom(s) => return Ok(self.to_specifier(s.into(), slug)),
277 _ => {
278 unreachable!(
279 "Node path provider does not support using `{:?}` as a target file name",
280 target
281 )
282 }
283 };
284 let mut base = match base {
285 FileName::Real(v) => Cow::Borrowed(
286 v.parent()
287 .ok_or_else(|| anyhow!("failed to get parent of {:?}", v))?,
288 ),
289 FileName::Anon => match &self.config.base_dir {
290 Some(v) => Cow::Borrowed(&**v),
291 None => {
292 if cfg!(target_arch = "wasm32") {
293 panic!("Please specify `filename`")
294 } else {
295 Cow::Owned(current_dir().expect("failed to get current directory"))
296 }
297 }
298 },
299 _ => {
300 unreachable!(
301 "Node path provider does not support using `{:?}` as a base file name",
302 base
303 )
304 }
305 };
306
307 if base.is_absolute() != target.is_absolute() {
308 base = Cow::Owned(absolute_path(self.config.base_dir.as_deref(), &base)?);
309 target = absolute_path(self.config.base_dir.as_deref(), &target)?;
310 }
311
312 debug!(
313 "Comparing values (after normalizing absoluteness)\nbase={}\ntarget={}",
314 base.display(),
315 target.display()
316 );
317
318 let rel_path = diff_paths(&target, &*base);
319
320 let rel_path = match rel_path {
321 Some(v) => v,
322 None => return Ok(self.to_specifier(target, slug)),
323 };
324
325 debug!("Relative path: {}", rel_path.display());
326
327 {
328 for component in rel_path.components() {
331 match component {
332 Component::Prefix(_) => {}
333 Component::RootDir => {}
334 Component::CurDir => {}
335 Component::ParentDir => {}
336 Component::Normal(c) => {
337 if c == "node_modules" {
338 return Ok(module_specifier.into());
339 }
340 }
341 }
342 }
343 }
344
345 let s = rel_path.to_string_lossy();
346 let s = if s.starts_with('.') || s.starts_with('/') || rel_path.is_absolute() {
347 s
348 } else {
349 Cow::Owned(format!("./{}", s))
350 };
351
352 Ok(self.to_specifier(s.into_owned().into(), slug))
353 }
354}
355
356impl<R> ImportResolver for NodeImportResolver<R>
357where
358 R: Resolve,
359{
360 fn resolve_import(&self, base: &FileName, module_specifier: &str) -> Result<Atom, Error> {
361 self.try_resolve_import(base, module_specifier)
362 .or_else(|err| {
363 warn!("Failed to resolve import: {}", err);
364 Ok(module_specifier.into())
365 })
366 }
367}
368
369macro_rules! impl_ref {
370 ($P:ident, $T:ty) => {
371 impl<$P> ImportResolver for $T
372 where
373 $P: ImportResolver,
374 {
375 fn resolve_import(&self, base: &FileName, target: &str) -> Result<Atom, Error> {
376 (**self).resolve_import(base, target)
377 }
378 }
379 };
380}
381
382impl_ref!(P, &'_ P);
383impl_ref!(P, Box<P>);
384impl_ref!(P, Arc<P>);
385
386fn absolute_path(base_dir: Option<&Path>, path: &Path) -> io::Result<PathBuf> {
387 let absolute_path = if path.is_absolute() {
388 path.to_path_buf()
389 } else {
390 match base_dir {
391 Some(base_dir) => base_dir.join(path),
392 None => current_dir()?.join(path),
393 }
394 }
395 .clean();
396
397 Ok(absolute_path)
398}