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