diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2024-05-08 20:34:46 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-08 12:34:46 -0700 |
commit | 4e23a5b1fc2ba0e26f1832a2c374a1f3aef9e7ff (patch) | |
tree | 4e71d07cce253b3a8035285e11999c1294643074 /cli/tools/registry/pm.rs | |
parent | 525b3c8d746b8eb358ed6466cd4b68ebd6542392 (diff) |
FUTURE: `deno install` changes (#23498)
This PR implements the changes we plan to make to `deno install` in deno
2.0.
- `deno install` without arguments caches dependencies from
`package.json` / `deno.json` and sets up the `node_modules` folder
- `deno install <pkg>` adds the package to the config file (either
`package.json` or `deno.json`), i.e. it aliases `deno add`
- `deno add` can also add deps to `package.json` (this is gated behind
`DENO_FUTURE` due to uncertainty around handling projects with both
`deno.json` and `package.json`)
- `deno install -g <bin>` installs a package as a globally available
binary (the same as `deno install <bin>` in 1.0)
---------
Co-authored-by: Nathan Whitaker <nathan@deno.com>
Diffstat (limited to 'cli/tools/registry/pm.rs')
-rw-r--r-- | cli/tools/registry/pm.rs | 242 |
1 files changed, 198 insertions, 44 deletions
diff --git a/cli/tools/registry/pm.rs b/cli/tools/registry/pm.rs index 699b476cb..e37ee3d82 100644 --- a/cli/tools/registry/pm.rs +++ b/cli/tools/registry/pm.rs @@ -1,19 +1,23 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use std::collections::HashMap; +use std::borrow::Cow; use std::path::PathBuf; use std::sync::Arc; use deno_ast::TextChange; use deno_config::FmtOptionsConfig; +use deno_core::anyhow::anyhow; 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_core::ModuleSpecifier; +use deno_runtime::deno_node; use deno_semver::jsr::JsrPackageReqReference; use deno_semver::npm::NpmPackageReqReference; +use indexmap::IndexMap; use jsonc_parser::ast::ObjectProp; use jsonc_parser::ast::Value; @@ -25,22 +29,164 @@ use crate::file_fetcher::FileFetcher; use crate::jsr::JsrFetchResolver; use crate::npm::NpmFetchResolver; +enum DenoConfigFormat { + Json, + Jsonc, +} + +impl DenoConfigFormat { + fn from_specifier(spec: &ModuleSpecifier) -> Result<Self, AnyError> { + let file_name = spec + .path_segments() + .ok_or_else(|| anyhow!("Empty path in deno config specifier: {spec}"))? + .last() + .unwrap(); + match file_name { + "deno.json" => Ok(Self::Json), + "deno.jsonc" => Ok(Self::Jsonc), + _ => bail!("Unsupported deno config file: {file_name}"), + } + } +} + +enum DenoOrPackageJson { + Deno(deno_config::ConfigFile, DenoConfigFormat), + Npm(deno_node::PackageJson, Option<FmtOptionsConfig>), +} + +impl DenoOrPackageJson { + fn specifier(&self) -> Cow<ModuleSpecifier> { + match self { + Self::Deno(d, ..) => Cow::Borrowed(&d.specifier), + Self::Npm(n, ..) => Cow::Owned(n.specifier()), + } + } + + /// Returns the existing imports/dependencies from the config. + fn existing_imports(&self) -> Result<IndexMap<String, String>, AnyError> { + match self { + DenoOrPackageJson::Deno(deno, ..) => { + if let Some(imports) = deno.json.imports.clone() { + match serde_json::from_value(imports) { + Ok(map) => Ok(map), + Err(err) => { + bail!("Malformed \"imports\" configuration: {err}") + } + } + } else { + Ok(Default::default()) + } + } + DenoOrPackageJson::Npm(npm, ..) => { + Ok(npm.dependencies.clone().unwrap_or_default()) + } + } + } + + fn fmt_options(&self) -> FmtOptionsConfig { + match self { + DenoOrPackageJson::Deno(deno, ..) => deno + .to_fmt_config() + .ok() + .flatten() + .map(|f| f.options) + .unwrap_or_default(), + DenoOrPackageJson::Npm(_, config) => config.clone().unwrap_or_default(), + } + } + + fn imports_key(&self) -> &'static str { + match self { + DenoOrPackageJson::Deno(..) => "imports", + DenoOrPackageJson::Npm(..) => "dependencies", + } + } + + fn file_name(&self) -> &'static str { + match self { + DenoOrPackageJson::Deno(_, format) => match format { + DenoConfigFormat::Json => "deno.json", + DenoConfigFormat::Jsonc => "deno.jsonc", + }, + DenoOrPackageJson::Npm(..) => "package.json", + } + } + + fn is_npm(&self) -> bool { + matches!(self, Self::Npm(..)) + } + + /// Get the preferred config file to operate on + /// given the flags. If no config file is present, + /// creates a `deno.json` file - in this case + /// we also return a new `CliFactory` that knows about + /// the new config + fn from_flags(flags: Flags) -> Result<(Self, CliFactory), AnyError> { + let factory = CliFactory::from_flags(flags.clone())?; + let options = factory.cli_options().clone(); + + match (options.maybe_config_file(), options.maybe_package_json()) { + // when both are present, for now, + // default to deno.json + (Some(deno), Some(_) | None) => Ok(( + DenoOrPackageJson::Deno( + deno.clone(), + DenoConfigFormat::from_specifier(&deno.specifier)?, + ), + factory, + )), + (None, Some(package_json)) if options.enable_future_features() => { + Ok((DenoOrPackageJson::Npm(package_json.clone(), None), factory)) + } + (None, Some(_) | None) => { + std::fs::write(options.initial_cwd().join("deno.json"), "{}\n") + .context("Failed to create deno.json file")?; + log::info!("Created deno.json configuration file."); + let new_factory = CliFactory::from_flags(flags.clone())?; + let new_options = new_factory.cli_options().clone(); + Ok(( + DenoOrPackageJson::Deno( + new_options + .maybe_config_file() + .as_ref() + .ok_or_else(|| { + anyhow!("config not found, but it was just created") + })? + .clone(), + DenoConfigFormat::Json, + ), + new_factory, + )) + } + } + } +} + +fn package_json_dependency_entry( + selected: SelectedPackage, +) -> (String, String) { + if let Some(npm_package) = selected.package_name.strip_prefix("npm:") { + (npm_package.into(), selected.version_req) + } else if let Some(jsr_package) = selected.package_name.strip_prefix("jsr:") { + let jsr_package = jsr_package.strip_prefix('@').unwrap_or(jsr_package); + let scope_replaced = jsr_package.replace('/', "__"); + let version_req = + format!("npm:@jsr/{scope_replaced}@{}", selected.version_req); + (selected.import_name, version_req) + } else { + (selected.package_name, selected.version_req) + } +} + pub async fn add(flags: Flags, add_flags: AddFlags) -> Result<(), AnyError> { - let cli_factory = CliFactory::from_flags(flags.clone())?; - 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; - }; + let (config_file, cli_factory) = + DenoOrPackageJson::from_flags(flags.clone())?; - if config_file.specifier.scheme() != "file" { + let config_specifier = config_file.specifier(); + if config_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 config_file_path = config_specifier.to_file_path().unwrap(); let http_client = cli_factory.http_client(); @@ -49,10 +195,13 @@ pub async fn add(flags: Flags, add_flags: AddFlags) -> Result<(), AnyError> { 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) - })?; + let pkg_req = NpmPackageReqReference::from_str(&format!( + "npm:{}", + package_name.strip_prefix("npm:").unwrap_or(package_name) + )) + .with_context(|| { + format!("Failed to parse package required: {}", package_name) + })?; AddPackageReq::Npm(pkg_req) } else { let pkg_req = JsrPackageReqReference::from_str(&format!( @@ -128,16 +277,9 @@ pub async fn add(flags: Flags, add_flags: AddFlags) -> Result<(), AnyError> { _ => 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::<HashMap<String, String>>(imports) { - Ok(i) => i, - Err(_) => bail!("Malformed \"imports\" configuration"), - } - } else { - HashMap::default() - }; + let mut existing_imports = config_file.existing_imports()?; + let is_npm = config_file.is_npm(); for selected_package in selected_packages { log::info!( "Add {} - {}@{}", @@ -145,13 +287,19 @@ pub async fn add(flags: Flags, add_flags: AddFlags) -> Result<(), AnyError> { selected_package.package_name, selected_package.version_req ); - existing_imports.insert( - selected_package.import_name, - format!( - "{}@{}", - selected_package.package_name, selected_package.version_req - ), - ); + + if is_npm { + let (name, version) = package_json_dependency_entry(selected_package); + existing_imports.insert(name, version) + } else { + 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(); @@ -159,25 +307,29 @@ pub async fn add(flags: Flags, add_flags: AddFlags) -> Result<(), AnyError> { 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 fmt_config_options = config_file.fmt_options(); let new_text = update_config_file_content( obj, &config_file_contents, generated_imports, fmt_config_options, + config_file.imports_key(), + config_file.file_name(), ); 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. + // TODO(bartlomieju): we should now cache the imports from the deno.json. + + // make a new CliFactory to pick up the updated config file + let cli_factory = CliFactory::from_flags(flags)?; + // cache deps + if cli_factory.cli_options().enable_future_features() { + crate::module_loader::load_top_level_deps(&cli_factory).await?; + } Ok(()) } @@ -259,10 +411,12 @@ fn update_config_file_content( config_file_contents: &str, generated_imports: String, fmt_options: FmtOptionsConfig, + imports_key: &str, + file_name: &str, ) -> String { let mut text_changes = vec![]; - match obj.get("imports") { + match obj.get(imports_key) { Some(ObjectProp { value: Value::Object(lit), .. @@ -282,10 +436,10 @@ fn update_config_file_content( // "<package_name>": "<registry>:<package_name>@<semver>" // } // } - new_text: format!("\"imports\": {{\n {} }}", generated_imports), + new_text: format!("\"{imports_key}\": {{\n {generated_imports} }}"), }) } - // we verified the shape of `imports` above + // we verified the shape of `imports`/`dependencies` above Some(_) => unreachable!(), } @@ -293,7 +447,7 @@ fn update_config_file_content( deno_ast::apply_text_changes(config_file_contents, text_changes); crate::tools::fmt::format_json( - &PathBuf::from("deno.json"), + &PathBuf::from(file_name), &new_text, &fmt_options, ) |