oxc_resolver/
lib.rs

1//! # Oxc Resolver
2//!
3//! Node.js [CommonJS][cjs] and [ECMAScript][esm] Module Resolution.
4//!
5//! Released on [crates.io](https://crates.io/crates/oxc_resolver) and [npm](https://www.npmjs.com/package/oxc-resolver).
6//!
7//! A module resolution is the process of finding the file referenced by a module specifier in
8//! `import "specifier"` or `require("specifier")`.
9//!
10//! All [configuration options](ResolveOptions) are aligned with webpack's [enhanced-resolve].
11//!
12//! ## Terminology
13//!
14//! ### Specifier
15//!
16//! For [CommonJS modules][cjs],
17//! the specifier is the string passed to the `require` function. e.g. `"id"` in `require("id")`.
18//!
19//! For [ECMAScript modules][esm],
20//! the specifier of an `import` statement is the string after the `from` keyword,
21//! e.g. `'specifier'` in `import 'specifier'` or `import { sep } from 'specifier'`.
22//! Specifiers are also used in export from statements, and as the argument to an `import()` expression.
23//!
24//! This is also named "request" in some places.
25//!
26//! ## References:
27//!
28//! * Algorithm adapted from Node.js [CommonJS Module Resolution Algorithm] and [ECMAScript Module Resolution Algorithm].
29//! * Tests are ported from [enhanced-resolve].
30//! * Some code is adapted from [parcel-resolver].
31//! * The documentation is copied from [webpack's resolve configuration](https://webpack.js.org/configuration/resolve).
32//!
33//! [enhanced-resolve]: https://github.com/webpack/enhanced-resolve
34//! [CommonJS Module Resolution Algorithm]: https://nodejs.org/api/modules.html#all-together
35//! [ECMAScript Module Resolution Algorithm]: https://nodejs.org/api/esm.html#resolution-algorithm-specification
36//! [parcel-resolver]: https://github.com/parcel-bundler/parcel/blob/v2/packages/utils/node-resolver-rs
37//! [cjs]: https://nodejs.org/api/modules.html
38//! [esm]: https://nodejs.org/api/esm.html
39//!
40//! ## Feature flags
41#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
42//!
43//! ## Example
44//!
45//! ```rust,ignore
46#![doc = include_str!("../examples/resolver.rs")]
47//! ```
48
49mod builtins;
50mod cache;
51mod context;
52mod error;
53mod file_system;
54mod options;
55mod package_json;
56mod path;
57mod resolution;
58mod specifier;
59mod tsconfig;
60mod tsconfig_resolver;
61#[cfg(target_os = "windows")]
62mod windows;
63
64#[cfg(test)]
65mod tests;
66
67pub use crate::{
68    builtins::NODEJS_BUILTINS,
69    cache::{Cache, CachedPath},
70    error::{JSONError, ResolveError, SpecifierError},
71    file_system::{FileMetadata, FileSystem, FileSystemOs},
72    options::{
73        Alias, AliasValue, EnforceExtension, ResolveOptions, Restriction, TsconfigDiscovery,
74        TsconfigOptions, TsconfigReferences,
75    },
76    package_json::{
77        ImportsExportsArray, ImportsExportsEntry, ImportsExportsKind, ImportsExportsMap,
78        PackageJson, PackageType, SideEffects,
79    },
80    path::PathUtil,
81    resolution::{ModuleType, Resolution},
82    tsconfig::{
83        CompilerOptions, CompilerOptionsPathsMap, ExtendsField, ProjectReference, TsConfig,
84    },
85};
86
87use std::{
88    borrow::Cow,
89    cmp::Ordering,
90    ffi::OsStr,
91    fmt, iter,
92    path::{Component, Path, PathBuf},
93    sync::Arc,
94};
95
96use rustc_hash::FxHashSet;
97
98use crate::{context::ResolveContext as Ctx, path::SLASH_START, specifier::Specifier};
99
100type ResolveResult = Result<Option<CachedPath>, ResolveError>;
101
102/// Context returned from the [Resolver::resolve_with_context] API
103#[derive(Debug, Default, Clone)]
104pub struct ResolveContext {
105    /// Files that was found on file system
106    pub file_dependencies: FxHashSet<PathBuf>,
107
108    /// Dependencies that was not found on file system
109    pub missing_dependencies: FxHashSet<PathBuf>,
110}
111
112/// Resolver with the current operating system as the file system
113pub type Resolver = ResolverGeneric<FileSystemOs>;
114
115/// Generic implementation of the resolver, can be configured by the [Cache] trait
116pub struct ResolverGeneric<Fs> {
117    options: ResolveOptions,
118    cache: Arc<Cache<Fs>>,
119}
120
121impl<Fs> fmt::Debug for ResolverGeneric<Fs> {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        self.options.fmt(f)
124    }
125}
126
127impl<Fs: FileSystem> Default for ResolverGeneric<Fs> {
128    fn default() -> Self {
129        Self::new(ResolveOptions::default())
130    }
131}
132
133impl<Fs: FileSystem> ResolverGeneric<Fs> {
134    #[must_use]
135    pub fn new(options: ResolveOptions) -> Self {
136        cfg_if::cfg_if! {
137            if #[cfg(feature = "yarn_pnp")] {
138                let fs = Fs::new(options.yarn_pnp);
139            } else {
140                let fs = Fs::new();
141            }
142        }
143        let cache = Arc::new(Cache::new(fs));
144        Self { options: options.sanitize(), cache }
145    }
146}
147
148impl<Fs: FileSystem> ResolverGeneric<Fs> {
149    pub fn new_with_file_system(file_system: Fs, options: ResolveOptions) -> Self {
150        Self { cache: Arc::new(Cache::new(file_system)), options: options.sanitize() }
151    }
152
153    /// Clone the resolver using the same underlying cache.
154    #[must_use]
155    pub fn clone_with_options(&self, options: ResolveOptions) -> Self {
156        Self { options: options.sanitize(), cache: Arc::clone(&self.cache) }
157    }
158
159    /// Returns the options.
160    #[must_use]
161    pub const fn options(&self) -> &ResolveOptions {
162        &self.options
163    }
164
165    /// Clear the underlying cache.
166    pub fn clear_cache(&self) {
167        self.cache.clear();
168    }
169
170    /// Resolve `specifier` at an absolute path to a `directory`.
171    ///
172    /// A specifier is the string passed to require or import, i.e. `require("specifier")` or `import "specifier"`.
173    ///
174    /// `directory` must be an **absolute** path to a directory where the specifier is resolved against.
175    /// For CommonJS modules, it is the `__dirname` variable that contains the absolute path to the folder containing current module.
176    /// For ECMAScript modules, it is the value of `import.meta.url`.
177    ///
178    /// NOTE: [TsconfigDiscovery::Auto] does not work for this API, use
179    /// `ResolverGeneric::resolve_file` instead.
180    ///
181    /// # Errors
182    ///
183    /// * See [ResolveError]
184    pub fn resolve<P: AsRef<Path>>(
185        &self,
186        directory: P,
187        specifier: &str,
188    ) -> Result<Resolution, ResolveError> {
189        let mut ctx = Ctx::default();
190        let path = directory.as_ref();
191        let tsconfig = match &self.options.tsconfig {
192            Some(TsconfigDiscovery::Manual(o)) => self.find_tsconfig_manual(o)?,
193            _ => None,
194        };
195        self.resolve_tracing(path, specifier, tsconfig.as_deref(), &mut ctx)
196    }
197
198    /// Resolve `specifier` for an absolute path to a file.
199    ///
200    /// NOTE: [TsconfigDiscovery::Auto] only work for this API, use `ResolverGeneric::resolve_file` instead.
201    ///
202    /// # Errors
203    ///
204    /// * See [ResolveError]
205    ///
206    /// # Panics
207    ///
208    /// * If the provided path is not a file.
209    pub fn resolve_file<P: AsRef<Path>>(
210        &self,
211        file: P,
212        specifier: &str,
213    ) -> Result<Resolution, ResolveError> {
214        self.resolve_file_impl(file.as_ref(), specifier)
215    }
216
217    fn resolve_file_impl(&self, path: &Path, specifier: &str) -> Result<Resolution, ResolveError> {
218        let mut ctx = Ctx::default();
219        let dir = path.parent().unwrap();
220        let tsconfig = self.find_tsconfig(path)?;
221        self.resolve_tracing(dir, specifier, tsconfig.as_deref(), &mut ctx)
222    }
223
224    /// Resolve `specifier` at absolute `path` with [ResolveContext]
225    ///
226    /// # Errors
227    ///
228    /// * See [ResolveError]
229    pub fn resolve_with_context<P: AsRef<Path>>(
230        &self,
231        directory: P,
232        specifier: &str,
233        tsconfig: Option<&TsConfig>,
234        resolve_context: &mut ResolveContext,
235    ) -> Result<Resolution, ResolveError> {
236        let mut ctx = Ctx::default();
237        ctx.init_file_dependencies();
238        let result = self.resolve_tracing(directory.as_ref(), specifier, tsconfig, &mut ctx);
239        if let Some(deps) = &mut ctx.file_dependencies {
240            resolve_context.file_dependencies.extend(deps.drain(..));
241        }
242        if let Some(deps) = &mut ctx.missing_dependencies {
243            resolve_context.missing_dependencies.extend(deps.drain(..));
244        }
245        result
246    }
247
248    /// Wrap `resolve_impl` with `tracing` information
249    fn resolve_tracing(
250        &self,
251        directory: &Path,
252        specifier: &str,
253        tsconfig: Option<&TsConfig>,
254        ctx: &mut Ctx,
255    ) -> Result<Resolution, ResolveError> {
256        let span = tracing::debug_span!("resolve", path = ?directory, specifier = specifier);
257        let _enter = span.enter();
258        let r = self.resolve_impl(directory, specifier, tsconfig, ctx);
259        match &r {
260            Ok(r) => {
261                tracing::debug!(options = ?self.options, path = ?directory, specifier = specifier, ret = ?r.path);
262            }
263            Err(err) => {
264                tracing::debug!(options = ?self.options, path = ?directory, specifier = specifier, err = ?err);
265            }
266        }
267        r
268    }
269
270    fn resolve_impl(
271        &self,
272        path: &Path,
273        specifier: &str,
274        tsconfig: Option<&TsConfig>,
275        ctx: &mut Ctx,
276    ) -> Result<Resolution, ResolveError> {
277        ctx.with_fully_specified(self.options.fully_specified);
278
279        let cached_path = self.cache.value(path);
280        let cached_path = self.require(&cached_path, specifier, tsconfig, ctx)?;
281        let path = self.load_realpath(&cached_path)?;
282
283        let package_json = self.find_package_json_for_a_package(&cached_path, ctx)?;
284        if let Some(package_json) = &package_json {
285            // path must be inside the package.
286            debug_assert!(path.starts_with(package_json.directory()));
287        }
288        let module_type = self.esm_file_format(&cached_path, ctx)?;
289
290        Ok(Resolution {
291            path,
292            query: ctx.query.take(),
293            fragment: ctx.fragment.take(),
294            package_json,
295            module_type,
296        })
297    }
298
299    fn find_package_json_for_a_package(
300        &self,
301        cached_path: &CachedPath,
302        ctx: &mut Ctx,
303    ) -> Result<Option<Arc<PackageJson>>, ResolveError> {
304        // Algorithm:
305        // Find `node_modules/package/package.json`
306        // or the first package.json if the path is not inside node_modules.
307        let inside_node_modules = cached_path.inside_node_modules();
308        if inside_node_modules {
309            let mut last = None;
310            for cp in iter::successors(Some(cached_path.clone()), CachedPath::parent) {
311                if cp.is_node_modules() {
312                    break;
313                }
314                if self.cache.is_dir(&cp, ctx)
315                    && let Some(package_json) =
316                        self.cache.get_package_json(&cp, &self.options, ctx)?
317                {
318                    last = Some(package_json);
319                }
320            }
321            Ok(last)
322        } else {
323            self.cache.find_package_json(cached_path, &self.options, ctx)
324        }
325    }
326
327    /// require(X) from module at path Y
328    ///
329    /// X: specifier
330    /// Y: path
331    ///
332    /// <https://nodejs.org/api/modules.html#all-together>
333    fn require(
334        &self,
335        cached_path: &CachedPath,
336        specifier: &str,
337        tsconfig: Option<&TsConfig>,
338        ctx: &mut Ctx,
339    ) -> Result<CachedPath, ResolveError> {
340        ctx.test_for_infinite_recursion()?;
341
342        // enhanced-resolve: parse
343        let (parsed, try_fragment_as_path) =
344            self.load_parse(cached_path, specifier, tsconfig, ctx)?;
345        if let Some(path) = try_fragment_as_path {
346            return Ok(path);
347        }
348
349        self.require_without_parse(cached_path, parsed.path(), tsconfig, ctx)
350    }
351
352    fn require_without_parse(
353        &self,
354        cached_path: &CachedPath,
355        specifier: &str,
356        tsconfig: Option<&TsConfig>,
357        ctx: &mut Ctx,
358    ) -> Result<CachedPath, ResolveError> {
359        // tsconfig-paths
360        if let Some(path) = self.load_tsconfig_paths(cached_path, specifier, tsconfig)? {
361            return Ok(path);
362        }
363
364        // enhanced-resolve: try alias
365        if let Some(path) =
366            self.load_alias(cached_path, specifier, &self.options.alias, tsconfig, ctx)?
367        {
368            return Ok(path);
369        }
370
371        cfg_if::cfg_if! {
372            if #[cfg(not(target_arch = "wasm32"))] {
373                let specifier = resolve_file_protocol(specifier)?;
374                let specifier = specifier.as_ref();
375            }
376        };
377
378        let result = match Path::new(&specifier).components().next() {
379            // 2. If X begins with '/'
380            Some(Component::RootDir | Component::Prefix(_)) => {
381                self.require_absolute(cached_path, specifier, tsconfig, ctx)
382            }
383            // 3. If X is '.' or begins with './' or '/' or '../'
384            Some(Component::CurDir | Component::ParentDir) => {
385                self.require_relative(cached_path, specifier, tsconfig, ctx)
386            }
387            // 4. If X begins with '#'
388            Some(Component::Normal(_)) if specifier.as_bytes()[0] == b'#' => {
389                self.require_hash(cached_path, specifier, tsconfig, ctx)
390            }
391            _ => {
392                // 1. If X is a core module,
393                //   a. return the core module
394                //   b. STOP
395                self.require_core(specifier)?;
396
397                // (ESM) 5. Otherwise,
398                // Note: specifier is now a bare specifier.
399                // Set resolved the result of PACKAGE_RESOLVE(specifier, parentURL).
400                self.require_bare(cached_path, specifier, tsconfig, ctx)
401            }
402        };
403
404        result.or_else(|err| {
405            if err.is_ignore() {
406                return Err(err);
407            }
408            // enhanced-resolve: try fallback
409            self.load_alias(cached_path, specifier, &self.options.fallback, tsconfig, ctx)
410                .and_then(|value| value.ok_or(err))
411        })
412    }
413
414    // PACKAGE_RESOLVE(packageSpecifier, parentURL)
415    // 3. If packageSpecifier is a Node.js builtin module name, then
416    //   1. Return the string "node:" concatenated with packageSpecifier.
417    fn require_core(&self, specifier: &str) -> Result<(), ResolveError> {
418        if self.options.builtin_modules {
419            let is_runtime_module = specifier.starts_with("node:");
420            if is_runtime_module || NODEJS_BUILTINS.binary_search(&specifier).is_ok() {
421                let resolved = if is_runtime_module {
422                    specifier.to_string()
423                } else {
424                    format!("node:{specifier}")
425                };
426                return Err(ResolveError::Builtin { resolved, is_runtime_module });
427            }
428        }
429        Ok(())
430    }
431
432    fn require_absolute(
433        &self,
434        cached_path: &CachedPath,
435        specifier: &str,
436        tsconfig: Option<&TsConfig>,
437        ctx: &mut Ctx,
438    ) -> Result<CachedPath, ResolveError> {
439        // Make sure only path prefixes gets called
440        debug_assert!(
441            Path::new(specifier)
442                .components()
443                .next()
444                .is_some_and(|c| matches!(c, Component::RootDir | Component::Prefix(_)))
445        );
446        if !self.options.prefer_relative
447            && self.options.prefer_absolute
448            && let Ok(path) =
449                self.load_package_self_or_node_modules(cached_path, specifier, tsconfig, ctx)
450        {
451            return Ok(path);
452        }
453        if let Some(path) = self.load_roots(cached_path, specifier, tsconfig, ctx) {
454            return Ok(path);
455        }
456        // 2. If X begins with '/'
457        //   a. set Y to be the file system root
458        let path = self.cache.value(Path::new(specifier));
459        if let Some(path) = self.load_as_file_or_directory(&path, specifier, tsconfig, ctx)? {
460            return Ok(path);
461        }
462        Err(ResolveError::NotFound(specifier.to_string()))
463    }
464
465    // 3. If X is '.' or begins with './' or '/' or '../'
466    fn require_relative(
467        &self,
468        cached_path: &CachedPath,
469        specifier: &str,
470        tsconfig: Option<&TsConfig>,
471        ctx: &mut Ctx,
472    ) -> Result<CachedPath, ResolveError> {
473        // Make sure only relative or normal paths gets called
474        debug_assert!(Path::new(specifier).components().next().is_some_and(|c| matches!(
475            c,
476            Component::CurDir | Component::ParentDir | Component::Normal(_)
477        )));
478        let cached_path = cached_path.normalize_with(specifier, self.cache.as_ref());
479        // a. LOAD_AS_FILE(Y + X)
480        // b. LOAD_AS_DIRECTORY(Y + X)
481        if let Some(path) = self.load_as_file_or_directory(
482            &cached_path,
483            // ensure resolve directory only when specifier is `.`
484            if specifier == "." { "./" } else { specifier },
485            tsconfig,
486            ctx,
487        )? {
488            return Ok(path);
489        }
490        // c. THROW "not found"
491        Err(ResolveError::NotFound(specifier.to_string()))
492    }
493
494    fn require_hash(
495        &self,
496        cached_path: &CachedPath,
497        specifier: &str,
498        tsconfig: Option<&TsConfig>,
499        ctx: &mut Ctx,
500    ) -> Result<CachedPath, ResolveError> {
501        debug_assert_eq!(specifier.chars().next(), Some('#'));
502        // a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
503        self.load_package_imports(cached_path, specifier, tsconfig, ctx)?
504            .map_or_else(|| Err(ResolveError::NotFound(specifier.to_string())), Ok)
505    }
506
507    fn require_bare(
508        &self,
509        cached_path: &CachedPath,
510        specifier: &str,
511        tsconfig: Option<&TsConfig>,
512        ctx: &mut Ctx,
513    ) -> Result<CachedPath, ResolveError> {
514        // Make sure no other path prefixes gets called
515        debug_assert!(
516            Path::new(specifier)
517                .components()
518                .next()
519                .is_some_and(|c| matches!(c, Component::Normal(_)))
520        );
521        if self.options.prefer_relative
522            && let Ok(path) = self.require_relative(cached_path, specifier, tsconfig, ctx)
523        {
524            return Ok(path);
525        }
526        self.load_package_self_or_node_modules(cached_path, specifier, tsconfig, ctx)
527    }
528
529    /// enhanced-resolve: ParsePlugin.
530    ///
531    /// It's allowed to escape # as \0# to avoid parsing it as fragment.
532    /// enhanced-resolve will try to resolve requests containing `#` as path and as fragment,
533    /// so it will automatically figure out if `./some#thing` means `.../some.js#thing` or `.../some#thing.js`.
534    /// When a # is resolved as path it will be escaped in the result. Here: `.../some\0#thing.js`.
535    ///
536    /// <https://github.com/webpack/enhanced-resolve#escaping>
537    fn load_parse<'s>(
538        &self,
539        cached_path: &CachedPath,
540        specifier: &'s str,
541        tsconfig: Option<&TsConfig>,
542        ctx: &mut Ctx,
543    ) -> Result<(Specifier<'s>, Option<CachedPath>), ResolveError> {
544        let parsed = Specifier::parse(specifier).map_err(ResolveError::Specifier)?;
545        ctx.with_query_fragment(parsed.query, parsed.fragment);
546
547        // There is an edge-case where a request with # can be a path or a fragment -> try both
548        if ctx.fragment.is_some() && ctx.query.is_none() {
549            let specifier = parsed.path();
550            let fragment = ctx.fragment.take().unwrap();
551            let path = format!("{specifier}{fragment}");
552            if let Ok(path) = self.require_without_parse(cached_path, &path, tsconfig, ctx) {
553                return Ok((parsed, Some(path)));
554            }
555            ctx.fragment.replace(fragment);
556        }
557        Ok((parsed, None))
558    }
559
560    fn load_package_self_or_node_modules(
561        &self,
562        cached_path: &CachedPath,
563        specifier: &str,
564        tsconfig: Option<&TsConfig>,
565        ctx: &mut Ctx,
566    ) -> Result<CachedPath, ResolveError> {
567        let (package_name, subpath) = Self::parse_package_specifier(specifier);
568        if subpath.is_empty() {
569            ctx.with_fully_specified(false);
570        }
571        // 5. LOAD_PACKAGE_SELF(X, dirname(Y))
572        if let Some(path) = self.load_package_self(cached_path, specifier, tsconfig, ctx)? {
573            return Ok(path);
574        }
575        // 6. LOAD_NODE_MODULES(X, dirname(Y))
576        if let Some(path) =
577            self.load_node_modules(cached_path, specifier, package_name, subpath, tsconfig, ctx)?
578        {
579            return Ok(path);
580        }
581
582        // TODO: add a new option for this legacy behavior?
583        // abnormal relative specifier like `jest-runner-../../..`
584        // which only works with `require` not ESM
585        // see also https://github.com/jestjs/jest/issues/15712
586        // it's kind of bug feature
587        if specifier.contains("/../..") || specifier.contains("../../") {
588            let path = Path::new(specifier).normalize_relative();
589            let mut owned = path.to_string_lossy().into_owned();
590
591            if specifier.ends_with('/') {
592                owned += "/";
593            }
594
595            let specifier_owned = Some(owned);
596            let normalized_specifier = specifier_owned.as_deref().unwrap();
597
598            let (package_name, subpath) = Self::parse_package_specifier(normalized_specifier);
599
600            if package_name == ".."
601                && let Some(path) = self.load_node_modules(
602                    cached_path,
603                    normalized_specifier,
604                    package_name,
605                    subpath,
606                    tsconfig,
607                    ctx,
608                )?
609            {
610                return Ok(path);
611            }
612        }
613
614        // 7. THROW "not found"
615        Err(ResolveError::NotFound(specifier.to_string()))
616    }
617
618    /// LOAD_PACKAGE_IMPORTS(X, DIR)
619    fn load_package_imports(
620        &self,
621        cached_path: &CachedPath,
622        specifier: &str,
623        tsconfig: Option<&TsConfig>,
624        ctx: &mut Ctx,
625    ) -> ResolveResult {
626        // 1. Find the closest package scope SCOPE to DIR.
627        // 2. If no scope was found, return.
628        let Some(package_json) = self.cache.find_package_json(cached_path, &self.options, ctx)?
629        else {
630            return Ok(None);
631        };
632        // 3. If the SCOPE/package.json "imports" is null or undefined, return.
633        // 4. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE), ["node", "require"]) defined in the ESM resolver.
634        if let Some(path) = self.package_imports_resolve(specifier, &package_json, tsconfig, ctx)? {
635            // 5. RESOLVE_ESM_MATCH(MATCH).
636            return self.resolve_esm_match(specifier, &path, tsconfig, ctx);
637        }
638        Ok(None)
639    }
640
641    fn load_as_file(
642        &self,
643        cached_path: &CachedPath,
644        tsconfig: Option<&TsConfig>,
645        ctx: &mut Ctx,
646    ) -> ResolveResult {
647        // enhanced-resolve feature: extension_alias
648        if let Some(path) = self.load_extension_alias(cached_path, tsconfig, ctx)? {
649            return Ok(Some(path));
650        }
651        if self.options.enforce_extension.is_disabled() {
652            // 1. If X is a file, load X as its file extension format. STOP
653            if let Some(path) = self.load_alias_or_file(cached_path, tsconfig, ctx)? {
654                return Ok(Some(path));
655            }
656        }
657        // 2. If X.js is a file, load X.js as JavaScript text. STOP
658        // 3. If X.json is a file, parse X.json to a JavaScript Object. STOP
659        // 4. If X.node is a file, load X.node as binary addon. STOP
660        if let Some(path) =
661            self.load_extensions(cached_path, &self.options.extensions, tsconfig, ctx)?
662        {
663            return Ok(Some(path));
664        }
665        Ok(None)
666    }
667
668    fn load_as_directory(
669        &self,
670        cached_path: &CachedPath,
671        tsconfig: Option<&TsConfig>,
672        ctx: &mut Ctx,
673    ) -> ResolveResult {
674        // 1. If X/package.json is a file,
675        // a. Parse X/package.json, and look for "main" field.
676        if let Some(package_json) = self.cache.get_package_json(cached_path, &self.options, ctx)? {
677            // b. If "main" is a falsy value, GOTO 2.
678            for main_field in package_json.main_fields(&self.options.main_fields) {
679                // ref https://github.com/webpack/enhanced-resolve/blob/main/lib/MainFieldPlugin.js#L66-L67
680                let main_field = if main_field.starts_with("./") || main_field.starts_with("../") {
681                    Cow::Borrowed(main_field)
682                } else {
683                    Cow::Owned(format!("./{main_field}"))
684                };
685
686                // c. let M = X + (json main field)
687                let cached_path =
688                    cached_path.normalize_with(main_field.as_ref(), self.cache.as_ref());
689                // d. LOAD_AS_FILE(M)
690                if let Some(path) = self.load_as_file(&cached_path, tsconfig, ctx)? {
691                    return Ok(Some(path));
692                }
693                // e. LOAD_INDEX(M)
694                if let Some(path) = self.load_index(&cached_path, tsconfig, ctx)? {
695                    return Ok(Some(path));
696                }
697            }
698            // f. LOAD_INDEX(X) DEPRECATED
699            // g. THROW "not found"
700
701            // Allow `exports` field in `require('../directory')`.
702            // This is not part of the spec but some vite projects rely on this behavior.
703            // See
704            // * <https://github.com/vitejs/vite/pull/20252>
705            // * <https://github.com/nodejs/node/issues/58827>
706            if self.options.allow_package_exports_in_directory_resolve {
707                for exports in package_json.exports_fields(&self.options.exports_fields) {
708                    if let Some(path) =
709                        self.package_exports_resolve(cached_path, ".", &exports, tsconfig, ctx)?
710                    {
711                        return Ok(Some(path));
712                    }
713                }
714            }
715        }
716
717        // 2. LOAD_INDEX(X)
718        self.load_index(cached_path, tsconfig, ctx)
719    }
720
721    fn load_as_file_or_directory(
722        &self,
723        cached_path: &CachedPath,
724        specifier: &str,
725        tsconfig: Option<&TsConfig>,
726        ctx: &mut Ctx,
727    ) -> ResolveResult {
728        if self.options.resolve_to_context {
729            return Ok(self.cache.is_dir(cached_path, ctx).then(|| cached_path.clone()));
730        }
731        if !specifier.ends_with('/')
732            && let Some(path) = self.load_as_file(cached_path, tsconfig, ctx)?
733        {
734            return Ok(Some(path));
735        }
736        if self.cache.is_dir(cached_path, ctx)
737            && let Some(path) = self.load_as_directory(cached_path, tsconfig, ctx)?
738        {
739            return Ok(Some(path));
740        }
741        Ok(None)
742    }
743
744    fn load_extensions(
745        &self,
746        path: &CachedPath,
747        extensions: &[String],
748        tsconfig: Option<&TsConfig>,
749        ctx: &mut Ctx,
750    ) -> ResolveResult {
751        if ctx.fully_specified {
752            return Ok(None);
753        }
754        for extension in extensions {
755            let cached_path = path.add_extension(extension, self.cache.as_ref());
756            if let Some(path) = self.load_alias_or_file(&cached_path, tsconfig, ctx)? {
757                return Ok(Some(path));
758            }
759        }
760        Ok(None)
761    }
762
763    fn load_realpath(&self, cached_path: &CachedPath) -> Result<PathBuf, ResolveError> {
764        if self.options.symlinks {
765            self.cache.canonicalize(cached_path)
766        } else {
767            Ok(cached_path.to_path_buf())
768        }
769    }
770
771    fn check_restrictions(&self, path: &Path) -> bool {
772        // https://github.com/webpack/enhanced-resolve/blob/a998c7d218b7a9ec2461fc4fddd1ad5dd7687485/lib/RestrictionsPlugin.js#L19-L24
773        fn is_inside(path: &Path, parent: &Path) -> bool {
774            if !path.starts_with(parent) {
775                return false;
776            }
777            if path.as_os_str().len() == parent.as_os_str().len() {
778                return true;
779            }
780            path.strip_prefix(parent).is_ok_and(|p| p == Path::new("./"))
781        }
782        for restriction in &self.options.restrictions {
783            match restriction {
784                Restriction::Path(restricted_path) => {
785                    if !is_inside(path, restricted_path) {
786                        return false;
787                    }
788                }
789                Restriction::Fn(f) => {
790                    if !f(path) {
791                        return false;
792                    }
793                }
794            }
795        }
796        true
797    }
798
799    fn load_index(
800        &self,
801        cached_path: &CachedPath,
802        tsconfig: Option<&TsConfig>,
803        ctx: &mut Ctx,
804    ) -> ResolveResult {
805        for main_file in &self.options.main_files {
806            let cached_path = cached_path.normalize_with(main_file, self.cache.as_ref());
807            if self.options.enforce_extension.is_disabled()
808                && let Some(path) = self.load_browser_field_or_alias(&cached_path, tsconfig, ctx)?
809                && self.check_restrictions(path.path())
810            {
811                return Ok(Some(path));
812            }
813            // 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
814            // 2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
815            // 3. If X/index.node is a file, load X/index.node as binary addon. STOP
816            if let Some(path) =
817                self.load_extensions(&cached_path, &self.options.extensions, tsconfig, ctx)?
818            {
819                return Ok(Some(path));
820            }
821        }
822        Ok(None)
823    }
824
825    fn load_browser_field_or_alias(
826        &self,
827        cached_path: &CachedPath,
828        tsconfig: Option<&TsConfig>,
829        ctx: &mut Ctx,
830    ) -> ResolveResult {
831        if !self.options.alias_fields.is_empty()
832            && let Some(package_json) =
833                self.cache.find_package_json(cached_path, &self.options, ctx)?
834            && let Some(path) = self.load_browser_field(cached_path, None, &package_json, ctx)?
835        {
836            return Ok(Some(path));
837        }
838        // enhanced-resolve: try file as alias
839        // Guard this because this is on a hot path, and `.to_string_lossy()` has a cost.
840        if !self.options.alias.is_empty() {
841            let alias_specifier = cached_path.path().to_string_lossy();
842            if let Some(path) =
843                self.load_alias(cached_path, &alias_specifier, &self.options.alias, tsconfig, ctx)?
844            {
845                return Ok(Some(path));
846            }
847        }
848        Ok(None)
849    }
850
851    fn load_alias_or_file(
852        &self,
853        cached_path: &CachedPath,
854        tsconfig: Option<&TsConfig>,
855        ctx: &mut Ctx,
856    ) -> ResolveResult {
857        if let Some(path) = self.load_browser_field_or_alias(cached_path, tsconfig, ctx)? {
858            return Ok(Some(path));
859        }
860        if self.cache.is_file(cached_path, ctx) && self.check_restrictions(cached_path.path()) {
861            return Ok(Some(cached_path.clone()));
862        }
863        Ok(None)
864    }
865
866    fn load_node_modules(
867        &self,
868        cached_path: &CachedPath,
869        specifier: &str,
870        package_name: &str,
871        subpath: &str,
872        tsconfig: Option<&TsConfig>,
873        ctx: &mut Ctx,
874    ) -> ResolveResult {
875        #[cfg(feature = "yarn_pnp")]
876        if self.options.yarn_pnp
877            && let Some(resolved_path) = self.load_pnp(cached_path, specifier, tsconfig, ctx)?
878        {
879            return Ok(Some(resolved_path));
880        }
881
882        // 1. let DIRS = NODE_MODULES_PATHS(START)
883        // 2. for each DIR in DIRS:
884        for module_name in &self.options.modules {
885            for cached_path in std::iter::successors(Some(cached_path.clone()), CachedPath::parent)
886            {
887                // Skip if /path/to/node_modules does not exist
888                if !self.cache.is_dir(&cached_path, ctx) {
889                    continue;
890                }
891
892                let Some(cached_path) = self.get_module_directory(&cached_path, module_name, ctx)
893                else {
894                    continue;
895                };
896                // Optimize node_modules lookup by inspecting whether the package exists
897                // From LOAD_PACKAGE_EXPORTS(X, DIR)
898                // 1. Try to interpret X as a combination of NAME and SUBPATH where the name
899                //    may have a @scope/ prefix and the subpath begins with a slash (`/`).
900                if !package_name.is_empty() {
901                    let cached_path = cached_path.normalize_with(package_name, self.cache.as_ref());
902                    // Try foo/node_modules/package_name
903                    if self.cache.is_dir(&cached_path, ctx) {
904                        // a. LOAD_PACKAGE_EXPORTS(X, DIR)
905                        if let Some(path) = self.load_package_exports(
906                            specifier,
907                            subpath,
908                            &cached_path,
909                            tsconfig,
910                            ctx,
911                        )? {
912                            return Ok(Some(path));
913                        }
914                    } else {
915                        // foo/node_modules/package_name is not a directory, so useless to check inside it
916                        if !subpath.is_empty() {
917                            continue;
918                        }
919                        // Skip if the directory lead to the scope package does not exist
920                        // i.e. `foo/node_modules/@scope` is not a directory for `foo/node_modules/@scope/package`
921                        if package_name.starts_with('@')
922                            && let Some(path) = cached_path.parent().as_ref()
923                            && !self.cache.is_dir(path, ctx)
924                        {
925                            continue;
926                        }
927                    }
928                }
929
930                // Try as file or directory for all other cases
931                // b. LOAD_AS_FILE(DIR/X)
932                // c. LOAD_AS_DIRECTORY(DIR/X)
933
934                let cached_path = cached_path.normalize_with(specifier, self.cache.as_ref());
935
936                if self.options.resolve_to_context {
937                    return Ok(self.cache.is_dir(&cached_path, ctx).then(|| cached_path.clone()));
938                }
939
940                // Perf: try LOAD_AS_DIRECTORY first. No modern package manager creates `node_modules/X.js`.
941                if self.cache.is_dir(&cached_path, ctx) {
942                    if let Some(path) =
943                        self.load_browser_field_or_alias(&cached_path, tsconfig, ctx)?
944                    {
945                        return Ok(Some(path));
946                    }
947                    if let Some(path) = self.load_as_directory(&cached_path, tsconfig, ctx)? {
948                        return Ok(Some(path));
949                    }
950                } else if let Some(path) = self.load_as_file(&cached_path, tsconfig, ctx)? {
951                    return Ok(Some(path));
952                }
953            }
954        }
955        Ok(None)
956    }
957
958    #[cfg(feature = "yarn_pnp")]
959    fn load_pnp(
960        &self,
961        cached_path: &CachedPath,
962        specifier: &str,
963        tsconfig: Option<&TsConfig>,
964        ctx: &mut Ctx,
965    ) -> Result<Option<CachedPath>, ResolveError> {
966        let pnp_manifest = self.cache.get_yarn_pnp_manifest(self.options.cwd.as_deref())?;
967
968        // "pnpapi" in a P'n'P builtin module
969        if specifier == "pnpapi" {
970            return Ok(Some(self.cache.value(pnp_manifest.manifest_path.as_path())));
971        }
972
973        // `resolve_to_unqualified` requires a trailing slash
974        let mut path = cached_path.to_path_buf();
975        path.push("");
976
977        let resolution = pnp::resolve_to_unqualified_via_manifest(pnp_manifest, specifier, &path);
978
979        match resolution {
980            Ok(pnp::Resolution::Resolved(path, subpath)) => {
981                let cached_path = self.cache.value(&path);
982                let cached_path_string = cached_path.path().to_string_lossy();
983
984                let export_resolution =
985                    self.load_package_self(&cached_path, specifier, tsconfig, ctx)?;
986                // can be found in pnp cached folder
987                if export_resolution.is_some() {
988                    return Ok(export_resolution);
989                }
990
991                // symbol linked package doesn't have node_modules structure
992                let pkg_name = cached_path_string.rsplit_once("node_modules/").map_or(
993                    "",
994                    // remove trailing slash
995                    |(_, last)| last.strip_suffix('/').unwrap_or(last),
996                );
997
998                let inner_request = if pkg_name.is_empty() {
999                    subpath.map_or_else(
1000                        || ".".to_string(),
1001                        |mut p| {
1002                            p.insert_str(0, "./");
1003                            p
1004                        },
1005                    )
1006                } else {
1007                    let (first, rest) = specifier.split_once('/').unwrap_or((specifier, ""));
1008                    // the original `pkg_name` in cached path could be different with specifier
1009                    // due to alias like `"custom-minimist": "npm:minimist@^1.2.8"`
1010                    // in this case, `specifier` is `pkg_name`'s source of truth
1011                    let pkg_name = if first.starts_with('@') {
1012                        &format!("{first}/{}", rest.split_once('/').unwrap_or((rest, "")).0)
1013                    } else {
1014                        first
1015                    };
1016                    let inner_specifier = specifier.strip_prefix(pkg_name).unwrap();
1017                    String::from("./")
1018                        + inner_specifier.strip_prefix("/").unwrap_or(inner_specifier)
1019                };
1020
1021                // it could be a directory with `package.json` that redirects to another file,
1022                // take `@atlaskit/pragmatic-drag-and-drop` for example, as described at import-js/eslint-import-resolver-typescript#409
1023                if let Ok(Some(result)) = self.load_as_directory(
1024                    &self.cache.value(&path.join(inner_request.clone()).normalize()),
1025                    tsconfig,
1026                    ctx,
1027                ) {
1028                    return Ok(Some(result));
1029                }
1030
1031                // try as file or directory `path` in the pnp folder
1032                let cached_path = self.cache.value(&path);
1033                let Ok(inner_resolution) = self.require(&cached_path, &inner_request, None, ctx)
1034                else {
1035                    return Err(ResolveError::NotFound(specifier.to_string()));
1036                };
1037
1038                Ok(Some(self.cache.value(inner_resolution.path())))
1039            }
1040
1041            Ok(pnp::Resolution::Skipped) => Ok(None),
1042            Err(_) => Err(ResolveError::NotFound(specifier.to_string())),
1043        }
1044    }
1045
1046    fn get_module_directory(
1047        &self,
1048        cached_path: &CachedPath,
1049        module_name: &str,
1050        ctx: &mut Ctx,
1051    ) -> Option<CachedPath> {
1052        if module_name == "node_modules" {
1053            cached_path.cached_node_modules(self.cache.as_ref(), ctx)
1054        } else if cached_path.path().components().next_back()
1055            == Some(Component::Normal(OsStr::new(module_name)))
1056        {
1057            Some(cached_path.clone())
1058        } else {
1059            cached_path.module_directory(module_name, self.cache.as_ref(), ctx)
1060        }
1061    }
1062
1063    fn load_package_exports(
1064        &self,
1065        specifier: &str,
1066        subpath: &str,
1067        cached_path: &CachedPath,
1068        tsconfig: Option<&TsConfig>,
1069        ctx: &mut Ctx,
1070    ) -> ResolveResult {
1071        // 2. If X does not match this pattern or DIR/NAME/package.json is not a file,
1072        //    return.
1073        let Some(package_json) = self.cache.get_package_json(cached_path, &self.options, ctx)?
1074        else {
1075            return Ok(None);
1076        };
1077        // 3. Parse DIR/NAME/package.json, and look for "exports" field.
1078        // 4. If "exports" is null or undefined, return.
1079        // 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
1080        //    `package.json` "exports", ["node", "require"]) defined in the ESM resolver.
1081        // Note: The subpath is not prepended with a dot on purpose
1082        for exports in package_json.exports_fields(&self.options.exports_fields) {
1083            if let Some(path) = self.package_exports_resolve(
1084                cached_path,
1085                &format!(".{subpath}"),
1086                &exports,
1087                tsconfig,
1088                ctx,
1089            )? {
1090                // 6. RESOLVE_ESM_MATCH(MATCH)
1091                return self.resolve_esm_match(specifier, &path, tsconfig, ctx);
1092            }
1093        }
1094        Ok(None)
1095    }
1096
1097    fn load_package_self(
1098        &self,
1099        cached_path: &CachedPath,
1100        specifier: &str,
1101        tsconfig: Option<&TsConfig>,
1102        ctx: &mut Ctx,
1103    ) -> ResolveResult {
1104        // 1. Find the closest package scope SCOPE to DIR.
1105        // 2. If no scope was found, return.
1106        let Some(package_json) = self.cache.find_package_json(cached_path, &self.options, ctx)?
1107        else {
1108            return Ok(None);
1109        };
1110        // 3. If the SCOPE/package.json "exports" is null or undefined, return.
1111        // 4. If the SCOPE/package.json "name" is not the first segment of X, return.
1112        if let Some(subpath) = package_json
1113            .name()
1114            .and_then(|package_name| Self::strip_package_name(specifier, package_name))
1115        {
1116            // 5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE),
1117            // "." + X.slice("name".length), `package.json` "exports", ["node", "require"])
1118            // defined in the ESM resolver.
1119            // Note: The subpath is not prepended with a dot on purpose
1120            // because `package_exports_resolve` matches subpath without the leading dot.
1121            let package_url = self.cache.value(package_json.path.parent().unwrap());
1122            for exports in package_json.exports_fields(&self.options.exports_fields) {
1123                if let Some(cached_path) = self.package_exports_resolve(
1124                    &package_url,
1125                    &format!(".{subpath}"),
1126                    &exports,
1127                    tsconfig,
1128                    ctx,
1129                )? {
1130                    // 6. RESOLVE_ESM_MATCH(MATCH)
1131                    return self.resolve_esm_match(specifier, &cached_path, tsconfig, ctx);
1132                }
1133            }
1134        }
1135        self.load_browser_field(cached_path, Some(specifier), &package_json, ctx)
1136    }
1137
1138    /// RESOLVE_ESM_MATCH(MATCH)
1139    fn resolve_esm_match(
1140        &self,
1141        specifier: &str,
1142        cached_path: &CachedPath,
1143        tsconfig: Option<&TsConfig>,
1144        ctx: &mut Ctx,
1145    ) -> ResolveResult {
1146        // 1. let RESOLVED_PATH = fileURLToPath(MATCH)
1147        // 2. If the file at RESOLVED_PATH exists, load RESOLVED_PATH as its extension format. STOP
1148        //
1149        // Non-compliant ESM can result in a directory, so directory is tried as well.
1150        if let Some(path) = self.load_as_file_or_directory(cached_path, "", tsconfig, ctx)? {
1151            return Ok(Some(path));
1152        }
1153
1154        // 3. THROW "not found"
1155        Err(ResolveError::NotFound(specifier.to_string()))
1156    }
1157
1158    /// enhanced-resolve: AliasFieldPlugin for [ResolveOptions::alias_fields]
1159    fn load_browser_field(
1160        &self,
1161        cached_path: &CachedPath,
1162        module_specifier: Option<&str>,
1163        package_json: &PackageJson,
1164        ctx: &mut Ctx,
1165    ) -> ResolveResult {
1166        let path = cached_path.path();
1167        let Some(new_specifier) = package_json.resolve_browser_field(
1168            path,
1169            module_specifier,
1170            &self.options.alias_fields,
1171        )?
1172        else {
1173            return Ok(None);
1174        };
1175        // Abort when resolving recursive module
1176        if module_specifier.is_some_and(|s| s == new_specifier) {
1177            return Ok(None);
1178        }
1179        if ctx.resolving_alias.as_ref().is_some_and(|s| s == new_specifier) {
1180            // Complete when resolving to self `{"./a.js": "./a.js"}`
1181            if new_specifier.strip_prefix("./").filter(|s| path.ends_with(Path::new(s))).is_some() {
1182                return if self.cache.is_file(cached_path, ctx) {
1183                    if self.check_restrictions(cached_path.path()) {
1184                        Ok(Some(cached_path.clone()))
1185                    } else {
1186                        Ok(None)
1187                    }
1188                } else {
1189                    Err(ResolveError::NotFound(new_specifier.to_string()))
1190                };
1191            }
1192            return Err(ResolveError::Recursion);
1193        }
1194        ctx.with_resolving_alias(new_specifier.to_string());
1195        ctx.with_fully_specified(false);
1196        let package_url = self.cache.value(package_json.path().parent().unwrap());
1197        self.require(&package_url, new_specifier, None, ctx).map(Some)
1198    }
1199
1200    /// enhanced-resolve: AliasPlugin for [ResolveOptions::alias] and [ResolveOptions::fallback].
1201    fn load_alias(
1202        &self,
1203        cached_path: &CachedPath,
1204        specifier: &str,
1205        aliases: &Alias,
1206        tsconfig: Option<&TsConfig>,
1207        ctx: &mut Ctx,
1208    ) -> ResolveResult {
1209        for (alias_key_raw, specifiers) in aliases {
1210            let mut alias_key_has_wildcard = false;
1211            let alias_key = if let Some(alias_key) = alias_key_raw.strip_suffix('$') {
1212                if alias_key != specifier {
1213                    continue;
1214                }
1215                alias_key
1216            } else if alias_key_raw.contains('*') {
1217                alias_key_has_wildcard = true;
1218                alias_key_raw
1219            } else {
1220                let strip_package_name = Self::strip_package_name(specifier, alias_key_raw);
1221                if strip_package_name.is_none() {
1222                    continue;
1223                }
1224                alias_key_raw
1225            };
1226            // It should stop resolving when all of the tried alias values
1227            // failed to resolve.
1228            // <https://github.com/webpack/enhanced-resolve/blob/570337b969eee46120a18b62b72809a3246147da/lib/AliasPlugin.js#L65>
1229            let mut should_stop = false;
1230            for r in specifiers {
1231                match r {
1232                    AliasValue::Path(alias_value) => {
1233                        if let Some(path) = self.load_alias_value(
1234                            cached_path,
1235                            alias_key,
1236                            alias_key_has_wildcard,
1237                            alias_value,
1238                            specifier,
1239                            tsconfig,
1240                            ctx,
1241                            &mut should_stop,
1242                        )? {
1243                            return Ok(Some(path));
1244                        }
1245                    }
1246                    AliasValue::Ignore => {
1247                        let cached_path =
1248                            cached_path.normalize_with(alias_key, self.cache.as_ref());
1249                        return Err(ResolveError::Ignored(cached_path.to_path_buf()));
1250                    }
1251                }
1252            }
1253            if should_stop {
1254                return Err(ResolveError::MatchedAliasNotFound(
1255                    specifier.to_string(),
1256                    alias_key.to_string(),
1257                ));
1258            }
1259        }
1260        Ok(None)
1261    }
1262
1263    fn load_alias_value(
1264        &self,
1265        cached_path: &CachedPath,
1266        alias_key: &str,
1267        alias_key_has_wild_card: bool,
1268        alias_value: &str,
1269        request: &str,
1270        tsconfig: Option<&TsConfig>,
1271        ctx: &mut Ctx,
1272        should_stop: &mut bool,
1273    ) -> ResolveResult {
1274        if request != alias_value
1275            && !request.strip_prefix(alias_value).is_some_and(|prefix| prefix.starts_with('/'))
1276        {
1277            let new_specifier = if alias_key_has_wild_card {
1278                // Resolve wildcard, e.g. `@/*` -> `./src/*`
1279                let Some(alias_key) = alias_key.split_once('*').and_then(|(prefix, suffix)| {
1280                    request
1281                        .strip_prefix(prefix)
1282                        .and_then(|specifier| specifier.strip_suffix(suffix))
1283                }) else {
1284                    return Ok(None);
1285                };
1286                if alias_value.contains('*') {
1287                    Cow::Owned(alias_value.replacen('*', alias_key, 1))
1288                } else {
1289                    Cow::Borrowed(alias_value)
1290                }
1291            } else {
1292                let tail = &request[alias_key.len()..];
1293                if tail.is_empty() {
1294                    Cow::Borrowed(alias_value)
1295                } else {
1296                    let alias_path = Path::new(alias_value).normalize();
1297                    // Must not append anything to alias_value if it is a file.
1298                    let cached_alias_path = self.cache.value(&alias_path);
1299                    if self.cache.is_file(&cached_alias_path, ctx) {
1300                        return Ok(None);
1301                    }
1302                    // Remove the leading slash so the final path is concatenated.
1303                    let tail = tail.trim_start_matches(SLASH_START);
1304                    if tail.is_empty() {
1305                        Cow::Borrowed(alias_value)
1306                    } else {
1307                        let normalized = alias_path.normalize_with(tail);
1308                        Cow::Owned(normalized.to_string_lossy().to_string())
1309                    }
1310                }
1311            };
1312
1313            *should_stop = true;
1314            ctx.with_fully_specified(false);
1315            return match self.require(cached_path, new_specifier.as_ref(), tsconfig, ctx) {
1316                Err(ResolveError::NotFound(_) | ResolveError::MatchedAliasNotFound(_, _)) => {
1317                    Ok(None)
1318                }
1319                Ok(path) => return Ok(Some(path)),
1320                Err(err) => return Err(err),
1321            };
1322        }
1323        Ok(None)
1324    }
1325
1326    /// Given an extension alias map `{".js": [".ts", ".js"]}`,
1327    /// load the mapping instead of the provided extension
1328    ///
1329    /// This is an enhanced-resolve feature
1330    ///
1331    /// # Errors
1332    ///
1333    /// * [ResolveError::ExtensionAlias]: When all of the aliased extensions are not found
1334    fn load_extension_alias(
1335        &self,
1336        cached_path: &CachedPath,
1337        tsconfig: Option<&TsConfig>,
1338        ctx: &mut Ctx,
1339    ) -> ResolveResult {
1340        if self.options.extension_alias.is_empty() {
1341            return Ok(None);
1342        }
1343        let Some(path_extension) = cached_path.path().extension() else {
1344            return Ok(None);
1345        };
1346        let Some((_, extensions)) = self
1347            .options
1348            .extension_alias
1349            .iter()
1350            .find(|(ext, _)| OsStr::new(ext.trim_start_matches('.')) == path_extension)
1351        else {
1352            return Ok(None);
1353        };
1354        let path = cached_path.path();
1355        let Some(filename) = path.file_name() else { return Ok(None) };
1356        ctx.with_fully_specified(true);
1357        for extension in extensions {
1358            let cached_path = cached_path.replace_extension(extension, self.cache.as_ref());
1359            if let Some(path) = self.load_alias_or_file(&cached_path, tsconfig, ctx)? {
1360                ctx.with_fully_specified(false);
1361                return Ok(Some(path));
1362            }
1363        }
1364        // Bail if path is module directory such as `ipaddr.js`
1365        if !self.cache.is_file(cached_path, ctx) {
1366            ctx.with_fully_specified(false);
1367            return Ok(None);
1368        } else if !self.check_restrictions(cached_path.path()) {
1369            return Ok(None);
1370        }
1371        // Create a meaningful error message.
1372        let dir = path.parent().unwrap().to_path_buf();
1373        let filename_without_extension = Path::new(filename).with_extension("");
1374        let filename_without_extension = filename_without_extension.to_string_lossy();
1375        let files = extensions
1376            .iter()
1377            .map(|ext| format!("{filename_without_extension}{ext}"))
1378            .collect::<Vec<_>>()
1379            .join(",");
1380        Err(ResolveError::ExtensionAlias(filename.to_string_lossy().to_string(), files, dir))
1381    }
1382
1383    /// enhanced-resolve: RootsPlugin
1384    ///
1385    /// A list of directories where requests of server-relative URLs (starting with '/') are resolved,
1386    /// defaults to context configuration option.
1387    ///
1388    /// On non-Windows systems these requests are resolved as an absolute path first.
1389    fn load_roots(
1390        &self,
1391        cached_path: &CachedPath,
1392        specifier: &str,
1393        tsconfig: Option<&TsConfig>,
1394        ctx: &mut Ctx,
1395    ) -> Option<CachedPath> {
1396        if self.options.roots.is_empty() {
1397            return None;
1398        }
1399        if let Some(specifier) = specifier.strip_prefix(SLASH_START) {
1400            if specifier.is_empty() {
1401                if self.options.roots.iter().any(|root| root.as_path() == cached_path.path())
1402                    && let Ok(path) = self.require_relative(cached_path, "./", tsconfig, ctx)
1403                {
1404                    return Some(path);
1405                }
1406            } else {
1407                for root in &self.options.roots {
1408                    let cached_path = self.cache.value(root);
1409                    if let Ok(path) = self.require_relative(&cached_path, specifier, tsconfig, ctx)
1410                    {
1411                        return Some(path);
1412                    }
1413                }
1414            }
1415        }
1416        None
1417    }
1418
1419    /// PACKAGE_RESOLVE(packageSpecifier, parentURL)
1420    fn package_resolve(
1421        &self,
1422        cached_path: &CachedPath,
1423        specifier: &str,
1424        tsconfig: Option<&TsConfig>,
1425        ctx: &mut Ctx,
1426    ) -> ResolveResult {
1427        let (package_name, subpath) = Self::parse_package_specifier(specifier);
1428
1429        // 3. If packageSpecifier is a Node.js builtin module name, then
1430        //   1. Return the string "node:" concatenated with packageSpecifier.
1431        self.require_core(package_name)?;
1432
1433        // 11. While parentURL is not the file system root,
1434        for module_name in &self.options.modules {
1435            for cached_path in std::iter::successors(Some(cached_path.clone()), CachedPath::parent)
1436            {
1437                // 1. Let packageURL be the URL resolution of "node_modules/" concatenated with packageSpecifier, relative to parentURL.
1438                let Some(cached_path) = self.get_module_directory(&cached_path, module_name, ctx)
1439                else {
1440                    continue;
1441                };
1442                // 2. Set parentURL to the parent folder URL of parentURL.
1443                let cached_path = cached_path.normalize_with(package_name, self.cache.as_ref());
1444                // 3. If the folder at packageURL does not exist, then
1445                //   1. Continue the next loop iteration.
1446                if self.cache.is_dir(&cached_path, ctx) {
1447                    // 4. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
1448                    if let Some(package_json) =
1449                        self.cache.get_package_json(&cached_path, &self.options, ctx)?
1450                    {
1451                        // 5. If pjson is not null and pjson.exports is not null or undefined, then
1452                        // 1. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
1453                        for exports in package_json.exports_fields(&self.options.exports_fields) {
1454                            if let Some(path) = self.package_exports_resolve(
1455                                &cached_path,
1456                                &format!(".{subpath}"),
1457                                &exports,
1458                                tsconfig,
1459                                ctx,
1460                            )? {
1461                                return Ok(Some(path));
1462                            }
1463                        }
1464                        // 6. Otherwise, if packageSubpath is equal to ".", then
1465                        if subpath == "." {
1466                            // 1. If pjson.main is a string, then
1467                            for main_field in package_json.main_fields(&self.options.main_fields) {
1468                                // 1. Return the URL resolution of main in packageURL.
1469                                let cached_path =
1470                                    cached_path.normalize_with(main_field, self.cache.as_ref());
1471                                if self.cache.is_file(&cached_path, ctx)
1472                                    && self.check_restrictions(cached_path.path())
1473                                {
1474                                    return Ok(Some(cached_path));
1475                                }
1476                            }
1477                        }
1478                    }
1479                    let subpath = format!(".{subpath}");
1480                    ctx.with_fully_specified(false);
1481                    return self.require(&cached_path, &subpath, tsconfig, ctx).map(Some);
1482                }
1483            }
1484        }
1485
1486        Err(ResolveError::NotFound(specifier.to_string()))
1487    }
1488
1489    /// PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions)
1490    fn package_exports_resolve(
1491        &self,
1492        package_url: &CachedPath,
1493        subpath: &str,
1494        exports: &ImportsExportsEntry<'_>,
1495        tsconfig: Option<&TsConfig>,
1496        ctx: &mut Ctx,
1497    ) -> ResolveResult {
1498        let conditions = &self.options.condition_names;
1499        // 1. If exports is an Object with both a key starting with "." and a key not starting with ".", throw an Invalid Package Configuration error.
1500        if let Some(map) = exports.as_map() {
1501            let mut has_dot = false;
1502            let mut without_dot = false;
1503            for key in map.keys() {
1504                let starts_with_dot_or_hash = key.starts_with(['.', '#']);
1505                has_dot = has_dot || starts_with_dot_or_hash;
1506                without_dot = without_dot || !starts_with_dot_or_hash;
1507                if has_dot && without_dot {
1508                    return Err(ResolveError::InvalidPackageConfig(
1509                        package_url.path().join("package.json"),
1510                    ));
1511                }
1512            }
1513        }
1514        // 2. If subpath is equal to ".", then
1515        // Note: subpath is not prepended with a dot when passed in.
1516        if subpath == "." {
1517            // 1. Let mainExport be undefined.
1518            let main_export = match exports.kind() {
1519                // 2. If exports is a String or Array, or an Object containing no keys starting with ".", then
1520                ImportsExportsKind::String | ImportsExportsKind::Array => {
1521                    // 1. Set mainExport to exports.
1522                    Some(Cow::Borrowed(exports))
1523                }
1524                // 3. Otherwise if exports is an Object containing a "." property, then
1525                _ => exports.as_map().and_then(|map| {
1526                    map.get(".").map_or_else(
1527                        || {
1528                            if map.keys().any(|key| key.starts_with("./") || key.starts_with('#')) {
1529                                None
1530                            } else {
1531                                Some(Cow::Borrowed(exports))
1532                            }
1533                        },
1534                        |entry| Some(Cow::Owned(entry)),
1535                    )
1536                }),
1537            };
1538            // 4. If mainExport is not undefined, then
1539            if let Some(main_export) = main_export {
1540                // 1. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, mainExport, null, false, conditions).
1541                let resolved = self.package_target_resolve(
1542                    package_url,
1543                    ".",
1544                    main_export.as_ref(),
1545                    None,
1546                    /* is_imports */ false,
1547                    conditions,
1548                    tsconfig,
1549                    ctx,
1550                )?;
1551                // 2. If resolved is not null or undefined, return resolved.
1552                if let Some(path) = resolved {
1553                    return Ok(Some(path));
1554                }
1555            }
1556        }
1557        // 3. Otherwise, if exports is an Object and all keys of exports start with ".", then
1558        if let Some(exports) = exports.as_map() {
1559            // 1. Let matchKey be the string "./" concatenated with subpath.
1560            // Note: `package_imports_exports_resolve` does not require the leading dot.
1561            let match_key = &subpath;
1562            // 2. Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( matchKey, exports, packageURL, false, conditions).
1563            if let Some(path) = self.package_imports_exports_resolve(
1564                match_key,
1565                &exports,
1566                package_url,
1567                /* is_imports */ false,
1568                conditions,
1569                tsconfig,
1570                ctx,
1571            )? {
1572                // 3. If resolved is not null or undefined, return resolved.
1573                return Ok(Some(path));
1574            }
1575        }
1576        // 4. Throw a Package Path Not Exported error.
1577        Err(ResolveError::PackagePathNotExported {
1578            subpath: subpath.to_string(),
1579            package_path: package_url.path().to_path_buf(),
1580            package_json_path: package_url.path().join("package.json"),
1581            conditions: self.options.condition_names.clone().into(),
1582        })
1583    }
1584
1585    /// PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, conditions)
1586    fn package_imports_resolve(
1587        &self,
1588        specifier: &str,
1589        package_json: &PackageJson,
1590        tsconfig: Option<&TsConfig>,
1591        ctx: &mut Ctx,
1592    ) -> Result<Option<CachedPath>, ResolveError> {
1593        // 1. Assert: specifier begins with "#".
1594        debug_assert!(specifier.starts_with('#'), "{specifier}");
1595        //   2. If specifier is exactly equal to "#" or starts with "#/", then
1596        //   1. Throw an Invalid Module Specifier error.
1597        // 3. Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL).
1598        // 4. If packageURL is not null, then
1599
1600        // 1. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
1601        // 2. If pjson.imports is a non-null Object, then
1602
1603        // 1. Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( specifier, pjson.imports, packageURL, true, conditions).
1604        let mut has_imports = false;
1605        for imports in package_json.imports_fields(&self.options.imports_fields) {
1606            if !has_imports {
1607                has_imports = true;
1608                // TODO: fill in test case for this case
1609                if specifier == "#" || specifier.starts_with("#/") {
1610                    return Err(ResolveError::InvalidModuleSpecifier(
1611                        specifier.to_string(),
1612                        package_json.path().to_path_buf(),
1613                    ));
1614                }
1615            }
1616            if let Some(path) = self.package_imports_exports_resolve(
1617                specifier,
1618                &imports,
1619                &self.cache.value(package_json.directory()),
1620                /* is_imports */ true,
1621                &self.options.condition_names,
1622                tsconfig,
1623                ctx,
1624            )? {
1625                // 2. If resolved is not null or undefined, return resolved.
1626                return Ok(Some(path));
1627            }
1628        }
1629
1630        // 5. Throw a Package Import Not Defined error.
1631        if has_imports {
1632            Err(ResolveError::PackageImportNotDefined(
1633                specifier.to_string(),
1634                package_json.path().to_path_buf(),
1635            ))
1636        } else {
1637            Ok(None)
1638        }
1639    }
1640
1641    /// PACKAGE_IMPORTS_EXPORTS_RESOLVE(matchKey, matchObj, packageURL, isImports, conditions)
1642    fn package_imports_exports_resolve(
1643        &self,
1644        match_key: &str,
1645        match_obj: &ImportsExportsMap<'_>,
1646        package_url: &CachedPath,
1647        is_imports: bool,
1648        conditions: &[String],
1649        tsconfig: Option<&TsConfig>,
1650        ctx: &mut Ctx,
1651    ) -> ResolveResult {
1652        // enhanced-resolve behaves differently, it throws
1653        // Error: CachedPath to directories is not possible with the exports field (specifier was ./dist/)
1654        if match_key.ends_with('/') {
1655            return Ok(None);
1656        }
1657        // 1. If matchKey is a key of matchObj and does not contain "*", then
1658        if !match_key.contains('*') {
1659            // 1. Let target be the value of matchObj[matchKey].
1660            if let Some(target) = match_obj.get(match_key) {
1661                // 2. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, null, isImports, conditions).
1662                return self.package_target_resolve(
1663                    package_url,
1664                    match_key,
1665                    &target,
1666                    None,
1667                    is_imports,
1668                    conditions,
1669                    tsconfig,
1670                    ctx,
1671                );
1672            }
1673        }
1674
1675        let mut best_target = None;
1676        let mut best_match = "";
1677        let mut best_key = "";
1678        // 2. Let expansionKeys be the list of keys of matchObj containing only a single "*", sorted by the sorting function PATTERN_KEY_COMPARE which orders in descending order of specificity.
1679        // 3. For each key expansionKey in expansionKeys, do
1680        for (expansion_key, target) in match_obj.iter() {
1681            if expansion_key.starts_with("./") || expansion_key.starts_with('#') {
1682                // 1. Let patternBase be the substring of expansionKey up to but excluding the first "*" character.
1683                if let Some((pattern_base, pattern_trailer)) = expansion_key.split_once('*') {
1684                    // 2. If matchKey starts with but is not equal to patternBase, then
1685                    if match_key.starts_with(pattern_base)
1686                        // 1. Let patternTrailer be the substring of expansionKey from the index after the first "*" character.
1687                        && !pattern_trailer.contains('*')
1688                        // 2. If patternTrailer has zero length, or if matchKey ends with patternTrailer and the length of matchKey is greater than or equal to the length of expansionKey, then
1689                        && (pattern_trailer.is_empty()
1690                        || (match_key.len() >= expansion_key.len()
1691                        && match_key.ends_with(pattern_trailer)))
1692                        && Self::pattern_key_compare(best_key, expansion_key).is_gt()
1693                    {
1694                        // 1. Let target be the value of matchObj[expansionKey].
1695                        best_target = Some(target);
1696                        // 2. Let patternMatch be the substring of matchKey starting at the index of the length of patternBase up to the length of matchKey minus the length of patternTrailer.
1697                        best_match =
1698                            &match_key[pattern_base.len()..match_key.len() - pattern_trailer.len()];
1699                        best_key = expansion_key;
1700                    }
1701                } else if expansion_key.ends_with('/')
1702                    && match_key.starts_with(expansion_key)
1703                    && Self::pattern_key_compare(best_key, expansion_key).is_gt()
1704                {
1705                    // TODO: [DEP0148] DeprecationWarning: Use of deprecated folder mapping "./dist/" in the "exports" field module resolution of the package at xxx/package.json.
1706                    best_target = Some(target);
1707                    best_match = &match_key[expansion_key.len()..];
1708                    best_key = expansion_key;
1709                }
1710            }
1711        }
1712        if let Some(best_target) = best_target {
1713            // 3. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, isImports, conditions).
1714            return self.package_target_resolve(
1715                package_url,
1716                best_key,
1717                &best_target,
1718                Some(best_match),
1719                is_imports,
1720                conditions,
1721                tsconfig,
1722                ctx,
1723            );
1724        }
1725        // 4. Return null.
1726        Ok(None)
1727    }
1728
1729    /// PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, isImports, conditions)
1730    fn package_target_resolve(
1731        &self,
1732        package_url: &CachedPath,
1733        target_key: &str,
1734        target: &ImportsExportsEntry<'_>,
1735        pattern_match: Option<&str>,
1736        is_imports: bool,
1737        conditions: &[String],
1738        tsconfig: Option<&TsConfig>,
1739        ctx: &mut Ctx,
1740    ) -> ResolveResult {
1741        fn normalize_string_target<'a>(
1742            target_key: &'a str,
1743            target: &'a str,
1744            pattern_match: Option<&'a str>,
1745            package_url: &CachedPath,
1746        ) -> Result<Cow<'a, str>, ResolveError> {
1747            let target = if let Some(pattern_match) = pattern_match {
1748                if !target_key.contains('*') && !target.contains('*') {
1749                    // enhanced-resolve behaviour
1750                    // TODO: [DEP0148] DeprecationWarning: Use of deprecated folder mapping "./dist/" in the "exports" field module resolution of the package at xxx/package.json.
1751                    if target_key.ends_with('/') && target.ends_with('/') {
1752                        Cow::Owned(format!("{target}{pattern_match}"))
1753                    } else {
1754                        return Err(ResolveError::InvalidPackageConfigDirectory(
1755                            package_url.path().join("package.json"),
1756                        ));
1757                    }
1758                } else {
1759                    Cow::Owned(target.replace('*', pattern_match))
1760                }
1761            } else {
1762                Cow::Borrowed(target)
1763            };
1764            Ok(target)
1765        }
1766
1767        // 1. If target is a String, then
1768        if let Some(target) = target.as_string() {
1769            // Target string con contain queries or fragments:
1770            // `"exports": { ".": { "default": "./foo.js?query#fragment" }`
1771            let parsed = Specifier::parse(target).map_err(ResolveError::Specifier)?;
1772            ctx.with_query_fragment(parsed.query, parsed.fragment);
1773            let target = parsed.path();
1774
1775            // 1. If target does not start with "./", then
1776            if !target.starts_with("./") {
1777                // 1. If isImports is false, or if target starts with "../" or "/", or if target is a valid URL, then
1778                if !is_imports || target.starts_with("../") || target.starts_with('/') {
1779                    // 1. Throw an Invalid Package Target error.
1780                    return Err(ResolveError::InvalidPackageTarget(
1781                        (*target).to_string(),
1782                        target_key.to_string(),
1783                        package_url.path().join("package.json"),
1784                    ));
1785                }
1786                // 2. If patternMatch is a String, then
1787                //   1. Return PACKAGE_RESOLVE(target with every instance of "*" replaced by patternMatch, packageURL + "/").
1788                let target =
1789                    normalize_string_target(target_key, target, pattern_match, package_url)?;
1790                // // 3. Return PACKAGE_RESOLVE(target, packageURL + "/").
1791                return self.package_resolve(package_url, &target, tsconfig, ctx);
1792            }
1793
1794            // 2. If target split on "/" or "\" contains any "", ".", "..", or "node_modules" segments after the first "." segment, case insensitive and including percent encoded variants, throw an Invalid Package Target error.
1795            // 3. Let resolvedTarget be the URL resolution of the concatenation of packageURL and target.
1796            // 4. Assert: resolvedTarget is contained in packageURL.
1797            // 5. If patternMatch is null, then
1798            let target = normalize_string_target(target_key, target, pattern_match, package_url)?;
1799            if Path::new(target.as_ref()).is_invalid_exports_target() {
1800                return Err(ResolveError::InvalidPackageTarget(
1801                    target.to_string(),
1802                    target_key.to_string(),
1803                    package_url.path().join("package.json"),
1804                ));
1805            }
1806            // 6. If patternMatch split on "/" or "\" contains any "", ".", "..", or "node_modules" segments, case insensitive and including percent encoded variants, throw an Invalid Module Specifier error.
1807            // 7. Return the URL resolution of resolvedTarget with every instance of "*" replaced with patternMatch.
1808            return Ok(Some(package_url.normalize_with(target.as_ref(), self.cache.as_ref())));
1809        }
1810        // 2. Otherwise, if target is a non-null Object, then
1811        else if let Some(target) = target.as_map() {
1812            // 1. If exports contains any index property keys, as defined in ECMA-262 6.1.7 Array Index, throw an Invalid Package Configuration error.
1813            // 2. For each property p of target, in object insertion order as,
1814            for (key, target_value) in target.iter() {
1815                // 1. If p equals "default" or conditions contains an entry for p, then
1816                if key == "default" || conditions.iter().any(|condition| condition == key) {
1817                    // 1. Let targetValue be the value of the p property in target.
1818                    // 2. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions).
1819                    let resolved = self.package_target_resolve(
1820                        package_url,
1821                        target_key,
1822                        &target_value,
1823                        pattern_match,
1824                        is_imports,
1825                        conditions,
1826                        tsconfig,
1827                        ctx,
1828                    );
1829                    // 3. If resolved is equal to undefined, continue the loop.
1830                    if let Some(path) = resolved? {
1831                        // 4. Return resolved.
1832                        return Ok(Some(path));
1833                    }
1834                }
1835            }
1836            // 3. Return undefined.
1837            return Ok(None);
1838        }
1839        // 3. Otherwise, if target is an Array, then
1840        else if let Some(targets) = target.as_array() {
1841            // 1. If _target.length is zero, return null.
1842            if targets.is_empty() {
1843                // Note: return PackagePathNotExported has the same effect as return because there are no matches.
1844                return Err(ResolveError::PackagePathNotExported {
1845                    subpath: pattern_match.unwrap_or(".").to_string(),
1846                    package_path: package_url.path().to_path_buf(),
1847                    package_json_path: package_url.path().join("package.json"),
1848                    conditions: self.options.condition_names.clone().into(),
1849                });
1850            }
1851            // 2. For each item targetValue in target, do
1852            for (i, target_value) in targets.iter().enumerate() {
1853                // 1. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions), continuing the loop on any Invalid Package Target error.
1854                let resolved = self.package_target_resolve(
1855                    package_url,
1856                    target_key,
1857                    &target_value,
1858                    pattern_match,
1859                    is_imports,
1860                    conditions,
1861                    tsconfig,
1862                    ctx,
1863                );
1864
1865                if resolved.is_err() && i == targets.len() {
1866                    return resolved;
1867                }
1868
1869                // 2. If resolved is undefined, continue the loop.
1870                if let Ok(Some(path)) = resolved {
1871                    // 3. Return resolved.
1872                    return Ok(Some(path));
1873                }
1874            }
1875            // 3. Return or throw the last fallback resolution null return or error.
1876            // Note: see `resolved.is_err() && i == targets.len()`
1877        }
1878        // 4. Otherwise, if target is null, return null.
1879        Ok(None)
1880        // 5. Otherwise throw an Invalid Package Target error.
1881    }
1882
1883    // Returns (module, subpath)
1884    // https://github.com/nodejs/node/blob/8f0f17e1e3b6c4e58ce748e06343c5304062c491/lib/internal/modules/esm/resolve.js#L688
1885    fn parse_package_specifier(specifier: &str) -> (&str, &str) {
1886        let mut separator_index = specifier.as_bytes().iter().position(|b| *b == b'/');
1887        // let mut valid_package_name = true;
1888        // let mut is_scoped = false;
1889        if specifier.starts_with('@') {
1890            // is_scoped = true;
1891            if separator_index.is_none() || specifier.is_empty() {
1892                // valid_package_name = false;
1893            } else if let Some(index) = &separator_index {
1894                separator_index = specifier.as_bytes()[*index + 1..]
1895                    .iter()
1896                    .position(|b| *b == b'/')
1897                    .map(|i| i + *index + 1);
1898            }
1899        }
1900        let package_name =
1901            separator_index.map_or(specifier, |separator_index| &specifier[..separator_index]);
1902
1903        // TODO: https://github.com/nodejs/node/blob/8f0f17e1e3b6c4e58ce748e06343c5304062c491/lib/internal/modules/esm/resolve.js#L705C1-L714C1
1904        // Package name cannot have leading . and cannot have percent-encoding or
1905        // \\ separators.
1906        // if (RegExpPrototypeExec(invalidPackageNameRegEx, packageName) !== null)
1907        // validPackageName = false;
1908
1909        // if (!validPackageName) {
1910        // throw new ERR_INVALID_MODULE_SPECIFIER(
1911        // specifier, 'is not a valid package name', fileURLToPath(base));
1912        // }
1913        let package_subpath =
1914            separator_index.map_or("", |separator_index| &specifier[separator_index..]);
1915        (package_name, package_subpath)
1916    }
1917
1918    /// PATTERN_KEY_COMPARE(keyA, keyB)
1919    fn pattern_key_compare(key_a: &str, key_b: &str) -> Ordering {
1920        if key_a.is_empty() {
1921            return Ordering::Greater;
1922        }
1923        // 1. Assert: keyA ends with "/" or contains only a single "*".
1924        debug_assert!(key_a.ends_with('/') || key_a.match_indices('*').count() == 1, "{key_a}");
1925        // 2. Assert: keyB ends with "/" or contains only a single "*".
1926        debug_assert!(key_b.ends_with('/') || key_b.match_indices('*').count() == 1, "{key_b}");
1927        // 3. Let baseLengthA be the index of "*" in keyA plus one, if keyA contains "*", or the length of keyA otherwise.
1928        let a_pos = key_a.bytes().position(|c| c == b'*');
1929        let base_length_a = a_pos.map_or(key_a.len(), |p| p + 1);
1930        // 4. Let baseLengthB be the index of "*" in keyB plus one, if keyB contains "*", or the length of keyB otherwise.
1931        let b_pos = key_b.bytes().position(|c| c == b'*');
1932        let base_length_b = b_pos.map_or(key_b.len(), |p| p + 1);
1933        // 5. If baseLengthA is greater than baseLengthB, return -1.
1934        if base_length_a > base_length_b {
1935            return Ordering::Less;
1936        }
1937        // 6. If baseLengthB is greater than baseLengthA, return 1.
1938        if base_length_b > base_length_a {
1939            return Ordering::Greater;
1940        }
1941        // 7. If keyA does not contain "*", return 1.
1942        if a_pos.is_none() {
1943            return Ordering::Greater;
1944        }
1945        // 8. If keyB does not contain "*", return -1.
1946        if b_pos.is_none() {
1947            return Ordering::Less;
1948        }
1949        // 9. If the length of keyA is greater than the length of keyB, return -1.
1950        if key_a.len() > key_b.len() {
1951            return Ordering::Less;
1952        }
1953        // 10. If the length of keyB is greater than the length of keyA, return 1.
1954        if key_b.len() > key_a.len() {
1955            return Ordering::Greater;
1956        }
1957        // 11. Return 0.
1958        Ordering::Equal
1959    }
1960
1961    fn strip_package_name<'a>(specifier: &'a str, package_name: &'a str) -> Option<&'a str> {
1962        specifier
1963            .strip_prefix(package_name)
1964            .filter(|tail| tail.is_empty() || tail.starts_with(SLASH_START))
1965    }
1966
1967    /// ESM_FILE_FORMAT(url)
1968    ///
1969    /// <https://nodejs.org/docs/latest/api/esm.html#resolution-algorithm-specification>
1970    fn esm_file_format(
1971        &self,
1972        cached_path: &CachedPath,
1973        ctx: &mut Ctx,
1974    ) -> Result<Option<ModuleType>, ResolveError> {
1975        if !self.options.module_type {
1976            return Ok(None);
1977        }
1978        // 1. Assert: url corresponds to an existing file.
1979        let ext = cached_path.path().extension().and_then(|ext| ext.to_str());
1980        match ext {
1981            // 2. If url ends in ".mjs", then
1982            //   1. Return "module".
1983            Some("mjs" | "mts") => Ok(Some(ModuleType::Module)),
1984            // 3. If url ends in ".cjs", then
1985            //   1. Return "commonjs".
1986            Some("cjs" | "cts") => Ok(Some(ModuleType::CommonJs)),
1987            // 4. If url ends in ".json", then
1988            //   1. Return "json".
1989            Some("json") => Ok(Some(ModuleType::Json)),
1990            // 5. If --experimental-wasm-modules is enabled and url ends in ".wasm", then
1991            //   1. Return "wasm".
1992            Some("wasm") => Ok(Some(ModuleType::Wasm)),
1993            // 6. If --experimental-addon-modules is enabled and url ends in ".node", then
1994            //   1. Return "addon".
1995            Some("node") => Ok(Some(ModuleType::Addon)),
1996            // 11. If url ends in ".js", then
1997            //   1. If packageType is not null, then
1998            //     1. Return packageType.
1999            Some("js" | "ts") => {
2000                // 7. Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(url).
2001                // 8. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
2002                let package_json = self.cache.find_package_json(cached_path, &self.options, ctx)?;
2003                // 9. Let packageType be null.
2004                if let Some(package_json) = package_json {
2005                    // 10. If pjson?.type is "module" or "commonjs", then
2006                    //   1. Set packageType to pjson.type.
2007                    if let Some(ty) = package_json.r#type() {
2008                        return Ok(Some(match ty {
2009                            PackageType::Module => ModuleType::Module,
2010                            PackageType::CommonJs => ModuleType::CommonJs,
2011                        }));
2012                    }
2013                }
2014                Ok(None)
2015            }
2016            // Step 11.2 .. 12 omitted, which involves detecting file content.
2017            _ => Ok(None),
2018        }
2019    }
2020}
2021
2022#[cfg(not(target_arch = "wasm32"))]
2023fn resolve_file_protocol(specifier: &str) -> Result<Cow<'_, str>, ResolveError> {
2024    if specifier.starts_with("file://") {
2025        url::Url::parse(specifier)
2026            .map_err(|_| ())
2027            .and_then(|url| {
2028                url.to_file_path().map(|path| {
2029                    let mut result = path.to_string_lossy().to_string();
2030                    // Preserve query and fragment from the URL
2031                    if let Some(query) = url.query() {
2032                        result.push('?');
2033                        result.push_str(query);
2034                    }
2035                    if let Some(fragment) = url.fragment() {
2036                        result.push('#');
2037                        result.push_str(fragment);
2038                    }
2039                    Cow::Owned(result)
2040                })
2041            })
2042            .map_err(|()| ResolveError::PathNotSupported(PathBuf::from(specifier)))
2043    } else {
2044        Ok(Cow::Borrowed(specifier))
2045    }
2046}
2047
2048/// Strip BOM in place by replacing with spaces (no reallocation)
2049/// UTF-8 BOM is 3 bytes: 0xEF, 0xBB, 0xBF
2050pub(crate) fn replace_bom_with_whitespace(s: &mut [u8]) {
2051    if s.starts_with(b"\xEF\xBB\xBF") {
2052        s[0] = b' ';
2053        s[1] = b' ';
2054        s[2] = b' ';
2055    }
2056}