diff --git a/src/graph.rs b/src/graph.rs index 39971b7ca..47f6ce08e 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -39,6 +39,8 @@ use deno_semver::npm::NpmPackageReqReference; use deno_semver::package::PackageNv; use deno_semver::package::PackageNvReference; use deno_semver::package::PackageReq; +use deno_semver::package::PackageReqReferenceParseError; +use deno_semver::RangeSetOrTag; use deno_semver::Version; use futures::future::LocalBoxFuture; use futures::future::Shared; @@ -62,6 +64,7 @@ use std::collections::HashSet; use std::collections::VecDeque; use std::fmt; use std::sync::Arc; +use thiserror::Error; use url::Url; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] @@ -2770,6 +2773,14 @@ struct Builder<'a, 'graph> { diagnostics: Vec, } +#[derive(Error, Debug, Clone)] +pub enum JsrPackageFormatError { + #[error(transparent)] + JsrPackageParseError(PackageReqReferenceParseError), + #[error("Version tag not supported in jsr specifiers.")] + JsrPackageVersionTagNotSupportedError, +} + impl<'a, 'graph> Builder<'a, 'graph> { #[allow(clippy::too_many_arguments)] pub async fn build( @@ -3508,7 +3519,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { maybe_range: Option, is_dynamic: bool, ) { - match JsrPackageReqReference::from_specifier(&specifier) { + match validate_jsr_specifier(&specifier) { Ok(package_ref) => { if let Some(range) = &maybe_range { if let Some(nv) = @@ -4120,6 +4131,19 @@ impl<'a, 'graph> Builder<'a, 'graph> { } } +fn validate_jsr_specifier( + specifier: &Url, +) -> Result { + let package_ref = JsrPackageReqReference::from_specifier(&specifier) + .map_err(|err| JsrPackageFormatError::JsrPackageParseError(err))?; + match package_ref.req().version_req.inner() { + RangeSetOrTag::Tag(_) => { + Err(JsrPackageFormatError::JsrPackageVersionTagNotSupportedError) + } + RangeSetOrTag::RangeSet(_) => Ok(package_ref), + } +} + fn normalize_export_name(sub_path: Option<&str>) -> Cow { let Some(sub_path) = sub_path else { return Cow::Borrowed("."); @@ -4483,6 +4507,28 @@ mod tests { })); } + #[test] + fn test_jsr_import_format() { + assert!( + validate_jsr_specifier(&Url::parse("jsr:@scope/mod@tag").unwrap()) + .is_err(), + "jsr import specifier with tag should be an error" + ); + + assert!( + validate_jsr_specifier(&Url::parse("jsr:@scope/mod@").unwrap()).is_err() + ); + + assert!(validate_jsr_specifier( + &Url::parse("jsr:@scope/mod@1.2.3").unwrap() + ) + .is_ok()); + + assert!( + validate_jsr_specifier(&Url::parse("jsr:@scope/mod").unwrap()).is_ok() + ); + } + #[test] fn test_module_dependency_includes() { let specifier = ModuleSpecifier::parse("file:///a.ts").unwrap(); diff --git a/tests/specs/graph/JsrSpecifiers_VersionTagNotSupported.txt b/tests/specs/graph/JsrSpecifiers_VersionTagNotSupported.txt new file mode 100644 index 000000000..0178f8dfb --- /dev/null +++ b/tests/specs/graph/JsrSpecifiers_VersionTagNotSupported.txt @@ -0,0 +1,47 @@ +# https://jsr.io/@scope/a/meta.json +{ + "versions": { + "1.0.0": {} + } +} + +# mod.ts +import 'jsr:@scope/a@tag'; + +# output +{ + "roots": [ + "file:///mod.ts" + ], + "modules": [ + { + "kind": "esm", + "dependencies": [ + { + "specifier": "jsr:@scope/a@tag", + "code": { + "specifier": "jsr:@scope/a@tag", + "span": { + "start": { + "line": 0, + "character": 7 + }, + "end": { + "line": 0, + "character": 25 + } + } + } + } + ], + "size": 27, + "mediaType": "TypeScript", + "specifier": "file:///mod.ts" + }, + { + "specifier": "jsr:@scope/a@tag", + "error": "Version tag not supported in jsr specifiers." + } + ], + "redirects": {} +}