mirror of
https://github.com/denoland/deno.git
synced 2024-11-24 15:19:26 -05:00
291 lines
8.4 KiB
Rust
291 lines
8.4 KiB
Rust
|
// 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::<Vec<_>>();
|
||
|
|
||
|
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::<HashMap<String, String>>(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<PackageAndVersion, AnyError> {
|
||
|
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<PackageAndVersion, AnyError> {
|
||
|
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)
|
||
|
}
|