use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::sync::Arc; use deno_semver::{Version, jsr::JsrPackageReqReference}; use serde::Deserialize; use url::Url; use super::http::http_url; use super::{Access, MAX_ARTIFACT_SIZE, PackageStore, checksum, valid_checksum, verify}; /// Failures resolving and fetching JSR packages. #[derive(Clone, Debug, thiserror::Error)] pub enum JsrError { #[error("Not a registry module: {name}")] NotRegistryModule { name: String }, #[error("Invalid registry module URL: {name}")] InvalidRegistryModuleUrl { name: String }, #[error("Unsupported remote module: {name}")] UnsupportedRemoteModule { name: String }, #[error("Invalid registry module version: {name}")] InvalidRegistryModuleVersion { name: String }, #[error("Expected a registry module file: {name}")] ExpectedRegistryModuleFile { name: String }, #[error("Conflicting version selections for {key}")] ConflictingVersionSelections { key: String }, #[error("Package is not prepared for synchronous resolution: {key}")] PackageNotPreparedSync { key: String }, #[error("Package metadata is not prepared for synchronous resolution: {key}")] PackageMetadataNotPreparedSync { key: String }, #[error("{detail}")] JsrRequest { detail: String }, #[error("JSR version tags are not supported; use a semver range")] JsrVersionTagsUnsupported, #[error("Package {key} has no export {export}")] PackageMissingExport { key: String, export: String }, #[error("Export {export} is not in the package manifest")] ExportNotInManifest { export: String }, #[error("Invalid locked package version")] InvalidLockedPackageVersion, #[error("Locked version {version} does not satisfy {key}")] LockedVersionUnsatisfied { version: String, key: String }, #[error("Frozen lockfile is missing {name}")] FrozenLockfileMissing { name: String }, #[error("No non-yanked version satisfies {key}")] NoMatchingVersion { key: String }, #[error("Invalid package index {url}: {source}")] InvalidPackageIndex { url: String, source: Arc, }, #[error("Invalid version metadata {url}: {source}")] InvalidVersionMetadata { url: String, source: Arc, }, #[error("Invalid file manifest in {url}")] InvalidFileManifest { url: String }, #[error("Module is not in the package manifest: {name}")] ModuleNotInManifest { name: String }, #[error("Manifest size mismatch: {name}")] ManifestSizeMismatch { name: String }, #[error("JSR packages must be scoped: {name}")] JsrPackageUnscoped { name: String }, #[error("Invalid JSR package name: {name}")] InvalidJsrPackageName { name: String }, #[error("Package export must be a relative file path")] PackageExportNotRelative, #[error("Registry module queries and fragments are not supported")] RegistryModuleQueryFragment, #[error("Registry module URL is not canonical")] RegistryModuleNotCanonical, #[error("Module escapes its registry package")] ModuleEscapesRegistryPackage, #[error("Invalid registry module path")] InvalidRegistryModulePath, #[error("Module path must be UTF-8")] ModulePathNotUtf8, #[error("Vendor artifact is outside the registry")] VendorArtifactOutsideRegistry, #[error("Vendor path must be UTF-8")] VendorPathNotUtf8, #[error("Invalid vendor artifact path")] InvalidVendorArtifactPath, #[error("Module is not UTF-8: {name}")] ModuleNotUtf8 { name: String }, #[error(transparent)] Registry(Box), #[error(transparent)] Http(#[from] super::http::HttpError), #[error(transparent)] Caps(#[from] crate::caps::CapabilitiesError), #[error(transparent)] Url(#[from] url::ParseError), } /// A parsed `jsr:` request. Unlike `JsrPackageReqReference`, the specifier /// string is retained verbatim: the canonical key drops a leading `jsr:/` /// slash that import maps and metadata must reproduce byte-for-byte. #[derive(Clone, Debug, PartialEq, Eq)] pub struct JsrRequest { pub name: String, pub range: Option, pub version_req: deno_semver::VersionReq, pub subpath: Option, pub specifier: String, } impl JsrRequest { /// The canonical lock and specifier key: `jsr:{name}` or `jsr:{name}@{range}`. pub fn request_key(&self) -> String { match &self.range { None => format!("jsr:{}", self.name), Some(range) => format!("jsr:{}@{range}", self.name), } } /// The normalized export name (`./sub` or `.`), mirroring deno's resolution. pub fn export_name(&self) -> String { deno_semver::jsr::normalized_export_name(self.subpath.as_deref()).into_owned() } } /// Parses a `jsr:` specifier once; validation beyond the grammar /// (whole-package shape, version tags) happens at each call site. pub fn parse_jsr_request(specifier: &str) -> Result { let reference = JsrPackageReqReference::from_str(specifier) .map_err(|detail| JsrError::JsrRequest { detail: detail.to_string() })?; let req = reference.req(); let range = (req.version_req.version_text() != "*").then(|| req.version_req.version_text().to_owned()); Ok(JsrRequest { name: req.name.to_string(), range, version_req: req.version_req.clone(), subpath: reference.sub_path().map(str::to_owned), specifier: specifier.to_owned(), }) } impl From for JsrError { fn from(error: super::RegistryError) -> Self { Self::Registry(Box::new(error)) } } #[derive(Deserialize)] pub(super) struct PackageIndex { versions: BTreeMap, } #[derive(Deserialize)] struct IndexVersion { #[serde(default)] yanked: bool, } #[derive(Clone, Deserialize)] pub(super) struct ManifestEntry { size: usize, pub(super) checksum: String, } #[derive(Deserialize)] pub(super) struct VersionMetadata { pub(super) manifest: BTreeMap, exports: BTreeMap, #[serde(default, rename = "moduleGraph2")] graph: serde_json::Value, } impl PackageStore { pub fn module_base(&self, name: &str) -> Result { let url = Url::parse(name).map_err(|_| JsrError::NotRegistryModule { name: name.to_owned() })?; if url.query().is_some() || url.fragment().is_some() || url.as_str() != name || crate::caps::canonical_remote_url(&url)? != url { return Err(JsrError::InvalidRegistryModuleUrl { name: name.to_owned() }); } let relative = name .strip_prefix(self.options.jsr_registry.as_str()) .ok_or_else(|| JsrError::UnsupportedRemoteModule { name: name.to_owned() })?; let mut parts = relative.splitn(4, '/'); let scope = parts.next().unwrap_or_default(); let package = parts.next().unwrap_or_default(); let version = parts.next().unwrap_or_default(); let path = parts.next().unwrap_or_default(); validate_name(&format!("{scope}/{package}"))?; Version::parse_standard(version) .map_err(|_| JsrError::InvalidRegistryModuleVersion { name: name.to_owned() })?; if path.is_empty() { return Err(JsrError::ExpectedRegistryModuleFile { name: name.to_owned() }); } self.options .jsr_registry .join(&format!("{scope}/{package}/{version}/")) .map_err(JsrError::from) } pub async fn resolve(&self, specifier: &str) -> Result { let request = parse_jsr_request(specifier)?; self.resolve_request(&request).await } pub async fn resolve_request(&self, request: &JsrRequest) -> Result { self.check_package(request)?; let key = request.request_key(); let version = self.select(request).await?; let base = self .options .jsr_registry .join(&format!("{}/{version}/", request.name))?; let metadata = self.version_metadata(&base).await?; let name = self.export_target(request, &base, &metadata)?; let mut lock = self.lock.lock().unwrap(); if let Some(existing) = lock.specifiers.get(&key) && existing != &version { return Err(JsrError::ConflictingVersionSelections { key: key.to_owned() }); } let mut used = self.used.lock().unwrap(); used.specifiers.insert(key.clone()); if lock.specifiers.get(&key) != Some(&version) || self.options.update { used.pending_specifiers.insert(key.clone()); } lock.specifiers.insert(key, version); Ok(name.to_string()) } fn check_package(&self, request: &JsrRequest) -> Result<(), JsrError> { validate_name(&request.name)?; let package = self .options .jsr_registry .join(&format!("{}/", request.name))?; Access::Package(package).check(self.authority.as_ref())?; if request.version_req.tag().is_some() { return Err(JsrError::JsrVersionTagsUnsupported); } Ok(()) } pub fn resolve_import_meta(&self, specifier: &str) -> Result { self.resolve_import_meta_request(&parse_jsr_request(specifier)?) } pub fn resolve_import_meta_request(&self, request: &JsrRequest) -> Result { self.check_package(request)?; let key = request.request_key(); let version = self .locked_version(request)? .ok_or_else(|| JsrError::PackageNotPreparedSync { key: key.to_owned() })?; let base = self .options .jsr_registry .join(&format!("{}/{version}/", request.name))?; let metadata_url = format!("{}_meta.json", base.as_str().trim_end_matches('/')); let metadata = self .metadata .lock() .unwrap() .get(metadata_url.as_str()) .cloned() .ok_or_else(|| JsrError::PackageMetadataNotPreparedSync { key: key.to_owned() })?; self.export_target(request, &base, &metadata) .map(String::from) } fn export_target( &self, request: &JsrRequest, base: &Url, metadata: &VersionMetadata, ) -> Result { let key = request.request_key(); let export = request.export_name(); let target = metadata.exports.get(export.as_str()).ok_or_else(|| { JsrError::PackageMissingExport { key: key.to_owned(), export: format!("{export:?}"), } })?; let name = exported_file(base, target)?; if !metadata.manifest.contains_key(&manifest_path(base, &name)?) { return Err(JsrError::ExportNotInManifest { export: format!("{export:?}") }); } Access::Module(name.clone()).check(self.authority.as_ref())?; Ok(name) } fn locked_version(&self, request: &JsrRequest) -> Result, JsrError> { let key = request.request_key(); let selected = self.lock.lock().unwrap().specifiers.get(&key).cloned(); if let Some(version) = &selected { let parsed = Version::parse_standard(version) .map_err(|_| JsrError::InvalidLockedPackageVersion)?; if !request.version_req.matches(&parsed) { return Err(JsrError::LockedVersionUnsatisfied { version: version.to_owned(), key: key.to_owned(), }); } } Ok(selected) } /// The newest non-yanked version of a package, excluding prereleases unless requested. pub async fn latest(&self, name: &str, prerelease: bool) -> Result, JsrError> { validate_name(name)?; Ok(self .index(name) .await? .versions .iter() .filter(|(_, data)| !data.yanked) .filter_map(|(version, _)| Version::parse_standard(version).ok()) .filter(|version| prerelease || version.pre.is_empty()) .max() .map(|version| version.to_string())) } /// The export keys of the version a `jsr:` specifier selects. pub async fn exports(&self, specifier: &str) -> Result, JsrError> { let request = parse_jsr_request(specifier)?; validate_name(&request.name)?; if request.version_req.tag().is_some() { return Err(JsrError::JsrVersionTagsUnsupported); } let version = self.select(&request).await?; let base = self .options .jsr_registry .join(&format!("{}/{version}/", request.name))?; Ok(self .version_metadata(&base) .await? .exports .keys() .cloned() .collect()) } async fn select(&self, request: &JsrRequest) -> Result { let key = request.request_key(); Ok(match self.locked_version(request)? { Some(version) => version, None => { if self.options.frozen { return Err(JsrError::FrozenLockfileMissing { name: key.clone() }); } let index = self.index(request.name.as_str()).await?; index .versions .iter() .filter(|(_, data)| !data.yanked) .filter_map(|(version, _)| Version::parse_standard(version).ok()) .filter(|version| request.version_req.matches(version)) .max() .ok_or_else(|| JsrError::NoMatchingVersion { key: key.clone() })? .to_string() } }) } async fn index(&self, name: &str) -> Result, JsrError> { let url = self .options .jsr_registry .join(&format!("{name}/meta.json"))?; let cached = self.indexes.lock().unwrap().get(url.as_str()).cloned(); Ok(match cached { Some(index) => index, None => { let package = self.options.jsr_registry.join(&format!("{name}/"))?; let bytes = self .bytes( url.clone(), None, !self.options.offline, Access::Package(package), ) .await?; let index: PackageIndex = serde_json::from_slice(&bytes).map_err(|source| { JsrError::InvalidPackageIndex { url: url.to_string(), source: std::sync::Arc::new(source), } })?; let index = Arc::new(index); self.indexes .lock() .unwrap() .insert(url.to_string(), index.clone()); index } }) } pub(super) async fn version_metadata( &self, base: &Url, ) -> Result, JsrError> { Access::Package(base.clone()).check(self.authority.as_ref())?; let metadata_url = Url::parse(&format!( "{}_meta.json", base.as_str().trim_end_matches('/') ))?; let cached = self .metadata .lock() .unwrap() .get(metadata_url.as_str()) .cloned(); if let Some(metadata) = cached { let mut used = self.used.lock().unwrap(); if used.packages.insert(metadata_url.to_string()) { used.pending_packages.insert(metadata_url.to_string()); } return Ok(metadata); } let expected = self .lock .lock() .unwrap() .packages .get(metadata_url.as_str()) .cloned(); if expected.is_none() && self.options.frozen { return Err(JsrError::FrozenLockfileMissing { name: metadata_url.to_string() }); } let bytes = self .bytes( metadata_url.clone(), expected.clone(), expected.is_none() && !self.options.offline, Access::Package(base.clone()), ) .await?; let metadata: VersionMetadata = serde_json::from_slice(&bytes).map_err(|source| JsrError::InvalidVersionMetadata { url: metadata_url.to_string(), source: std::sync::Arc::new(source), })?; for entry in metadata.manifest.values() { if entry.size > MAX_ARTIFACT_SIZE || !valid_checksum(&entry.checksum) { return Err(JsrError::InvalidFileManifest { url: metadata_url.to_string() }); } } let checksum = checksum(&bytes); let mut lock = self.lock.lock().unwrap(); if let Some(existing) = lock.packages.get(metadata_url.as_str()) { verify(&bytes, existing)?; } let mut used = self.used.lock().unwrap(); used.packages.insert(metadata_url.to_string()); if lock.packages.get(metadata_url.as_str()) != Some(&checksum) { used.pending_packages.insert(metadata_url.to_string()); } lock.packages.insert(metadata_url.to_string(), checksum); let metadata = Arc::new(metadata); self.metadata .lock() .unwrap() .insert(metadata_url.to_string(), metadata.clone()); Ok(metadata) } pub fn resolve_relative(&self, specifier: &str, referrer: &str) -> Result { let base = self.module_base(referrer)?; let url = Url::parse(referrer)?.join(specifier)?; manifest_path(&base, &url)?; Ok(url.to_string()) } pub async fn source(&self, name: &str) -> Result { let base = self.module_base(name)?; let url = Url::parse(name)?; Access::Module(url.clone()).check(self.authority.as_ref())?; let metadata = self.version_metadata(&base).await?; let path = manifest_path(&base, &url)?; let entry = metadata .manifest .get(&path) .ok_or_else(|| JsrError::ModuleNotInManifest { name: name.to_owned() })?; let bytes = self .bytes( url.clone(), Some(entry.checksum.clone()), false, Access::Module(url), ) .await?; if bytes.len() != entry.size { return Err(JsrError::ManifestSizeMismatch { name: name.to_owned() }); } let source = String::from_utf8(bytes) .map_err(|_| JsrError::ModuleNotUtf8 { name: name.to_owned() })?; self.used.lock().unwrap().sources.insert(name.to_owned()); Ok(source) } pub async fn hints(&self, name: &str) -> Result, JsrError> { if !name.starts_with(self.options.jsr_registry.as_str()) { http_url(name)?; return Ok(vec![]); } let base = self.module_base(name)?; let metadata = self.version_metadata(&base).await?; let url = Url::parse(name)?; let path = manifest_path(&base, &url)?; let Some(dependencies) = metadata .graph .get(&path) .and_then(|info| info.get("dependencies")) .and_then(|value| value.as_array()) else { return Ok(vec![]); }; Ok(dependencies .iter() .filter(|dep| { dep.get("type").and_then(|value| value.as_str()) == Some("static") && dep.get("kind").and_then(|value| value.as_str()) == Some("import") }) .filter_map(|dep| dep.get("specifier").and_then(|value| value.as_str())) .filter(|specifier| specifier.starts_with("./") || specifier.starts_with("../")) .filter_map(|specifier| url.join(specifier).ok()) .filter(|url| { manifest_path(&base, url).is_ok_and(|path| metadata.manifest.contains_key(&path)) }) .filter(|url| { matches!( Path::new(url.path()) .extension() .and_then(|ext| ext.to_str()), Some("js" | "mjs" | "ts" | "mts") ) && !url.path().ends_with(".d.ts") && !url.path().ends_with(".d.mts") }) .take(32) .map(String::from) .collect()) } } pub(crate) fn validate_name(name: &str) -> Result<(), JsrError> { let Some((scope, package)) = name.strip_prefix('@').and_then(|name| name.split_once('/')) else { return Err(JsrError::JsrPackageUnscoped { name: name.to_owned() }); }; if [scope, package].iter().any(|part| { part.is_empty() || !part .chars() .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' || ch == '_') }) { return Err(JsrError::InvalidJsrPackageName { name: name.to_owned() }); } Ok(()) } fn exported_file(base: &Url, target: &str) -> Result { let target = target .strip_prefix("./") .or_else(|| target.strip_prefix('/')) .ok_or(JsrError::PackageExportNotRelative)?; let url = base.join(target)?; manifest_path(base, &url)?; Ok(url) } pub(super) fn manifest_path(base: &Url, url: &Url) -> Result { if url.query().is_some() || url.fragment().is_some() { return Err(JsrError::RegistryModuleQueryFragment); } if crate::caps::canonical_remote_url(url)? != *url { return Err(JsrError::RegistryModuleNotCanonical); } let suffix = url .as_str() .strip_prefix(base.as_str()) .ok_or(JsrError::ModuleEscapesRegistryPackage)?; let lower = suffix.to_ascii_lowercase(); if lower.contains("%2f") || lower.contains("%5c") || suffix.is_empty() { return Err(JsrError::InvalidRegistryModulePath); } let decoded = percent_encoding::percent_decode_str(suffix) .decode_utf8() .map_err(|_| JsrError::ModulePathNotUtf8)?; Ok(format!("/{decoded}")) } pub(super) fn vendor_path(registry: &Url, url: &Url) -> Result { let suffix = url .as_str() .strip_prefix(registry.as_str()) .ok_or(JsrError::VendorArtifactOutsideRegistry)?; let decoded = percent_encoding::percent_decode_str(suffix) .decode_utf8() .map_err(|_| JsrError::VendorPathNotUtf8)?; let path = PathBuf::from(decoded.as_ref()); if path .components() .any(|component| !matches!(component, std::path::Component::Normal(_))) { return Err(JsrError::InvalidVendorArtifactPath); } Ok(path) }