diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9338fac9..dbcb81f97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,11 +77,6 @@ jobs: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: cargo publish - - name: Get tag version - if: contains(matrix.os, 'ubuntu') && startsWith(github.ref, 'refs/tags/') - id: get_tag_version - run: echo TAG_VERSION=${GITHUB_REF/refs\/tags\//} >> "$GITHUB_OUTPUT" - - name: Publish JSR if: contains(matrix.os, 'ubuntu') run: | diff --git a/lib/lib.rs b/lib/lib.rs index cb9587baf..6528ca510 100644 --- a/lib/lib.rs +++ b/lib/lib.rs @@ -276,6 +276,7 @@ pub async fn js_create_graph( file_system: &NullFileSystem, jsr_url_provider: Default::default(), npm_resolver: None, + locker: None, passthrough_jsr_specifiers: false, module_analyzer: Default::default(), imports, diff --git a/src/graph.rs b/src/graph.rs index 0fc7af1f3..ff0ae8944 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -29,7 +29,6 @@ use crate::rt::Executor; use crate::source::*; -use anyhow::anyhow; use deno_ast::dep::DependencyKind; use deno_ast::dep::ImportAttributes; use deno_ast::LineAndColumnIndex; @@ -163,33 +162,91 @@ impl Range { } } -#[derive(Debug, Clone)] -pub enum ModuleError { - LoadingErr(ModuleSpecifier, Option, Arc), - Missing(ModuleSpecifier, Option), - MissingDynamic(ModuleSpecifier, Range), - MissingWorkspaceMemberExports { - specifier: ModuleSpecifier, - maybe_range: Option, - nv: PackageNv, - }, - UnknownPackage { - specifier: ModuleSpecifier, - maybe_range: Option, - package_name: String, - }, - UnknownPackageReq { - specifier: ModuleSpecifier, - maybe_range: Option, - package_req: PackageReq, - }, +#[derive(Debug, Clone, Error)] +pub enum JsrLoadError { + #[error( + "Unsupported checksum in JSR package manifest. Maybe try upgrading deno?" + )] + UnsupportedManifestChecksum, + #[error(transparent)] + ContentChecksumIntegrity(ChecksumIntegrityError), + #[error("Loader should never return an external specifier for a jsr: specifier content load.")] + ContentLoadExternalSpecifier, + #[error(transparent)] + ContentLoad(Arc), + #[error("JSR package manifest for '{}' failed to load: {:#}", .0, .1)] + PackageManifestLoad(String, Arc), + #[error("JSR package not found: {}", .0)] + PackageNotFound(String), + #[error("JSR package version not found: {}", .0)] + PackageVersionNotFound(PackageNv), + #[error("JSR package version manifest for '{}' failed to load: {:#}", .0, .1)] + PackageVersionManifestLoad(PackageNv, Arc), + #[error("JSR package version manifest for '{}' failed to load: {:#}", .0, .1)] + PackageVersionManifestChecksumIntegrity(PackageNv, ChecksumIntegrityError), + #[error(transparent)] + PackageFormat(JsrPackageFormatError), + #[error("Could not find version of '{}' that matches specified version constraint '{}'", .0.name, .0.version_req)] + PackageReqNotFound(PackageReq), + #[error("Redirects in the JSR registry are not supported (redirected to '{}')", .0)] + RedirectInPackage(ModuleSpecifier), + #[error("Unknown export '{}' for '{}'.\n Package exports:\n{}", export_name, .nv, .exports.iter().map(|e| format!(" * {}", e)).collect::>().join("\n"))] UnknownExport { - specifier: ModuleSpecifier, - maybe_range: Option, nv: PackageNv, export_name: String, exports: Vec, }, +} + +#[derive(Debug, Clone, Error)] +pub enum NpmLoadError { + #[error("npm specifiers are not supported in this environment")] + NotSupportedEnvironment, + #[error(transparent)] + PackageReqResolution(Arc), + #[error(transparent)] + PackageReqReferenceParse(PackageReqReferenceParseError), + #[error(transparent)] + RegistryInfo(Arc), +} + +#[derive(Debug, Clone, Error)] +pub enum WorkspaceLoadError { + #[error("Failed joining '{}' to '{}'. {:#}", .sub_path, .base, .error)] + MemberInvalidExportPath { + base: Url, + sub_path: String, + error: url::ParseError, + }, + #[error("Expected workspace package '{}' to define exports in its deno.json.", .nv)] + MissingMemberExports { nv: PackageNv }, +} + +#[derive(Debug, Error, Clone)] +pub enum ModuleLoadError { + #[error(transparent)] + HttpsChecksumIntegrity(ChecksumIntegrityError), + #[error(transparent)] + Decode(Arc), + #[error(transparent)] + Loader(Arc), + #[error(transparent)] + Jsr(#[from] JsrLoadError), + #[error(transparent)] + NodeUnknownBuiltinModule(#[from] UnknownBuiltInNodeModuleError), + #[error(transparent)] + Npm(#[from] NpmLoadError), + #[error("Too many redirects.")] + TooManyRedirects, + #[error(transparent)] + Workspace(#[from] WorkspaceLoadError), +} + +#[derive(Debug, Clone)] +pub enum ModuleError { + LoadingErr(ModuleSpecifier, Option, ModuleLoadError), + Missing(ModuleSpecifier, Option), + MissingDynamic(ModuleSpecifier, Range), ParseErr(ModuleSpecifier, deno_ast::ParseDiagnostic), UnsupportedMediaType(ModuleSpecifier, MediaType, Option), InvalidTypeAssertion { @@ -213,10 +270,6 @@ impl ModuleError { | Self::UnsupportedMediaType(s, _, _) | Self::Missing(s, _) | Self::MissingDynamic(s, _) - | Self::MissingWorkspaceMemberExports { specifier: s, .. } - | Self::UnknownExport { specifier: s, .. } - | Self::UnknownPackage { specifier: s, .. } - | Self::UnknownPackageReq { specifier: s, .. } | Self::InvalidTypeAssertion { specifier: s, .. } | Self::UnsupportedImportAttributeType { specifier: s, .. } => s, } @@ -227,12 +280,6 @@ impl ModuleError { Self::LoadingErr(_, maybe_referrer, _) => maybe_referrer.as_ref(), Self::Missing(_, maybe_referrer) => maybe_referrer.as_ref(), Self::MissingDynamic(_, range) => Some(range), - Self::MissingWorkspaceMemberExports { maybe_range, .. } => { - maybe_range.as_ref() - } - Self::UnknownExport { maybe_range, .. } => maybe_range.as_ref(), - Self::UnknownPackage { maybe_range, .. } => maybe_range.as_ref(), - Self::UnknownPackageReq { maybe_range, .. } => maybe_range.as_ref(), Self::UnsupportedMediaType(_, _, maybe_referrer) => { maybe_referrer.as_ref() } @@ -255,14 +302,10 @@ impl ModuleError { impl std::error::Error for ModuleError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { - Self::LoadingErr(_, _, err) => Some(err.as_ref().as_ref()), + Self::LoadingErr(_, _, err) => Some(err), Self::Missing(_, _) | Self::MissingDynamic(_, _) | Self::ParseErr(_, _) - | Self::MissingWorkspaceMemberExports { .. } - | Self::UnknownExport { .. } - | Self::UnknownPackage { .. } - | Self::UnknownPackageReq { .. } | Self::UnsupportedMediaType(_, _, _) | Self::InvalidTypeAssertion { .. } | Self::UnsupportedImportAttributeType { .. } => None, @@ -275,21 +318,10 @@ impl fmt::Display for ModuleError { match self { Self::LoadingErr(_, _, err) => err.fmt(f), Self::ParseErr(_, diagnostic) => write!(f, "The module's source code could not be parsed: {diagnostic}"), - Self::UnknownExport { export_name, exports, nv, specifier, .. } => { - let exports_text = exports.iter().map(|e| format!(" * {}", e)).collect::>().join("\n"); - write!(f, "Unknown export '{export_name}' for '{nv}'.\n Specifier: {specifier}\n Package exports:\n{exports_text}") - } - Self::UnknownPackage { package_name, specifier, .. } => - write!(f, "Unknown package: {package_name}\n Specifier: {specifier}"), - Self::UnknownPackageReq { package_req, specifier, .. } => - write!(f, "Could not find constraint in the list of versions: {package_req}\n Specifier: {specifier}"), Self::UnsupportedMediaType(specifier, MediaType::Json, ..) => write!(f, "Expected a JavaScript or TypeScript module, but identified a Json module. Consider importing Json modules with an import attribute with the type of \"json\".\n Specifier: {specifier}"), Self::UnsupportedMediaType(specifier, media_type, ..) => write!(f, "Expected a JavaScript or TypeScript module, but identified a {media_type} module. Importing these types of modules is currently not supported.\n Specifier: {specifier}"), Self::Missing(specifier, _) => write!(f, "Module not found \"{specifier}\"."), Self::MissingDynamic(specifier, _) => write!(f, "Dynamic import not found \"{specifier}\"."), - Self::MissingWorkspaceMemberExports { nv, specifier, .. } => { - write!(f, "Expected workspace package '{nv}' to define exports in its deno.json.\n Specifier: {specifier}") - } Self::InvalidTypeAssertion { specifier, actual_media_type: MediaType::Json, expected_media_type, .. } => write!(f, "Expected a {expected_media_type} module, but identified a Json module. Consider importing Json modules with an import attribute with the type of \"json\".\n Specifier: {specifier}"), Self::InvalidTypeAssertion { specifier, actual_media_type, expected_media_type, .. } => @@ -1121,6 +1153,7 @@ pub struct BuildOptions<'a> { /// runtime types. pub imports: Vec, pub executor: &'a dyn Executor, + pub locker: Option<&'a mut dyn Locker>, pub file_system: &'a dyn FileSystem, pub jsr_url_provider: &'a dyn JsrUrlProvider, /// Whether to pass through JSR specifiers to the resolver instead of @@ -1547,26 +1580,10 @@ impl ModuleGraph { pub async fn build<'a>( &mut self, roots: Vec, - loader: &dyn Loader, + loader: &'a dyn Loader, options: BuildOptions<'a>, ) -> Vec { - Builder::build( - self, - roots, - options.imports, - options.is_dynamic, - options.file_system, - options.jsr_url_provider, - options.passthrough_jsr_specifiers, - options.resolver, - options.npm_resolver, - loader, - options.module_analyzer, - options.reporter, - options.workspace_members, - options.executor, - ) - .await + Builder::build(self, roots, loader, options).await } #[cfg(feature = "fast_check")] @@ -2080,6 +2097,20 @@ impl ModuleSourceAndInfo { Self::Js { specifier, .. } => specifier, } } + + pub fn media_type(&self) -> MediaType { + match self { + Self::Json { .. } => MediaType::Json, + Self::Js { media_type, .. } => *media_type, + } + } + + pub fn source(&self) -> &str { + match self { + Self::Json { source, .. } => source, + Self::Js { source, .. } => source, + } + } } pub(crate) struct ParseModuleAndSourceInfoOptions<'a> { @@ -2132,7 +2163,7 @@ pub(crate) async fn parse_module_source_and_info( Err(err) => Err(ModuleError::LoadingErr( opts.specifier, None, - Arc::new(err.into()), + ModuleLoadError::Decode(Arc::new(err)), )), }; } @@ -2985,14 +3016,14 @@ impl JsrPackageVersionInfoExt { specifier.as_str().strip_prefix(base_url) } - pub fn get_checksum(&self, sub_path: &str) -> Result<&str, anyhow::Error> { + pub fn get_checksum(&self, sub_path: &str) -> Result<&str, ModuleLoadError> { match self.inner.manifest.get(sub_path) { Some(manifest_entry) => { match manifest_entry.checksum.strip_prefix("sha256-") { Some(checksum) => Ok(checksum), - None => { - Err(anyhow!("Unsupported checksum in package manifest. Maybe try upgrading deno?")) - } + None => Err(ModuleLoadError::Jsr( + JsrLoadError::UnsupportedManifestChecksum, + )), } } // If the checksum is missing then leave it up to the loader to error @@ -3165,6 +3196,7 @@ struct Builder<'a, 'graph> { jsr_url_provider: &'a dyn JsrUrlProvider, passthrough_jsr_specifiers: bool, loader: &'a dyn Loader, + locker: Option<&'a mut dyn Locker>, resolver: Option<&'a dyn Resolver>, npm_resolver: Option<&'a dyn NpmResolver>, module_analyzer: &'a dyn ModuleAnalyzer, @@ -3178,45 +3210,35 @@ struct Builder<'a, 'graph> { } impl<'a, 'graph> Builder<'a, 'graph> { - #[allow(clippy::too_many_arguments)] pub async fn build( graph: &'graph mut ModuleGraph, roots: Vec, - imports: Vec, - is_dynamic: bool, - file_system: &'a dyn FileSystem, - jsr_url_provider: &'a dyn JsrUrlProvider, - passthrough_jsr_specifiers: bool, - resolver: Option<&'a dyn Resolver>, - npm_resolver: Option<&'a dyn NpmResolver>, loader: &'a dyn Loader, - module_analyzer: &'a dyn ModuleAnalyzer, - reporter: Option<&'a dyn Reporter>, - workspace_members: &'a [WorkspaceMember], - executor: &'a dyn Executor, + options: BuildOptions<'a>, ) -> Vec { let fill_pass_mode = match graph.roots.is_empty() { true => FillPassMode::AllowRestart, false => FillPassMode::NoRestart, }; let mut builder = Self { - in_dynamic_branch: is_dynamic, - file_system, - jsr_url_provider, - passthrough_jsr_specifiers, + in_dynamic_branch: options.is_dynamic, + file_system: options.file_system, + jsr_url_provider: options.jsr_url_provider, + passthrough_jsr_specifiers: options.passthrough_jsr_specifiers, loader, - resolver, - npm_resolver, - module_analyzer, - reporter, + locker: options.locker, + resolver: options.resolver, + npm_resolver: options.npm_resolver, + module_analyzer: options.module_analyzer, + reporter: options.reporter, graph, state: PendingState::default(), fill_pass_mode, - workspace_members, + workspace_members: options.workspace_members, diagnostics: Vec::new(), - executor, + executor: options.executor, }; - builder.fill(roots, imports).await; + builder.fill(roots, options.imports).await; builder.diagnostics } @@ -3365,7 +3387,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { .get_package_metadata(package_name) .unwrap(); match fut.await { - Ok(Some(info)) => { + Ok(info) => { // resolve the best version out of the existing versions first let package_req = pending_resolution.package_ref.req(); match self.resolve_jsr_version(&info, package_req) { @@ -3400,33 +3422,24 @@ impl<'a, 'graph> Builder<'a, 'graph> { } else { self.graph.module_slots.insert( pending_resolution.specifier.clone(), - ModuleSlot::Err(ModuleError::UnknownPackageReq { - specifier: pending_resolution.specifier.clone(), - maybe_range: pending_resolution.maybe_range.clone(), - package_req: package_req.clone(), - }), + ModuleSlot::Err(ModuleError::LoadingErr( + pending_resolution.specifier.clone(), + pending_resolution.maybe_range.clone(), + JsrLoadError::PackageReqNotFound(package_req.clone()) + .into(), + )), ); } } } } - Ok(None) => { - self.graph.module_slots.insert( - pending_resolution.specifier.clone(), - ModuleSlot::Err(ModuleError::UnknownPackage { - specifier: pending_resolution.specifier.clone(), - maybe_range: pending_resolution.maybe_range.clone(), - package_name: package_name.clone(), - }), - ); - } Err(err) => { self.graph.module_slots.insert( pending_resolution.specifier.clone(), ModuleSlot::Err(ModuleError::LoadingErr( pending_resolution.specifier, pending_resolution.maybe_range, - err.clone(), + err.into(), )), ); } @@ -3487,16 +3500,19 @@ impl<'a, 'graph> Builder<'a, 'graph> { None => { self.graph.module_slots.insert( resolution_item.specifier.clone(), - ModuleSlot::Err(ModuleError::UnknownExport { - specifier: resolution_item.specifier, - maybe_range: resolution_item.maybe_range, - export_name: export_name.to_string(), - nv: resolution_item.nv_ref.into_inner().nv, - exports: version_info - .exports() - .map(|(k, _)| k.to_string()) - .collect::>(), - }), + ModuleSlot::Err(ModuleError::LoadingErr( + resolution_item.specifier, + resolution_item.maybe_range, + JsrLoadError::UnknownExport { + export_name: export_name.to_string(), + nv: resolution_item.nv_ref.into_inner().nv, + exports: version_info + .exports() + .map(|(k, _)| k.to_string()) + .collect::>(), + } + .into(), + )), ); } } @@ -3507,7 +3523,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { ModuleSlot::Err(ModuleError::LoadingErr( resolution_item.specifier, resolution_item.maybe_range, - err.clone(), + err.into(), )), ); } @@ -3553,13 +3569,11 @@ impl<'a, 'graph> Builder<'a, 'graph> { // was setup incorrectly, so return an error self.graph.module_slots.insert( item.specifier.clone(), - ModuleSlot::Err( - ModuleError::LoadingErr( - item.specifier, - item.maybe_range, - Arc::new(anyhow!("Loader should never return an external specifier for a jsr: specifier.")), - ), - ), + ModuleSlot::Err(ModuleError::LoadingErr( + item.specifier, + item.maybe_range, + JsrLoadError::ContentLoadExternalSpecifier.into(), + )), ); } LoadResponse::Module { @@ -3622,10 +3636,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { ModuleSlot::Err(ModuleError::LoadingErr( item.specifier, item.maybe_range, - Arc::new(anyhow!( - "Redirects in the JSR registry are not supported (redirected to '{}')", - specifier - )), + JsrLoadError::RedirectInPackage(specifier).into(), )), ); } @@ -3646,7 +3657,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { ModuleSlot::Err(ModuleError::LoadingErr( item.specifier, item.maybe_range, - Arc::new(err), + JsrLoadError::ContentLoad(Arc::new(err)).into(), )), ); } @@ -3841,7 +3852,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { ModuleSlot::Err(ModuleError::LoadingErr( specifier.clone(), maybe_range.cloned(), - Arc::new(err), + err, )), ); return; @@ -3872,7 +3883,9 @@ impl<'a, 'graph> Builder<'a, 'graph> { let result = match response { Ok(None) => { parse_module_source_and_info( - &ProvidedModuleAnalyzer(RefCell::new(Some(module_info.clone()))), + &ProvidedModuleAnalyzer(RefCell::new(Some( + module_info.clone(), + ))), ParseModuleAndSourceInfoOptions { specifier: requested_specifier.clone(), maybe_headers: Default::default(), @@ -3881,54 +3894,58 @@ impl<'a, 'graph> Builder<'a, 'graph> { maybe_referrer: maybe_range.as_ref(), is_root, is_dynamic_branch, - } - ).await.map(|module_source_and_info| { + }, + ) + .await + .map(|module_source_and_info| { PendingInfoResponse::Module { specifier: requested_specifier.clone(), module_source_and_info, pending_load: Some(Box::new((checksum, module_info))), } }) - }, - Ok(Some(response)) => { - match response { - LoadResponse::External { specifier } => { - Ok(PendingInfoResponse::External { specifier }) - } - LoadResponse::Redirect { specifier } => Err(ModuleError::LoadingErr( + } + Ok(Some(response)) => match response { + LoadResponse::External { specifier } => { + Ok(PendingInfoResponse::External { specifier }) + } + LoadResponse::Redirect { specifier } => { + Err(ModuleError::LoadingErr( requested_specifier.clone(), maybe_range.clone(), - Arc::new(anyhow!("Redirects in the JSR registry are not supported (redirected to '{}')", specifier))) - ), - LoadResponse::Module { - content, - specifier, + JsrLoadError::RedirectInPackage(specifier).into(), + )) + } + LoadResponse::Module { + content, + specifier, + maybe_headers, + } => parse_module_source_and_info( + module_analyzer, + ParseModuleAndSourceInfoOptions { + specifier: specifier.clone(), maybe_headers, - } => { - parse_module_source_and_info( - module_analyzer, - ParseModuleAndSourceInfoOptions { - specifier: specifier.clone(), - maybe_headers, - content, - maybe_attribute_type: maybe_attribute_type.as_ref(), - maybe_referrer: maybe_range.as_ref(), - is_root, - is_dynamic_branch, - } - ).await.map(|module_source_and_info| { - PendingInfoResponse::Module { - specifier: specifier.clone(), - module_source_and_info, - pending_load: None, - } - }) + content, + maybe_attribute_type: maybe_attribute_type.as_ref(), + maybe_referrer: maybe_range.as_ref(), + is_root, + is_dynamic_branch, + }, + ) + .await + .map(|module_source_and_info| { + PendingInfoResponse::Module { + specifier: specifier.clone(), + module_source_and_info, + pending_load: None, } - } - } - Err(err) => { - Err(ModuleError::LoadingErr(requested_specifier.clone(), maybe_range.clone(), Arc::new(err))) - } + }), + }, + Err(err) => Err(ModuleError::LoadingErr( + requested_specifier.clone(), + maybe_range.clone(), + ModuleLoadError::Loader(Arc::new(err)), + )), }; PendingInfo { requested_specifier, @@ -3994,7 +4011,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { ModuleSlot::Err(ModuleError::LoadingErr( specifier.clone(), maybe_range, - Arc::new(err.into()), + err.into(), )), ); return; @@ -4103,7 +4120,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { ModuleSlot::Err(ModuleError::LoadingErr( specifier, maybe_range, - Arc::new(err.into()), + JsrLoadError::PackageFormat(err).into(), )), ); } @@ -4118,37 +4135,42 @@ impl<'a, 'graph> Builder<'a, 'graph> { workspace_member: &WorkspaceMember, ) -> Result> { if workspace_member.exports.is_empty() { - return Err(Box::new(ModuleError::MissingWorkspaceMemberExports { - specifier: specifier.clone(), - maybe_range: maybe_range.map(ToOwned::to_owned), - nv: workspace_member.nv.clone(), - })); + return Err(Box::new(ModuleError::LoadingErr( + specifier.clone(), + maybe_range.map(ToOwned::to_owned), + WorkspaceLoadError::MissingMemberExports { + nv: workspace_member.nv.clone(), + } + .into(), + ))); } let export_name = normalize_export_name(package_ref.sub_path()); if let Some(sub_path) = workspace_member.exports.get(export_name.as_ref()) { match workspace_member.base.join(sub_path) { Ok(load_specifier) => Ok(load_specifier), - Err(err) => { - let err: anyhow::Error = err.into(); - Err(Box::new(ModuleError::LoadingErr( - specifier.clone(), - maybe_range.map(ToOwned::to_owned), - Arc::new(err.context(format!( - "Failed joining '{}' to '{}'.", - sub_path, workspace_member.base - ))), - ))) - } + Err(err) => Err(Box::new(ModuleError::LoadingErr( + specifier.clone(), + maybe_range.map(ToOwned::to_owned), + WorkspaceLoadError::MemberInvalidExportPath { + base: workspace_member.base.clone(), + sub_path: sub_path.to_string(), + error: err, + } + .into(), + ))), } } else { - Err(Box::new(ModuleError::UnknownExport { - specifier: specifier.clone(), - maybe_range: maybe_range.map(ToOwned::to_owned), - nv: workspace_member.nv.clone(), - export_name: export_name.to_string(), - exports: workspace_member.exports.keys().cloned().collect(), - })) + Err(Box::new(ModuleError::LoadingErr( + specifier.clone(), + maybe_range.map(ToOwned::to_owned), + JsrLoadError::UnknownExport { + nv: workspace_member.nv.clone(), + export_name: export_name.to_string(), + exports: workspace_member.exports.keys().cloned().collect(), + } + .into(), + ))) } } @@ -4180,14 +4202,15 @@ impl<'a, 'graph> Builder<'a, 'graph> { // request to load let package_name = package_ref.req().name.clone(); let fut = npm_resolver.load_and_cache_npm_package_info(&package_name); - self.state.npm.pending_registry_info_loads.push(Box::pin( - async move { + self.state.npm.pending_registry_info_loads.push( + crate::rt::spawn(self.executor, async move { PendingNpmRegistryInfoLoad { package_name, result: fut.await.map_err(Arc::new), } - }, - )); + }) + .boxed_local(), + ); } self .state @@ -4205,7 +4228,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { ModuleSlot::Err(ModuleError::LoadingErr( specifier, maybe_range, - Arc::new(err.into()), + NpmLoadError::PackageReqReferenceParse(err).into(), )), ); } @@ -4221,7 +4244,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { maybe_range, load_specifier, is_dynamic, - maybe_checksum, + mut maybe_checksum, mut maybe_version_info, } = item; self @@ -4250,6 +4273,12 @@ impl<'a, 'graph> Builder<'a, 'graph> { .unwrap(); (package_nv, fut) }); + if maybe_checksum.is_none() { + maybe_checksum = self + .locker + .as_ref() + .and_then(|l| l.get_checksum(&requested_specifier)); + } let fut = async move { #[allow(clippy::too_many_arguments)] async fn try_load( @@ -4275,7 +4304,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { ModuleError::LoadingErr( jsr_url_provider.package_url(&package_nv), maybe_range.cloned(), - err, + err.into(), ) })?; let info = JsrPackageVersionInfoExt { @@ -4290,7 +4319,7 @@ impl<'a, 'graph> Builder<'a, 'graph> { ModuleError::LoadingErr( load_specifier.clone(), maybe_range.cloned(), - Arc::new(err), + err, ) })? .to_string(), @@ -4322,15 +4351,24 @@ impl<'a, 'graph> Builder<'a, 'graph> { Err(ModuleError::LoadingErr( load_specifier.clone(), maybe_range.cloned(), - Arc::new(anyhow!( - "Redirects within a JSR package are not supported.", - )), + JsrLoadError::RedirectInPackage(specifier.clone()).into(), + )) + } else if let Some(expected_checksum) = maybe_checksum { + Err(ModuleError::LoadingErr( + load_specifier.clone(), + maybe_range.cloned(), + ModuleLoadError::HttpsChecksumIntegrity( + ChecksumIntegrityError { + actual: format!("Redirect to {}", specifier), + expected: expected_checksum.into_string(), + }, + ), )) } else if redirect_count >= loader.max_redirects() { Err(ModuleError::LoadingErr( load_specifier.clone(), maybe_range.cloned(), - Arc::new(anyhow!("Too many redirects.")), + ModuleLoadError::TooManyRedirects, )) } else { Ok(PendingInfoResponse::Redirect { @@ -4373,11 +4411,20 @@ impl<'a, 'graph> Builder<'a, 'graph> { load_specifier.clone(), maybe_range.cloned(), )), - Err(err) => Err(ModuleError::LoadingErr( - load_specifier.clone(), - maybe_range.cloned(), - Arc::new(err), - )), + Err(err) => match err.downcast::() { + // try to return the context of a checksum integrity error + // so that it can be more easily enhanced + Ok(err) => Err(ModuleError::LoadingErr( + load_specifier.clone(), + maybe_range.cloned(), + ModuleLoadError::HttpsChecksumIntegrity(err), + )), + Err(err) => Err(ModuleError::LoadingErr( + load_specifier.clone(), + maybe_range.cloned(), + ModuleLoadError::Loader(Arc::new(err)), + )), + }, } } @@ -4498,6 +4545,21 @@ impl<'a, 'graph> Builder<'a, 'graph> { } .boxed_local() }); + } else if maybe_version_info.is_none() + // do not insert checksums for declaration files + && !module_source_and_info.media_type().is_declaration() + && matches!(specifier.scheme(), "https" | "http") + { + if let Some(locker) = &mut self.locker { + if !locker.has_checksum(&specifier) { + locker.set_checksum( + &specifier, + LoaderChecksum::new(LoaderChecksum::gen( + module_source_and_info.source().as_bytes(), + )), + ); + } + } } let module_slot = @@ -4791,7 +4853,7 @@ impl<'a> NpmSpecifierResolver<'a> { ModuleSlot::Err(ModuleError::LoadingErr( item.specifier.clone(), item.maybe_range.clone(), - err.clone(), + NpmLoadError::RegistryInfo(err.clone()).into(), )), ); } @@ -4863,7 +4925,7 @@ impl<'a> NpmSpecifierResolver<'a> { ModuleSlot::Err(ModuleError::LoadingErr( item.specifier, item.maybe_range, - Arc::new(err), + NpmLoadError::PackageReqResolution(Arc::new(err)).into(), )), ); } @@ -4874,9 +4936,7 @@ impl<'a> NpmSpecifierResolver<'a> { ModuleSlot::Err(ModuleError::LoadingErr( item.specifier, item.maybe_range, - Arc::new(anyhow::anyhow!( - "npm specifiers are not supported in this environment" - )), + NpmLoadError::NotSupportedEnvironment.into(), )), ); } @@ -4925,7 +4985,7 @@ fn new_source_with_text( Box::new(ModuleError::LoadingErr( specifier.clone(), None, - Arc::new(err.into()), + ModuleLoadError::Decode(err.into()), )) }) } diff --git a/src/jsr.rs b/src/jsr.rs index dad555c69..50b3d7fe8 100644 --- a/src/jsr.rs +++ b/src/jsr.rs @@ -8,11 +8,13 @@ use deno_semver::package::PackageNv; use futures::future::Shared; use futures::FutureExt; +use crate::graph::JsrLoadError; use crate::packages::JsrPackageInfo; use crate::packages::JsrPackageVersionInfo; use crate::rt::spawn; use crate::rt::JoinHandle; use crate::source::CacheSetting; +use crate::source::ChecksumIntegrityError; use crate::source::JsrUrlProvider; use crate::source::LoadOptions; use crate::source::LoadResponse; @@ -26,7 +28,7 @@ pub struct PendingJsrPackageVersionInfoLoadItem { pub info: Arc, } -pub type PendingResult = Shared>>>; +pub type PendingResult = Shared>>; #[derive(Clone, Copy)] pub struct JsrMetadataStoreServices<'a> { @@ -38,7 +40,7 @@ pub struct JsrMetadataStoreServices<'a> { #[derive(Debug, Default)] pub struct JsrMetadataStore { pending_package_info_loads: - HashMap>>>, + HashMap>>, pending_package_version_info_loads: HashMap>, } @@ -47,7 +49,7 @@ impl JsrMetadataStore { pub fn get_package_metadata( &self, package_name: &str, - ) -> Option>>> { + ) -> Option>> { self.pending_package_info_loads.get(package_name).cloned() } @@ -81,7 +83,15 @@ impl JsrMetadataStore { /* checksum */ None, |content| { let package_info: JsrPackageInfo = serde_json::from_slice(content)?; - Ok(Some(Arc::new(package_info))) + Ok(Arc::new(package_info)) + }, + { + let package_name = package_name.to_string(); + |e| JsrLoadError::PackageManifestLoad(package_name, Arc::new(e)) + }, + { + let package_name = package_name.to_string(); + || JsrLoadError::PackageNotFound(package_name) }, ); self @@ -132,19 +142,44 @@ impl JsrMetadataStore { info: Arc::new(version_info), }) }, + { + let package_nv = package_nv.clone(); + |e| { + match e.downcast::() { + Ok(err) => { + // use a more specific variant in order to allow the + // cli to enhance this error message + JsrLoadError::PackageVersionManifestChecksumIntegrity( + package_nv, err, + ) + } + Err(err) => JsrLoadError::PackageVersionManifestLoad( + package_nv, + Arc::new(err), + ), + } + } + }, + { + let package_nv = package_nv.clone(); + || JsrLoadError::PackageVersionNotFound(package_nv) + }, ); self .pending_package_version_info_loads .insert(package_nv.clone(), fut); } + #[allow(clippy::too_many_arguments)] fn load_data( &self, specifier: ModuleSpecifier, services: JsrMetadataStoreServices, cache_setting: CacheSetting, maybe_expected_checksum: Option, - handle_content: impl FnOnce(&[u8]) -> Result + 'static, + handle_content: impl FnOnce(&[u8]) -> Result + 'static, + create_failed_load_err: impl FnOnce(anyhow::Error) -> JsrLoadError + 'static, + create_not_found_error: impl FnOnce() -> JsrLoadError + 'static, ) -> PendingResult { let fut = services.loader.load( &specifier, @@ -157,18 +192,19 @@ impl JsrMetadataStore { let fut = spawn( services.executor, async move { - let data = fut.await.map_err(Arc::new)?; + let data = match fut.await { + Ok(data) => data, + Err(err) => return Err(create_failed_load_err(err)), + }; match data { Some(LoadResponse::Module { content, .. }) => { - handle_content(&content).map_err(Arc::new) + handle_content(&content) + .map_err(|e| create_failed_load_err(e.into())) } Some(LoadResponse::Redirect { specifier }) => { - Err(Arc::new(anyhow::anyhow!( - "Redirects in the JSR registry are not supported (redirected to '{}')", - specifier - ))) + Err(JsrLoadError::RedirectInPackage(specifier)) } - _ => Err(Arc::new(anyhow::anyhow!("Not found: {}", specifier))), + _ => Err(create_not_found_error()), } } .boxed_local(), diff --git a/src/lib.rs b/src/lib.rs index df2458a97..990a9e982 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,11 +69,14 @@ pub use graph::GraphImport; pub use graph::GraphKind; pub use graph::JsModule; pub use graph::JsonModule; +pub use graph::JsrLoadError; pub use graph::Module; pub use graph::ModuleEntryRef; pub use graph::ModuleError; pub use graph::ModuleGraph; pub use graph::ModuleGraphError; +pub use graph::ModuleLoadError; +pub use graph::NpmLoadError; pub use graph::NpmModule; pub use graph::Position; pub use graph::Range; @@ -84,6 +87,7 @@ pub use graph::TypesDependency; pub use graph::WalkOptions; #[cfg(feature = "fast_check")] pub use graph::WorkspaceFastCheckOption; +pub use graph::WorkspaceLoadError; pub use graph::WorkspaceMember; pub use module_specifier::resolve_import; pub use module_specifier::ModuleSpecifier; diff --git a/src/source/mod.rs b/src/source/mod.rs index e787208d8..a8027653b 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -119,7 +119,7 @@ impl CacheSetting { pub static DEFAULT_JSR_URL: Lazy = Lazy::new(|| Url::parse("https://jsr.io").unwrap()); -#[derive(Debug, Error)] +#[derive(Debug, Clone, Error)] #[error("Integrity check failed.\n\nActual: {}\nExpected: {}", .actual, .expected)] pub struct ChecksumIntegrityError { pub actual: String, @@ -128,7 +128,7 @@ pub struct ChecksumIntegrityError { /// A SHA-256 checksum to verify the contents of a module /// with while loading. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct LoaderChecksum(String); impl LoaderChecksum { @@ -168,6 +168,53 @@ impl LoaderChecksum { } } +impl fmt::Display for LoaderChecksum { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +pub trait Locker { + fn get_checksum(&self, specifier: &ModuleSpecifier) + -> Option; + fn has_checksum(&self, specifier: &ModuleSpecifier) -> bool; + fn set_checksum( + &mut self, + specifier: &ModuleSpecifier, + checksum: LoaderChecksum, + ); +} + +#[derive(Debug, Default, Clone)] +pub struct HashMapLocker(HashMap); + +impl HashMapLocker { + pub fn inner(&self) -> &HashMap { + &self.0 + } +} + +impl Locker for HashMapLocker { + fn get_checksum( + &self, + specifier: &ModuleSpecifier, + ) -> Option { + self.0.get(specifier).cloned() + } + + fn has_checksum(&self, specifier: &ModuleSpecifier) -> bool { + self.0.contains_key(specifier) + } + + fn set_checksum( + &mut self, + specifier: &ModuleSpecifier, + checksum: LoaderChecksum, + ) { + self.0.insert(specifier.clone(), checksum); + } +} + #[derive(Debug, Clone)] pub struct LoadOptions { pub is_dynamic: bool, @@ -197,6 +244,10 @@ pub trait Loader { /// A method that given a specifier that asynchronously returns the /// source of the file. + /// + /// To ensure errors surfaced in the graph are more specific for checksum + /// integrity errors, ensure this returns a `ChecksumIntegrityError` when + /// the checksum on `LoadOptions` does not match the loaded source. fn load( &self, specifier: &ModuleSpecifier, @@ -344,7 +395,7 @@ pub trait Resolver: fmt::Debug { } } -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] #[error("Unknown built-in \"node:\" module: {module_name}")] pub struct UnknownBuiltInNodeModuleError { /// Name of the invalid module. @@ -425,12 +476,17 @@ pub struct RawDataUrl { } impl RawDataUrl { - pub fn parse(specifier: &ModuleSpecifier) -> Result { - let url = DataUrl::process(specifier.as_str()) - .map_err(|_| anyhow!("Unable to decode data url."))?; - let (bytes, _) = url - .decode_to_vec() - .map_err(|_| anyhow!("Unable to decode data url."))?; + pub fn parse(specifier: &ModuleSpecifier) -> Result { + use std::io::Error; + use std::io::ErrorKind; + + fn unable_to_decode() -> Error { + Error::new(ErrorKind::InvalidData, "Unable to decode data url.") + } + + let url = + DataUrl::process(specifier.as_str()).map_err(|_| unable_to_decode())?; + let (bytes, _) = url.decode_to_vec().map_err(|_| unable_to_decode())?; Ok(RawDataUrl { mime_type: url.mime_type().to_string(), bytes, diff --git a/tests/ecosystem_test.rs b/tests/ecosystem_test.rs index eef40350d..9cb1d3b96 100644 --- a/tests/ecosystem_test.rs +++ b/tests/ecosystem_test.rs @@ -282,6 +282,7 @@ async fn test_version( module_analyzer: &module_analyzer, workspace_members: &workspace_members, file_system: &NullFileSystem, + locker: None, resolver: None, npm_resolver: None, reporter: None, diff --git a/tests/helpers/mod.rs b/tests/helpers/mod.rs index af1fa7889..b9ea546fd 100644 --- a/tests/helpers/mod.rs +++ b/tests/helpers/mod.rs @@ -6,9 +6,12 @@ use std::collections::BTreeMap; use deno_ast::ModuleSpecifier; use deno_graph::source::CacheInfo; use deno_graph::source::CacheSetting; +use deno_graph::source::HashMapLocker; use deno_graph::source::LoadFuture; use deno_graph::source::LoadOptions; use deno_graph::source::Loader; +use deno_graph::source::LoaderChecksum; +use deno_graph::source::Locker; use deno_graph::source::MemoryLoader; use deno_graph::source::NpmResolver; use deno_graph::BuildDiagnostic; @@ -71,6 +74,7 @@ pub mod symbols { } pub struct BuildResult { + pub locker: Option, pub graph: ModuleGraph, pub diagnostics: Vec, pub analyzer: deno_graph::CapturingModuleAnalyzer, @@ -155,6 +159,8 @@ impl FastCheckCache for TestFastCheckCache { } pub struct TestBuilder { + locker: Option, + graph: ModuleGraph, loader: TestLoader, entry_point: String, entry_point_types: String, @@ -162,11 +168,14 @@ pub struct TestBuilder { workspace_members: Vec, workspace_fast_check: bool, lockfile_jsr_packages: BTreeMap, + verify_and_fill_checksums: bool, } impl TestBuilder { pub fn new() -> Self { Self { + locker: Default::default(), + graph: ModuleGraph::new(GraphKind::All), loader: Default::default(), entry_point: "file:///mod.ts".to_string(), entry_point_types: "file:///mod.ts".to_string(), @@ -174,6 +183,7 @@ impl TestBuilder { workspace_members: Default::default(), workspace_fast_check: false, lockfile_jsr_packages: Default::default(), + verify_and_fill_checksums: false, } } @@ -227,8 +237,23 @@ impl TestBuilder { self } + #[allow(unused)] + pub fn verify_and_fill_checksums(&mut self, value: bool) -> &mut Self { + self.verify_and_fill_checksums = value; + self + } + + #[allow(unused)] + pub fn add_checksum(&mut self, specifier: &str, checksum: &str) -> &mut Self { + let specifier = ModuleSpecifier::parse(specifier).unwrap(); + let loader_checksum = LoaderChecksum::new(checksum.to_string()); + let checksums = self.locker.get_or_insert_with(Default::default); + checksums.set_checksum(&specifier, loader_checksum); + self + } + pub async fn build(&mut self) -> BuildResult { - let mut graph = deno_graph::ModuleGraph::new(GraphKind::All); + let mut graph = self.graph.clone(); for (req, nv) in &self.lockfile_jsr_packages { graph.packages.add_nv(req.clone(), nv.clone()); } @@ -243,6 +268,7 @@ impl TestBuilder { workspace_members: &self.workspace_members, module_analyzer: &capturing_analyzer, npm_resolver: Some(&TestNpmResolver), + locker: self.locker.as_mut().map(|l| l as _), ..Default::default() }, ) @@ -270,6 +296,7 @@ impl TestBuilder { ); } BuildResult { + locker: self.locker.clone(), graph, diagnostics, analyzer: capturing_analyzer, diff --git a/tests/specs/graph/checksums/invalid.txt b/tests/specs/graph/checksums/invalid.txt new file mode 100644 index 000000000..8e986cbfd --- /dev/null +++ b/tests/specs/graph/checksums/invalid.txt @@ -0,0 +1,53 @@ +~~ { + "checksums": { + "https://localhost/mod.ts": "invalid" + } +} ~~ +# https://localhost/mod.ts +console.log(123); + +# mod.ts +import 'https://localhost/mod.ts' + +# output +{ + "roots": [ + "file:///mod.ts" + ], + "modules": [ + { + "kind": "esm", + "dependencies": [ + { + "specifier": "https://localhost/mod.ts", + "code": { + "specifier": "https://localhost/mod.ts", + "span": { + "start": { + "line": 0, + "character": 7 + }, + "end": { + "line": 0, + "character": 33 + } + } + } + } + ], + "size": 34, + "mediaType": "TypeScript", + "specifier": "file:///mod.ts" + }, + { + "specifier": "https://localhost/mod.ts", + "error": "Integrity check failed.\n\nActual: 57fe7492e0def06ef2f6863f5cab3a5d6ec41ac68f2de7c0e27928cc6fef0ccb\nExpected: invalid" + } + ], + "redirects": {} +} + +checksums: +{ + "https://localhost/mod.ts": "invalid" +} diff --git a/tests/specs/graph/checksums/redirect_with_checksum.txt b/tests/specs/graph/checksums/redirect_with_checksum.txt new file mode 100644 index 000000000..c7d7e0138 --- /dev/null +++ b/tests/specs/graph/checksums/redirect_with_checksum.txt @@ -0,0 +1,56 @@ +~~ { + "checksums": { + "https://localhost/mod.ts": "57fe7492e0def06ef2f6863f5cab3a5d6ec41ac68f2de7c0e27928cc6fef0ccb" + } +} ~~ +# https://localhost/mod.ts +HEADERS: {"location":"./redirected.ts"} + +# https://localhost/redirected.ts +console.log('hi'); + +# mod.ts +import 'https://localhost/mod.ts' + +# output +{ + "roots": [ + "file:///mod.ts" + ], + "modules": [ + { + "kind": "esm", + "dependencies": [ + { + "specifier": "https://localhost/mod.ts", + "code": { + "specifier": "https://localhost/mod.ts", + "span": { + "start": { + "line": 0, + "character": 7 + }, + "end": { + "line": 0, + "character": 33 + } + } + } + } + ], + "size": 34, + "mediaType": "TypeScript", + "specifier": "file:///mod.ts" + }, + { + "specifier": "https://localhost/mod.ts", + "error": "Integrity check failed.\n\nActual: Redirect to https://localhost/redirected.ts\nExpected: 57fe7492e0def06ef2f6863f5cab3a5d6ec41ac68f2de7c0e27928cc6fef0ccb" + } + ], + "redirects": {} +} + +checksums: +{ + "https://localhost/mod.ts": "57fe7492e0def06ef2f6863f5cab3a5d6ec41ac68f2de7c0e27928cc6fef0ccb" +} diff --git a/tests/specs/graph/checksums/redirected_to_invalid.txt b/tests/specs/graph/checksums/redirected_to_invalid.txt new file mode 100644 index 000000000..79e717ca6 --- /dev/null +++ b/tests/specs/graph/checksums/redirected_to_invalid.txt @@ -0,0 +1,58 @@ +~~ { + "checksums": { + "https://localhost/redirected.ts": "invalid" + } +} ~~ +# https://localhost/mod.ts +HEADERS: {"location":"./redirected.ts"} + +# https://localhost/redirected.ts +console.log('hi'); + +# mod.ts +import 'https://localhost/mod.ts' + +# output +{ + "roots": [ + "file:///mod.ts" + ], + "modules": [ + { + "kind": "esm", + "dependencies": [ + { + "specifier": "https://localhost/mod.ts", + "code": { + "specifier": "https://localhost/mod.ts", + "span": { + "start": { + "line": 0, + "character": 7 + }, + "end": { + "line": 0, + "character": 33 + } + } + } + } + ], + "size": 34, + "mediaType": "TypeScript", + "specifier": "file:///mod.ts" + }, + { + "specifier": "https://localhost/redirected.ts", + "error": "Integrity check failed.\n\nActual: 19ef95471e555cd6cbdf54bc242eee9ed916b282f8902bc75e7639955802c458\nExpected: invalid" + } + ], + "redirects": { + "https://localhost/mod.ts": "https://localhost/redirected.ts" + } +} + +checksums: +{ + "https://localhost/redirected.ts": "invalid" +} diff --git a/tests/specs/graph/checksums/valid.txt b/tests/specs/graph/checksums/valid.txt new file mode 100644 index 000000000..9bd8a3d3d --- /dev/null +++ b/tests/specs/graph/checksums/valid.txt @@ -0,0 +1,55 @@ +~~ { + "checksums": { + "https://localhost/mod.ts": "57fe7492e0def06ef2f6863f5cab3a5d6ec41ac68f2de7c0e27928cc6fef0ccb" + } +} ~~ +# https://localhost/mod.ts +console.log(123); + +# mod.ts +import 'https://localhost/mod.ts' + +# output +{ + "roots": [ + "file:///mod.ts" + ], + "modules": [ + { + "kind": "esm", + "dependencies": [ + { + "specifier": "https://localhost/mod.ts", + "code": { + "specifier": "https://localhost/mod.ts", + "span": { + "start": { + "line": 0, + "character": 7 + }, + "end": { + "line": 0, + "character": 33 + } + } + } + } + ], + "size": 34, + "mediaType": "TypeScript", + "specifier": "file:///mod.ts" + }, + { + "kind": "esm", + "size": 18, + "mediaType": "TypeScript", + "specifier": "https://localhost/mod.ts" + } + ], + "redirects": {} +} + +checksums: +{ + "https://localhost/mod.ts": "57fe7492e0def06ef2f6863f5cab3a5d6ec41ac68f2de7c0e27928cc6fef0ccb" +} diff --git a/tests/specs/graph/jsr/Checksum_Unsupported.txt b/tests/specs/graph/jsr/Checksum_Unsupported.txt index ebbc047ac..d151b24b9 100644 --- a/tests/specs/graph/jsr/Checksum_Unsupported.txt +++ b/tests/specs/graph/jsr/Checksum_Unsupported.txt @@ -56,7 +56,7 @@ console.log('HI'); }, { "specifier": "https://jsr.io/@scope/a/1.0.0/mod.ts", - "error": "Unsupported checksum in package manifest. Maybe try upgrading deno?" + "error": "Unsupported checksum in JSR package manifest. Maybe try upgrading deno?" } ], "redirects": { diff --git a/tests/specs/graph/jsr/invalid_export.txt b/tests/specs/graph/jsr/invalid_export.txt index d7d7b5239..a7e5a50ef 100644 --- a/tests/specs/graph/jsr/invalid_export.txt +++ b/tests/specs/graph/jsr/invalid_export.txt @@ -50,7 +50,7 @@ import 'jsr:@scope/a/non_existent' // invalid export }, { "specifier": "jsr:@scope/a/non_existent", - "error": "Unknown export './non_existent' for '@scope/a@1.0.0'.\n Specifier: jsr:@scope/a/non_existent\n Package exports:\n * .\n * ./sub\n * ./other" + "error": "Unknown export './non_existent' for '@scope/a@1.0.0'.\n Package exports:\n * .\n * ./sub\n * ./other" } ], "redirects": {}, diff --git a/tests/specs/graph/jsr/package_not_exist.txt b/tests/specs/graph/jsr/package_not_exist.txt index e4fbc7b37..ec41067b4 100644 --- a/tests/specs/graph/jsr/package_not_exist.txt +++ b/tests/specs/graph/jsr/package_not_exist.txt @@ -33,7 +33,7 @@ import 'jsr:@scope/a/mod.ts'; }, { "specifier": "jsr:@scope/a/mod.ts", - "error": "Not found: https://jsr.io/@scope/a/meta.json" + "error": "JSR package not found: @scope/a" } ], "redirects": {} diff --git a/tests/specs/graph/jsr/redirect_content.txt b/tests/specs/graph/jsr/redirect_content.txt index 285c9bc05..2cb781d0e 100644 --- a/tests/specs/graph/jsr/redirect_content.txt +++ b/tests/specs/graph/jsr/redirect_content.txt @@ -58,7 +58,7 @@ import 'jsr:@scope/a'; }, { "specifier": "https://jsr.io/@scope/a/1.0.0/mod.ts", - "error": "Redirects within a JSR package are not supported." + "error": "Redirects in the JSR registry are not supported (redirected to 'https://jsr.io/@scope/a/1.0.0/other.ts')" } ], "redirects": { diff --git a/tests/specs/graph/jsr/version_not_exist.txt b/tests/specs/graph/jsr/version_not_exist.txt index 928526364..3f9343845 100644 --- a/tests/specs/graph/jsr/version_not_exist.txt +++ b/tests/specs/graph/jsr/version_not_exist.txt @@ -40,7 +40,7 @@ import 'jsr:@scope/a@1.0.1/mod.ts'; }, { "specifier": "jsr:@scope/a@1.0.1/mod.ts", - "error": "Could not find constraint in the list of versions: @scope/a@1.0.1\n Specifier: jsr:@scope/a@1.0.1/mod.ts" + "error": "Could not find version of '@scope/a' that matches specified version constraint '1.0.1'" } ], "redirects": {} diff --git a/tests/specs/graph/jsr/workspace_no_exports.txt b/tests/specs/graph/jsr/workspace_no_exports.txt index e639bc6b5..2755553d1 100644 --- a/tests/specs/graph/jsr/workspace_no_exports.txt +++ b/tests/specs/graph/jsr/workspace_no_exports.txt @@ -79,7 +79,7 @@ console.log("Test"); }, { "specifier": "jsr:@scope/b@2/export", - "error": "Expected workspace package '@scope/b@2.0.0' to define exports in its deno.json.\n Specifier: jsr:@scope/b@2/export" + "error": "Expected workspace package '@scope/b@2.0.0' to define exports in its deno.json." } ], "redirects": {} diff --git a/tests/specs_test.rs b/tests/specs_test.rs index 633ad00fd..d840b0e96 100644 --- a/tests/specs_test.rs +++ b/tests/specs_test.rs @@ -86,6 +86,12 @@ fn run_graph_test(test: &CollectedTest) { if let Some(options) = &spec.options { builder.workspace_fast_check(options.workspace_fast_check); builder.fast_check_cache(options.fast_check_cache); + if let Some(checksums) = options.checksums.as_ref() { + builder.verify_and_fill_checksums(true); + for (specifier, checksum) in checksums { + builder.add_checksum(specifier, checksum); + } + } } let rt = tokio::runtime::Builder::new_current_thread() @@ -95,6 +101,14 @@ fn run_graph_test(test: &CollectedTest) { let result = rt.block_on(async { builder.build().await }); let mut output_text = serde_json::to_string_pretty(&result.graph).unwrap(); output_text.push('\n'); + // include the checksums if non-empty + if let Some(locker) = &result.locker { + let sorted_checksums = locker.inner().iter().collect::>(); + output_text.push_str("\nchecksums:\n"); + output_text + .push_str(&serde_json::to_string_pretty(&sorted_checksums).unwrap()); + output_text.push('\n'); + } // include the list of jsr dependencies let jsr_deps = result .graph @@ -285,6 +299,9 @@ fn run_symbol_test(test: &CollectedTest) { #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SpecOptions { + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub checksums: Option>, #[serde(default)] #[serde(skip_serializing_if = "is_false")] pub workspace_fast_check: bool, @@ -311,7 +328,11 @@ impl Spec { let mut text = String::new(); if let Some(options) = &self.options { text.push_str("~~ "); - text.push_str(&serde_json::to_string(options).unwrap()); + if options.checksums.is_some() { + text.push_str(&serde_json::to_string_pretty(options).unwrap()); + } else { + text.push_str(&serde_json::to_string(options).unwrap()); + } text.push_str(" ~~"); text.push('\n'); } @@ -484,12 +505,13 @@ pub fn parse_spec(text: String) -> Spec { let mut files = Vec::new(); let mut current_file = None; let mut options: Option = None; - for (i, line) in text.split('\n').enumerate() { - if i == 0 && line.starts_with("~~ ") { - let line = line.replace("~~", "").trim().to_string(); // not ideal, being lazy - options = Some(serde_json::from_str(&line).unwrap()); - continue; - } + let mut text = text.as_str(); + if text.starts_with("~~ ") { + let end = text.find(" ~~\n").unwrap(); + options = Some(serde_json::from_str(&text[3..end]).unwrap()); + text = &text[end + 4..]; + } + for line in text.split('\n') { if let Some(specifier) = line.strip_prefix("# ") { if let Some(file) = current_file.take() { files.push(file);