// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::collections::HashMap; use std::path::PathBuf; use deno_ast::TextChange; use deno_config::FmtOptionsConfig; use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::futures::FutureExt; use deno_core::futures::StreamExt; use deno_core::serde_json; use deno_semver::jsr::JsrPackageReqReference; use deno_semver::npm::NpmPackageReqReference; use deno_semver::package::PackageReq; use jsonc_parser::ast::ObjectProp; use jsonc_parser::ast::Value; use crate::args::AddFlags; use crate::args::CacheSetting; use crate::args::Flags; use crate::factory::CliFactory; use crate::file_fetcher::FileFetcher; use crate::lsp::jsr::CliJsrSearchApi; use crate::lsp::search::PackageSearchApi; pub async fn add(flags: Flags, add_flags: AddFlags) -> Result<(), AnyError> { let cli_factory = CliFactory::from_flags(flags.clone()).await?; let cli_options = cli_factory.cli_options(); let Some(config_file) = cli_options.maybe_config_file() else { tokio::fs::write(cli_options.initial_cwd().join("deno.json"), "{}\n") .await .context("Failed to create deno.json file")?; log::info!("Created deno.json configuration file."); return add(flags, add_flags).boxed_local().await; }; if config_file.specifier.scheme() != "file" { bail!("Can't add dependencies to a remote configuration file"); } let config_file_path = config_file.specifier.to_file_path().unwrap(); let http_client = cli_factory.http_client(); let mut selected_packages = Vec::with_capacity(add_flags.packages.len()); let mut package_reqs = Vec::with_capacity(add_flags.packages.len()); for package_name in add_flags.packages.iter() { let req = if package_name.starts_with("npm:") { let pkg_req = NpmPackageReqReference::from_str(package_name) .with_context(|| { format!("Failed to parse package required: {}", package_name) })?; AddPackageReq::Npm(pkg_req) } else { let pkg_req = JsrPackageReqReference::from_str(&format!( "jsr:{}", package_name.strip_prefix("jsr:").unwrap_or(package_name) )) .with_context(|| { format!("Failed to parse package required: {}", package_name) })?; AddPackageReq::Jsr(pkg_req) }; package_reqs.push(req); } let deps_http_cache = cli_factory.global_http_cache()?; let mut deps_file_fetcher = FileFetcher::new( deps_http_cache.clone(), CacheSetting::ReloadAll, true, http_client.clone(), Default::default(), None, ); deps_file_fetcher.set_download_log_level(log::Level::Trace); let jsr_search_api = CliJsrSearchApi::new(deps_file_fetcher); let package_futures = package_reqs .into_iter() .map(|package_req| { find_package_and_select_version_for_req( jsr_search_api.clone(), package_req, ) .boxed_local() }) .collect::>(); let stream_of_futures = deno_core::futures::stream::iter(package_futures); let mut buffered = stream_of_futures.buffer_unordered(10); while let Some(package_and_version_result) = buffered.next().await { let package_and_version = package_and_version_result?; match package_and_version { PackageAndVersion::NotFound(package_name) => { bail!("{} was not found.", crate::colors::red(package_name)); } PackageAndVersion::Selected(selected) => { selected_packages.push(selected); } } } let config_file_contents = tokio::fs::read_to_string(&config_file_path).await.unwrap(); let ast = jsonc_parser::parse_to_ast( &config_file_contents, &Default::default(), &Default::default(), )?; let obj = match ast.value { Some(Value::Object(obj)) => obj, _ => bail!("Failed updating config file due to no object."), }; let mut existing_imports = if let Some(imports) = config_file.json.imports.clone() { match serde_json::from_value::>(imports) { Ok(i) => i, Err(_) => bail!("Malformed \"imports\" configuration"), } } else { HashMap::default() }; for selected_package in selected_packages { log::info!( "Add {} - {}@{}", crate::colors::green(&selected_package.import_name), selected_package.package_name, selected_package.version_req ); existing_imports.insert( selected_package.import_name, format!( "{}@{}", selected_package.package_name, selected_package.version_req ), ); } let mut import_list: Vec<(String, String)> = existing_imports.into_iter().collect(); import_list.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); let generated_imports = generate_imports(import_list); let fmt_config_options = config_file .to_fmt_config() .ok() .flatten() .map(|config| config.options) .unwrap_or_default(); let new_text = update_config_file_content( obj, &config_file_contents, generated_imports, fmt_config_options, ); tokio::fs::write(&config_file_path, new_text) .await .context("Failed to update configuration file")?; // TODO(bartlomieju): we should now cache the imports from the config file. Ok(()) } struct SelectedPackage { import_name: String, package_name: String, version_req: String, } enum PackageAndVersion { NotFound(String), Selected(SelectedPackage), } async fn jsr_find_package_and_select_version( jsr_search_api: CliJsrSearchApi, req: &PackageReq, ) -> Result { let jsr_prefixed_name = format!("jsr:{}", req.name); // TODO(bartlomieju): Need to do semver as well - @luca/flag@^0.14 should use to // highest possible `0.14.x` version. let version_req = req.version_req.version_text(); if version_req != "*" { bail!("Specifying version constraints is currently not supported. Package: {}@{}", jsr_prefixed_name, version_req); } let Ok(versions) = jsr_search_api.versions(&req.name).await else { return Ok(PackageAndVersion::NotFound(jsr_prefixed_name)); }; let Some(latest_version) = versions.first() else { return Ok(PackageAndVersion::NotFound(jsr_prefixed_name)); }; Ok(PackageAndVersion::Selected(SelectedPackage { import_name: req.name.to_string(), package_name: jsr_prefixed_name, // TODO(bartlomieju): fix it, it should not always be caret version_req: format!("^{}", latest_version), })) } async fn find_package_and_select_version_for_req( jsr_search_api: CliJsrSearchApi, add_package_req: AddPackageReq, ) -> Result { match add_package_req { AddPackageReq::Jsr(pkg_ref) => { jsr_find_package_and_select_version(jsr_search_api, pkg_ref.req()).await } AddPackageReq::Npm(pkg_req) => { bail!( "Adding npm: packages is currently not supported. Package: npm:{}", pkg_req.req().name ); } } } enum AddPackageReq { Jsr(JsrPackageReqReference), Npm(NpmPackageReqReference), } fn generate_imports(packages_to_version: Vec<(String, String)>) -> String { let mut contents = vec![]; let len = packages_to_version.len(); for (index, (package, version)) in packages_to_version.iter().enumerate() { // TODO(bartlomieju): fix it, once we start support specifying version on the cli contents.push(format!("\"{}\": \"{}\"", package, version)); if index != len - 1 { contents.push(",".to_string()); } } contents.join("\n") } fn update_config_file_content( obj: jsonc_parser::ast::Object, config_file_contents: &str, generated_imports: String, fmt_options: FmtOptionsConfig, ) -> String { let mut text_changes = vec![]; match obj.get("imports") { Some(ObjectProp { value: Value::Object(lit), .. }) => text_changes.push(TextChange { range: (lit.range.start + 1)..(lit.range.end - 1), new_text: generated_imports, }), None => { let insert_position = obj.range.end - 1; text_changes.push(TextChange { range: insert_position..insert_position, new_text: format!("\"imports\": {{ {} }}", generated_imports), }) } // we verified the shape of `imports` above Some(_) => unreachable!(), } let new_text = deno_ast::apply_text_changes(config_file_contents, text_changes); crate::tools::fmt::format_json( &PathBuf::from("deno.json"), &new_text, &fmt_options, ) .ok() .map(|formatted_text| formatted_text.unwrap_or_else(|| new_text.clone())) .unwrap_or(new_text) }