use std::cell::RefCell; use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::Arc; use avril_core::{Error, ModuleFormat, ModuleLoader}; use oxc::span::SourceType; use crate::authority::SourceAuthority; use crate::caps::PathTarget; use crate::data_modules::DataModule; use crate::local_packages::ResolveMode; pub use crate::registry::npm::resolution::ResolutionMode; use crate::registry::npm::resolution::{ NpmResolver, NpmRootRequest, PreparedNpmRoots, ResolutionError, ResolvedNpmModule, }; pub use crate::loader::BuiltinWarning; pub(crate) use crate::loader::{compile, transpile}; pub use crate::type_graph::TypeGraph; #[derive(Debug, thiserror::Error)] pub(crate) enum ResolutionFailure { #[error("Unsupported module specifier: {0}")] UnboundSpecifier(String), #[error(transparent)] Npm(#[from] ResolutionError), #[error(transparent)] Other(#[from] Error), } impl ResolutionFailure { pub(crate) fn can_be_ambient(&self) -> bool { matches!( self, Self::UnboundSpecifier(_) | Self::Npm(ResolutionError::NpmPackageNotDeclared { .. }) ) } } impl From for Error { fn from(failure: ResolutionFailure) -> Self { match failure { ResolutionFailure::UnboundSpecifier(specifier) => { Error::Module(format!("Unsupported module specifier: {specifier}")) } ResolutionFailure::Npm(error) => Error::Module(error.to_string()), ResolutionFailure::Other(error) => error, } } } pub(crate) enum ResolutionTarget { Module(String), Jsr(String), Npm(String), Remote(String), } pub(crate) struct ResolvedRequest { pub(crate) name: String, pub(crate) tracked: Option, // The npm export still eligible for type overrides and automatic discovery. pub(crate) type_subpath: Option, } impl ResolvedRequest { fn module(name: String, tracked: Option) -> Self { Self { name, tracked, type_subpath: None, } } fn npm( module: ResolvedNpmModule, specifier: &str, tracked: Option, ) -> Result { let subpath = if specifier.starts_with("npm:") { crate::registry::npm::parse_npm_request(specifier) .map_err(|error| Error::Module(error.to_string()))? .subpath } else if !specifier.starts_with(['.', '/', '#']) && !specifier.contains(':') { crate::registry::npm::package::split_bare(specifier) .1 .map(str::to_owned) } else { return Ok(Self::module( module.path.to_string_lossy().into_owned(), tracked, )); }; Ok(Self { name: module.path.to_string_lossy().into_owned(), tracked, type_subpath: Some(subpath.unwrap_or_default()), }) } } /// Normalized name, local path, and remote/npm flags for one referrer. pub(crate) struct Referrer { pub(crate) name: String, pub(crate) path: Option, pub(crate) remote: bool, pub(crate) inline: bool, pub(crate) npm: bool, } #[derive(Clone)] pub struct ModuleGraph { pub(crate) authority: SourceAuthority, pub(crate) packages: Rc, pub(crate) store: Arc, pub(crate) remote: Rc, pub(crate) npm: Rc, pub(crate) npm_handles: Rc>>, pub(crate) types: Rc, pub(crate) loader: Rc>, } impl ModuleGraph { /// A graph with internally owned package loaders sharing one authority. /// The store is built with [`crate::registry::PackageStore::new_with_authority`]. pub fn open( runtime: tokio::runtime::Handle, authority: SourceAuthority, options: crate::registry::Options, ) -> Result { let store = Arc::new( crate::registry::PackageStore::new_with_authority(runtime, options, authority.clone()) .map_err(|error| Error::Module(error.to_string()))?, ); Ok(Self::assemble(authority, store, false)) } /// Like [`ModuleGraph::open`], resolving npm packages for browser bundles. pub fn open_browser( runtime: tokio::runtime::Handle, authority: SourceAuthority, options: crate::registry::Options, ) -> Result { let store = Arc::new( crate::registry::PackageStore::new_with_authority(runtime, options, authority.clone()) .map_err(|error| Error::Module(error.to_string()))?, ); Ok(Self::assemble(authority, store, true)) } /// Reuse an execution store, as workers do with their parent's store. /// Graph checks retain the caller's authority; acquisition uses the store's. /// Read graphs should use `open` or `open_browser` so acquisition shares /// their read/network policy. pub fn open_shared( authority: SourceAuthority, store: Arc, ) -> Self { Self::assemble(authority, store, false) } /// The store this graph acquires through. pub fn store(&self) -> &Arc { &self.store } pub fn set_typescript_version(&self, version: &str) -> Result<(), Error> { self.npm .set_typescript_version(version) .map_err(|error| Error::Module(error.to_string())) } pub fn audit_npm(&self) -> Result<(), Error> { self.npm .audit() .map_err(|error| Error::Module(error.to_string())) } /// The project root owning `anchor`, for store and lock bootstrap. pub fn project_root(authority: &SourceAuthority, anchor: &str) -> Result { crate::local_packages::LocalPackages::new(authority.clone()).root(anchor) } /// The trusted config a REPL session belongs to, for storage bootstrap. pub fn project_root_for_repl( authority: &SourceAuthority, anchor: &str, ) -> Result { crate::local_packages::LocalPackages::new(authority.clone()).root_for_repl(anchor) } /// The owning config path of a REPL session, if it has one. pub fn project_config_for_repl( authority: &SourceAuthority, anchor: &str, ) -> Result, Error> { crate::local_packages::LocalPackages::new(authority.clone()).config_for_repl(anchor) } /// Locate the project root for `add`, rejecting untrusted configs. pub fn project_root_for_add( authority: &SourceAuthority, anchor: &str, ) -> Result { crate::local_packages::LocalPackages::new(authority.clone()).root_for_add(anchor) } fn assemble( authority: SourceAuthority, store: Arc, browser: bool, ) -> Self { let packages = Rc::new(crate::local_packages::LocalPackages::new(authority.clone())); let remote = Rc::new(crate::remote_packages::RemotePackages::new( authority.clone(), store.clone(), )); let npm = Rc::new(if browser { NpmResolver::for_browser(store.clone()) } else { NpmResolver::new(store.clone()) }); Self { packages, store, remote, npm, npm_handles: Rc::default(), types: Rc::default(), authority, loader: Rc::default(), } } pub(crate) fn side_effects(&self, name: &str) -> Option { self.npm.side_effects(Path::new(name)) } pub async fn initialize_package(&self, name: &str) -> Result<(), Error> { if data_url(name).is_some() { return Ok(()); } if http_url(name).is_some() { self.remote.initialize(name).await } else if self.npm.is_reserved_npm_path(Path::new(name)) { self.npm .check_access(Path::new(name)) .map(|_| ()) .map_err(|error| Error::Module(error.to_string())) } else { self.packages.initialize(name, &self.remote).await } } pub async fn original_source(&self, name: &str) -> Result { self.original_source_for(name, ResolutionMode::Import).await } pub(crate) async fn original_source_for( &self, name: &str, mode: ResolutionMode, ) -> Result { self.check_source_access(name, mode)?; let source = if data_url(name).is_some() { DataModule::parse(name)?.source()? } else if http_url(name).is_some() { self.store .remote_source(name) .await .map_err(|error| Error::Module(error.to_string()))? } else if self.npm.owning_instance(Path::new(name)).is_some() { self.npm .npm_source(Path::new(name)) .map_err(|error| Error::Module(error.to_string()))? } else { crate::sources::read_text(&self.target(name)?, &self.authority) .map_err(|error| self.path_error(name, error))? }; self.check_source_access(name, mode)?; Ok(source) } pub(crate) fn check_source_access( &self, name: &str, mode: ResolutionMode, ) -> Result<(), Error> { if mode.is_type() && self.npm.is_reserved_npm_path(Path::new(name)) { self.npm .check_access(Path::new(name)) .map(|_| ()) .map_err(|error| Error::Module(error.to_string())) } else { self.check_access(name) } } pub(crate) fn package_metadata(&self, name: &str) -> Result, Error> { if data_url(name).is_some() { return Ok(None); } if Path::new(name).is_absolute() { if let Some(instance) = self.npm.owning_instance(Path::new(name)) { return self.npm_package_metadata(&instance, name); } self.target(name)?; self.packages.metadata(name) } else { self.remote.metadata(name) } } fn npm_package_metadata( &self, instance: &str, name: &str, ) -> Result, Error> { let identity = self .npm .describe_instance(instance) .map_err(|error| Error::Module(error.to_string()))?; Ok(Some(serde_json::json!({ "registry": self.store.options().npm_registry.as_str(), "name": identity.name, "version": identity.version, "instance": instance, "file": name, }))) } /// Workspace ownership, not guest write permission. Package-store bytes are /// immutable inputs even when their materialized identity is an absolute path. pub fn is_workspace_source(&self, name: &str) -> bool { let path = Path::new(name); let options = self.store.options(); path.is_absolute() && !self.npm.is_reserved_npm_path(path) && !path.starts_with(&options.cache) && options .vendor .as_ref() .is_none_or(|vendor| !path.starts_with(vendor)) } pub fn config_inputs(&self) -> BTreeSet { self.packages.config_inputs.borrow().clone() } pub(crate) fn jsx_config(&self, name: &str) -> Result { if http_url(name).is_some() || data_url(name).is_some() || self.npm.is_reserved_npm_path(Path::new(name)) { Ok(crate::project::JsxConfig::default()) } else { self.packages.jsx_config(name) } } pub(crate) fn source_type(&self, name: &str) -> Result { if data_url(name).is_some() { return Ok(DataModule::parse(name)?.source_type); } if self.npm.owning_instance(Path::new(name)).is_some() { // Descriptive only; execution policy lives in check_access and // the resolver, so type tooling keeps working on declarations. return Ok(SourceType::from_path(name).unwrap_or_else(|_| SourceType::mjs())); } let base = if http_url(name).is_some() { self.store .remote_base(name) .map_err(|error| Error::Module(error.to_string()))? } else { name.to_owned() }; let url = http_url(&base); Ok( SourceType::from_path(url.as_ref().map_or(base.as_str(), url::Url::path)) .unwrap_or_else(|_| SourceType::mjs()), ) } pub(crate) fn target(&self, name: &str) -> Result { let target = PathTarget::resolve(name).map_err(|error| self.path_error(name, error))?; if target.as_path() != Path::new(name) { return Err(self.path_error(name, "Module path changed after resolution")); } self.authority .check_local(&target) .map_err(|error| self.path_error(name, error))?; Ok(target) } fn resolve_local_target( &self, path: &Path, specifier: &str, allow_missing: bool, ) -> Result { // A cache symlink must not turn a registry-owned URL into an unchecked local import. let logical = lexical_absolute(path)?; if self.npm.is_reserved_npm_path(&logical) { self.npm .check_access(&logical) .map_err(|error| Error::Module(error.to_string()))?; return logical .into_os_string() .into_string() .map_err(|_| Error::Module("Module paths must be valid UTF-8".into())); } let target = if allow_missing { PathTarget::for_creation(path) } else { PathTarget::resolve(path) } .map_err(|error| self.path_error(specifier, error))?; self.authority .check_local(&target) .map_err(|error| self.path_error(specifier, error))?; target .into_path() .into_os_string() .into_string() .map_err(|_| Error::Module("Module paths must be valid UTF-8".into())) } pub(crate) fn path_error(&self, name: &str, error: impl std::fmt::Display) -> Error { if self.authority.unrestricted_local() { Error::Module(format!("{name}: {error}")) } else if self.authority.is_read() { Error::Module(format!("PermissionDenied: read {name}")) } else { Error::Module(format!("PermissionDenied: js-exec {name}")) } } } impl ModuleGraph { pub(crate) async fn resolve_tracked( &self, specifier: &str, referrer: &str, ) -> Result<(String, Option), Error> { let resolved = self .resolve_tracked_for(specifier, referrer, ResolutionMode::Import, false) .await .map_err(Error::from)?; Ok((resolved.name, resolved.tracked)) } pub(crate) async fn resolve_tracked_for( &self, specifier: &str, referrer: &str, mode: ResolutionMode, overrides: bool, ) -> Result { let referrer = self.classify_referrer(referrer)?; if mode.is_type() && specifier.starts_with("node:") { return Ok(ResolvedRequest::module(specifier.to_owned(), None)); } if let Some(name) = Self::early_scheme(specifier)? { return Ok(ResolvedRequest::module(name, None)); } if overrides && let Some(target) = self.type_override(specifier, &referrer.name)? { let anchor = self.type_config_referrer(&referrer.name); let mut resolved = Box::pin(self.resolve_tracked_for(&target, &anchor, mode, false)) .await .map_err(|failure| ResolutionFailure::Other(failure.into()))?; resolved.type_subpath = None; return Ok(resolved); } if let Some(path) = referrer.path.as_deref() && referrer.npm { if let Some(url) = data_url(specifier) { return Ok(ResolvedRequest::module(url.to_string(), None)); } if self.npm.owning_instance(path).is_none() { return Err(Error::Module(format!( "npm cache path is not owned by a prepared instance: {}", path.display() )) .into()); } return self.resolve_npm_from_file(specifier, path, mode).await; } let mapped = if referrer.inline { None } else if referrer.remote { self.remote.resolve(specifier, &referrer.name).await? } else { self.packages .resolve(specifier, &referrer.name, &self.remote) .await? }; let effective = match &mapped { Some(crate::local_packages::Resolution::Specifier(target)) => target.as_str(), _ => specifier, }; if mapped.is_none() && !effective.contains(':') && !effective.starts_with(['.', '/', '#']) { return Err(ResolutionFailure::UnboundSpecifier(effective.to_owned())); } if overrides && effective != specifier && let Some(target) = self.type_override(effective, &referrer.name)? { let mut resolved = Box::pin(self.resolve_tracked_for(&target, &referrer.name, mode, false)) .await .map_err(|failure| ResolutionFailure::Other(failure.into()))?; resolved.type_subpath = None; return Ok(resolved); } if effective.starts_with("npm:") { return self .resolve_npm_specifier(effective, &referrer, mode, !overrides) .await; } match self.resolve_target(specifier, &referrer, mapped, ResolveMode::Acquire)? { ResolutionTarget::Module(name) => Ok(ResolvedRequest::module(name, None)), ResolutionTarget::Npm(specifier) => { self.resolve_npm_specifier(&specifier, &referrer, mode, !overrides) .await } ResolutionTarget::Jsr(specifier) => { let request = crate::registry::parse_jsr_request(&specifier) .map_err(|error| Error::Module(error.to_string()))?; let key = request.request_key(); let name = self .store .resolve_request(&request) .await .map_err(|error| Error::Module(error.to_string()))?; Ok(ResolvedRequest::module(name, Some(key))) } ResolutionTarget::Remote(name) => { let declarations = if referrer.remote || referrer.inline { Vec::new() } else { self.packages.remote_dependencies(&referrer.name)? }; self.remote .import(&name, &referrer.name, &declarations) .await?; self.check_access(&name)?; Ok(ResolvedRequest::module(name, None)) } } } /// Classify the referrer once per resolution. The local path reuses /// `require_referrer_path`; npm/remote are checked here, ownership at /// use sites. Invalid file URLs and non-UTF8 paths error. pub(crate) fn classify_referrer(&self, referrer: &str) -> Result { let path = Self::require_referrer_path(referrer); if path.is_none() && url::Url::parse(referrer).is_ok_and(|url| url.scheme() == "file") { return Err(Error::Module(format!("Invalid file referrer: {referrer}"))); } let npm = path .as_deref() .is_some_and(|path| self.npm.is_reserved_npm_path(path)); let name = match &path { Some(path) => path .to_str() .ok_or_else(|| Error::Module("Module paths must be valid UTF-8".into()))? .to_owned(), None => referrer.to_owned(), }; Ok(Referrer { remote: http_url(referrer).is_some(), inline: data_url(referrer).is_some(), name, path, npm, }) } /// `ext:` rejects, `avril:` passes through untouched, `node:` rejects. /// Returns `Some` when the specifier decides the resolution before any /// mapping; `None` continues the tree. pub(crate) fn early_scheme(specifier: &str) -> Result, Error> { if specifier.starts_with("ext:") { return Err(Error::Module("Extension modules are private".into())); } if specifier.starts_with("avril:") { return Ok(Some(specifier.to_owned())); } if specifier.starts_with("node:") { return Err(Error::Module(format!( "node: builtin modules are not supported: {specifier}" ))); } Ok(None) } /// Scheme dispatch over the (possibly mapped) specifier. /// `mode.allow_missing()` controls target existence probing at every call /// site: `Acquire` requires the target, `PreparedOnly` only forms it. /// Every other branch is mode-independent. pub(crate) fn resolve_target( &self, specifier: &str, referrer: &Referrer, mapped: Option, mode: ResolveMode, ) -> Result { let allow_missing = mode.allow_missing(); let mapped = match mapped { Some(crate::local_packages::Resolution::Module(name)) => { let logical = lexical_absolute(Path::new(&name))?; if self.npm.is_reserved_npm_path(&logical) { self.npm .check_access(&logical) .map_err(|error| Error::Module(error.to_string()))?; let name = logical .into_os_string() .into_string() .map_err(|_| Error::Module("Module paths must be valid UTF-8".into()))?; return Ok(ResolutionTarget::Module(name)); } if !allow_missing { self.target(&name)?; } return Ok(ResolutionTarget::Module(name)); } Some(crate::local_packages::Resolution::Remote(name)) => { self.check_access(&name)?; return Ok(ResolutionTarget::Module(name)); } Some(crate::local_packages::Resolution::Specifier(target)) => Some(target), None => None, }; let specifier = mapped.as_deref().unwrap_or(specifier); if let Some(name) = Self::early_scheme(specifier)? { return Ok(ResolutionTarget::Module(name)); } if let Some(url) = data_url(specifier) { return Ok(ResolutionTarget::Module(url.to_string())); } if referrer.inline && url::Url::parse(specifier).is_err() { return Err(Error::Module(format!( "Data modules require absolute import URLs: {specifier}" ))); } if specifier.starts_with("jsr:") { return Ok(ResolutionTarget::Jsr(specifier.to_owned())); } if specifier.starts_with("npm:") { return Ok(ResolutionTarget::Npm(specifier.to_owned())); } if referrer .name .starts_with(self.store.options().jsr_registry.as_str()) { if specifier.starts_with("./") || specifier.starts_with("../") || specifier.starts_with('/') || specifier.starts_with(self.store.options().jsr_registry.as_str()) { let name = self .store .resolve_relative(specifier, &referrer.name) .map_err(|error| Error::Module(error.to_string()))?; self.check_access(&name)?; return Ok(ResolutionTarget::Module(name)); } return Err(Error::Module(format!( "Unsupported module specifier: {specifier}" ))); } let remote = if let Some(url) = http_url(specifier) { Some(url) } else if referrer.remote && (specifier.starts_with("./") || specifier.starts_with("../") || specifier.starts_with('/') || specifier.starts_with('?') || specifier.starts_with('#')) { let base = self.store.remote_base(&referrer.name); let base = base.map_err(|error| Error::Module(error.to_string()))?; Some( url::Url::parse(&base) .and_then(|url| url.join(specifier)) .map_err(|error| Error::Module(error.to_string()))?, ) } else { None }; if let Some(url) = remote { let name = url.to_string(); self.check_access(&name)?; return Ok(ResolutionTarget::Remote(name)); } if referrer.remote { return Err(Error::Module(format!( "Unsupported module specifier: {specifier}" ))); } let path = Path::new(specifier); let path = if specifier .get(..5) .is_some_and(|scheme| scheme.eq_ignore_ascii_case("file:")) { let url = url::Url::parse(specifier) .map_err(|_| Error::Module(format!("Invalid file URL: {specifier}")))?; if url.query().is_some() || url.fragment().is_some() { return Err(Error::Module( "File URL queries and fragments are not supported yet".into(), )); } url.to_file_path() .map_err(|_| Error::Module(format!("Invalid file URL: {specifier}")))? } else if path.is_absolute() { path.to_owned() } else if specifier.starts_with("./") || specifier.starts_with("../") { Path::new(&referrer.name).parent().unwrap().join(path) } else { return Err(Error::Module(format!( "Unsupported module specifier: {specifier}" ))); }; self.resolve_local_target(&path, specifier, allow_missing) .map(ResolutionTarget::Module) } /// Prepare every declared npm dependency of the owning config together /// with on-demand requests, keeping one resolution handle per config /// root so different peer environments never share bindings. pub(crate) async fn prepare_npm_for_config( &self, referrer: &str, extra: &[String], ) -> Result { // Reserved npm paths are never owned by a local config: resolve // them through the instance tree instead of config discovery. let canonical = Path::new(referrer) .canonicalize() .unwrap_or_else(|_| Path::new(referrer).to_owned()); if self.npm.is_reserved_npm_path(&canonical) { return Err(Error::Module(format!( "npm cache paths resolve through their owning instance, not a config: {referrer}" ))); } let root = self .packages .root(referrer) .map_err(|error| Error::Module(error.to_string()))?; if let Some(handle) = self.npm_handles.borrow().get(&root) && extra.iter().all(|request| { self.npm .resolve_in(handle, request, ResolutionMode::Import) .is_ok() }) { return Ok(handle.clone()); } let mut requests = self .packages .npm_dependencies(referrer) .map_err(|error| Error::Module(error.to_string()))?; requests.extend(extra.iter().cloned()); requests.sort(); requests.dedup(); let handle = self .npm .prepare( &requests .into_iter() .map(|request| NpmRootRequest { request, context: BTreeMap::new(), }) .collect::>(), ) .await .map_err(|error| Error::Module(error.to_string()))?; let mut handles = self.npm_handles.borrow_mut(); let entry = handles.entry(root).or_default(); for (key, instance) in &handle.roots { let request = key.split('\0').next().unwrap(); if entry.keys_for_request(request).is_empty() { entry.roots.insert(key.clone(), instance.clone()); } } Ok(entry.clone()) } /// Prepares the npm roots a local module can reach before its edges /// resolve, returning lock keys to record for CommonJS `require`. pub(crate) async fn prepare_npm_roots( &self, name: &str, format: ModuleFormat, specifiers: &[String], ) -> Result, Error> { // Remote and npm-owned modules resolve on demand instead. if self.npm.is_reserved_npm_path(Path::new(name)) || http_url(name).is_some() || data_url(name).is_some() { return Ok(Vec::new()); } if format == ModuleFormat::CommonJs { let handle = self.prepare_npm_for_config(name, &[]).await?; return Ok(handle.roots.into_keys().collect()); } // Literal npm: imports of one local module resolve together // so their peer environments preselect jointly. Mapped-away // literals never become roots: only what the import map // still selects as npm: is prepared. let mut literal = Vec::new(); for specifier in specifiers .iter() .filter(|specifier| specifier.starts_with("npm:")) { match self.packages.resolve(specifier, name, &self.remote).await? { Some(crate::local_packages::Resolution::Specifier(target)) if target.starts_with("npm:") => { literal.push(target); } Some(_) => {} None => literal.push(specifier.clone()), } } if !literal.is_empty() { self.prepare_npm_for_config(name, &literal).await?; } Ok(Vec::new()) } async fn resolve_npm_from_file( &self, specifier: &str, path: &Path, mode: ResolutionMode, ) -> Result { if specifier.starts_with("node:") { return Err(Error::Module(format!( "node: builtin modules are not supported: {specifier}" )) .into()); } if specifier.starts_with("npm:") { let request = crate::registry::npm::parse_npm_request(specifier) .map_err(|error| Error::Module(error.to_string()))?; let handle = self .npm .prepare(&[NpmRootRequest { request: request.request_key(), context: BTreeMap::new(), }]) .await .map_err(|error| Error::Module(error.to_string()))?; let resolved = self.npm.resolve_in(&handle, specifier, mode)?; let key = handle .roots .iter() .find(|(_, instance)| *instance == &resolved.instance) .map(|(key, _)| key.clone()) .ok_or_else(|| { Error::Module(format!("npm package is not prepared: {specifier}")) })?; return ResolvedRequest::npm(resolved, specifier, Some(key)) .map_err(ResolutionFailure::Other); } let resolved = self.npm.resolve_from_file(specifier, path, mode)?; ResolvedRequest::npm(resolved, specifier, None).map_err(ResolutionFailure::Other) } async fn resolve_npm_specifier( &self, specifier: &str, referrer: &Referrer, mode: ResolutionMode, type_root: bool, ) -> Result { if referrer.path.is_some() && !referrer.remote && !(type_root && mode.is_type()) { let handle = self .prepare_npm_for_config(&referrer.name, std::slice::from_ref(&specifier.to_owned())) .await?; let resolved = self.npm.resolve_in(&handle, specifier, mode)?; let key = self.npm_root_key(&handle, specifier, &resolved); return ResolvedRequest::npm(resolved, specifier, Some(key)) .map_err(ResolutionFailure::Other); } let request = crate::registry::npm::parse_npm_request(specifier) .map_err(|error| Error::Module(error.to_string()))?; if !mode.is_type() { let resolved = self .npm .resolve_request_parsed(&request, mode) .await .map_err(|error| Error::Module(error.to_string()))?; let tracked = self .npm .tracking_key(&request.request_key(), &resolved.instance) .unwrap_or_else(|| request.request_key()); return ResolvedRequest::npm(resolved, specifier, Some(tracked)) .map_err(ResolutionFailure::Other); } let context = self.types.context.borrow().clone(); let handle = self .npm .prepare(&[NpmRootRequest { request: request.request_key(), context, }]) .await .map_err(|error| Error::Module(error.to_string()))?; let resolved = self.npm.resolve_in(&handle, specifier, mode)?; let tracked = self.npm_root_key(&handle, specifier, &resolved); ResolvedRequest::npm(resolved, specifier, Some(tracked)).map_err(ResolutionFailure::Other) } /// The exact recorded root key behind a handle resolution, so lock /// tracking keeps successful contexts only. fn npm_root_key( &self, handle: &PreparedNpmRoots, specifier: &str, resolved: &ResolvedNpmModule, ) -> String { let request = crate::registry::npm::parse_npm_request(specifier).map(|request| request.request_key()); let request = request.unwrap_or_else(|_| specifier.to_owned()); handle .keys_for_request(&request) .into_iter() .find(|key| { handle .roots .get(key) .is_some_and(|instance| instance == &resolved.instance) }) .unwrap_or(request) } /// Synchronous metadata-only npm resolution: prepared handles first, /// then globally prepared roots. Never performs I/O. pub(crate) fn resolve_import_meta_npm( &self, specifier: &str, name: &str, remote: bool, ) -> Result { let request = crate::registry::npm::parse_npm_request(specifier) .map_err(|error| Error::Module(error.to_string()))?; if !remote && data_url(name).is_none() && let Ok(root) = self.packages.root(name) && let Some(handle) = self.npm_handles.borrow().get(&root) && let Ok(resolved) = self .npm .resolve_in(handle, specifier, ResolutionMode::Import) { let path = resolved .path .to_str() .ok_or_else(|| Error::Module("Invalid module path".into()))?; return self .module_url(path) .ok_or_else(|| Error::Module("Invalid module path".into())); } let path = self .npm .resolve_import_meta_parsed(&request) .map_err(|error| Error::Module(error.to_string()))?; self.module_url(&path) .ok_or_else(|| Error::Module("Invalid module path".into())) } } pub(crate) fn lexical_absolute(path: &Path) -> Result { if !path.is_absolute() { return Err(Error::Module("Expected an absolute module path".into())); } let mut normalized = PathBuf::new(); for component in path.components() { match component { std::path::Component::Prefix(prefix) => normalized.push(prefix.as_os_str()), std::path::Component::RootDir => normalized.push(component.as_os_str()), std::path::Component::CurDir => {} std::path::Component::ParentDir => { normalized.pop(); } std::path::Component::Normal(part) => normalized.push(part), } } Ok(normalized) } pub fn data_url(name: &str) -> Option { url::Url::parse(name) .ok() .filter(|url| url.scheme() == "data") } pub fn http_url(name: &str) -> Option { url::Url::parse(name) .ok() .filter(|url| matches!(url.scheme(), "http" | "https")) } /// An offline graph over a scratch project directory, for tests that never /// touch the network: registries point at `.invalid`, the lock is never /// written, and the runtime is returned so it outlives the graph. #[cfg(test)] pub(crate) fn offline_graph(dir: &Path) -> (tokio::runtime::Runtime, ModuleGraph) { let caps = crate::caps::Capabilities::new(crate::caps::Initial::new(false), dir.to_owned()); let authority = crate::authority::SourceAuthority::execute(caps); let runtime = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build() .unwrap(); let graph = ModuleGraph::open( runtime.handle().clone(), authority, crate::registry::Options { jsr_registry: url::Url::parse("https://example.invalid/").unwrap(), npm_registry: url::Url::parse("https://example.invalid/").unwrap(), cache: dir.join("cache"), lock: dir.join("avril.lock"), project_root: dir.to_owned(), offline: true, frozen: false, update: false, ephemeral_lock: false, vendor: None, }, ) .unwrap(); (runtime, graph) }