diff options
Diffstat (limited to 'cli/graph_util.rs')
-rw-r--r-- | cli/graph_util.rs | 356 |
1 files changed, 237 insertions, 119 deletions
diff --git a/cli/graph_util.rs b/cli/graph_util.rs index ed56cf9f7..81ae21d0d 100644 --- a/cli/graph_util.rs +++ b/cli/graph_util.rs @@ -18,6 +18,9 @@ use crate::tools::check; use crate::tools::check::TypeChecker; use crate::util::file_watcher::WatcherCommunicator; use crate::util::fs::canonicalize_path; +use deno_emit::LoaderChecksum; +use deno_graph::JsrLoadError; +use deno_graph::ModuleLoadError; use deno_runtime::fs_util::specifier_to_file_path; use deno_config::WorkspaceMemberConfig; @@ -51,6 +54,9 @@ pub struct GraphValidOptions { pub check_js: bool, pub follow_type_only: bool, pub is_vendoring: bool, + /// Whether to exit the process for lockfile errors. + /// Otherwise, surfaces lockfile errors as errors. + pub exit_lockfile_errors: bool, } /// Check if `roots` and their deps are available. Returns `Ok(())` if @@ -62,10 +68,14 @@ pub struct GraphValidOptions { /// for the CLI. pub fn graph_valid( graph: &ModuleGraph, - fs: Arc<dyn FileSystem>, + fs: &Arc<dyn FileSystem>, roots: &[ModuleSpecifier], options: GraphValidOptions, ) -> Result<(), AnyError> { + if options.exit_lockfile_errors { + graph_exit_lock_errors(graph); + } + let mut errors = graph .walk( roots, @@ -95,8 +105,10 @@ pub fn graph_valid( enhanced_resolution_error_message(resolution_error) ) } - ModuleGraphError::ModuleError(e) => { - enhanced_module_error_message(fs.clone(), e) + ModuleGraphError::ModuleError(error) => { + enhanced_lockfile_error_message(error) + .or_else(|| enhanced_sloppy_imports_error_message(fs, error)) + .unwrap_or_else(|| format!("{}", error)) } }; @@ -152,40 +164,16 @@ pub fn graph_valid( } } -/// Checks the lockfile against the graph and exits on errors. -pub fn graph_lock_or_exit(graph: &ModuleGraph, lockfile: &mut Lockfile) { - for module in graph.modules() { - let source = match module { - Module::Js(module) if module.media_type.is_declaration() => continue, // skip declaration files - Module::Js(module) => &module.source, - Module::Json(module) => &module.source, - Module::Node(_) | Module::Npm(_) | Module::External(_) => continue, - }; - - // skip over any specifiers in JSR packages because those - // are enforced via the integrity - if deno_graph::source::recommended_registry_package_url_to_nv( - jsr_url(), - module.specifier(), - ) - .is_some() - { - continue; - } +pub fn graph_exit_lock_errors(graph: &ModuleGraph) { + for error in graph.module_errors() { + exit_for_lockfile_error(error); + } +} - if !lockfile.check_or_insert_remote(module.specifier().as_str(), source) { - let err = format!( - concat!( - "The source code is invalid, as it does not match the expected hash in the lock file.\n", - " Specifier: {}\n", - " Lock file: {}", - ), - module.specifier(), - lockfile.filename.display(), - ); - log::error!("{} {}", colors::red("error:"), err); - std::process::exit(10); - } +fn exit_for_lockfile_error(err: &ModuleError) { + if let Some(err_message) = enhanced_lockfile_error_message(err) { + log::error!("{} {}", colors::red("error:"), err_message); + std::process::exit(10); } } @@ -201,7 +189,6 @@ pub struct ModuleGraphCreator { options: Arc<CliOptions>, npm_resolver: Arc<dyn CliNpmResolver>, module_graph_builder: Arc<ModuleGraphBuilder>, - lockfile: Option<Arc<Mutex<Lockfile>>>, type_checker: Arc<TypeChecker>, } @@ -210,13 +197,11 @@ impl ModuleGraphCreator { options: Arc<CliOptions>, npm_resolver: Arc<dyn CliNpmResolver>, module_graph_builder: Arc<ModuleGraphBuilder>, - lockfile: Option<Arc<Mutex<Lockfile>>>, type_checker: Arc<TypeChecker>, ) -> Self { Self { options, npm_resolver, - lockfile, module_graph_builder, type_checker, } @@ -317,9 +302,6 @@ impl ModuleGraphCreator { .await?; self.graph_valid(&graph)?; - if let Some(lockfile) = &self.lockfile { - graph_lock_or_exit(&graph, &mut lockfile.lock()); - } if self.options.type_check_mode().is_true() { // provide the graph to the type checker, then get it back after it's done @@ -426,6 +408,67 @@ impl ModuleGraphBuilder { } } + struct LockfileLocker<'a>(&'a Mutex<Lockfile>); + + impl<'a> deno_graph::source::Locker for LockfileLocker<'a> { + fn get_remote_checksum( + &self, + specifier: &deno_ast::ModuleSpecifier, + ) -> Option<LoaderChecksum> { + self + .0 + .lock() + .remote() + .get(specifier.as_str()) + .map(|s| LoaderChecksum::new(s.clone())) + } + + fn has_remote_checksum( + &self, + specifier: &deno_ast::ModuleSpecifier, + ) -> bool { + self.0.lock().remote().contains_key(specifier.as_str()) + } + + fn set_remote_checksum( + &mut self, + specifier: &deno_ast::ModuleSpecifier, + checksum: LoaderChecksum, + ) { + self + .0 + .lock() + .insert_remote(specifier.to_string(), checksum.into_string()) + } + + fn get_pkg_manifest_checksum( + &self, + package_nv: &PackageNv, + ) -> Option<LoaderChecksum> { + self + .0 + .lock() + .content + .packages + .jsr + .get(&package_nv.to_string()) + .map(|s| LoaderChecksum::new(s.integrity.clone())) + } + + fn set_pkg_manifest_checksum( + &mut self, + package_nv: &PackageNv, + checksum: LoaderChecksum, + ) { + // a value would only exist in here if two workers raced + // to insert the same package manifest checksum + self + .0 + .lock() + .insert_package(package_nv.to_string(), checksum.into_string()); + } + } + let maybe_imports = self.options.to_maybe_imports()?; let parser = self.parsed_source_cache.as_capturing_parser(); let analyzer = self.module_info_cache.as_module_analyzer(&parser); @@ -442,6 +485,10 @@ impl ModuleGraphBuilder { .map(|r| r.as_reporter()); let workspace_members = self.options.resolve_deno_graph_workspace_members()?; + let mut locker = self + .lockfile + .as_ref() + .map(|lockfile| LockfileLocker(lockfile)); self .build_graph_with_npm_resolution_and_build_options( graph, @@ -459,6 +506,7 @@ impl ModuleGraphBuilder { module_analyzer: &analyzer, reporter: maybe_file_watcher_reporter, resolver: Some(graph_resolver), + locker: locker.as_mut().map(|l| l as _), }, ) .await @@ -468,7 +516,7 @@ impl ModuleGraphBuilder { &self, graph: &mut ModuleGraph, roots: Vec<ModuleSpecifier>, - loader: &mut dyn deno_graph::source::Loader, + loader: &'a mut dyn deno_graph::source::Loader, options: deno_graph::BuildOptions<'a>, ) -> Result<(), AnyError> { // ensure an "npm install" is done if the user has explicitly @@ -479,8 +527,10 @@ impl ModuleGraphBuilder { } } - // add the lockfile redirects to the graph if it's the first time executing - if graph.redirects.is_empty() { + // fill the graph with the information from the lockfile + let is_first_execution = graph.roots.is_empty(); + if is_first_execution { + // populate the information from the lockfile if let Some(lockfile) = &self.lockfile { let lockfile = lockfile.lock(); for (from, to) in &lockfile.content.redirects { @@ -492,13 +542,6 @@ impl ModuleGraphBuilder { } } } - } - } - - // add the jsr specifiers to the graph if it's the first time executing - if graph.packages.is_empty() { - if let Some(lockfile) = &self.lockfile { - let lockfile = lockfile.lock(); for (key, value) in &lockfile.content.packages.specifiers { if let Some(key) = key .strip_prefix("jsr:") @@ -512,59 +555,15 @@ impl ModuleGraphBuilder { } } } - for (nv, value) in &lockfile.content.packages.jsr { - if let Ok(nv) = PackageNv::from_str(nv) { - graph - .packages - .add_manifest_checksum(nv, value.integrity.clone()) - .map_err(|err| deno_lockfile::IntegrityCheckFailedError { - package_display_id: format!("jsr:{}", err.nv), - actual: err.actual, - expected: err.expected, - filename: lockfile.filename.display().to_string(), - })?; - } - } } } - graph.build(roots, loader, options).await; - - // add the redirects in the graph to the lockfile - if !graph.redirects.is_empty() { - if let Some(lockfile) = &self.lockfile { - let graph_redirects = graph.redirects.iter().filter(|(from, _)| { - !matches!(from.scheme(), "npm" | "file" | "deno") - }); - let mut lockfile = lockfile.lock(); - for (from, to) in graph_redirects { - lockfile.insert_redirect(from.to_string(), to.to_string()); - } - } - } + let initial_redirects_len = graph.redirects.len(); + let initial_package_deps_len = graph.packages.package_deps_sum(); + let initial_package_mappings_len = graph.packages.mappings().len(); + let initial_npm_packages = graph.npm_packages.len(); - // add the jsr specifiers in the graph to the lockfile - if !graph.packages.is_empty() { - if let Some(lockfile) = &self.lockfile { - let mappings = graph.packages.mappings(); - let mut lockfile = lockfile.lock(); - for (from, to) in mappings { - lockfile.insert_package_specifier( - format!("jsr:{}", from), - format!("jsr:{}", to), - ); - } - for (name, checksum, deps) in - graph.packages.packages_with_checksum_and_deps() - { - lockfile.insert_package( - name.to_string(), - checksum.clone(), - deps.map(|s| s.to_string()), - ); - } - } - } + graph.build(roots, loader, options).await; if let Some(npm_resolver) = self.npm_resolver.as_managed() { // ensure that the top level package.json is installed if a @@ -578,6 +577,53 @@ impl ModuleGraphBuilder { npm_resolver.resolve_pending().await?; } + let has_redirects_changed = graph.redirects.len() != initial_redirects_len; + let has_jsr_package_deps_changed = + graph.packages.package_deps_sum() != initial_package_deps_len; + let has_jsr_package_mappings_changed = + graph.packages.mappings().len() != initial_package_mappings_len; + let has_npm_packages_changed = + graph.npm_packages.len() != initial_npm_packages; + + if has_redirects_changed + || has_jsr_package_deps_changed + || has_jsr_package_mappings_changed + || has_npm_packages_changed + { + if let Some(lockfile) = &self.lockfile { + let mut lockfile = lockfile.lock(); + // https redirects + if has_redirects_changed { + let graph_redirects = graph.redirects.iter().filter(|(from, _)| { + !matches!(from.scheme(), "npm" | "file" | "deno") + }); + for (from, to) in graph_redirects { + lockfile.insert_redirect(from.to_string(), to.to_string()); + } + } + // jsr package mappings + if has_jsr_package_mappings_changed { + for (from, to) in graph.packages.mappings() { + lockfile.insert_package_specifier( + format!("jsr:{}", from), + format!("jsr:{}", to), + ); + } + } + // jsr packages + if has_jsr_package_deps_changed { + for (name, deps) in graph.packages.packages_with_deps() { + lockfile + .add_package_deps(&name.to_string(), deps.map(|s| s.to_string())); + } + } + // npm packages + if has_npm_packages_changed { + self.npm_resolver.as_managed().unwrap().lock(&mut lockfile); + } + } + } + Ok(()) } @@ -658,12 +704,13 @@ impl ModuleGraphBuilder { ) -> Result<(), AnyError> { graph_valid( graph, - self.fs.clone(), + &self.fs, roots, GraphValidOptions { is_vendoring: false, follow_type_only: self.options.type_check_mode().is_true(), check_js: self.options.check_js(), + exit_lockfile_errors: true, }, ) } @@ -701,28 +748,99 @@ pub fn enhanced_resolution_error_message(error: &ResolutionError) -> String { message } -pub fn enhanced_module_error_message( - fs: Arc<dyn FileSystem>, +fn enhanced_sloppy_imports_error_message( + fs: &Arc<dyn FileSystem>, error: &ModuleError, -) -> String { - let additional_message = match error { - ModuleError::LoadingErr(specifier, _, _) // ex. "Is a directory" error +) -> Option<String> { + match error { + ModuleError::LoadingErr(specifier, _, ModuleLoadError::Loader(_)) // ex. "Is a directory" error | ModuleError::Missing(specifier, _) => { - SloppyImportsResolver::new(fs).resolve( + let additional_message = SloppyImportsResolver::new(fs.clone()) + .resolve(specifier, ResolutionMode::Execution) + .as_suggestion_message()?; + Some(format!( + "{} {} or run with --unstable-sloppy-imports", + error, + additional_message, + )) + } + _ => None, + } +} + +fn enhanced_lockfile_error_message(err: &ModuleError) -> Option<String> { + match err { + ModuleError::LoadingErr( + specifier, + _, + ModuleLoadError::Jsr(JsrLoadError::ContentChecksumIntegrity( + checksum_err, + )), + ) => { + Some(format!( + concat!( + "Integrity check failed in package. The package may have been tampered with.\n\n", + " Specifier: {}\n", + " Actual: {}\n", + " Expected: {}\n\n", + "If you modified your global cache, run again with the --reload flag to restore ", + "its state. If you want to modify dependencies locally run again with the ", + "--vendor flag or specify `\"vendor\": true` in a deno.json then modify the contents ", + "of the vendor/ folder." + ), specifier, - ResolutionMode::Execution, - ) - .as_suggestion_message() + checksum_err.actual, + checksum_err.expected, + )) + } + ModuleError::LoadingErr( + _specifier, + _, + ModuleLoadError::Jsr( + JsrLoadError::PackageVersionManifestChecksumIntegrity( + package_nv, + checksum_err, + ), + ), + ) => { + Some(format!( + concat!( + "Integrity check failed for package. The source code is invalid, as it does not match the expected hash in the lock file.\n\n", + " Package: {}\n", + " Actual: {}\n", + " Expected: {}\n\n", + "This could be caused by:\n", + " * the lock file may be corrupt\n", + " * the source itself may be corrupt\n\n", + "Use the --lock-write flag to regenerate the lockfile or --reload to reload the source code from the server." + ), + package_nv, + checksum_err.actual, + checksum_err.expected, + )) + } + ModuleError::LoadingErr( + specifier, + _, + ModuleLoadError::HttpsChecksumIntegrity(checksum_err), + ) => { + Some(format!( + concat!( + "Integrity check failed for remote specifier. The source code is invalid, as it does not match the expected hash in the lock file.\n\n", + " Specifier: {}\n", + " Actual: {}\n", + " Expected: {}\n\n", + "This could be caused by:\n", + " * the lock file may be corrupt\n", + " * the source itself may be corrupt\n\n", + "Use the --lock-write flag to regenerate the lockfile or --reload to reload the source code from the server." + ), + specifier, + checksum_err.actual, + checksum_err.expected, + )) } _ => None, - }; - if let Some(message) = additional_message { - format!( - "{} {} or run with --unstable-sloppy-imports", - error, message - ) - } else { - format!("{}", error) } } |