swc_ecma_loader/resolvers/
node.rs1use std::{
6 env::current_dir,
7 fs::File,
8 io::BufReader,
9 path::{Component, Path, PathBuf},
10};
11
12use anyhow::{bail, Context, Error};
13use dashmap::DashMap;
14#[cfg(windows)]
15use normpath::BasePath;
16use once_cell::sync::Lazy;
17use path_clean::PathClean;
18use pathdiff::diff_paths;
19use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
20use serde::Deserialize;
21use swc_common::FileName;
22use tracing::{debug, trace, Level};
23
24use crate::{
25 resolve::{Resolution, Resolve},
26 TargetEnv, NODE_BUILTINS,
27};
28
29static PACKAGE: &str = "package.json";
30
31static BROWSER_CACHE: Lazy<DashMap<PathBuf, BrowserCache, FxBuildHasher>> =
38 Lazy::new(Default::default);
39
40#[derive(Debug, Default)]
41struct BrowserCache {
42 rewrites: FxHashMap<PathBuf, PathBuf>,
43 ignores: FxHashSet<PathBuf>,
44 module_rewrites: FxHashMap<String, PathBuf>,
45 module_ignores: FxHashSet<String>,
46}
47
48fn find_package_root(path: &Path) -> Option<PathBuf> {
51 let mut parent = path.parent();
52 while let Some(p) = parent {
53 let pkg = p.join(PACKAGE);
54 if pkg.is_file() {
55 return Some(p.to_path_buf());
56 }
57 parent = p.parent();
58 }
59 None
60}
61
62pub fn to_absolute_path(path: &Path) -> Result<PathBuf, Error> {
63 let absolute_path = if path.is_absolute() {
64 path.to_path_buf()
65 } else {
66 current_dir()?.join(path)
67 }
68 .clean();
69
70 Ok(absolute_path)
71}
72
73pub(crate) fn is_core_module(s: &str) -> bool {
74 NODE_BUILTINS.contains(&s)
75}
76
77#[derive(Deserialize)]
78struct PackageJson {
79 #[serde(default)]
80 main: Option<String>,
81 #[serde(default)]
82 browser: Option<Browser>,
83 #[serde(default)]
84 module: Option<String>,
85}
86
87#[derive(Deserialize)]
88#[serde(untagged)]
89enum Browser {
90 Str(String),
91 Obj(FxHashMap<String, StringOrBool>),
92}
93
94#[derive(Deserialize, Clone)]
95#[serde(untagged)]
96enum StringOrBool {
97 Str(String),
98 Bool(bool),
99}
100
101#[derive(Debug, Default)]
102pub struct NodeModulesResolver {
103 target_env: TargetEnv,
104 alias: FxHashMap<String, String>,
105 preserve_symlinks: bool,
107 ignore_node_modules: bool,
108}
109
110static EXTENSIONS: &[&str] = &["ts", "tsx", "js", "jsx", "node"];
111
112impl NodeModulesResolver {
113 pub fn new(
115 target_env: TargetEnv,
116 alias: FxHashMap<String, String>,
117 preserve_symlinks: bool,
118 ) -> Self {
119 Self {
120 target_env,
121 alias,
122 preserve_symlinks,
123 ignore_node_modules: false,
124 }
125 }
126
127 pub fn without_node_modules(
129 target_env: TargetEnv,
130 alias: FxHashMap<String, String>,
131 preserve_symlinks: bool,
132 ) -> Self {
133 Self {
134 target_env,
135 alias,
136 preserve_symlinks,
137 ignore_node_modules: true,
138 }
139 }
140
141 fn wrap(&self, path: Option<PathBuf>) -> Result<FileName, Error> {
142 if let Some(path) = path {
143 if self.preserve_symlinks {
144 return Ok(FileName::Real(path.clean()));
145 } else {
146 return Ok(FileName::Real(path.canonicalize()?));
147 }
148 }
149 bail!("index not found")
150 }
151
152 fn resolve_as_file(&self, path: &Path) -> Result<Option<PathBuf>, Error> {
155 let _tracing = if cfg!(debug_assertions) {
156 Some(
157 tracing::span!(
158 Level::TRACE,
159 "resolve_as_file",
160 path = tracing::field::display(path.display())
161 )
162 .entered(),
163 )
164 } else {
165 None
166 };
167
168 if cfg!(debug_assertions) {
169 trace!("resolve_as_file({})", path.display());
170 }
171
172 let try_exact = path.extension().is_some();
173 if try_exact {
174 if path.is_file() {
175 return Ok(Some(path.to_path_buf()));
176 }
177 } else {
178 let mut path = path.to_path_buf();
180 path.set_extension("js");
181 if path.is_file() {
182 return Ok(Some(path));
183 }
184 }
185
186 if !try_exact && path.is_file() {
188 return Ok(Some(path.to_path_buf()));
189 }
190
191 if let Some(name) = path.file_name() {
192 let mut ext_path = path.to_path_buf();
193 let name = name.to_string_lossy();
194 for ext in EXTENSIONS {
195 ext_path.set_file_name(format!("{name}.{ext}"));
196 if ext_path.is_file() {
197 return Ok(Some(ext_path));
198 }
199 }
200
201 ext_path.set_file_name(name.into_owned());
204 let old_ext = path.extension().and_then(|ext| ext.to_str());
205
206 if let Some(old_ext) = old_ext {
207 let extensions: &[&str] = match old_ext {
208 "js" => &["ts", "tsx"],
211 "jsx" => &["ts", "tsx"],
212 "mjs" => &["mts"],
213 "cjs" => &["cts"],
214 _ => &[],
215 };
216
217 for ext in extensions {
218 ext_path.set_extension(ext);
219
220 if ext_path.is_file() {
221 return Ok(Some(ext_path));
222 }
223 }
224 }
225 }
226
227 bail!("file not found: {}", path.display())
228 }
229
230 fn resolve_as_directory(
233 &self,
234 path: &Path,
235 allow_package_entry: bool,
236 ) -> Result<Option<PathBuf>, Error> {
237 let _tracing = if cfg!(debug_assertions) {
238 Some(
239 tracing::span!(
240 Level::TRACE,
241 "resolve_as_directory",
242 path = tracing::field::display(path.display())
243 )
244 .entered(),
245 )
246 } else {
247 None
248 };
249
250 if cfg!(debug_assertions) {
251 trace!("resolve_as_directory({})", path.display());
252 }
253
254 let pkg_path = path.join(PACKAGE);
255 if allow_package_entry && pkg_path.is_file() {
256 if let Some(main) = self.resolve_package_entry(path, &pkg_path)? {
257 return Ok(Some(main));
258 }
259 }
260
261 for ext in EXTENSIONS {
263 let ext_path = path.join(format!("index.{ext}"));
264 if ext_path.is_file() {
265 return Ok(Some(ext_path));
266 }
267 }
268 Ok(None)
269 }
270
271 fn resolve_package_entry(
273 &self,
274 pkg_dir: &Path,
275 pkg_path: &Path,
276 ) -> Result<Option<PathBuf>, Error> {
277 let _tracing = if cfg!(debug_assertions) {
278 Some(
279 tracing::span!(
280 Level::TRACE,
281 "resolve_package_entry",
282 pkg_dir = tracing::field::display(pkg_dir.display()),
283 pkg_path = tracing::field::display(pkg_path.display()),
284 )
285 .entered(),
286 )
287 } else {
288 None
289 };
290
291 let file = File::open(pkg_path)?;
292 let reader = BufReader::new(file);
293 let pkg: PackageJson = serde_json::from_reader(reader)
294 .context(format!("failed to deserialize {}", pkg_path.display()))?;
295
296 let main_fields = match self.target_env {
297 TargetEnv::Node => {
298 vec![pkg.module.as_ref(), pkg.main.as_ref()]
299 }
300 TargetEnv::Browser => {
301 if let Some(browser) = &pkg.browser {
302 match browser {
303 Browser::Str(path) => {
304 vec![Some(path), pkg.module.as_ref(), pkg.main.as_ref()]
305 }
306 Browser::Obj(map) => {
307 let mut bucket = BrowserCache::default();
308
309 for (k, v) in map {
310 let target_key = Path::new(k);
311 let mut components = target_key.components();
312
313 let source = if let Some(Component::CurDir) = components.next() {
315 let path = pkg_dir.join(k);
316 if let Ok(file) = self
317 .resolve_as_file(&path)
318 .or_else(|_| self.resolve_as_directory(&path, false))
319 {
320 file.map(|file| file.clean())
321 } else {
322 None
323 }
324 } else {
325 None
326 };
327
328 match v {
329 StringOrBool::Str(dest) => {
330 let path = pkg_dir.join(dest);
331 let file = self
332 .resolve_as_file(&path)
333 .or_else(|_| self.resolve_as_directory(&path, false))?;
334 if let Some(file) = file {
335 let target = file.clean();
336 let target = target
337 .strip_prefix(current_dir().unwrap_or_default())
338 .map(|target| target.to_path_buf())
339 .unwrap_or(target);
340
341 if let Some(source) = source {
342 bucket.rewrites.insert(source, target);
343 } else {
344 bucket.module_rewrites.insert(k.clone(), target);
345 }
346 }
347 }
348 StringOrBool::Bool(flag) => {
349 if !flag {
352 if let Some(source) = source {
353 bucket.ignores.insert(source);
354 } else {
355 bucket.module_ignores.insert(k.clone());
356 }
357 }
358 }
359 }
360 }
361
362 BROWSER_CACHE.insert(pkg_dir.to_path_buf(), bucket);
363
364 vec![pkg.module.as_ref(), pkg.main.as_ref()]
365 }
366 }
367 } else {
368 vec![pkg.module.as_ref(), pkg.main.as_ref()]
369 }
370 }
371 };
372
373 if let Some(Some(target)) = main_fields.iter().find(|x| x.is_some()) {
374 let path = pkg_dir.join(target);
375 return self
376 .resolve_as_file(&path)
377 .or_else(|_| self.resolve_as_directory(&path, false));
378 }
379
380 Ok(None)
381 }
382
383 fn resolve_node_modules(
385 &self,
386 base_dir: &Path,
387 target: &str,
388 ) -> Result<Option<PathBuf>, Error> {
389 if self.ignore_node_modules {
390 return Ok(None);
391 }
392
393 let absolute_path = to_absolute_path(base_dir)?;
394 let mut path = Some(&*absolute_path);
395 while let Some(dir) = path {
396 let node_modules = dir.join("node_modules");
397 if node_modules.is_dir() {
398 let path = node_modules.join(target);
399 if let Some(result) = self
400 .resolve_as_file(&path)
401 .ok()
402 .or_else(|| self.resolve_as_directory(&path, true).ok())
403 .flatten()
404 {
405 return Ok(Some(result));
406 }
407 }
408 path = dir.parent();
409 }
410
411 Ok(None)
412 }
413
414 fn resolve_filename(&self, base: &FileName, module_specifier: &str) -> Result<FileName, Error> {
415 debug!(
416 "Resolving {} from {:#?} for {:#?}",
417 module_specifier, base, self.target_env
418 );
419
420 let path = Path::new(module_specifier);
421 if path.is_absolute() {
422 if let Ok(file) = self
423 .resolve_as_file(path)
424 .or_else(|_| self.resolve_as_directory(path, false))
425 {
426 if let Ok(file) = self.wrap(file) {
427 return Ok(file);
428 }
429 }
430 }
431
432 let base = match base {
433 FileName::Real(v) => v,
434 _ => bail!("node-resolver supports only files"),
435 };
436
437 let base_dir = if base.is_file() {
438 let cwd = &Path::new(".");
439 base.parent().unwrap_or(cwd)
440 } else {
441 base
442 };
443
444 if let TargetEnv::Browser = self.target_env {
447 if let Some(pkg_base) = find_package_root(base) {
448 if let Some(item) = BROWSER_CACHE.get(&pkg_base) {
449 let value = item.value();
450 if value.module_ignores.contains(module_specifier) {
451 return Ok(FileName::Custom(module_specifier.into()));
452 }
453 if let Some(rewrite) = value.module_rewrites.get(module_specifier) {
454 return self.wrap(Some(rewrite.to_path_buf()));
455 }
456 }
457 }
458 }
459
460 if let TargetEnv::Node = self.target_env {
462 if module_specifier.starts_with("node:") {
463 return Ok(FileName::Custom(module_specifier.into()));
464 }
465
466 if is_core_module(module_specifier) {
467 return Ok(FileName::Custom(format!("node:{module_specifier}")));
468 }
469 }
470
471 let target = if let Some(alias) = self.alias.get(module_specifier) {
474 &alias[..]
475 } else {
476 module_specifier
477 };
478
479 let target_path = Path::new(target);
480
481 let file_name = {
482 if target_path.is_absolute() {
483 let path = PathBuf::from(target_path);
484 self.resolve_as_file(&path)
485 .or_else(|_| self.resolve_as_directory(&path, true))
486 .and_then(|p| self.wrap(p))
487 } else {
488 let mut components = target_path.components();
489
490 if let Some(Component::CurDir | Component::ParentDir) = components.next() {
491 #[cfg(windows)]
492 let path = {
493 let base_dir = BasePath::new(base_dir).unwrap();
494 base_dir
495 .join(target.replace('/', "\\"))
496 .normalize_virtually()
497 .unwrap()
498 .into_path_buf()
499 };
500 #[cfg(not(windows))]
501 let path = base_dir.join(target);
502 self.resolve_as_file(&path)
503 .or_else(|_| self.resolve_as_directory(&path, true))
504 .and_then(|p| self.wrap(p))
505 } else {
506 self.resolve_node_modules(base_dir, target)
507 .and_then(|path| {
508 let file_path = path.context("failed to get the node_modules path");
509 let current_directory = current_dir()?;
510 let relative_path = diff_paths(file_path?, current_directory);
511 self.wrap(relative_path)
512 })
513 }
514 }
515 }
516 .and_then(|v| {
517 if let TargetEnv::Browser = self.target_env {
519 if let FileName::Real(path) = &v {
520 if let Some(pkg_base) = find_package_root(path) {
521 let pkg_base = to_absolute_path(&pkg_base).unwrap();
522 if let Some(item) = BROWSER_CACHE.get(&pkg_base) {
523 let value = item.value();
524 let path = to_absolute_path(path).unwrap();
525 if value.ignores.contains(&path) {
526 return Ok(FileName::Custom(path.display().to_string()));
527 }
528 if let Some(rewrite) = value.rewrites.get(&path) {
529 return self.wrap(Some(rewrite.to_path_buf()));
530 }
531 }
532 }
533 }
534 }
535 Ok(v)
536 });
537
538 file_name
539 }
540}
541
542impl Resolve for NodeModulesResolver {
543 fn resolve(&self, base: &FileName, module_specifier: &str) -> Result<Resolution, Error> {
544 self.resolve_filename(base, module_specifier)
545 .map(|filename| Resolution {
546 filename,
547 slug: None,
548 })
549 }
550}