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