summaryrefslogtreecommitdiff
path: root/cli/tools/registry/pm.rs
diff options
context:
space:
mode:
authorBartek IwaƄczuk <biwanczuk@gmail.com>2024-05-08 20:34:46 +0100
committerGitHub <noreply@github.com>2024-05-08 12:34:46 -0700
commit4e23a5b1fc2ba0e26f1832a2c374a1f3aef9e7ff (patch)
tree4e71d07cce253b3a8035285e11999c1294643074 /cli/tools/registry/pm.rs
parent525b3c8d746b8eb358ed6466cd4b68ebd6542392 (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.rs242
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,
)