char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somnpm: assume "module" package.json entries are es modules61e3521

main
108.8 KiB2801 linesraw
1use std::collections::{BTreeMap, BTreeSet, HashMap};
2use std::path::{Path, PathBuf};
3use std::sync::{Arc, Mutex};
4
5use super::package::{
6    NpmImportsTarget, NpmManifest, NpmModuleKind, NpmPackageScope, NpmPackageType,
7    classify_npm_module, npm_conditions, parse_npm_manifest, parse_npm_scope, platform_satisfied,
8    resolve_exports_target, resolve_imports_target, split_bare,
9};
10use super::{
11    MAX_NPM_CONCURRENT_DOWNLOADS, NpmArtifact, NpmIdentity, NpmRequest, npm_virtual_module,
12    parse_npm_request, storage, validate_npm_name,
13};
14use crate::registry::{Access, NpmInstancePin, PackageStore};
15
16/// Failures preparing and resolving npm package graphs.
17#[derive(Clone, Debug, thiserror::Error)]
18pub enum ResolutionError {
19    #[error("Frozen lockfile is missing {name}")]
20    FrozenLockfileMissing { name: String },
21    #[error("Invalid npm export subpath: {subpath}")]
22    InvalidNpmExportSubpath { subpath: String },
23    #[error("Invalid npm lock identity")]
24    InvalidNpmLockIdentity,
25    #[error("Invalid npm relative import: {specifier:?}")]
26    InvalidNpmRelativeImport { specifier: String },
27    #[error("Invalid npm version range {range:?}: {detail}")]
28    InvalidNpmVersionRange { range: String, detail: String },
29    #[error("Invalid TypeScript version {version:?}: {detail}")]
30    InvalidTypeScriptVersion { version: String, detail: String },
31    #[error("Package {name} uses typesVersions; call set_typescript_version first")]
32    TypeScriptVersionRequired { name: String },
33    #[error("JSON modules are not supported: {rel}")]
34    JsonModuleUnsupported { rel: String },
35    #[error("Locked npm instance changed for {id}")]
36    LockedNpmInstanceChanged { id: String },
37    #[error("Lockfile changed concurrently for {key}")]
38    LockfileChanged { key: String },
39    #[error("Native addons are not supported: {rel}/index.node")]
40    NativeAddonUnsupported { rel: String },
41    #[error("node: builtin modules are not supported: {specifier}")]
42    NodeBuiltinUnsupported { specifier: String },
43    #[error("npm browser remap of {specifier:?} is cyclic or too deep")]
44    NpmBrowserRemapCyclic { specifier: String },
45    #[error("npm browser remap target is not in the package manifest: {target:?}")]
46    NpmBrowserRemapTargetNotInManifest { target: String },
47    #[error("npm cache path is not owned by a prepared instance: {}", path.display())]
48    NpmCachePathUnowned { path: std::path::PathBuf },
49    #[error("npm declaration has no executable code: {rel}")]
50    NpmDeclarationNoCode { rel: String },
51    #[error("npm dependency graph is too deep")]
52    NpmDependencyGraphTooDeep,
53    #[error("npm dependency graph is too large")]
54    NpmDependencyGraphTooLarge,
55    #[error("npm dependency {alias:?} could not be resolved")]
56    NpmDependencyUnresolvable { alias: String },
57    #[error("npm dependency {alias:?} uses unsupported protocol {protocol:?}")]
58    NpmDependencyUnsupportedProtocol { alias: String, protocol: String },
59    #[error("npm export target escapes its package: {target:?}")]
60    NpmExportTargetEscapes { target: String },
61    #[error("npm export target is not in the package manifest: {target:?}")]
62    NpmExportTargetNotInManifest { target: String },
63    #[error("npm exports mixes subpath and condition keys")]
64    NpmExportsMixesKeys,
65    #[error("npm file integrity mismatch: {rel}")]
66    NpmFileIntegrityMismatch { rel: String },
67    #[error("npm file is not in the package manifest: {path}")]
68    NpmFileNotInManifest { path: String },
69    #[error("Frozen lockfile is missing npm instance {id}")]
70    NpmFrozenLockfileMissingInstance { id: String },
71    #[error("npm import target is not in the package manifest: {specifier:?}")]
72    NpmImportTargetNotInManifest { specifier: String },
73    #[error("npm imports target {target:?} is not a declared dependency")]
74    NpmImportsTargetUndeclared { target: String },
75    #[error("npm instance is not prepared: {id}")]
76    NpmInstanceNotPrepared { id: String },
77    #[error("npm module is not UTF-8: {}", path.display())]
78    NpmModuleNotUtf8 { path: std::path::PathBuf },
79    #[error("npm package {name} has no export {export:?}")]
80    NpmPackageMissingExport { name: String, export: String },
81    #[error("npm package {name} has no file {export:?}")]
82    NpmPackageMissingFile { name: String, export: String },
83    #[error("npm package {name} has no import {specifier:?}")]
84    NpmPackageMissingImport { name: String, specifier: String },
85    #[error("npm package {name} has no usable entry point")]
86    NpmPackageNoEntryPoint { name: String },
87    #[error("npm package {name:?} is not a declared dependency of {owner}")]
88    NpmPackageNotDeclared { name: String, owner: String },
89    #[error("npm package is not prepared: {request_key}")]
90    NpmPackageNotPrepared { request_key: String },
91    #[error("npm package is not prepared: {request}")]
92    NpmPackageNotPreparedRequest { request: String },
93    #[error("npm package is not prepared: {target}")]
94    NpmPackageNotPreparedTarget { target: String },
95    #[error("npm package is not supported here: {request_key}")]
96    NpmPackageNotSupportedHere { request_key: String },
97    #[error("npm package {name}@{version} is not supported on this platform")]
98    NpmPackagePlatformUnsupported { name: String, version: String },
99    #[error("npm path escapes its instance: {}", path.display())]
100    NpmPathEscapesInstance { path: std::path::PathBuf },
101    #[error("npm path escapes its package: {rel:?}")]
102    NpmPathEscapesPackage { rel: String },
103    #[error("npm path must be UTF-8")]
104    NpmPathMustBeUtf8,
105    #[error("npm path must be UTF-8: {}", path.display())]
106    NpmPathNotUtf8 { path: std::path::PathBuf },
107    #[error("npm peer dependency {peer:?} could not be resolved")]
108    NpmPeerUnresolvable { peer: String },
109    #[error("npm peer dependency {peer:?} wants {wanted} but the context provides {provided}")]
110    NpmPeerWantsMismatch {
111        peer: String,
112        wanted: String,
113        provided: String,
114    },
115    #[error("npm referrer must be an absolute path")]
116    NpmReferrerNotAbsolute,
117    #[error("npm relative import escapes its package")]
118    NpmRelativeImportEscapes,
119    #[error("npm relative imports must name a file")]
120    NpmRelativeImportMustNameFile,
121    #[error("npm relative import is not in the package manifest: {specifier:?}")]
122    NpmRelativeImportNotInManifest { specifier: String },
123    #[error("npm relative import must be UTF-8")]
124    NpmRelativeImportNotUtf8,
125    #[error(
126        "npm request {request_key} is prepared in several peer contexts; resolve it from an owning file instead"
127    )]
128    NpmRequestAmbiguousContexts { request_key: String },
129    #[error("npm request {request_key} is not prepared in this context")]
130    NpmRequestNotPrepared { request_key: String },
131    #[error(
132        "npm request {request_key} is not prepared in this context (sync require cannot acquire new packages)"
133    )]
134    NpmRequestNotPreparedSync { request_key: String },
135    #[error("npm require lookup is too deep")]
136    NpmRequireLookupTooDeep,
137    #[error("npm require lookup is too deep: {specifier:?}")]
138    NpmRequireLookupTooDeepSpecifier { specifier: String },
139    #[error("npm resolver does not own {}", path.display())]
140    NpmResolverNotOwner { path: std::path::PathBuf },
141    #[error(transparent)]
142    NpmUnsupportedModule(#[from] super::package::NpmUnsupported),
143    #[error(transparent)]
144    Npm(#[from] super::NpmError),
145    #[error(transparent)]
146    Storage(#[from] super::storage::StorageError),
147    #[error(transparent)]
148    Package(#[from] super::package::PackageError),
149}
150use futures::stream::{self, StreamExt};
151
152/// One root the caller wants prepared, with the peer providers its consumer
153/// already selected: every root in a `prepare` call shares this context, so a
154/// plugin and its host bind the same provider instances.
155#[derive(Clone, Debug)]
156pub struct NpmRootRequest {
157    pub request: String,
158    pub context: BTreeMap<String, String>,
159}
160
161/// The roots one `prepare` call established: request lock keys to instance
162/// ids. Later integration layers keep one handle per owning local config
163/// instead of re-deriving contexts from paths.
164#[derive(Clone, Debug, Default)]
165pub struct PreparedNpmRoots {
166    pub roots: BTreeMap<String, String>,
167}
168
169impl PreparedNpmRoots {
170    /// Exact lock keys in this handle for one package request.
171    pub fn keys_for_request(&self, request: &str) -> Vec<String> {
172        let mut keys: Vec<String> = self
173            .roots
174            .keys()
175            .filter(|key| key.as_str() == request || key.starts_with(&format!("{request}\0")))
176            .cloned()
177            .collect();
178        keys.sort();
179        keys
180    }
181}
182
183/// A file resolved inside an npm instance, with a canonical absolute path.
184#[derive(Clone, Debug)]
185pub struct ResolvedNpmModule {
186    pub path: PathBuf,
187    pub format: NpmModuleKind,
188    pub instance: String,
189}
190
191/// How a specifier is loaded: `import` or `require` conditions, and
192/// whether the target must be executable code or may be a declaration.
193#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
194pub enum ResolutionMode {
195    Import,
196    /// CommonJS `require` conditions, with Node-style extension and
197    /// directory inference for package-relative paths.
198    Require,
199    TypeImport,
200    TypeRequire,
201}
202
203impl ResolutionMode {
204    pub fn is_require(self) -> bool {
205        matches!(self, Self::Require | Self::TypeRequire)
206    }
207
208    pub fn is_type(self) -> bool {
209        matches!(self, Self::TypeImport | Self::TypeRequire)
210    }
211}
212
213enum RequireLookup {
214    Missing,
215    Err(ResolutionError),
216}
217
218/// Stands in for files and bare specifiers a package's `browser` field maps
219/// to `false`: an empty CommonJS module with an identity per excluded key.
220pub const BROWSER_EXCLUDED: &str = "\0browser-false.cjs";
221
222#[derive(Debug)]
223struct ResolvedNode {
224    instance: String,
225    identity: NpmIdentity,
226    dir: PathBuf,
227    files: BTreeMap<String, String>,
228    manifest: NpmManifest,
229    scopes: Mutex<BTreeMap<String, Arc<NpmPackageScope>>>,
230    deps: BTreeMap<String, String>,
231    peers: BTreeMap<String, String>,
232}
233
234/// Upper bound on resolution graph size; malicious dependency graphs fail
235/// with an error instead of expanding forever.
236const MAX_NPM_GRAPH_NODES: usize = 4096;
237
238/// Upper bound on traversal depth. The node cap alone does not bound async
239/// recursion depth, so deep chains fail instead of overflowing the stack.
240const MAX_NPM_GRAPH_DEPTH: usize = 256;
241
242#[derive(Debug)]
243struct PreparedInstance {
244    pin: NpmInstancePin,
245    dir: PathBuf,
246    loaded: Mutex<Option<Arc<ResolvedNode>>>,
247}
248
249#[derive(Clone, Debug, Default)]
250struct ResolverState {
251    roots: BTreeMap<String, String>,
252    nodes: BTreeMap<String, Arc<PreparedInstance>>,
253    instances_by_dir: HashMap<PathBuf, String>,
254}
255
256/// Local per-prepare occurrence graph. Memoized by (identity, context) with
257/// an in-progress stack for cycles; refinement later merges behaviorally
258/// identical occurrences into shared instances.
259#[derive(Default)]
260struct OccGraph {
261    occs: Vec<Occurrence>,
262    index: BTreeMap<(String, String), usize>,
263    roots: BTreeMap<String, usize>,
264}
265
266/// Mutable accumulators carried through one occurrence-graph walk.
267struct Walk<'a> {
268    build: &'a mut OccGraph,
269    acq: &'a mut BTreeMap<String, NpmArtifact>,
270    stack: &'a mut Vec<usize>,
271}
272
273struct Occurrence {
274    identity: NpmIdentity,
275    artifact: NpmArtifact,
276    manifest: NpmManifest,
277    dep_edges: BTreeMap<String, usize>,
278    peer_edges: BTreeMap<String, usize>,
279}
280
281struct FinalizedGraph {
282    roots: BTreeMap<String, String>,
283    nodes: BTreeMap<String, Arc<PreparedInstance>>,
284}
285
286/// Standalone npm package resolver. It prepares version graphs into
287/// peer-scoped instances and resolves specifiers to verified files without
288/// consulting project configs, import maps, or the local filesystem beyond
289/// the reserved instance tree.
290pub struct NpmResolver {
291    store: Arc<PackageStore>,
292    state: Mutex<ResolverState>,
293    /// Serializes graph builds. Preparation stages bindings locally and
294    /// never holds the state lock across an await.
295    prepare_gate: futures::lock::Mutex<()>,
296    /// Browser bundles select `browser` conditions and honor the `browser`
297    /// field instead of the runtime's `avril` branches; fixed at construction.
298    browser: bool,
299    typescript_version: Mutex<Option<deno_semver::Version>>,
300}
301
302impl NpmResolver {
303    pub fn new(store: Arc<PackageStore>) -> Self {
304        Self {
305            store,
306            state: Mutex::new(ResolverState::default()),
307            prepare_gate: futures::lock::Mutex::new(()),
308            browser: false,
309            typescript_version: Mutex::new(None),
310        }
311    }
312
313    pub fn for_browser(store: Arc<PackageStore>) -> Self {
314        Self {
315            browser: true,
316            ..Self::new(store)
317        }
318    }
319
320    fn conditions(&self, mode: ResolutionMode) -> &'static [&'static str] {
321        match (self.browser, mode) {
322            (false, ResolutionMode::Import) => &["avril", "import", "module", "default"],
323            (false, ResolutionMode::Require) => &["avril", "require", "default"],
324            (true, ResolutionMode::Import) => &["browser", "import", "module", "default"],
325            (true, ResolutionMode::Require) => &["browser", "require", "default"],
326            (false, ResolutionMode::TypeImport) => {
327                &["types", "avril", "import", "module", "default"]
328            }
329            (true, ResolutionMode::TypeImport) => {
330                &["types", "browser", "import", "module", "default"]
331            }
332            (false, ResolutionMode::TypeRequire) => &["types", "avril", "require", "default"],
333            (true, ResolutionMode::TypeRequire) => &["types", "browser", "require", "default"],
334        }
335    }
336
337    /// Declared `sideEffects` for one resolved file, if the package says anything usable.
338    pub fn side_effects(&self, path: &Path) -> Option<bool> {
339        let (node, rel) = self.identify_locked(path).ok()?;
340        super::package::declared_side_effects(node.manifest.side_effects.as_ref()?, &rel)
341    }
342
343    pub fn store(&self) -> &Arc<PackageStore> {
344        &self.store
345    }
346
347    pub fn set_typescript_version(&self, version: &str) -> Result<(), ResolutionError> {
348        let parsed = deno_semver::Version::parse_from_npm(version).map_err(|detail| {
349            ResolutionError::InvalidTypeScriptVersion {
350                version: version.to_owned(),
351                detail: detail.to_string(),
352            }
353        })?;
354        *self.typescript_version.lock().unwrap() = Some(parsed);
355        Ok(())
356    }
357
358    pub fn audit(&self) -> Result<(), ResolutionError> {
359        let state = self.state.lock().unwrap();
360        let nodes: Vec<_> = state
361            .nodes
362            .keys()
363            .map(|id| self.node_locked(&state, id))
364            .collect::<Result<_, _>>()?;
365        drop(state);
366        let options = self.store.options();
367        let mut audited = BTreeSet::new();
368        for node in nodes {
369            super::check_npm_exec(
370                self.store.authority.as_ref(),
371                &node.identity.registry,
372                &node.identity.name,
373                &node.identity.version,
374                &node.files,
375            )?;
376            let backing = storage::npm_package_dir(
377                options,
378                super::NPM_DEFAULT_INSTANCE,
379                &node.identity.name,
380                &node.identity.version,
381            );
382            for dir in [&node.dir, &backing] {
383                if audited.insert(dir.clone()) {
384                    storage::verify_and_load_npm_dir(&options.project_root, dir, &node.files)?;
385                }
386            }
387        }
388        Ok(())
389    }
390
391    /// Prepare several roots together. Root identities are selected first
392    /// so every root traverses against the same provider context; late roots
393    /// reuse established instances instead of rebinding them. Returns a
394    /// handle callers keep per owning config for context-pinned resolution.
395    pub async fn prepare(
396        &self,
397        roots: &[NpmRootRequest],
398    ) -> Result<PreparedNpmRoots, ResolutionError> {
399        let mut normalized: Vec<(String, BTreeMap<String, String>)> = Vec::new();
400        for root in roots {
401            let (request_key, _) = split_npm_request(&root.request)?;
402            for (peer, provider) in &root.context {
403                validate_npm_name(peer)?;
404                super::parse_npm_key(provider)?;
405            }
406            normalized.push((request_key, root.context.clone()));
407        }
408        // Exact tuples only: same request under different caller contexts
409        // resolves independently and must not be merged away.
410        normalized.sort();
411        normalized.dedup();
412        if normalized.is_empty() {
413            return Ok(PreparedNpmRoots::default());
414        }
415        let _gate = self.prepare_gate.lock().await;
416        let mut handle = PreparedNpmRoots::default();
417        let finalized = if let Some(graph) = self.restore_locked(&normalized).await? {
418            graph
419        } else {
420            if let Some(sink) = &self.store.progress {
421                sink(&super::super::ProgressEvent::NpmResolving);
422            }
423            // All root identities are selected before any traversal so roots
424            // share providers deterministically regardless of root order.
425            // Selection goes through acquisition (one code path, cached per
426            // prepare), never a parallel probe that could diverge from it.
427            let mut preselected: BTreeMap<String, String> = BTreeMap::new();
428            let mut build = OccGraph::default();
429            let mut acq: BTreeMap<String, NpmArtifact> = BTreeMap::new();
430            let preselect_keys: Vec<String> =
431                normalized.iter().map(|(key, _)| key.clone()).collect();
432            self.acquire_many(&mut acq, &preselect_keys).await?;
433            for request_key in &preselect_keys {
434                let request = super::parse_npm_request(request_key)?;
435                let identity = acq
436                    .get(request_key)
437                    .ok_or_else(|| ResolutionError::NpmPackageNotPrepared {
438                        request_key: request_key.to_owned(),
439                    })?
440                    .identity
441                    .key();
442                preselected.insert(request.name.clone(), identity);
443            }
444            // Roots and instances are recorded only by record_graph_locked on
445            // success below. Artifact pins that acquisition caches along the way
446            // are verified truth reusable like the JSR cache, so failures leave
447            // no graph behind to poison a retry; deleting shared usage here
448            // would race concurrent resolvers and workers instead.
449            {
450                let mut stack = Vec::new();
451                let mut walk = Walk {
452                    build: &mut build,
453                    acq: &mut acq,
454                    stack: &mut stack,
455                };
456                self.build_roots(&mut walk, &mut handle, &normalized, &preselected)
457                    .await?;
458            }
459            if build.roots.is_empty() {
460                return Ok(handle);
461            }
462            self.finalize_build(build).await?
463        };
464        self.record_graph_locked(&finalized)?;
465        {
466            let mut state = self.state.lock().unwrap();
467            for (key, instance) in &finalized.roots {
468                state
469                    .roots
470                    .entry(key.clone())
471                    .or_insert_with(|| instance.clone());
472                handle.roots.insert(key.clone(), instance.clone());
473            }
474            for (id, node) in &finalized.nodes {
475                let directory = state
476                    .nodes
477                    .entry(id.clone())
478                    .or_insert_with(|| node.clone())
479                    .dir
480                    .clone();
481                state
482                    .instances_by_dir
483                    .entry(directory)
484                    .or_insert_with(|| id.clone());
485            }
486        }
487        Ok(handle)
488    }
489
490    /// Restore bindings, not resolution inputs. The lock owns dependency and
491    /// peer choices; package documents are authenticated only when consumed.
492    async fn restore_locked(
493        &self,
494        normalized: &[(String, BTreeMap<String, String>)],
495    ) -> Result<Option<FinalizedGraph>, ResolutionError> {
496        if self.store.options().update {
497            return Ok(None);
498        }
499        let lock = self.store.lock.lock().unwrap().npm.clone();
500        let options = self.store.options();
501        let mut selected = BTreeMap::new();
502        for (key, _) in normalized {
503            let Some(version) = lock.specifiers.get(key) else {
504                if options.frozen {
505                    return Err(ResolutionError::FrozenLockfileMissing { name: key.clone() });
506                }
507                return Ok(None);
508            };
509            let request = parse_npm_request(key)?;
510            selected.insert(
511                request.name.clone(),
512                format!("{}|{}|{version}", options.npm_registry, request.name),
513            );
514        }
515        let mut roots = BTreeMap::new();
516        for (request, context) in normalized {
517            let key = root_lock_key(request, &effective_context(&selected, context).merged());
518            let Some(id) = lock.roots.get(&key) else {
519                if options.frozen {
520                    return Err(ResolutionError::FrozenLockfileMissing { name: key });
521                }
522                return Ok(None);
523            };
524            let pin = lock.instances.get(id).ok_or_else(|| {
525                super::NpmError::LockfileMissingNpmInstance { instance: id.clone() }
526            })?;
527            let name = parse_npm_request(request)?.name;
528            let expected = format!(
529                "{}|{name}|{}",
530                options.npm_registry, lock.specifiers[request]
531            );
532            if pin.package != expected {
533                return Err(ResolutionError::InvalidNpmLockIdentity);
534            }
535            roots.insert(key, id.clone());
536        }
537        let mut nodes = BTreeMap::new();
538        let mut pending: Vec<_> = roots.values().cloned().collect();
539        let mut acquired = BTreeMap::<String, NpmArtifact>::new();
540        while let Some(id) = pending.pop() {
541            if nodes.contains_key(&id) {
542                continue;
543            }
544            if nodes.len() >= MAX_NPM_GRAPH_NODES {
545                return Err(ResolutionError::NpmDependencyGraphTooLarge);
546            }
547            let pin = lock.instances.get(&id).ok_or_else(|| {
548                super::NpmError::LockfileMissingNpmInstance { instance: id.clone() }
549            })?;
550            let (registry, name, version) = super::parse_npm_key(&pin.package)?;
551            if registry != options.npm_registry {
552                return Err(super::NpmError::NpmOriginOutsideRegistry {
553                    url: registry.to_string(),
554                }
555                .into());
556            }
557            let base = super::npm_version_base(&registry, &name, &version)?;
558            Access::Package(base)
559                .check(self.store.authority.as_ref())
560                .map_err(super::NpmError::from)?;
561            let package_pin = lock.packages.get(&pin.package).ok_or_else(|| {
562                super::NpmError::LockfileMissingNpmPackage { package: pin.package.clone() }
563            })?;
564            let disk = storage::npm_disk_instance(&id, &pin.package);
565            let dir = storage::npm_package_dir(options, disk, &name, &version);
566            let backing =
567                storage::npm_package_dir(options, super::NPM_DEFAULT_INSTANCE, &name, &version);
568            if !storage::npm_instance_ready(options, &dir, &backing, package_pin)? {
569                if !acquired.contains_key(&pin.package) {
570                    let identity = NpmIdentity { registry, name, version };
571                    let artifact = self
572                        .store
573                        .npm_acquire_locked(&identity, package_pin)
574                        .await?;
575                    acquired.insert(pin.package.clone(), artifact);
576                }
577                storage::ensure_npm_instance(options, &acquired[&pin.package], disk)?;
578            }
579            pending.extend(pin.deps.values().chain(pin.peers.values()).cloned());
580            nodes.insert(
581                id,
582                Arc::new(PreparedInstance {
583                    pin: pin.clone(),
584                    dir,
585                    loaded: Mutex::new(None),
586                }),
587            );
588        }
589        Ok(Some(FinalizedGraph { roots, nodes }))
590    }
591
592    /// Resolve an explicit `npm:` request, preparing its package on demand.
593    /// A context-free request matches the single prepared root for its
594    /// package request; several contexts for one request are ambiguous.
595    pub async fn resolve_request(
596        &self,
597        specifier: &str,
598        mode: ResolutionMode,
599    ) -> Result<ResolvedNpmModule, ResolutionError> {
600        let request = super::parse_npm_request(specifier)?;
601        self.resolve_request_parsed(&request, mode).await
602    }
603
604    pub async fn resolve_request_parsed(
605        &self,
606        request: &super::NpmRequest,
607        mode: ResolutionMode,
608    ) -> Result<ResolvedNpmModule, ResolutionError> {
609        let request_key = request.request_key();
610        let subpath = request.subpath.clone();
611        let instance = {
612            let state = self.state.lock().unwrap();
613            self.lookup_root_locked(&state, &request_key)?
614        };
615        let instance = match instance {
616            Some(instance) => instance,
617            None => {
618                let handle = self
619                    .prepare(&[NpmRootRequest {
620                        request: request_key.clone(),
621                        context: BTreeMap::new(),
622                    }])
623                    .await?;
624                handle.roots.values().next().cloned().ok_or_else(|| {
625                    ResolutionError::NpmPackageNotPrepared {
626                        request_key: request_key.to_owned(),
627                    }
628                })?
629            }
630        };
631        let export = match subpath {
632            Some(path) => subpath_to_export(&path)?,
633            None => ".".into(),
634        };
635        self.resolve_in_instance(&instance, &export, mode)
636    }
637
638    /// ESM-warmable requests for a package or the export prefix its
639    /// subpath names, in the specifier's own range form so frozen runs pin
640    /// the same roots. Exact export keys resolve directly; wildcard
641    /// patterns invert over verified files to their public subpaths, never
642    /// the private file names. Conditions select executable branches only:
643    /// misses, blocks, and non-executable kinds are skipped, never
644    /// compiled. Any other resolution or security error fails the whole
645    /// enumeration instead of warming a partial prefix.
646    pub async fn export_requests(&self, specifier: &str) -> Result<Vec<String>, ResolutionError> {
647        let (request_key, subpath) = split_npm_request(specifier)?;
648        let scope = subpath
649            .map(|path| path.trim_end_matches('/').to_owned())
650            .unwrap_or_default();
651        if self
652            .lookup_root_locked(&self.state.lock().unwrap(), &request_key)?
653            .is_none()
654        {
655            self.prepare(&[NpmRootRequest {
656                request: request_key.clone(),
657                context: BTreeMap::new(),
658            }])
659            .await?;
660        }
661        let state = self.state.lock().unwrap();
662        let instance = self
663            .lookup_root_locked(&state, &request_key)?
664            .ok_or_else(|| ResolutionError::NpmPackageNotPrepared {
665                request_key: request_key.to_owned(),
666            })?;
667        let node = self.node_locked(&state, &instance)?;
668        self.export_requests_in(&node, &request_key, &scope)
669    }
670
671    fn export_requests_in(
672        &self,
673        node: &ResolvedNode,
674        base: &str,
675        scope: &str,
676    ) -> Result<Vec<String>, ResolutionError> {
677        match &node.manifest.exports_raw {
678            // Without an exports map only the entry file is public; legacy
679            // subpaths warm per file beneath a prefix scope instead.
680            None if scope.is_empty() => Ok(vec![base.to_owned()]),
681            None => {
682                let mut requests = Vec::new();
683                for rel in node.files.keys() {
684                    if rel != scope && !rel.starts_with(&format!("{scope}/")) {
685                        continue;
686                    }
687                    if !self.export_file_executable(node, rel)? {
688                        continue;
689                    }
690                    requests.push(format!("{base}/{rel}"));
691                }
692                requests.sort();
693                requests.dedup();
694                Ok(requests)
695            }
696            Some(exports) => self.export_requests_mapped(node, base, scope, exports),
697        }
698    }
699
700    fn export_requests_mapped(
701        &self,
702        node: &ResolvedNode,
703        base: &str,
704        scope: &str,
705        exports: &serde_json::Value,
706    ) -> Result<Vec<String>, ResolutionError> {
707        // A bare target, a fallback list, or a condition object only
708        // ever selects the root.
709        let map = match exports {
710            serde_json::Value::Object(map) => map,
711            _ => {
712                if scope.is_empty() && self.export_executable_in(node, exports, ".")?.is_some() {
713                    return Ok(vec![base.to_owned()]);
714                }
715                return Ok(Vec::new());
716            }
717        };
718        let subpaths = map.keys().filter(|key| key.starts_with('.')).count();
719        if subpaths > 0 && subpaths != map.len() {
720            return Err(ResolutionError::NpmExportsMixesKeys);
721        }
722        if subpaths == 0 {
723            if scope.is_empty() && self.export_executable_in(node, exports, ".")?.is_some() {
724                return Ok(vec![base.to_owned()]);
725            }
726            return Ok(Vec::new());
727        }
728        let mut subs: BTreeSet<String> = BTreeSet::new();
729        if scope.is_empty() && map.contains_key(".") {
730            subs.insert(".".to_owned());
731        }
732        for key in map.keys() {
733            if !key.starts_with("./") || key.contains('*') {
734                continue;
735            }
736            if map.get(key).is_some_and(|value| value.is_null()) {
737                continue;
738            }
739            if under_export_scope(&key["./".len()..], scope) {
740                subs.insert(key.clone());
741            }
742        }
743        for key in map.keys() {
744            let Some((key_prefix, _)) = key.split_once('*') else {
745                continue;
746            };
747            if !key.starts_with("./") || key[key_prefix.len() + 1..].contains('*') {
748                continue;
749            }
750            let (_, key_suffix) = key.split_once('*').unwrap();
751            let mut targets = Vec::new();
752            pattern_targets(&map[key], &mut targets);
753            for target in targets {
754                // File keys omit the trailer's leading `./`.
755                let target = target.strip_prefix("./").unwrap_or(&target);
756                let Some((target_prefix, target_suffix)) = target.split_once('*') else {
757                    continue;
758                };
759                for file in node.files.keys() {
760                    if file.len() < target_prefix.len() + target_suffix.len()
761                        || !file.starts_with(target_prefix)
762                        || !file.ends_with(&target_suffix)
763                    {
764                        continue;
765                    }
766                    let captured = &file[target_prefix.len()..file.len() - target_suffix.len()];
767                    let public = format!("{key_prefix}{captured}{key_suffix}");
768                    if !public.starts_with("./") {
769                        continue;
770                    }
771                    if !under_export_scope(&public["./".len()..], scope) {
772                        continue;
773                    }
774                    match resolve_exports_target(exports, &public, &npm_conditions()) {
775                        Ok(Some(_)) => {
776                            subs.insert(public);
777                        }
778                        Ok(None) => {}
779                        Err(error) => return Err(error.into()),
780                    }
781                }
782            }
783        }
784        let mut requests = Vec::new();
785        for sub in subs {
786            if self.export_executable_in(node, exports, &sub)?.is_none() {
787                continue;
788            }
789            if sub == "." {
790                requests.push(base.to_owned());
791            } else {
792                requests.push(format!("{base}/{}", sub.strip_prefix("./").unwrap_or(&sub)));
793            }
794        }
795        requests.sort();
796        requests.dedup();
797        Ok(requests)
798    }
799
800    /// Resolve one public export to its verified file when the active
801    /// conditions select executable code. Misses and non-executable kinds
802    /// (including explicitly blocked entries) yield None without failing;
803    /// malformed, missing, escaping, or unauthorized targets fail.
804    fn export_executable_in(
805        &self,
806        node: &ResolvedNode,
807        exports: &serde_json::Value,
808        export: &str,
809    ) -> Result<Option<String>, ResolutionError> {
810        if exports
811            .as_object()
812            .and_then(|map| map.get(export))
813            .is_some_and(|value| value.is_null())
814        {
815            return Ok(None);
816        }
817        let target = match resolve_exports_target(exports, export, &npm_conditions()) {
818            Ok(Some(target)) => target,
819            Ok(None) => return Ok(None),
820            Err(error) => return Err(error.into()),
821        };
822        let rel = target
823            .strip_prefix("./")
824            .ok_or_else(|| ResolutionError::NpmExportTargetEscapes { target: target.to_owned() })?;
825        super::validate_npm_relpath(rel)?;
826        if rel.split('/').any(|part| part == "node_modules") {
827            return Err(ResolutionError::NpmExportTargetEscapes { target: target.to_owned() });
828        }
829        if !node.files.contains_key(rel) {
830            return Err(ResolutionError::NpmExportTargetNotInManifest {
831                target: target.to_owned(),
832            });
833        }
834        let url = super::npm_virtual_module(
835            &node.identity.registry,
836            &node.identity.name,
837            &node.identity.version,
838            rel,
839        )?;
840        super::check_npm_url(self.store.authority.as_ref(), &url)?;
841        match node.classify(&self.store.options().project_root, rel) {
842            Ok(NpmModuleKind::Esm)
843            | Ok(NpmModuleKind::TypeScript)
844            | Ok(NpmModuleKind::CommonJs) => Ok(Some(rel.to_owned())),
845            Ok(_) | Err(ResolutionError::NpmUnsupportedModule(_)) => Ok(None),
846            Err(error) => Err(error),
847        }
848    }
849
850    /// Whether one legacy file warms as executable code: same
851    /// classification execution applies, without an exports map.
852    fn export_file_executable(
853        &self,
854        node: &ResolvedNode,
855        rel: &str,
856    ) -> Result<bool, ResolutionError> {
857        let url = super::npm_virtual_module(
858            &node.identity.registry,
859            &node.identity.name,
860            &node.identity.version,
861            rel,
862        )?;
863        super::check_npm_url(self.store.authority.as_ref(), &url)?;
864        match node.classify(&self.store.options().project_root, rel) {
865            Ok(NpmModuleKind::Esm)
866            | Ok(NpmModuleKind::TypeScript)
867            | Ok(NpmModuleKind::CommonJs) => Ok(true),
868            Ok(_) | Err(ResolutionError::NpmUnsupportedModule(_)) => Ok(false),
869            Err(error) => Err(error),
870        }
871    }
872
873    /// The exact recorded root key behind one resolution, for lock
874    /// tracking that keeps successful contexts only.
875    pub fn tracking_key(&self, request_key: &str, instance: &str) -> Option<String> {
876        let state = self.state.lock().unwrap();
877        let mut keys: Vec<String> = state
878            .roots
879            .iter()
880            .filter(|(key, existing)| {
881                existing.as_str() == instance && root_request_part(key) == request_key
882            })
883            .map(|(key, _)| key.clone())
884            .collect();
885        keys.sort();
886        keys.into_iter().next()
887    }
888
889    pub(crate) fn resolve_require_path(
890        &self,
891        path: &Path,
892    ) -> Result<Option<ResolvedNpmModule>, ResolutionError> {
893        let state = self.state.lock().unwrap();
894        let instance = self.owning_instance_locked(&state, path)?;
895        let node = self.node_locked(&state, &instance)?;
896        let rel = path
897            .strip_prefix(&node.dir)
898            .map_err(|_| ResolutionError::NpmPathEscapesInstance { path: path.to_owned() })?
899            .to_str()
900            .ok_or_else(|| ResolutionError::NpmPathNotUtf8 { path: path.to_owned() })?
901            .replace('\\', "/");
902        match self.resolve_relative_require(&node, &rel, &path.display().to_string(), 0) {
903            Ok(module) => Ok(Some(module)),
904            Err(ResolutionError::NpmRelativeImportNotInManifest { .. }) => Ok(None),
905            Err(error) => Err(error),
906        }
907    }
908
909    /// Resolve `specifier` against the roots of one prepare handle.
910    pub fn resolve_in(
911        &self,
912        prepared: &PreparedNpmRoots,
913        specifier: &str,
914        mode: ResolutionMode,
915    ) -> Result<ResolvedNpmModule, ResolutionError> {
916        let (request_key, subpath) = split_npm_request(specifier)?;
917        let mut candidates = prepared
918            .roots
919            .iter()
920            .filter(|(key, _)| root_request_part(key) == request_key)
921            .map(|(_, instance)| instance.clone())
922            .collect::<Vec<_>>();
923        candidates.sort();
924        candidates.dedup();
925        if candidates.len() != 1 {
926            return Err(if mode == ResolutionMode::Require {
927                ResolutionError::NpmRequestNotPreparedSync {
928                    request_key: request_key.to_owned(),
929                }
930            } else {
931                ResolutionError::NpmRequestNotPrepared {
932                    request_key: request_key.to_owned(),
933                }
934            });
935        }
936        let export = match subpath {
937            Some(path) => subpath_to_export(&path)?,
938            None => ".".into(),
939        };
940        self.resolve_in_instance(&candidates.into_iter().next().unwrap(), &export, mode)
941    }
942
943    fn npm_absolute_path(specifier: &str) -> Option<PathBuf> {
944        let path = Path::new(specifier);
945        if path.is_absolute() {
946            return Some(path.to_owned());
947        }
948        if specifier.starts_with("file:")
949            && let Ok(url) = url::Url::parse(specifier)
950            && url.scheme() == "file"
951            && url.query().is_none()
952            && url.fragment().is_none()
953        {
954            return url.to_file_path().ok();
955        }
956        None
957    }
958
959    /// Resolve `specifier` against the instance owning `referrer`. The
960    /// referrer must be a canonical absolute path inside a prepared instance;
961    /// anything else (including unowned reserved cache paths) is an error.
962    /// `require` modes add Node-style extension/directory inference, still
963    /// only over verified manifests and never the filesystem directly.
964    pub fn resolve_from_file(
965        &self,
966        specifier: &str,
967        referrer: &Path,
968        mode: ResolutionMode,
969    ) -> Result<ResolvedNpmModule, ResolutionError> {
970        let state = self.state.lock().unwrap();
971        let instance = self.owning_instance_locked(&state, referrer)?;
972        let node = self.node_locked(&state, &instance)?;
973        if specifier.starts_with('.') || specifier.starts_with("file:") {
974            return self.resolve_relative_locked(&node, referrer, specifier, mode);
975        }
976        if let Some(absolute) = Self::npm_absolute_path(specifier) {
977            if let Ok(rel) = absolute.strip_prefix(&node.dir) {
978                let rel = rel
979                    .to_str()
980                    .ok_or(ResolutionError::NpmPathMustBeUtf8)?
981                    .replace('\\', "/");
982                let trimmed = rel.trim_end_matches('/');
983                if mode.is_type() {
984                    return self.resolve_types_target_locked(
985                        &node,
986                        &format!("./{trimmed}"),
987                        trimmed,
988                        mode,
989                    );
990                }
991                if mode.is_require() {
992                    return self.resolve_relative_require(&node, trimmed, specifier, 0);
993                }
994                super::validate_npm_relpath(trimmed)?;
995                if !node.files.contains_key(trimmed) {
996                    return Err(ResolutionError::NpmFileNotInManifest {
997                        path: specifier.to_owned(),
998                    });
999                }
1000                return self.finish_file_locked(&node, trimmed, mode);
1001            }
1002            return Err(ResolutionError::NpmPathEscapesPackage { rel: specifier.to_owned() });
1003        }
1004        if specifier.starts_with('#') {
1005            return self.resolve_hash_locked(&state, &node, specifier, mode);
1006        }
1007        if specifier.starts_with("npm:") {
1008            let (request_key, sub) = split_npm_request(specifier)?;
1009            let instance = state.roots.get(&request_key).cloned().ok_or_else(|| {
1010                ResolutionError::NpmPackageNotPrepared {
1011                    request_key: request_key.to_owned(),
1012                }
1013            })?;
1014            let node = self.node_locked(&state, &instance)?;
1015            let export = match sub {
1016                Some(path) => subpath_to_export(&path)?,
1017                None => ".".into(),
1018            };
1019            return self.resolve_in_node_locked(&node, &export, mode);
1020        }
1021        self.resolve_bare_locked(&state, &node, specifier, mode, 0)
1022    }
1023
1024    fn resolve_bare_locked(
1025        &self,
1026        state: &ResolverState,
1027        node: &ResolvedNode,
1028        specifier: &str,
1029        mode: ResolutionMode,
1030        depth: usize,
1031    ) -> Result<ResolvedNpmModule, ResolutionError> {
1032        let (name, sub) = split_bare(specifier);
1033        if name == "node" || name.starts_with("node:") {
1034            return Err(ResolutionError::NodeBuiltinUnsupported {
1035                specifier: specifier.to_owned(),
1036            });
1037        }
1038        // An exact `dep/sub` key replaces the whole specifier; a `dep` key keeps `/sub`.
1039        let map = self.browser_map(node);
1040        let remap = match map.and_then(|map| map.get(specifier)) {
1041            Some(target) => Some((target, None)),
1042            None => sub.and_then(|sub| Some((map?.get(name)?, Some(sub)))),
1043        };
1044        match remap {
1045            Some((serde_json::Value::Bool(false), _)) => return Ok(excluded(node, specifier)),
1046            Some((serde_json::Value::String(_), _)) if depth >= 8 => {
1047                return Err(ResolutionError::NpmBrowserRemapCyclic {
1048                    specifier: specifier.to_owned(),
1049                });
1050            }
1051            Some((serde_json::Value::String(target), None)) if target.starts_with('.') => {
1052                if mode.is_type() {
1053                    return self.resolve_types_target_locked(
1054                        node,
1055                        &format!("./{}", target.trim_start_matches("./")),
1056                        specifier,
1057                        mode,
1058                    );
1059                }
1060                return self.finish_file_locked(node, target.trim_start_matches("./"), mode);
1061            }
1062            Some((serde_json::Value::String(target), rest)) if !target.starts_with('.') => {
1063                let target = match rest {
1064                    Some(rest) => format!("{target}/{rest}"),
1065                    None => target.clone(),
1066                };
1067                return self.resolve_bare_locked(state, node, &target, mode, depth + 1);
1068            }
1069            _ => {}
1070        }
1071        if name == node.manifest.name && node.manifest.exports_raw.is_some() {
1072            let export = sub
1073                .map(|path| format!("./{path}"))
1074                .unwrap_or_else(|| ".".into());
1075            return self.resolve_in_node_locked(node, &export, mode);
1076        }
1077        let export = sub
1078            .map(|path| format!("./{path}"))
1079            .unwrap_or_else(|| ".".into());
1080        let resolved = if let Some(provider) = node.deps.get(name).or_else(|| node.peers.get(name))
1081        {
1082            let provider = self.node_locked(state, provider)?;
1083            Some(self.resolve_in_node_locked(&provider, &export, mode)?)
1084        } else {
1085            None
1086        };
1087        if mode.is_type()
1088            && !name.starts_with("@types/")
1089            && resolved.as_ref().is_none_or(|module| {
1090                !matches!(
1091                    module
1092                        .path
1093                        .extension()
1094                        .and_then(|extension| extension.to_str()),
1095                    Some("ts" | "tsx" | "mts" | "cts")
1096                )
1097            })
1098        {
1099            let types = super::package::types_package_name(name);
1100            if let Some(provider) = node.deps.get(&types).or_else(|| node.peers.get(&types)) {
1101                let provider = self.node_locked(state, provider)?;
1102                return self.resolve_in_node_locked(&provider, &export, mode);
1103            }
1104        }
1105        if let Some(resolved) = resolved {
1106            return Ok(resolved);
1107        }
1108        Err(ResolutionError::NpmPackageNotDeclared {
1109            name: name.to_owned(),
1110            owner: node.manifest.name.clone(),
1111        })
1112    }
1113
1114    /// Synchronous `import.meta`-style resolution from prepared metadata.
1115    pub fn resolve_import_meta(&self, specifier: &str) -> Result<String, ResolutionError> {
1116        self.resolve_import_meta_parsed(&parse_npm_request(specifier)?)
1117    }
1118
1119    pub fn resolve_import_meta_parsed(
1120        &self,
1121        request: &NpmRequest,
1122    ) -> Result<String, ResolutionError> {
1123        let request_key = request.request_key();
1124        let state = self.state.lock().unwrap();
1125        let instance = self
1126            .lookup_root_locked(&state, &request_key)?
1127            .ok_or_else(|| ResolutionError::NpmPackageNotPrepared {
1128                request_key: request_key.to_owned(),
1129            })?;
1130        let node = self.node_locked(&state, &instance)?;
1131        let export = match &request.subpath {
1132            Some(path) => subpath_to_export(path)?,
1133            None => ".".into(),
1134        };
1135        Ok(self
1136            .resolve_in_node_locked(&node, &export, ResolutionMode::Import)?
1137            .path
1138            .to_string_lossy()
1139            .into_owned())
1140    }
1141
1142    /// Read one resolved file as text, rechecking execution authority and
1143    /// integrity at use time.
1144    pub fn npm_source(&self, path: &Path) -> Result<String, ResolutionError> {
1145        let (node, rel) = self.identify_locked(path)?;
1146        let url = npm_virtual_module(
1147            &node.identity.registry,
1148            &node.identity.name,
1149            &node.identity.version,
1150            &rel,
1151        )?;
1152        super::check_npm_url(self.store.authority.as_ref(), &url)?;
1153        let bytes = node.read_verified(&self.store.options().project_root, &rel)?;
1154        String::from_utf8(bytes)
1155            .map_err(|_| ResolutionError::NpmModuleNotUtf8 { path: path.to_owned() })
1156    }
1157
1158    pub fn owning_instance(&self, path: &Path) -> Option<String> {
1159        let state = self.state.lock().unwrap();
1160        self.owning_instance_locked(&state, path).ok()
1161    }
1162
1163    pub fn describe_instance(&self, id: &str) -> Result<NpmIdentity, ResolutionError> {
1164        let state = self.state.lock().unwrap();
1165        let instance = state
1166            .nodes
1167            .get(id)
1168            .ok_or_else(|| ResolutionError::NpmInstanceNotPrepared { id: id.to_owned() })?;
1169        let (registry, name, version) = super::parse_npm_key(&instance.pin.package)?;
1170        Ok(NpmIdentity { registry, name, version })
1171    }
1172
1173    /// Authorize one reserved-tree file: ownership, kind, and virtual-URL
1174    /// execution authority. Local project grants never satisfy this; only
1175    /// registry `registry/name/version[/file]` grants do.
1176    pub fn check_access(
1177        &self,
1178        path: &Path,
1179    ) -> Result<super::package::NpmModuleKind, ResolutionError> {
1180        let (node, rel) = self.identify_locked(path)?;
1181        let url = super::npm_virtual_module(
1182            &node.identity.registry,
1183            &node.identity.name,
1184            &node.identity.version,
1185            &rel,
1186        )?;
1187        super::check_npm_url(self.store.authority.as_ref(), &url)?;
1188        node.classify(&self.store.options().project_root, &rel)
1189    }
1190
1191    /// Reserved tree membership, independent of prepared state: callers must
1192    /// never treat reserved-but-unowned paths as local files.
1193    pub fn is_reserved_npm_path(&self, path: &Path) -> bool {
1194        Self::reserved_npm_path(&self.store.options().project_root, path)
1195    }
1196
1197    fn owning_instance_locked(
1198        &self,
1199        state: &ResolverState,
1200        path: &Path,
1201    ) -> Result<String, ResolutionError> {
1202        for directory in path.ancestors() {
1203            if let Some(id) = state.instances_by_dir.get(directory) {
1204                return Ok(id.clone());
1205            }
1206        }
1207        if Self::reserved_npm_path(&self.store.options().project_root, path) {
1208            return Err(ResolutionError::NpmCachePathUnowned { path: path.to_owned() });
1209        }
1210        Err(ResolutionError::NpmResolverNotOwner { path: path.to_owned() })
1211    }
1212
1213    fn lookup_root_locked(
1214        &self,
1215        state: &ResolverState,
1216        request_key: &str,
1217    ) -> Result<Option<String>, ResolutionError> {
1218        if let Some(instance) = state.roots.get(request_key) {
1219            return Ok(Some(instance.clone()));
1220        }
1221        // Distinct contexts that agree on one instance are not ambiguous.
1222        let mut instances = state
1223            .roots
1224            .keys()
1225            .filter(|key| {
1226                key.split_once('\0')
1227                    .is_some_and(|(base, _)| base == request_key)
1228            })
1229            .filter_map(|key| state.roots.get(key))
1230            .collect::<Vec<_>>();
1231        instances.sort();
1232        instances.dedup();
1233        match instances.len() {
1234            0 => Ok(None),
1235            1 => Ok(Some(instances.into_iter().next().unwrap().clone())),
1236            _ => Err(ResolutionError::NpmRequestAmbiguousContexts {
1237                request_key: request_key.to_owned(),
1238            }),
1239        }
1240    }
1241
1242    fn node_locked(
1243        &self,
1244        state: &ResolverState,
1245        id: &str,
1246    ) -> Result<Arc<ResolvedNode>, ResolutionError> {
1247        let instance = state
1248            .nodes
1249            .get(id)
1250            .ok_or_else(|| ResolutionError::NpmInstanceNotPrepared { id: id.to_owned() })?;
1251        let mut loaded = instance.loaded.lock().unwrap();
1252        if let Some(node) = &*loaded {
1253            return Ok(node.clone());
1254        }
1255        let (registry, name, version) = super::parse_npm_key(&instance.pin.package)?;
1256        let base = super::npm_version_base(&registry, &name, &version)?;
1257        Access::Package(base)
1258            .check(self.store.authority.as_ref())
1259            .map_err(super::NpmError::from)?;
1260        let pin = self
1261            .store
1262            .lock
1263            .lock()
1264            .unwrap()
1265            .npm
1266            .packages
1267            .get(&instance.pin.package)
1268            .cloned()
1269            .ok_or_else(|| super::NpmError::LockfileMissingNpmPackage {
1270                package: instance.pin.package.clone(),
1271            })?;
1272        let files = match pin.files {
1273            Some(files) => files,
1274            None => storage::load_manifest_map(
1275                self.store.options(),
1276                pin.manifest
1277                    .as_deref()
1278                    .ok_or(storage::StorageError::NpmPinMissingManifest)?,
1279            )?
1280            .ok_or(storage::StorageError::NpmManifestMissing)?,
1281        };
1282        super::check_npm_exec(
1283            self.store.authority.as_ref(),
1284            &registry,
1285            &name,
1286            &version,
1287            &files,
1288        )?;
1289        let package = storage::verify_npm_metadata(
1290            &self.store.options().project_root,
1291            &instance.dir,
1292            &files,
1293        )?
1294        .ok_or(storage::StorageError::NpmManifestMissing)?;
1295        let manifest = parse_npm_manifest(&name, &version, &package)?;
1296        if !platform_satisfied(manifest.os.as_ref(), super::package::npm_host_os())
1297            || !platform_satisfied(manifest.cpu.as_ref(), super::package::npm_host_arch())
1298        {
1299            return Err(ResolutionError::NpmPackagePlatformUnsupported { name, version });
1300        }
1301        let node = Arc::new(ResolvedNode {
1302            instance: id.to_owned(),
1303            identity: NpmIdentity { registry, name, version },
1304            dir: instance.dir.clone(),
1305            files,
1306            manifest,
1307            scopes: Mutex::new(BTreeMap::new()),
1308            deps: instance.pin.deps.clone(),
1309            peers: instance.pin.peers.clone(),
1310        });
1311        *loaded = Some(node.clone());
1312        Ok(node)
1313    }
1314
1315    fn identify_locked(&self, path: &Path) -> Result<(Arc<ResolvedNode>, String), ResolutionError> {
1316        let state = self.state.lock().unwrap();
1317        let id = self.owning_instance_locked(&state, path)?;
1318        let node = self.node_locked(&state, &id)?;
1319        let rel = path
1320            .strip_prefix(&node.dir)
1321            .map_err(|_| ResolutionError::NpmPathEscapesInstance { path: path.to_owned() })?;
1322        let rel = rel
1323            .to_str()
1324            .ok_or_else(|| ResolutionError::NpmPathNotUtf8 { path: path.to_owned() })?
1325            .replace('\\', "/");
1326        super::validate_npm_relpath(&rel)?;
1327        if !node.files.contains_key(&rel) {
1328            return Err(ResolutionError::NpmFileNotInManifest { path: path.display().to_string() });
1329        }
1330        Ok((node, rel))
1331    }
1332
1333    fn resolve_in_instance(
1334        &self,
1335        instance: &str,
1336        export: &str,
1337        mode: ResolutionMode,
1338    ) -> Result<ResolvedNpmModule, ResolutionError> {
1339        let state = self.state.lock().unwrap();
1340        let node = self.node_locked(&state, instance)?;
1341        self.resolve_in_node_locked(&node, export, mode)
1342    }
1343
1344    fn resolve_in_node_locked(
1345        &self,
1346        node: &ResolvedNode,
1347        export: &str,
1348        mode: ResolutionMode,
1349    ) -> Result<ResolvedNpmModule, ResolutionError> {
1350        let target = if let Some(exports) = &node.manifest.exports_raw {
1351            resolve_exports_target(exports, export, self.conditions(mode))?.ok_or_else(|| {
1352                ResolutionError::NpmPackageMissingExport {
1353                    name: node.manifest.name.clone(),
1354                    export: export.to_owned(),
1355                }
1356            })?
1357        } else {
1358            if mode.is_type()
1359                && let Some(mapped) = self.resolve_types_versions_locked(node, export, mode)?
1360            {
1361                return Ok(mapped);
1362            }
1363            match self.legacy_entry(node, export, mode) {
1364                Err(ResolutionError::NpmPackageNoEntryPoint { .. })
1365                    if mode.is_type() && node.files.contains_key("index.d.ts") =>
1366                {
1367                    "./index.d.ts".into()
1368                }
1369                result => result?,
1370            }
1371        };
1372        if mode.is_type() {
1373            self.resolve_types_target_locked(node, &target, export, mode)
1374        } else {
1375            self.resolve_target_locked(node, &target, mode)
1376        }
1377    }
1378
1379    fn resolve_types_versions_locked(
1380        &self,
1381        node: &ResolvedNode,
1382        export: &str,
1383        mode: ResolutionMode,
1384    ) -> Result<Option<ResolvedNpmModule>, ResolutionError> {
1385        let Some(raw) = node.manifest.types_versions.as_ref() else {
1386            return Ok(None);
1387        };
1388        let Some(ranges) = raw.as_object() else {
1389            return Ok(None);
1390        };
1391        let mut valid_ranges = Vec::new();
1392        for (range, mappings) in ranges {
1393            let Ok(requirement) = deno_semver::VersionReq::parse_from_npm(range) else {
1394                continue;
1395            };
1396            if requirement.range().is_none() {
1397                continue;
1398            }
1399            let Some(mappings) = mappings.as_object() else {
1400                continue;
1401            };
1402            if !mappings.iter().all(|(pattern, targets)| {
1403                valid_types_versions_pattern(pattern)
1404                    && targets.as_array().is_some_and(|targets| {
1405                        targets
1406                            .iter()
1407                            .all(|target| target.as_str().is_some_and(valid_types_versions_target))
1408                    })
1409            }) {
1410                continue;
1411            }
1412            valid_ranges.push((range.as_str(), mappings));
1413        }
1414        if valid_ranges.is_empty() {
1415            return Ok(None);
1416        }
1417        let version = self
1418            .typescript_version
1419            .lock()
1420            .unwrap()
1421            .clone()
1422            .ok_or_else(|| ResolutionError::TypeScriptVersionRequired {
1423                name: node.manifest.name.clone(),
1424            })?;
1425        let Some((_, mappings)) = valid_ranges
1426            .into_iter()
1427            .find(|(range, _)| typescript_range_matches(range, &version))
1428        else {
1429            return Ok(None);
1430        };
1431        let module_name = if export == "." {
1432            node.manifest
1433                .types
1434                .as_ref()
1435                .and_then(serde_json::Value::as_str)
1436                .or_else(|| {
1437                    node.manifest
1438                        .typings
1439                        .as_ref()
1440                        .and_then(serde_json::Value::as_str)
1441                })
1442                .or(node.manifest.module.as_deref())
1443                .or(node.manifest.main.as_deref())
1444                .map(|path| path.trim_start_matches("./"))
1445                .unwrap_or("index")
1446        } else {
1447            export.strip_prefix("./").unwrap_or(export)
1448        };
1449        let Some((pattern, matched)) = match_types_versions_pattern(mappings, module_name) else {
1450            return Ok(None);
1451        };
1452        let Some(targets) = mappings.get(pattern).and_then(serde_json::Value::as_array) else {
1453            return Ok(None);
1454        };
1455        for target in targets.iter().filter_map(serde_json::Value::as_str) {
1456            let target = target.replacen('*', matched, 1);
1457            let target = target.strip_prefix("./").unwrap_or(&target);
1458            let target = format!("./{target}");
1459            match self.resolve_types_target_locked(node, &target, export, mode) {
1460                Ok(module) => return Ok(Some(module)),
1461                Err(ResolutionError::NpmExportTargetNotInManifest { .. }) => {}
1462                Err(error) => return Err(error),
1463            }
1464        }
1465        Ok(None)
1466    }
1467
1468    fn resolve_types_target_locked(
1469        &self,
1470        node: &ResolvedNode,
1471        target: &str,
1472        export: &str,
1473        mode: ResolutionMode,
1474    ) -> Result<ResolvedNpmModule, ResolutionError> {
1475        let rel = target
1476            .strip_prefix("./")
1477            .ok_or_else(|| ResolutionError::NpmExportTargetEscapes { target: target.into() })?;
1478        super::validate_npm_relpath(rel)?;
1479        if rel.split('/').any(|part| part == "node_modules") {
1480            return Err(ResolutionError::NpmExportTargetEscapes { target: target.into() });
1481        }
1482        if let Some(found) = declaration_candidates(rel)
1483            .iter()
1484            .find(|candidate| node.files.contains_key(*candidate))
1485        {
1486            return self.finish_file_locked(node, found, mode);
1487        }
1488        if export == "."
1489            && node.manifest.exports_raw.is_none()
1490            && node.manifest.types.is_none()
1491            && node.manifest.typings.is_none()
1492            && node.files.contains_key("index.d.ts")
1493        {
1494            return self.finish_file_locked(node, "index.d.ts", mode);
1495        }
1496        if node.files.contains_key(rel) {
1497            return self.finish_file_locked(node, rel, mode);
1498        }
1499        Err(ResolutionError::NpmExportTargetNotInManifest { target: target.into() })
1500    }
1501
1502    fn legacy_entry(
1503        &self,
1504        node: &ResolvedNode,
1505        export: &str,
1506        mode: ResolutionMode,
1507    ) -> Result<String, ResolutionError> {
1508        if mode.is_type() {
1509            if export != "." {
1510                return Ok(format!("./{}", export.strip_prefix("./").unwrap_or(export)));
1511            }
1512            if let Some((key, entry)) = node
1513                .manifest
1514                .types
1515                .as_ref()
1516                .map(|entry| ("types", entry))
1517                .or_else(|| {
1518                    node.manifest
1519                        .typings
1520                        .as_ref()
1521                        .map(|entry| ("typings", entry))
1522                })
1523            {
1524                let entry = entry.as_str().ok_or_else(|| {
1525                    super::package::PackageError::NpmMustBePathString { key: key.into() }
1526                })?;
1527                if entry.starts_with('/') || entry.contains('\\') {
1528                    return Err(ResolutionError::NpmExportTargetEscapes { target: entry.into() });
1529                }
1530                return Ok(format!("./{}", entry.strip_prefix("./").unwrap_or(entry)));
1531            }
1532        }
1533        if let Some(entry) = node
1534            .manifest
1535            .browser
1536            .as_ref()
1537            .and_then(|entry| entry.as_str())
1538            && self.browser
1539            && export == "."
1540        {
1541            match self.require_lookup_rel(node, entry.trim_start_matches("./"), 0) {
1542                Ok(found) => return Ok(format!("./{found}")),
1543                Err(RequireLookup::Err(error)) => return Err(error),
1544                Err(RequireLookup::Missing) => {}
1545            }
1546        }
1547        if mode.is_require() {
1548            return self.legacy_require_entry(node, export);
1549        }
1550        if export == "." {
1551            let entries = [&node.manifest.module, &node.manifest.main];
1552            for entry in entries.into_iter().flatten() {
1553                let rel = entry.strip_prefix("./").unwrap_or(entry);
1554                if node.files.contains_key(rel) {
1555                    return Ok(format!("./{rel}"));
1556                }
1557            }
1558            if node.files.contains_key("index.js") {
1559                return Ok("./index.js".into());
1560            }
1561            return Err(ResolutionError::NpmPackageNoEntryPoint {
1562                name: node.manifest.name.clone(),
1563            });
1564        }
1565        let rel = export.strip_prefix("./").unwrap_or(export);
1566        if node.files.contains_key(rel) {
1567            return Ok(export.to_owned());
1568        }
1569        Err(ResolutionError::NpmPackageMissingFile {
1570            name: node.manifest.name.clone(),
1571            export: export.to_owned(),
1572        })
1573    }
1574
1575    fn legacy_require_entry(
1576        &self,
1577        node: &ResolvedNode,
1578        export: &str,
1579    ) -> Result<String, ResolutionError> {
1580        if export == "." {
1581            match self.require_lookup_rel(node, "", 0) {
1582                Ok(found) => return Ok(format!("./{found}")),
1583                Err(RequireLookup::Missing) => {
1584                    return Err(ResolutionError::NpmPackageNoEntryPoint {
1585                        name: node.manifest.name.clone(),
1586                    });
1587                }
1588                Err(RequireLookup::Err(error)) => return Err(error),
1589            }
1590        }
1591        let rel = export.strip_prefix("./").unwrap_or(export);
1592        match self.require_lookup_rel(node, rel, 0) {
1593            Ok(found) => Ok(format!("./{found}")),
1594            Err(RequireLookup::Missing) => Err(ResolutionError::NpmPackageMissingFile {
1595                name: node.manifest.name.clone(),
1596                export: export.to_owned(),
1597            }),
1598            Err(RequireLookup::Err(error)) => Err(error),
1599        }
1600    }
1601
1602    fn require_lookup_rel(
1603        &self,
1604        node: &ResolvedNode,
1605        rel: &str,
1606        depth: usize,
1607    ) -> Result<String, RequireLookup> {
1608        if depth > 5 {
1609            return Err(RequireLookup::Err(ResolutionError::NpmRequireLookupTooDeep));
1610        }
1611        let rel = rel.trim_end_matches('/');
1612        if rel.is_empty() {
1613            if let Some(main) = &node.manifest.main {
1614                let main = main.strip_prefix("./").unwrap_or(main);
1615                if !matches!(main, "" | "." | "/") {
1616                    match self.require_lookup_rel(node, main, depth + 1) {
1617                        Ok(found) => return Ok(found),
1618                        Err(RequireLookup::Missing) => {}
1619                        Err(other) => return Err(other),
1620                    }
1621                }
1622            }
1623            for candidate in ["index.js", "index.json", "index.cjs"] {
1624                if node.files.contains_key(candidate) {
1625                    return Ok(candidate.to_owned());
1626                }
1627            }
1628            if node.files.contains_key("index.node") {
1629                return Err(RequireLookup::Err(
1630                    ResolutionError::NativeAddonUnsupported { rel: "index.node".to_owned() },
1631                ));
1632            }
1633            return Err(RequireLookup::Missing);
1634        }
1635        super::validate_npm_relpath(rel).map_err(|error| RequireLookup::Err(error.into()))?;
1636        if rel.split('/').any(|part| part == "node_modules") {
1637            return Err(RequireLookup::Err(ResolutionError::NpmPathEscapesPackage {
1638                rel: rel.to_owned(),
1639            }));
1640        }
1641        if node.files.contains_key(rel) {
1642            return Ok(rel.to_owned());
1643        }
1644        for candidate in [
1645            format!("{rel}.js"),
1646            format!("{rel}.json"),
1647            format!("{rel}.node"),
1648        ] {
1649            if node.files.contains_key(&candidate) {
1650                return Ok(candidate);
1651            }
1652        }
1653        let prefix = format!("{rel}/");
1654        if !node.files.keys().any(|file| file.starts_with(&prefix)) {
1655            return Err(RequireLookup::Missing);
1656        }
1657        let package_rel = format!("{rel}/package.json");
1658        if node.files.contains_key(&package_rel) {
1659            let project_root = self.store.options().project_root.clone();
1660            let url = super::npm_virtual_module(
1661                &node.identity.registry,
1662                &node.identity.name,
1663                &node.identity.version,
1664                &package_rel,
1665            )
1666            .map_err(|error| RequireLookup::Err(error.into()))?;
1667            super::check_npm_url(self.store.authority.as_ref(), &url)
1668                .map_err(|error| RequireLookup::Err(error.into()))?;
1669            let scope = node
1670                .scope(&project_root, &package_rel)
1671                .map_err(RequireLookup::Err)?;
1672            if let Some(main) = &scope.main {
1673                let main = main.strip_prefix("./").unwrap_or(main);
1674                if !matches!(main, "" | "." | "/") {
1675                    let candidate = format!("{rel}/{main}");
1676                    match self.require_lookup_rel(node, &candidate, depth + 1) {
1677                        Ok(found) => return Ok(found),
1678                        Err(RequireLookup::Missing) => {}
1679                        Err(other) => return Err(other),
1680                    }
1681                }
1682            }
1683        }
1684        for candidate in [
1685            format!("{rel}/index.js"),
1686            format!("{rel}/index.json"),
1687            format!("{rel}/index.cjs"),
1688        ] {
1689            if node.files.contains_key(&candidate) {
1690                return Ok(candidate);
1691            }
1692        }
1693        if node.files.contains_key(&format!("{rel}/index.node")) {
1694            return Err(RequireLookup::Err(
1695                ResolutionError::NativeAddonUnsupported { rel: rel.to_owned() },
1696            ));
1697        }
1698        Err(RequireLookup::Missing)
1699    }
1700
1701    fn resolve_relative_locked(
1702        &self,
1703        node: &ResolvedNode,
1704        referrer: &Path,
1705        specifier: &str,
1706        mode: ResolutionMode,
1707    ) -> Result<ResolvedNpmModule, ResolutionError> {
1708        if specifier.contains('\\')
1709            || specifier.contains('\0')
1710            || specifier.bytes().any(|byte| byte.is_ascii_control())
1711        {
1712            return Err(ResolutionError::InvalidNpmRelativeImport {
1713                specifier: specifier.to_owned(),
1714            });
1715        }
1716        let lower = specifier.to_ascii_lowercase();
1717        if lower.contains("%2f") || lower.contains("%5c") {
1718            return Err(ResolutionError::InvalidNpmRelativeImport {
1719                specifier: specifier.to_owned(),
1720            });
1721        }
1722        let base = url::Url::from_file_path(referrer)
1723            .map_err(|_| ResolutionError::NpmReferrerNotAbsolute)?;
1724        let joined =
1725            base.join(specifier)
1726                .map_err(|_| ResolutionError::InvalidNpmRelativeImport {
1727                    specifier: specifier.to_owned(),
1728                })?;
1729        if joined.scheme() != "file" || !joined.host_str().is_none_or(|host| host.is_empty()) {
1730            return Err(ResolutionError::InvalidNpmRelativeImport {
1731                specifier: specifier.to_owned(),
1732            });
1733        }
1734        let current =
1735            joined
1736                .to_file_path()
1737                .map_err(|_| ResolutionError::InvalidNpmRelativeImport {
1738                    specifier: specifier.to_owned(),
1739                })?;
1740        if mode.is_require() && current == node.dir {
1741            if mode.is_type() {
1742                return self.resolve_in_node_locked(node, ".", mode);
1743            }
1744            return self.resolve_relative_require(node, "", specifier, 0);
1745        }
1746        if current == node.dir {
1747            return Err(ResolutionError::NpmRelativeImportMustNameFile);
1748        }
1749        let rel = current
1750            .strip_prefix(&node.dir)
1751            .map_err(|_| ResolutionError::NpmRelativeImportEscapes)?
1752            .to_str()
1753            .ok_or(ResolutionError::NpmRelativeImportNotUtf8)?
1754            .replace('\\', "/");
1755        match mode {
1756            ResolutionMode::Import | ResolutionMode::TypeImport | ResolutionMode::TypeRequire => {
1757                super::validate_npm_relpath(&rel)?;
1758                if mode.is_type()
1759                    && let Some(found) = declaration_candidates(&rel)
1760                        .iter()
1761                        .find(|candidate| node.files.contains_key(*candidate))
1762                {
1763                    return self.finish_file_locked(node, found, mode);
1764                }
1765                if !node.files.contains_key(&rel) {
1766                    return Err(ResolutionError::NpmRelativeImportNotInManifest {
1767                        specifier: specifier.to_owned(),
1768                    });
1769                }
1770                self.finish_file_locked(node, &rel, mode)
1771            }
1772            ResolutionMode::Require => {
1773                let trimmed = rel.trim_end_matches('/');
1774                self.resolve_relative_require(node, trimmed, specifier, 0)
1775            }
1776        }
1777    }
1778
1779    /// Executable `require` of a package-relative path with Node-style
1780    /// extension and directory inference.
1781    fn resolve_relative_require(
1782        &self,
1783        node: &ResolvedNode,
1784        rel: &str,
1785        specifier: &str,
1786        depth: usize,
1787    ) -> Result<ResolvedNpmModule, ResolutionError> {
1788        if depth > 5 {
1789            return Err(ResolutionError::NpmRequireLookupTooDeepSpecifier {
1790                specifier: specifier.to_owned(),
1791            });
1792        }
1793        // Empty rel is the package root directory (`require('./')`).
1794        let trimmed = rel.trim_end_matches('/');
1795        match self.require_lookup_rel(node, trimmed, depth) {
1796            Ok(found) => self.finish_file_locked(node, &found, ResolutionMode::Require),
1797            Err(RequireLookup::Missing) => Err(ResolutionError::NpmRelativeImportNotInManifest {
1798                specifier: specifier.to_owned(),
1799            }),
1800            Err(RequireLookup::Err(error)) => Err(error),
1801        }
1802    }
1803
1804    fn resolve_hash_locked(
1805        &self,
1806        state: &ResolverState,
1807        node: &ResolvedNode,
1808        specifier: &str,
1809        mode: ResolutionMode,
1810    ) -> Result<ResolvedNpmModule, ResolutionError> {
1811        let imports = node.manifest.imports_raw.clone().ok_or_else(|| {
1812            ResolutionError::NpmPackageMissingImport {
1813                name: node.manifest.name.clone(),
1814                specifier: specifier.to_owned(),
1815            }
1816        })?;
1817        let declared: BTreeSet<String> =
1818            node.deps.keys().chain(node.peers.keys()).cloned().collect();
1819        let conditions = self.conditions(mode);
1820        match resolve_imports_target(&imports, specifier, conditions, &declared)? {
1821            None => Err(ResolutionError::NpmPackageMissingImport {
1822                name: node.manifest.name.clone(),
1823                specifier: specifier.to_owned(),
1824            }),
1825            Some(NpmImportsTarget::File { path }) => {
1826                super::validate_npm_relpath(&path)?;
1827                if mode.is_type() {
1828                    self.resolve_types_target_locked(node, &format!("./{path}"), specifier, mode)
1829                } else if !node.files.contains_key(&path) {
1830                    Err(ResolutionError::NpmImportTargetNotInManifest {
1831                        specifier: specifier.to_owned(),
1832                    })
1833                } else {
1834                    self.finish_file_locked(node, &path, mode)
1835                }
1836            }
1837            Some(NpmImportsTarget::External { request }) => {
1838                let (name, sub) = split_bare(request.trim_start_matches("npm:"));
1839                let provider = node
1840                    .deps
1841                    .get(name)
1842                    .or_else(|| node.peers.get(name))
1843                    .cloned()
1844                    .ok_or_else(|| ResolutionError::NpmImportsTargetUndeclared {
1845                        target: request.to_owned(),
1846                    })?;
1847                let provider = self.node_locked(state, &provider)?;
1848                let export = match sub {
1849                    Some(path) => subpath_to_export(path)?,
1850                    None => ".".into(),
1851                };
1852                self.resolve_in_node_locked(&provider, &export, mode)
1853            }
1854        }
1855    }
1856
1857    fn resolve_target_locked(
1858        &self,
1859        node: &ResolvedNode,
1860        target: &str,
1861        mode: ResolutionMode,
1862    ) -> Result<ResolvedNpmModule, ResolutionError> {
1863        let rel = target
1864            .strip_prefix("./")
1865            .ok_or_else(|| ResolutionError::NpmExportTargetEscapes { target: target.to_owned() })?;
1866        super::validate_npm_relpath(rel)?;
1867        if rel.split('/').any(|part| part == "node_modules") {
1868            return Err(ResolutionError::NpmExportTargetEscapes { target: target.to_owned() });
1869        }
1870        if !node.files.contains_key(rel) {
1871            return Err(ResolutionError::NpmExportTargetNotInManifest {
1872                target: target.to_owned(),
1873            });
1874        }
1875        // Export targets stay exact in both modes: no extension inference.
1876        self.finish_file_locked(node, rel, mode)
1877    }
1878
1879    fn browser_map<'a>(
1880        &self,
1881        node: &'a ResolvedNode,
1882    ) -> Option<&'a serde_json::Map<String, serde_json::Value>> {
1883        node.manifest
1884            .browser
1885            .as_ref()?
1886            .as_object()
1887            .filter(|_| self.browser)
1888    }
1889
1890    fn finish_file_locked(
1891        &self,
1892        node: &ResolvedNode,
1893        rel: &str,
1894        mode: ResolutionMode,
1895    ) -> Result<ResolvedNpmModule, ResolutionError> {
1896        // Exact `browser` file keys win over extensionless ones.
1897        let base = [".js", ".json", ".mjs", ".cjs"]
1898            .iter()
1899            .find_map(|extension| rel.strip_suffix(extension));
1900        let remap = self.browser_map(node).and_then(|map| {
1901            [Some(rel), base]
1902                .into_iter()
1903                .flatten()
1904                .find_map(|key| map.get(&format!("./{key}")).or_else(|| map.get(key)))
1905        });
1906        let rel = match remap {
1907            Some(serde_json::Value::Bool(false)) => return Ok(excluded(node, rel)),
1908            Some(serde_json::Value::String(target)) => {
1909                let target = target.trim_start_matches("./");
1910                super::validate_npm_relpath(target)?;
1911                if !node.files.contains_key(target) {
1912                    return Err(ResolutionError::NpmBrowserRemapTargetNotInManifest {
1913                        target: target.to_owned(),
1914                    });
1915                }
1916                target
1917            }
1918            _ => rel,
1919        };
1920        let url = npm_virtual_module(
1921            &node.identity.registry,
1922            &node.identity.name,
1923            &node.identity.version,
1924            rel,
1925        )?;
1926        super::check_npm_url(self.store.authority.as_ref(), &url)?;
1927        let kind = node.classify(&self.store.options().project_root, rel)?;
1928        if !mode.is_type() && kind == NpmModuleKind::Declaration {
1929            return Err(ResolutionError::NpmDeclarationNoCode { rel: rel.to_owned() });
1930        }
1931        // ESM `import` of JSON stays unsupported; `require` allows JSON and
1932        // CJS, while ESM `import` of CJS resolves for synthetic wrappers.
1933        if !mode.is_require() && kind == NpmModuleKind::Json && !self.browser {
1934            return Err(ResolutionError::JsonModuleUnsupported { rel: rel.to_owned() });
1935        }
1936        Ok(ResolvedNpmModule {
1937            path: node.dir.join(rel),
1938            format: kind,
1939            instance: node.instance.clone(),
1940        })
1941    }
1942
1943    async fn build_roots(
1944        &self,
1945        walk: &mut Walk<'_>,
1946        handle: &mut PreparedNpmRoots,
1947        normalized: &[(String, BTreeMap<String, String>)],
1948        preselected: &BTreeMap<String, String>,
1949    ) -> Result<(), ResolutionError> {
1950        for (request_key, context) in normalized {
1951            let effective = effective_context(preselected, context);
1952            let key = root_lock_key(request_key, &effective.merged());
1953            if let Some(instance) = self.state.lock().unwrap().roots.get(&key) {
1954                handle.roots.insert(key, instance.clone());
1955                continue;
1956            }
1957            let idx = self
1958                .build_occurrence(walk, request_key, &effective, false)
1959                .await?
1960                .ok_or_else(|| ResolutionError::NpmPackageNotSupportedHere {
1961                    request_key: request_key.to_owned(),
1962                })?;
1963            walk.build.roots.insert(key, idx);
1964        }
1965        Ok(())
1966    }
1967
1968    /// Build one occurrence: memoize, acquire (cached per prepare),
1969    /// then descend. Ancestor occurrences with a satisfying version are
1970    /// reused so shadowing binds the exact provider instance instead of
1971    /// re-instantiating it. Returns `None` for skippable (optional +
1972    /// platform-mismatched) packages only.
1973    async fn build_occurrence(
1974        &self,
1975        walk: &mut Walk<'_>,
1976        request_key: &str,
1977        ctx: &ScopeCtx,
1978        skippable: bool,
1979    ) -> Result<Option<usize>, ResolutionError> {
1980        let key = (request_key.to_owned(), scopeprint(ctx));
1981        if let Some(idx) = walk.build.index.get(&key) {
1982            return Ok(Some(*idx));
1983        }
1984        if walk.stack.len() >= MAX_NPM_GRAPH_DEPTH {
1985            return Err(ResolutionError::NpmDependencyGraphTooDeep);
1986        }
1987        if walk.build.occs.len() >= MAX_NPM_GRAPH_NODES {
1988            return Err(ResolutionError::NpmDependencyGraphTooLarge);
1989        }
1990        let artifact = self.acquire_cached(walk.acq, request_key).await?;
1991        let manifest = parse_npm_manifest(
1992            &artifact.identity.name,
1993            &artifact.identity.version,
1994            &artifact.package,
1995        )?;
1996        if !platform_satisfied(manifest.os.as_ref(), super::package::npm_host_os())
1997            || !platform_satisfied(manifest.cpu.as_ref(), super::package::npm_host_arch())
1998        {
1999            if skippable {
2000                return Ok(None);
2001            }
2002            return Err(ResolutionError::NpmPackagePlatformUnsupported {
2003                name: manifest.name.clone(),
2004                version: manifest.version.clone(),
2005            });
2006        }
2007        let idx = walk.build.occs.len();
2008        walk.build.occs.push(Occurrence {
2009            identity: artifact.identity.clone(),
2010            artifact,
2011            manifest: manifest.clone(),
2012            dep_edges: BTreeMap::new(),
2013            peer_edges: BTreeMap::new(),
2014        });
2015        walk.build.index.insert(key, idx);
2016        walk.stack.push(idx);
2017        let mut bindings: BTreeMap<String, (String, String)> = BTreeMap::new();
2018        let mut missing_peers: Vec<(String, String)> = Vec::new();
2019        for (peer, spec) in manifest.peers.clone() {
2020            if let Some(provider) = ctx.strict.get(&peer) {
2021                if !peer_version_satisfies(&spec.range, provider)? {
2022                    if spec.optional {
2023                        continue;
2024                    }
2025                    return Err(ResolutionError::NpmPeerWantsMismatch {
2026                        peer: peer.to_owned(),
2027                        wanted: spec.range.clone(),
2028                        provided: peer_identity_version(provider)?,
2029                    });
2030                }
2031                bindings.insert(
2032                    peer.clone(),
2033                    (provider.clone(), peer_exact_request(provider)?),
2034                );
2035                continue;
2036            }
2037            if let Some(provider) = ctx.flexible.get(&peer)
2038                && peer_version_satisfies(&spec.range, provider)?
2039            {
2040                bindings.insert(
2041                    peer.clone(),
2042                    (provider.clone(), peer_exact_request(provider)?),
2043                );
2044                continue;
2045            }
2046            if spec.optional {
2047                continue;
2048            }
2049            let request = format!("npm:{}@{}", peer, spec.range);
2050            super::parse_npm_request(&request)?;
2051            missing_peers.push((peer.clone(), request));
2052        }
2053        let missing: Vec<String> = missing_peers
2054            .iter()
2055            .map(|(_, request)| request.clone())
2056            .collect();
2057        self.acquire_many(walk.acq, &missing).await?;
2058        for (peer, request) in &missing_peers {
2059            let provider = walk
2060                .acq
2061                .get(request)
2062                .ok_or_else(|| ResolutionError::NpmPackageNotPreparedRequest {
2063                    request: request.to_owned(),
2064                })?
2065                .identity
2066                .key();
2067            bindings.insert(peer.clone(), (provider, request.clone()));
2068        }
2069        let mut dep_requests: BTreeMap<String, (String, String)> = BTreeMap::new();
2070        let mut missing_deps: Vec<(String, String)> = Vec::new();
2071        for (alias, dep) in manifest.dependencies.clone() {
2072            let target = match &dep.kind {
2073                super::package::NpmDepKind::Registry { request } => request.clone(),
2074                super::package::NpmDepKind::Alias { target } => target.clone(),
2075                super::package::NpmDepKind::Unsupported { protocol } => {
2076                    return Err(ResolutionError::NpmDependencyUnsupportedProtocol {
2077                        alias: alias.to_owned(),
2078                        protocol: protocol.to_owned(),
2079                    });
2080                }
2081            };
2082            super::parse_npm_request(&target)?;
2083            missing_deps.push((alias, target));
2084        }
2085        let missing: Vec<String> = missing_deps
2086            .iter()
2087            .map(|(_, target)| target.clone())
2088            .collect();
2089        self.acquire_many(walk.acq, &missing).await?;
2090        for (alias, target) in &missing_deps {
2091            let provider = walk
2092                .acq
2093                .get(target)
2094                .ok_or_else(|| ResolutionError::NpmPackageNotPreparedTarget {
2095                    target: target.to_owned(),
2096                })?
2097                .identity
2098                .key();
2099            dep_requests.insert(alias.clone(), (provider, target.clone()));
2100        }
2101        let mut child = ScopeCtx {
2102            strict: ctx.strict.clone(),
2103            flexible: ctx.flexible.clone(),
2104        };
2105        for (provider, _) in dep_requests.values() {
2106            let (_, name, _) = super::parse_npm_key(provider)?;
2107            child.flexible.insert(name, provider.clone());
2108        }
2109        for (peer, (provider, _)) in &bindings {
2110            child.flexible.insert(peer.clone(), provider.clone());
2111        }
2112        // Peers resolve from the importer's context, not its private
2113        // dependencies: otherwise a sibling dep (x2) would rebind an
2114        // already-selected provider's peer (x1) into a fresh instance
2115        // instead of sharing the established one.
2116        let mut peer_child = ScopeCtx {
2117            strict: ctx.strict.clone(),
2118            flexible: ctx.flexible.clone(),
2119        };
2120        for (peer, (provider, _)) in &bindings {
2121            peer_child.flexible.insert(peer.clone(), provider.clone());
2122        }
2123        let mut dep_edges: BTreeMap<String, usize> = BTreeMap::new();
2124        for (alias, (provider, request)) in &dep_requests {
2125            let optional = manifest
2126                .dependencies
2127                .get(alias)
2128                .is_some_and(|dep| dep.optional);
2129            let (_, name, _) = super::parse_npm_key(provider)?;
2130            match Box::pin(self.child_occurrence(walk, &name, request, &child, optional)).await? {
2131                Some(target) => {
2132                    dep_edges.insert(alias.clone(), target);
2133                }
2134                None if optional => {}
2135                None => {
2136                    return Err(ResolutionError::NpmDependencyUnresolvable {
2137                        alias: alias.to_owned(),
2138                    });
2139                }
2140            }
2141        }
2142        let mut peer_edges: BTreeMap<String, usize> = BTreeMap::new();
2143        for (peer, (_, request)) in &bindings {
2144            let optional = manifest.peers.get(peer).is_some_and(|entry| entry.optional);
2145            match Box::pin(self.child_occurrence(walk, peer, request, &peer_child, optional))
2146                .await?
2147            {
2148                Some(target) => {
2149                    peer_edges.insert(peer.clone(), target);
2150                }
2151                None if optional => {}
2152                None => {
2153                    return Err(ResolutionError::NpmPeerUnresolvable { peer: peer.to_owned() });
2154                }
2155            }
2156        }
2157        walk.build.occs[idx].dep_edges = dep_edges;
2158        walk.build.occs[idx].peer_edges = peer_edges;
2159        walk.stack.pop();
2160        Ok(Some(idx))
2161    }
2162
2163    /// Concurrent downloads across package names; same-name requests stay
2164    /// sequential so later ranges reuse earlier pins, and the first input
2165    /// failure wins once started groups finish.
2166    async fn acquire_many(
2167        &self,
2168        acq: &mut BTreeMap<String, NpmArtifact>,
2169        keys: &[String],
2170    ) -> Result<(), ResolutionError> {
2171        let mut ordered: Vec<String> = Vec::new();
2172        let mut seen: BTreeSet<String> = BTreeSet::new();
2173        for key in keys {
2174            if acq.contains_key(key) || !seen.insert(key.clone()) {
2175                continue;
2176            }
2177            ordered.push(key.clone());
2178        }
2179        let mut names: Vec<String> = Vec::new();
2180        let mut groups: BTreeMap<String, Vec<(usize, String)>> = BTreeMap::new();
2181        for (index, key) in ordered.iter().enumerate() {
2182            let name = super::parse_npm_request(key)?.name;
2183            if !groups.contains_key(&name) {
2184                names.push(name.clone());
2185            }
2186            groups.entry(name).or_default().push((index, key.clone()));
2187        }
2188        let outcomes = stream::iter(names.into_iter().map(|name| {
2189            let entries = groups.remove(&name).expect("grouped package name present");
2190            let store = self.store.clone();
2191            async move {
2192                let mut done: Vec<(String, NpmArtifact)> = Vec::new();
2193                for (index, key) in entries {
2194                    match store.npm_acquire_for_resolution(&key).await {
2195                        Ok(artifact) => done.push((key, artifact)),
2196                        Err(error) => return (done, Some((index, error))),
2197                    }
2198                }
2199                (done, None)
2200            }
2201        }))
2202        .buffer_unordered(MAX_NPM_CONCURRENT_DOWNLOADS)
2203        .collect::<Vec<_>>()
2204        .await;
2205        let mut failure: Option<(usize, ResolutionError)> = None;
2206        for (done, failed) in outcomes {
2207            for (key, artifact) in done {
2208                acq.insert(key, artifact);
2209            }
2210            if let Some((index, error)) = failed
2211                && failure.as_ref().is_none_or(|current| index < current.0)
2212            {
2213                failure = Some((index, error.into()));
2214            }
2215        }
2216        if let Some((_, error)) = failure {
2217            return Err(error);
2218        }
2219        Ok(())
2220    }
2221
2222    /// One acquisition per request per prepare; hits skip re-verification.
2223    /// Artifacts stay local to the build: nothing here bypasses the
2224    /// store's verification on first fetch.
2225    async fn acquire_cached(
2226        &self,
2227        acq: &mut BTreeMap<String, NpmArtifact>,
2228        request_key: &str,
2229    ) -> Result<NpmArtifact, ResolutionError> {
2230        if let Some(artifact) = acq.get(request_key) {
2231            return Ok(artifact.clone());
2232        }
2233        let artifact = self.store.npm_acquire_for_resolution(request_key).await?;
2234        acq.insert(request_key.to_owned(), artifact.clone());
2235        Ok(artifact)
2236    }
2237
2238    /// Reuse matching ancestor occurrences to close package cycles.
2239    async fn child_occurrence(
2240        &self,
2241        walk: &mut Walk<'_>,
2242        name: &str,
2243        request: &str,
2244        ctx: &ScopeCtx,
2245        skippable: bool,
2246    ) -> Result<Option<usize>, ResolutionError> {
2247        let parsed = super::parse_npm_request(request)?;
2248        for ancestor in walk.stack.iter().rev() {
2249            let occ = &walk.build.occs[*ancestor];
2250            if occ.identity.name != name {
2251                continue;
2252            }
2253            if request_matches_version(&parsed, &occ.identity.version)? {
2254                return Ok(Some(*ancestor));
2255            }
2256        }
2257        self.build_occurrence(walk, request, ctx, skippable).await
2258    }
2259
2260    fn canonicalize(build: &OccGraph) -> BTreeMap<usize, String> {
2261        let identities: BTreeSet<_> = build.occs.iter().map(|occ| occ.identity.key()).collect();
2262        let initial: BTreeMap<_, _> = identities
2263            .into_iter()
2264            .enumerate()
2265            .map(|(i, key)| (key, i))
2266            .collect();
2267        let mut labels: Vec<usize> = build
2268            .occs
2269            .iter()
2270            .map(|occ| initial[&occ.identity.key()])
2271            .collect();
2272        loop {
2273            let signatures: Vec<_> = build
2274                .occs
2275                .iter()
2276                .enumerate()
2277                .map(|(i, occ)| {
2278                    let edges = |edges: &BTreeMap<String, usize>| {
2279                        edges
2280                            .iter()
2281                            .map(|(name, target)| (name.clone(), labels[*target]))
2282                            .collect::<Vec<_>>()
2283                    };
2284                    (labels[i], edges(&occ.dep_edges), edges(&occ.peer_edges))
2285                })
2286                .collect();
2287            let classes: BTreeMap<_, _> = signatures
2288                .iter()
2289                .cloned()
2290                .collect::<BTreeSet<_>>()
2291                .into_iter()
2292                .enumerate()
2293                .map(|(i, signature)| (signature, i))
2294                .collect();
2295            let next: Vec<_> = signatures
2296                .iter()
2297                .map(|signature| classes[signature])
2298                .collect();
2299            let stable = classes.len() == labels.iter().copied().collect::<BTreeSet<_>>().len();
2300            labels = next;
2301            if stable {
2302                break;
2303            }
2304        }
2305        let representatives: BTreeMap<_, _> = labels
2306            .iter()
2307            .copied()
2308            .enumerate()
2309            .map(|(i, label)| (label, i))
2310            .collect();
2311        let ids: BTreeMap<_, _> = representatives
2312            .iter()
2313            .map(|(label, representative)| {
2314                let root = &build.occs[*representative];
2315                if root.dep_edges.is_empty() && root.peer_edges.is_empty() {
2316                    return (*label, root.identity.key());
2317                }
2318                // Number only the reachable quotient graph, so unrelated roots and
2319                // traversal order cannot change an instance's identity, even in cycles.
2320                let mut numbers = BTreeMap::from([(*label, 0usize)]);
2321                let mut pending = vec![*label];
2322                let mut graph = Vec::new();
2323                let mut cursor = 0;
2324                while cursor < pending.len() {
2325                    let occ = &build.occs[representatives[&pending[cursor]]];
2326                    let mut edges = |edges: &BTreeMap<String, usize>| {
2327                        edges
2328                            .iter()
2329                            .map(|(name, target)| {
2330                                let target = labels[*target];
2331                                let next = numbers.len();
2332                                let number = *numbers.entry(target).or_insert_with(|| {
2333                                    pending.push(target);
2334                                    next
2335                                });
2336                                (name.clone(), number)
2337                            })
2338                            .collect::<BTreeMap<_, _>>()
2339                    };
2340                    let deps = edges(&occ.dep_edges);
2341                    let peers = edges(&occ.peer_edges);
2342                    graph.push((occ.identity.key(), deps, peers));
2343                    cursor += 1;
2344                }
2345                let encoded = serde_json::to_string(&graph).unwrap();
2346                (
2347                    *label,
2348                    format!("p-{}", &super::artifact_key(&encoded)[..62]),
2349                )
2350            })
2351            .collect();
2352        labels
2353            .into_iter()
2354            .enumerate()
2355            .map(|(i, label)| (i, ids[&label].clone()))
2356            .collect()
2357    }
2358
2359    async fn finalize_build(&self, build: OccGraph) -> Result<FinalizedGraph, ResolutionError> {
2360        let ids = Self::canonicalize(&build);
2361        let mut order: BTreeMap<String, usize> = BTreeMap::new();
2362        for (idx, _) in build.occs.iter().enumerate() {
2363            order.entry(ids[&idx].clone()).or_insert(idx);
2364        }
2365        let mut nodes = BTreeMap::new();
2366        for (id, rep) in &order {
2367            let occ = &build.occs[*rep];
2368            let deps = occ
2369                .dep_edges
2370                .iter()
2371                .map(|(alias, target)| (alias.clone(), ids[target].clone()))
2372                .collect();
2373            let peers = occ
2374                .peer_edges
2375                .iter()
2376                .map(|(peer, target)| (peer.clone(), ids[target].clone()))
2377                .collect();
2378            let mut artifact = occ.artifact.clone();
2379            let package_key = artifact.identity.key();
2380            let disk_instance = storage::npm_disk_instance(id, &package_key);
2381            artifact.dir =
2382                storage::ensure_npm_instance(self.store.options(), &artifact, disk_instance)?;
2383            let node = Arc::new(ResolvedNode {
2384                instance: id.clone(),
2385                identity: artifact.identity,
2386                dir: artifact.dir,
2387                files: artifact.files,
2388                manifest: occ.manifest.clone(),
2389                scopes: Mutex::new(BTreeMap::new()),
2390                deps,
2391                peers,
2392            });
2393            nodes.insert(
2394                id.clone(),
2395                Arc::new(PreparedInstance {
2396                    pin: NpmInstancePin {
2397                        package: package_key,
2398                        deps: node.deps.clone(),
2399                        peers: node.peers.clone(),
2400                    },
2401                    dir: node.dir.clone(),
2402                    loaded: Mutex::new(Some(node)),
2403                }),
2404            );
2405        }
2406        let roots = build
2407            .roots
2408            .iter()
2409            .map(|(key, idx)| (key.clone(), ids[idx].clone()))
2410            .collect();
2411        Ok(FinalizedGraph { roots, nodes })
2412    }
2413
2414    pub fn reserved_npm_path(project_root: &Path, path: &Path) -> bool {
2415        path.strip_prefix(project_root.join(".avril").join("npm"))
2416            .is_ok()
2417    }
2418
2419    /// Staged bindings must still match the lock at publication. New resolutions
2420    /// cannot overwrite established instances; frozen never fills missing entries.
2421    fn record_graph_locked(&self, finalized: &FinalizedGraph) -> Result<(), ResolutionError> {
2422        let frozen = self.store.options().frozen;
2423        let pins: BTreeMap<&String, NpmInstancePin> = finalized
2424            .nodes
2425            .iter()
2426            .map(|(id, node)| (id, node.pin.clone()))
2427            .collect();
2428        let mut lock = self.store.lock.lock().unwrap();
2429        for (request, instance) in &finalized.roots {
2430            match lock.npm.roots.get(request) {
2431                Some(existing) if existing == instance => {}
2432                Some(_) => {
2433                    return Err(ResolutionError::LockfileChanged { key: request.to_owned() });
2434                }
2435                None if frozen => {
2436                    return Err(ResolutionError::FrozenLockfileMissing {
2437                        name: request.to_owned(),
2438                    });
2439                }
2440                None => {}
2441            }
2442        }
2443        for (id, pin) in &pins {
2444            match lock.npm.instances.get(*id) {
2445                Some(existing) if existing == pin => {}
2446                Some(_) => {
2447                    return Err(ResolutionError::LockedNpmInstanceChanged { id: id.to_string() });
2448                }
2449                None if frozen => {
2450                    return Err(ResolutionError::NpmFrozenLockfileMissingInstance {
2451                        id: id.to_string(),
2452                    });
2453                }
2454                None => {}
2455            }
2456        }
2457        let mut used = self.store.used.lock().unwrap();
2458        let packages: BTreeSet<_> = pins.values().map(|pin| pin.package.clone()).collect();
2459        for (request, version) in &lock.npm.specifiers {
2460            let name = parse_npm_request(request)?.name;
2461            let package = format!("{}|{name}|{version}", self.store.options().npm_registry);
2462            if packages.contains(&package)
2463                || finalized
2464                    .roots
2465                    .keys()
2466                    .any(|key| root_request_part(key) == request)
2467            {
2468                used.npm_specifiers.insert(request.clone());
2469            }
2470        }
2471        used.npm_packages.extend(packages);
2472        for (request, instance) in &finalized.roots {
2473            used.npm_roots.insert(request.clone());
2474            if lock.npm.roots.get(request) != Some(instance) {
2475                used.pending_npm_roots.insert(request.clone());
2476            }
2477            lock.npm.roots.insert(request.clone(), instance.clone());
2478        }
2479        for (id, pin) in &pins {
2480            used.npm_instances.insert((*id).clone());
2481            if lock.npm.instances.get(*id) != Some(pin) {
2482                used.pending_npm_instances.insert((*id).clone());
2483            }
2484            lock.npm.instances.insert((*id).clone(), pin.clone());
2485        }
2486        Ok(())
2487    }
2488}
2489
2490fn excluded(node: &ResolvedNode, key: &str) -> ResolvedNpmModule {
2491    ResolvedNpmModule {
2492        path: node.dir.join(format!("{key}{BROWSER_EXCLUDED}")),
2493        format: NpmModuleKind::CommonJs,
2494        instance: node.instance.clone(),
2495    }
2496}
2497
2498fn peer_identity_version(identity: &str) -> Result<String, ResolutionError> {
2499    let parts: Vec<&str> = identity.split('|').collect();
2500    if parts.len() != 3 {
2501        return Err(ResolutionError::InvalidNpmLockIdentity);
2502    }
2503    Ok(parts[2].to_owned())
2504}
2505
2506fn peer_version_satisfies(range: &str, identity: &str) -> Result<bool, ResolutionError> {
2507    request_matches_text(range, &peer_identity_version(identity)?)
2508}
2509
2510fn request_matches_version(
2511    request: &super::NpmRequest,
2512    version: &str,
2513) -> Result<bool, ResolutionError> {
2514    if request.version_req.tag().is_some() || request.version_req.version_text() == "*" {
2515        return Ok(true);
2516    }
2517    let parsed = super::validate_npm_version(version)?;
2518    Ok(request.version_req.matches(&parsed))
2519}
2520
2521fn request_matches_text(range: &str, version: &str) -> Result<bool, ResolutionError> {
2522    if range.is_empty() || range == "*" {
2523        return Ok(true);
2524    }
2525    let requirement = deno_semver::VersionReq::parse_from_npm(range).map_err(|detail| {
2526        ResolutionError::InvalidNpmVersionRange {
2527            range: range.to_owned(),
2528            detail: detail.to_string(),
2529        }
2530    })?;
2531    if requirement.tag().is_some() {
2532        return Ok(true);
2533    }
2534    let parsed = super::validate_npm_version(version)?;
2535    Ok(requirement.matches(&parsed))
2536}
2537
2538fn peer_exact_request(identity: &str) -> Result<String, ResolutionError> {
2539    let parts: Vec<&str> = identity.split('|').collect();
2540    if parts.len() != 3 {
2541        return Err(ResolutionError::InvalidNpmLockIdentity);
2542    }
2543    Ok(format!("npm:{}@{}", parts[1], parts[2]))
2544}
2545
2546fn ctxprint(ctx: &BTreeMap<String, String>) -> String {
2547    ctx.iter()
2548        .map(|(name, identity)| format!("{name}={identity}"))
2549        .collect::<Vec<_>>()
2550        .join(",")
2551}
2552
2553/// Traversal environments. `strict` holds caller-preselected choices whose
2554/// violation is an explicit error; `flexible` holds inherited selections
2555/// that a contradictory local range silently shadows with a nested copy.
2556#[derive(Clone, Debug, Default)]
2557struct ScopeCtx {
2558    strict: BTreeMap<String, String>,
2559    flexible: BTreeMap<String, String>,
2560}
2561
2562fn scopeprint(ctx: &ScopeCtx) -> String {
2563    format!("{}|{}", ctxprint(&ctx.strict), ctxprint(&ctx.flexible))
2564}
2565
2566fn root_lock_key(request_key: &str, context: &BTreeMap<String, String>) -> String {
2567    if context.is_empty() {
2568        return request_key.to_owned();
2569    }
2570    format!("{request_key}\0ctx:{}", ctxprint(context))
2571}
2572
2573fn root_request_part(key: &str) -> &str {
2574    key.split('\0').next().unwrap_or(key)
2575}
2576
2577fn effective_context(
2578    preselected: &BTreeMap<String, String>,
2579    caller: &BTreeMap<String, String>,
2580) -> ScopeCtx {
2581    ScopeCtx {
2582        strict: caller.clone(),
2583        flexible: preselected.clone(),
2584    }
2585}
2586
2587impl ScopeCtx {
2588    fn merged(&self) -> BTreeMap<String, String> {
2589        let mut merged = self.flexible.clone();
2590        for (peer, provider) in &self.strict {
2591            merged.insert(peer.clone(), provider.clone());
2592        }
2593        merged
2594    }
2595}
2596/// Whether an export subpath falls beneath a warm scope: the scope
2597/// itself, anything beneath it, or everything for a whole-package scope.
2598fn under_export_scope(sub: &str, scope: &str) -> bool {
2599    scope.is_empty() || sub == scope || sub.starts_with(&format!("{scope}/"))
2600}
2601
2602/// Every wildcard target pattern a pattern value can select, recursing
2603/// through fallback arrays and condition objects. Literal trailers cannot
2604/// capture and contribute nothing to inversion.
2605fn pattern_targets(value: &serde_json::Value, out: &mut Vec<String>) {
2606    match value {
2607        serde_json::Value::String(target) => {
2608            if target.contains('*') {
2609                out.push(target.clone());
2610            }
2611        }
2612        serde_json::Value::Array(targets) => {
2613            for target in targets {
2614                pattern_targets(target, out);
2615            }
2616        }
2617        serde_json::Value::Object(map) => {
2618            for target in map.values() {
2619                pattern_targets(target, out);
2620            }
2621        }
2622        _ => {}
2623    }
2624}
2625
2626fn split_npm_request(specifier: &str) -> Result<(String, Option<String>), ResolutionError> {
2627    let request = super::parse_npm_request(specifier)?;
2628    Ok((request.request_key(), request.subpath))
2629}
2630
2631fn typescript_range_matches(text: &str, version: &deno_semver::Version) -> bool {
2632    use deno_semver::{RangeBound, Version, VersionBoundKind, VersionReq};
2633
2634    let matches = |term: &str| {
2635        let Ok(requirement) = VersionReq::parse_from_npm(term) else {
2636            return false;
2637        };
2638        let shorthand = term.starts_with(['~', '^']) || term.contains(" - ");
2639        let operand = term.trim_start_matches(['>', '<', '=']);
2640        let partial = !shorthand && Version::parse_from_npm(operand).is_err();
2641        if partial && operand.starts_with(['v', 'V']) {
2642            return false;
2643        }
2644        requirement.range().is_some_and(|ranges| {
2645            ranges.0.iter().any(|range| {
2646                let mut range = range.clone();
2647                for (bound, lower) in [(&mut range.start, true), (&mut range.end, false)] {
2648                    if let RangeBound::Version(bound) = bound {
2649                        if partial {
2650                            bound.version.pre = ["0".into()].into();
2651                        } else if shorthand && !lower && bound.kind == VersionBoundKind::Exclusive {
2652                            // TS shorthand upper bounds include the next release's prereleases.
2653                            bound.version.pre = Default::default();
2654                        }
2655                    }
2656                }
2657                // Compiler ranges include prereleases; npm package selection deliberately does not.
2658                range.intersects_version(version)
2659            })
2660        })
2661    };
2662    text.split("||").any(|alternative| {
2663        if alternative.contains(" - ") {
2664            return matches(alternative.trim());
2665        }
2666        let mut terms = alternative.split_whitespace();
2667        while let Some(term) = terms.next() {
2668            let matched = if matches!(term, ">" | ">=" | "<" | "<=" | "=" | "~" | "^") {
2669                terms
2670                    .next()
2671                    .is_some_and(|operand| matches(&format!("{term}{operand}")))
2672            } else {
2673                matches(term)
2674            };
2675            if !matched {
2676                return false;
2677            }
2678        }
2679        true
2680    })
2681}
2682
2683fn valid_types_versions_pattern(pattern: &str) -> bool {
2684    pattern.matches('*').count() <= 1 && valid_types_versions_target(&pattern.replace('*', "x"))
2685}
2686
2687fn valid_types_versions_target(target: &str) -> bool {
2688    let path = target.strip_prefix("./").unwrap_or(target);
2689    super::validate_npm_relpath(path).is_ok()
2690        && !path
2691            .split('/')
2692            .any(|part| part.eq_ignore_ascii_case("node_modules"))
2693}
2694
2695fn match_types_versions_pattern<'m, 'c>(
2696    mappings: &'m serde_json::Map<String, serde_json::Value>,
2697    candidate: &'c str,
2698) -> Option<(&'m str, &'c str)> {
2699    if let Some((pattern, _)) = mappings.get_key_value(candidate) {
2700        return Some((pattern, ""));
2701    }
2702    // Equal-prefix patterns retain manifest order, unlike export patterns.
2703    mappings
2704        .keys()
2705        .enumerate()
2706        .filter_map(|(order, pattern)| {
2707            let (prefix, suffix) = pattern.split_once('*')?;
2708            let matched = candidate.strip_prefix(prefix)?.strip_suffix(suffix)?;
2709            Some((pattern.as_str(), matched, prefix.len(), order))
2710        })
2711        .max_by_key(|(_, _, prefix, order)| (*prefix, std::cmp::Reverse(*order)))
2712        .map(|(pattern, matched, _, _)| (pattern, matched))
2713}
2714
2715fn declaration_candidates(rel: &str) -> Vec<String> {
2716    if rel.ends_with(".d.ts") || rel.ends_with(".d.mts") || rel.ends_with(".d.cts") {
2717        return vec![rel.to_owned()];
2718    }
2719    let substitutions: &[(&str, &[&str])] = &[
2720        (".js", &[".ts", ".tsx", ".d.ts"]),
2721        (".mjs", &[".mts", ".d.mts"]),
2722        (".cjs", &[".cts", ".d.cts"]),
2723        (".tsx", &[".tsx", ".ts", ".d.ts"]),
2724        (".ts", &[".ts", ".tsx", ".d.ts"]),
2725        (".mts", &[".mts", ".d.mts"]),
2726        (".cts", &[".cts", ".d.cts"]),
2727    ];
2728    let (stem, suffixes) = substitutions
2729        .iter()
2730        .find_map(|(extension, suffixes)| rel.strip_suffix(extension).map(|stem| (stem, *suffixes)))
2731        .unwrap_or((rel, &[".ts", ".tsx", ".d.ts", "/index.ts", "/index.d.ts"]));
2732    suffixes
2733        .iter()
2734        .map(|suffix| format!("{stem}{suffix}"))
2735        .collect()
2736}
2737
2738fn subpath_to_export(subpath: &str) -> Result<String, ResolutionError> {
2739    if let Some(path) = subpath.strip_prefix("./") {
2740        if path.is_empty() {
2741            return Err(ResolutionError::InvalidNpmExportSubpath { subpath: subpath.to_owned() });
2742        }
2743        return Ok(subpath.to_owned());
2744    }
2745    if subpath == "." || subpath.is_empty() {
2746        return Ok(".".into());
2747    }
2748    Ok(format!("./{subpath}"))
2749}
2750
2751impl ResolvedNode {
2752    fn classify(&self, project_root: &Path, rel: &str) -> Result<NpmModuleKind, ResolutionError> {
2753        classify_npm_module(rel, || {
2754            // The module hint belongs to this file, regardless of how it was reached.
2755            if let Some(entry) = &self.manifest.module
2756                && entry.strip_prefix("./").unwrap_or(entry) == rel
2757            {
2758                return Ok(NpmPackageType::Module);
2759            }
2760            let mut directory = rel.rsplit_once('/').map_or("", |(parent, _)| parent);
2761            while !directory.is_empty() {
2762                let metadata = format!("{directory}/package.json");
2763                if self.files.contains_key(&metadata)
2764                    && let Some(package_type) = self.scope(project_root, &metadata)?.package_type
2765                {
2766                    return Ok(package_type);
2767                }
2768                directory = directory.rsplit_once('/').map_or("", |(parent, _)| parent);
2769            }
2770            Ok(self.manifest.package_type)
2771        })
2772    }
2773
2774    fn scope(
2775        &self,
2776        project_root: &Path,
2777        rel: &str,
2778    ) -> Result<Arc<NpmPackageScope>, ResolutionError> {
2779        let mut scopes = self.scopes.lock().unwrap();
2780        if let Some(scope) = scopes.get(rel) {
2781            return Ok(scope.clone());
2782        }
2783        let bytes = self.read_verified(project_root, rel)?;
2784        let scope = Arc::new(parse_npm_scope(&bytes, rel)?);
2785        scopes.insert(rel.to_owned(), scope.clone());
2786        Ok(scope)
2787    }
2788
2789    fn read_verified(&self, project_root: &Path, rel: &str) -> Result<Vec<u8>, ResolutionError> {
2790        let expected = self
2791            .files
2792            .get(rel)
2793            .ok_or_else(|| ResolutionError::NpmFileNotInManifest { path: rel.to_owned() })?;
2794        let bytes =
2795            storage::read_project_file(project_root, &self.dir, rel, super::MAX_NPM_FILE_BYTES)?;
2796        if crate::registry::checksum(&bytes) != *expected {
2797            return Err(ResolutionError::NpmFileIntegrityMismatch { rel: rel.to_owned() });
2798        }
2799        Ok(bytes)
2800    }
2801}