deno/cli/tools/installer/mod.rs
Bartek Iwańczuk 9a93c65ac8
Some checks are pending
ci / bench release linux-x86_64 (push) Blocked by required conditions
ci / lint debug linux-x86_64 (push) Blocked by required conditions
ci / lint debug macos-x86_64 (push) Blocked by required conditions
ci / lint debug windows-x86_64 (push) Blocked by required conditions
ci / test debug linux-x86_64 (push) Blocked by required conditions
ci / test release linux-x86_64 (push) Blocked by required conditions
ci / test debug macos-x86_64 (push) Blocked by required conditions
ci / test release macos-x86_64 (push) Blocked by required conditions
ci / test debug windows-x86_64 (push) Blocked by required conditions
ci / test release windows-x86_64 (push) Blocked by required conditions
ci / build libs (push) Blocked by required conditions
ci / pre-build (push) Waiting to run
ci / test debug linux-aarch64 (push) Blocked by required conditions
ci / test release linux-aarch64 (push) Blocked by required conditions
ci / test debug macos-aarch64 (push) Blocked by required conditions
ci / test release macos-aarch64 (push) Blocked by required conditions
ci / publish canary (push) Blocked by required conditions
feat(install): refresh install report (#30615)
Changes report format for `deno install` and other related package
management subcommands:
```
$ deno install
Installed 335 packages in 6s
Reused 40 packages from cache
++++++++++++++++++++++++++++++++++++++++
Downloaded 2 packages from JSR
++
Downloaded 666 packages from npm
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Dependencies:
+ jsr:@std/cli 1.0.21
+ jsr:@std/html 1.0.4
+ npm:@babel/core 7.28.3
+ npm:@babel/preset-react 7.27.1
+ npm:@mjackson/node-fetch-server 0.7.0
+ npm:@opentelemetry/api 1.9.0
+ npm:@preact/signals 2.3.1
```
2025-09-08 09:44:22 +00:00

1877 lines
52 KiB
Rust
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2018-2025 the Deno authors. MIT license.
use std::collections::HashSet;
use std::env;
use std::fs;
use std::fs::File;
use std::io;
use std::io::Write;
#[cfg(not(windows))]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use dashmap::DashSet;
use deno_cache_dir::file_fetcher::CacheSetting;
use deno_core::anyhow::Context;
use deno_core::anyhow::anyhow;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use deno_core::parking_lot::Mutex;
use deno_core::url::Url;
use deno_lib::args::CaData;
use deno_npm_installer::lifecycle_scripts::LifecycleScriptsWarning;
use deno_path_util::resolve_url_or_path;
use deno_resolver::workspace::WorkspaceResolver;
use deno_semver::npm::NpmPackageReqReference;
use log::Level;
use once_cell::sync::Lazy;
use regex::Regex;
use regex::RegexBuilder;
pub use self::bin_name_resolver::BinNameResolver;
use crate::args::AddFlags;
use crate::args::ConfigFlag;
use crate::args::Flags;
use crate::args::InstallFlags;
use crate::args::InstallFlagsGlobal;
use crate::args::InstallFlagsLocal;
use crate::args::TypeCheckMode;
use crate::args::UninstallFlags;
use crate::args::UninstallKind;
use crate::args::resolve_no_prompt;
use crate::factory::CliFactory;
use crate::file_fetcher::CreateCliFileFetcherOptions;
use crate::file_fetcher::create_cli_file_fetcher;
use crate::graph_container::CollectSpecifiersOptions;
use crate::graph_container::ModuleGraphContainer;
use crate::jsr::JsrFetchResolver;
use crate::npm::CliNpmResolver;
use crate::npm::NpmFetchResolver;
use crate::sys::CliSys;
use crate::util::display;
use crate::util::fs::canonicalize_path_maybe_not_exists;
mod bin_name_resolver;
#[derive(Debug, Default)]
pub struct Count {
value: AtomicUsize,
}
impl Count {
pub fn inc(&self) {
self.value.fetch_add(1, Ordering::Relaxed);
}
pub fn get(&self) -> usize {
self.value.load(Ordering::Relaxed)
}
}
#[derive(Default)]
pub struct InstallStats {
pub resolved_jsr: DashSet<String>,
pub downloaded_jsr: DashSet<String>,
pub reused_jsr: DashSet<String>,
pub resolved_npm: DashSet<String>,
pub downloaded_npm: Count,
pub intialized_npm: DashSet<String>,
pub reused_npm: Count,
}
impl std::fmt::Debug for InstallStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InstallStats")
.field(
"resolved_jsr",
&self
.resolved_jsr
.iter()
.map(|s| s.as_str().to_string())
.collect::<Vec<_>>(),
)
.field(
"downloaded_jsr",
&self
.downloaded_jsr
.iter()
.map(|s| s.as_str().to_string())
.collect::<Vec<_>>(),
)
.field("resolved_npm", &self.resolved_npm.len())
.field("resolved_jsr_count", &self.resolved_jsr.len())
.field("downloaded_npm", &self.downloaded_npm.get())
.field("downloaded_jsr_count", &self.downloaded_jsr.len())
.field(
"intialized_npm",
&self
.intialized_npm
.iter()
.map(|s| s.as_str().to_string())
.collect::<Vec<_>>(),
)
.field("intialized_npm_count", &self.intialized_npm.len())
.field("reused_npm", &self.reused_npm.get())
.finish()
}
}
#[derive(Debug)]
pub struct InstallReporter {
stats: Arc<InstallStats>,
scripts_warnings: Arc<Mutex<Vec<LifecycleScriptsWarning>>>,
deprecation_messages: Arc<Mutex<Vec<String>>>,
}
impl InstallReporter {
pub fn new() -> Self {
Self {
stats: Arc::new(InstallStats::default()),
scripts_warnings: Arc::new(Mutex::new(Vec::new())),
deprecation_messages: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn take_scripts_warnings(&self) -> Vec<LifecycleScriptsWarning> {
std::mem::take(&mut *self.scripts_warnings.lock())
}
pub fn take_deprecation_message(&self) -> Vec<String> {
std::mem::take(&mut *self.deprecation_messages.lock())
}
}
impl deno_npm_installer::InstallProgressReporter for InstallReporter {
fn initializing(&self, _nv: &deno_semver::package::PackageNv) {
// log::info!("initializing: {}", nv);
}
fn initialized(&self, nv: &deno_semver::package::PackageNv) {
// log::info!("initialized: {}", nv);
self.stats.intialized_npm.insert(nv.to_string());
}
fn blocking(&self, _message: &str) {
// log::info!("blocking: {}", message);
}
fn scripts_not_run_warning(
&self,
warning: deno_npm_installer::lifecycle_scripts::LifecycleScriptsWarning,
) {
self.scripts_warnings.lock().push(warning);
}
fn deprecated_message(&self, message: String) {
self.deprecation_messages.lock().push(message);
}
}
fn package_nv_from_url(url: &Url) -> Option<String> {
if !matches!(url.scheme(), "http" | "https") {
return None;
}
if !url.host_str().is_some_and(|h| h.contains("jsr.io")) {
return None;
}
let mut parts = url.path_segments()?;
let scope = parts.next()?;
let name = parts.next()?;
let version = parts.next()?;
if version.ends_with(".json") {
// don't include meta.json urls
return None;
}
Some(format!("{scope}/{name}@{version}"))
}
impl deno_graph::source::Reporter for InstallReporter {
fn on_resolve(
&self,
_req: &deno_semver::package::PackageReq,
package_nv: &deno_semver::package::PackageNv,
) {
self.stats.resolved_jsr.insert(package_nv.to_string());
}
}
impl deno_npm::resolution::Reporter for InstallReporter {
fn on_resolved(
&self,
_package_req: &deno_semver::package::PackageReq,
_nv: &deno_semver::package::PackageNv,
) {
self.stats.resolved_npm.insert(_package_req.to_string());
}
}
impl deno_npm_cache::TarballCacheReporter for InstallReporter {
fn download_started(&self, _nv: &deno_semver::package::PackageNv) {}
fn downloaded(&self, _nv: &deno_semver::package::PackageNv) {
self.stats.downloaded_npm.inc();
}
fn reused_cache(&self, _nv: &deno_semver::package::PackageNv) {
self.stats.reused_npm.inc();
}
}
impl deno_resolver::file_fetcher::GraphLoaderReporter for InstallReporter {
fn on_load(
&self,
specifier: &Url,
loaded_from: deno_cache_dir::file_fetcher::LoadedFrom,
) {
if let Some(nv) = package_nv_from_url(specifier) {
match loaded_from {
deno_cache_dir::file_fetcher::LoadedFrom::Cache => {
self.stats.reused_jsr.insert(nv);
}
deno_cache_dir::file_fetcher::LoadedFrom::Remote => {
self.stats.downloaded_jsr.insert(nv);
}
_ => {}
}
} else {
// it's a local file or http/https specifier
}
}
}
static EXEC_NAME_RE: Lazy<Regex> = Lazy::new(|| {
RegexBuilder::new(r"^[a-z0-9][\w-]*$")
.case_insensitive(true)
.build()
.expect("invalid regex")
});
fn validate_name(exec_name: &str) -> Result<(), AnyError> {
if EXEC_NAME_RE.is_match(exec_name) {
Ok(())
} else {
Err(anyhow!("Invalid executable name: {exec_name}"))
}
}
#[cfg(windows)]
/// On Windows, 2 files are generated.
/// One compatible with cmd & powershell with a .cmd extension
/// A second compatible with git bash / MINGW64
/// Generate batch script to satisfy that.
fn generate_executable_file(shim_data: &ShimData) -> Result<(), AnyError> {
let args: Vec<String> =
shim_data.args.iter().map(|c| format!("\"{c}\"")).collect();
let template = format!(
"% generated by deno install %\n@deno {} %*\n",
args
.iter()
.map(|arg| arg.replace('%', "%%"))
.collect::<Vec<_>>()
.join(" ")
);
let mut file = File::create(&shim_data.file_path)?;
file.write_all(template.as_bytes())?;
// write file for bash
// create filepath without extensions
let template = format!(
r#"#!/bin/sh
# generated by deno install
deno {} "$@"
"#,
args.join(" "),
);
let mut file = File::create(shim_data.file_path.with_extension(""))?;
file.write_all(template.as_bytes())?;
Ok(())
}
#[cfg(not(windows))]
fn generate_executable_file(shim_data: &ShimData) -> Result<(), AnyError> {
use shell_escape::escape;
let args: Vec<String> = shim_data
.args
.iter()
.map(|c| escape(c.into()).into_owned())
.collect();
let template = format!(
r#"#!/bin/sh
# generated by deno install
exec deno {} "$@"
"#,
args.join(" "),
);
let mut file = File::create(&shim_data.file_path)?;
file.write_all(template.as_bytes())?;
let _metadata = fs::metadata(&shim_data.file_path)?;
let mut permissions = _metadata.permissions();
permissions.set_mode(0o755);
fs::set_permissions(&shim_data.file_path, permissions)?;
Ok(())
}
fn get_installer_bin_dir(
cwd: &Path,
root_flag: Option<&str>,
) -> Result<PathBuf, AnyError> {
let root = if let Some(root) = root_flag {
canonicalize_path_maybe_not_exists(&cwd.join(root))?
} else {
get_installer_root()?
};
Ok(if !root.ends_with("bin") {
root.join("bin")
} else {
root
})
}
fn get_installer_root() -> Result<PathBuf, AnyError> {
if let Some(env_dir) = env::var_os("DENO_INSTALL_ROOT")
&& !env_dir.is_empty()
{
let env_dir = PathBuf::from(env_dir);
return canonicalize_path_maybe_not_exists(&env_dir).with_context(|| {
format!(
"Canonicalizing DENO_INSTALL_ROOT ('{}').",
env_dir.display()
)
});
}
// Note: on Windows, the $HOME environment variable may be set by users or by
// third party software, but it is non-standard and should not be relied upon.
let home_env_var = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
let mut home_path =
env::var_os(home_env_var)
.map(PathBuf::from)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("${home_env_var} is not defined"),
)
})?;
home_path.push(".deno");
Ok(home_path)
}
pub async fn uninstall(
flags: Arc<Flags>,
uninstall_flags: UninstallFlags,
) -> Result<(), AnyError> {
let uninstall_flags = match uninstall_flags.kind {
UninstallKind::Global(flags) => flags,
UninstallKind::Local(remove_flags) => {
return super::pm::remove(flags, remove_flags).await;
}
};
let cwd = std::env::current_dir().context("Unable to get CWD")?;
let installation_dir =
get_installer_bin_dir(&cwd, uninstall_flags.root.as_deref())?;
// ensure directory exists
if let Ok(metadata) = fs::metadata(&installation_dir)
&& !metadata.is_dir()
{
return Err(anyhow!("Installation path is not a directory"));
}
let file_path = installation_dir.join(&uninstall_flags.name);
let mut removed = remove_file_if_exists(&file_path)?;
if cfg!(windows) {
let file_path = file_path.with_extension("cmd");
removed |= remove_file_if_exists(&file_path)?;
}
if !removed {
return Err(anyhow!(
"No installation found for {}",
uninstall_flags.name
));
}
// There might be some extra files to delete
// Note: tsconfig.json is legacy. We renamed it to deno.json.
// Remove cleaning it up after January 2024
for ext in ["tsconfig.json", "deno.json", "lock.json"] {
let file_path = file_path.with_extension(ext);
remove_file_if_exists(&file_path)?;
}
log::info!("✅ Successfully uninstalled {}", uninstall_flags.name);
Ok(())
}
fn remove_file_if_exists(file_path: &Path) -> Result<bool, AnyError> {
if !file_path.exists() {
return Ok(false);
}
fs::remove_file(file_path)
.with_context(|| format!("Failed removing: {}", file_path.display()))?;
log::info!("deleted {}", file_path.display());
Ok(true)
}
pub(crate) async fn install_from_entrypoints(
flags: Arc<Flags>,
entrypoints: &[String],
) -> Result<(), AnyError> {
let started = std::time::Instant::now();
let factory = CliFactory::from_flags(flags.clone());
let emitter = factory.emitter()?;
let main_graph_container = factory.main_module_graph_container().await?;
let specifiers = main_graph_container.collect_specifiers(
entrypoints,
CollectSpecifiersOptions {
include_ignored_specified: true,
},
)?;
main_graph_container
.check_specifiers(
&specifiers,
crate::graph_container::CheckSpecifiersOptions {
ext_overwrite: None,
allow_unknown_media_types: true,
},
)
.await?;
emitter
.cache_module_emits(&main_graph_container.graph())
.await?;
print_install_report(
&factory.sys(),
started.elapsed(),
&factory.install_reporter()?.unwrap().clone(),
factory.workspace_resolver().await?,
factory.npm_resolver().await?,
);
Ok(())
}
async fn install_local(
flags: Arc<Flags>,
install_flags: InstallFlagsLocal,
) -> Result<(), AnyError> {
match install_flags {
InstallFlagsLocal::Add(add_flags) => {
super::pm::add(flags, add_flags, super::pm::AddCommandName::Install).await
}
InstallFlagsLocal::Entrypoints(entrypoints) => {
install_from_entrypoints(flags, &entrypoints).await
}
InstallFlagsLocal::TopLevel => {
let start = std::time::Instant::now();
let factory = CliFactory::from_flags(flags);
install_top_level(&factory, start).await
}
}
}
pub fn print_install_report(
sys: &dyn sys_traits::boxed::FsOpenBoxed,
elapsed: std::time::Duration,
install_reporter: &InstallReporter,
workspace: &WorkspaceResolver<CliSys>,
npm_resolver: &CliNpmResolver,
) {
// compute the summary info
let snapshot = npm_resolver.as_managed().unwrap().resolution().snapshot();
let top_level_packages = snapshot.top_level_packages();
// all this nonsense is to categorize into normal and dev deps
let mut normal_deps = HashSet::new();
let mut dev_deps = HashSet::new();
for package_json in workspace.package_jsons() {
let deps = package_json.resolve_local_package_json_deps();
for (_k, v) in deps.dependencies.iter() {
let Ok(s) = v else {
continue;
};
match s {
deno_package_json::PackageJsonDepValue::File(_) => {
// TODO(nathanwhit)
// TODO(bartlomieju)
}
deno_package_json::PackageJsonDepValue::Req(package_req) => {
normal_deps.insert(package_req.name.to_string());
}
deno_package_json::PackageJsonDepValue::Workspace(
_package_json_dep_workspace_req,
) => {
// ignore workspace deps
}
deno_package_json::PackageJsonDepValue::JsrReq(_package_req) => {
// ignore jsr deps
}
}
}
for (_k, v) in deps.dev_dependencies.iter() {
let Ok(s) = v else {
continue;
};
match s {
deno_package_json::PackageJsonDepValue::File(_) => todo!(),
deno_package_json::PackageJsonDepValue::Req(package_req) => {
dev_deps.insert(package_req.name.to_string());
}
deno_package_json::PackageJsonDepValue::Workspace(
_package_json_dep_workspace_req,
) => {
// ignore workspace deps
}
deno_package_json::PackageJsonDepValue::JsrReq(_package_req) => {
// ignore jsr deps
}
}
}
}
let mut installed_normal_deps = Vec::new();
let mut installed_dev_deps = Vec::new();
for pkg in top_level_packages {
if !install_reporter
.stats
.intialized_npm
.contains(&pkg.nv.to_string())
{
continue;
}
if normal_deps.contains(&pkg.nv.name.to_string()) {
installed_normal_deps.push(pkg);
} else if dev_deps.contains(&pkg.nv.name.to_string()) {
installed_dev_deps.push(pkg);
} else {
installed_normal_deps.push(pkg);
}
}
installed_normal_deps.sort_by(|a, b| a.nv.name.cmp(&b.nv.name));
installed_dev_deps.sort_by(|a, b| a.nv.name.cmp(&b.nv.name));
let rep = install_reporter;
if !rep.stats.intialized_npm.is_empty()
|| !rep.stats.downloaded_jsr.is_empty()
{
let total_installed =
rep.stats.intialized_npm.len() + rep.stats.downloaded_jsr.len();
log::info!(
"{} {} {} {} {}",
deno_terminal::colors::gray("Installed"),
total_installed,
deno_terminal::colors::gray(format!(
"package{}",
if total_installed > 1 { "s" } else { "" },
)),
deno_terminal::colors::gray("in"),
display::human_elapsed_with_ms_limit(elapsed.as_millis(), 3_000)
);
let total_reused = rep.stats.reused_npm.get() + rep.stats.reused_jsr.len();
log::info!(
"{} {} {}",
deno_terminal::colors::gray("Reused"),
total_reused,
deno_terminal::colors::gray(format!(
"package{} from cache",
if total_reused == 1 { "" } else { "s" },
)),
);
if total_reused > 0 {
log::info!(
"{}",
deno_terminal::colors::yellow_bold("+".repeat(total_reused))
);
}
let jsr_downloaded = rep.stats.downloaded_jsr.len();
log::info!(
"{} {} {}",
deno_terminal::colors::gray("Downloaded"),
jsr_downloaded,
deno_terminal::colors::gray(format!(
"package{} from JSR",
if jsr_downloaded == 1 { "" } else { "s" },
)),
);
if jsr_downloaded > 0 {
log::info!(
"{}",
deno_terminal::colors::green("+".repeat(jsr_downloaded))
);
}
let npm_download = rep.stats.downloaded_npm.get();
log::info!(
"{} {} {}",
deno_terminal::colors::gray("Downloaded"),
npm_download,
deno_terminal::colors::gray(format!(
"package{} from npm",
if npm_download == 1 { "" } else { "s" },
)),
);
if npm_download > 0 {
log::info!("{}", deno_terminal::colors::green("+".repeat(npm_download)));
}
}
if !installed_normal_deps.is_empty() || !rep.stats.downloaded_jsr.is_empty() {
log::info!("");
log::info!("{}", deno_terminal::colors::cyan("Dependencies:"));
let mut jsr_packages = rep
.stats
.downloaded_jsr
.clone()
.into_iter()
.collect::<Vec<_>>();
jsr_packages.sort();
for pkg in jsr_packages {
let (name, version) = pkg.rsplit_once("@").unwrap();
log::info!(
"{} {}{} {}",
deno_terminal::colors::green("+"),
deno_terminal::colors::gray("jsr:"),
name,
deno_terminal::colors::gray(version)
);
}
for pkg in &installed_normal_deps {
log::info!(
"{} {}{} {}",
deno_terminal::colors::green("+"),
deno_terminal::colors::gray("npm:"),
pkg.nv.name,
deno_terminal::colors::gray(pkg.nv.version.to_string())
);
}
log::info!("");
}
if !installed_dev_deps.is_empty() {
log::info!("{}", deno_terminal::colors::cyan("Dev dependencies:"));
for pkg in &installed_dev_deps {
log::info!(
"{} {}{} {}",
deno_terminal::colors::green("+"),
deno_terminal::colors::gray("npm:"),
pkg.nv.name,
deno_terminal::colors::gray(pkg.nv.version.to_string())
);
}
}
let warnings = install_reporter.take_scripts_warnings();
for warning in warnings {
log::warn!("{}", warning.into_message(sys));
}
let deprecation_messages = install_reporter.take_deprecation_message();
for message in deprecation_messages {
log::warn!("{}", message);
}
}
async fn install_top_level(
factory: &CliFactory,
started: std::time::Instant,
) -> Result<(), AnyError> {
// surface any errors in the package.json
factory
.npm_installer()
.await?
.ensure_no_pkg_json_dep_errors()?;
let npm_installer = factory.npm_installer().await?;
npm_installer.ensure_no_pkg_json_dep_errors()?;
// the actual work
crate::tools::pm::cache_top_level_deps(factory, None).await?;
if let Some(lockfile) = factory.maybe_lockfile().await? {
lockfile.write_if_changed()?;
}
let install_reporter = factory.install_reporter()?.unwrap().clone();
let workspace = factory.workspace_resolver().await?;
let npm_resolver = factory.npm_resolver().await?;
print_install_report(
&factory.sys(),
started.elapsed(),
&install_reporter,
workspace,
npm_resolver,
);
Ok(())
}
fn check_if_installs_a_single_package_globally(
maybe_add_flags: Option<&AddFlags>,
) -> Result<(), AnyError> {
let Some(add_flags) = maybe_add_flags else {
return Ok(());
};
if add_flags.packages.len() != 1 {
return Ok(());
}
let Ok(url) = Url::parse(&add_flags.packages[0]) else {
return Ok(());
};
if matches!(url.scheme(), "http" | "https") {
bail!(
"Failed to install \"{}\" specifier. If you are trying to install {} globally, run again with `-g` flag:\n deno install -g {}",
url.scheme(),
url.as_str(),
url.as_str()
);
}
Ok(())
}
pub async fn install_command(
flags: Arc<Flags>,
install_flags: InstallFlags,
) -> Result<(), AnyError> {
match install_flags {
InstallFlags::Global(global_flags) => {
install_global(flags, global_flags).await
}
InstallFlags::Local(local_flags) => {
if let InstallFlagsLocal::Add(add_flags) = &local_flags {
check_if_installs_a_single_package_globally(Some(add_flags))?;
}
install_local(flags, local_flags).await
}
}
}
async fn install_global(
flags: Arc<Flags>,
install_flags_global: InstallFlagsGlobal,
) -> Result<(), AnyError> {
// ensure the module is cached
let factory = CliFactory::from_flags(flags.clone());
let cli_options = factory.cli_options()?;
let http_client = factory.http_client_provider();
let deps_http_cache = factory.global_http_cache()?;
let deps_file_fetcher = create_cli_file_fetcher(
Default::default(),
deno_cache_dir::GlobalOrLocalHttpCache::Global(deps_http_cache.clone()),
http_client.clone(),
factory.memory_files().clone(),
factory.sys(),
CreateCliFileFetcherOptions {
allow_remote: true,
cache_setting: CacheSetting::ReloadAll,
download_log_level: log::Level::Trace,
progress_bar: None,
},
);
let npmrc = factory.npmrc()?;
let deps_file_fetcher = Arc::new(deps_file_fetcher);
let jsr_resolver = Arc::new(JsrFetchResolver::new(deps_file_fetcher.clone()));
let npm_resolver = Arc::new(NpmFetchResolver::new(
deps_file_fetcher.clone(),
npmrc.clone(),
));
let entry_text = install_flags_global.module_url.as_str();
if !cli_options.initial_cwd().join(entry_text).exists() {
// check for package requirement missing prefix
if let Ok(Err(package_req)) =
super::pm::AddRmPackageReq::parse(entry_text, None)
{
if jsr_resolver.req_to_nv(&package_req).await.is_some() {
bail!(
"{entry_text} is missing a prefix. Did you mean `{}`?",
crate::colors::yellow(format!("deno install -g jsr:{package_req}"))
);
} else if npm_resolver.req_to_nv(&package_req).await.is_some() {
bail!(
"{entry_text} is missing a prefix. Did you mean `{}`?",
crate::colors::yellow(format!("deno install -g npm:{package_req}"))
);
}
}
}
factory
.main_module_graph_container()
.await?
.load_and_type_check_files(
std::slice::from_ref(&install_flags_global.module_url),
CollectSpecifiersOptions {
include_ignored_specified: true,
},
)
.await?;
if matches!(flags.config_flag, ConfigFlag::Discover)
&& cli_options.workspace().deno_jsons().next().is_some()
{
log::warn!(
"{} discovered config file will be ignored in the installed command. Use the --config flag if you wish to include it.",
crate::colors::yellow("Warning")
);
}
let bin_name_resolver = factory.bin_name_resolver()?;
// create the install shim
create_install_shim(
&bin_name_resolver,
cli_options.initial_cwd(),
&flags,
install_flags_global,
)
.await
}
async fn create_install_shim(
bin_name_resolver: &BinNameResolver<'_>,
cwd: &Path,
flags: &Flags,
install_flags_global: InstallFlagsGlobal,
) -> Result<(), AnyError> {
let shim_data =
resolve_shim_data(bin_name_resolver, cwd, flags, &install_flags_global)
.await?;
// ensure directory exists
if let Ok(metadata) = fs::metadata(&shim_data.installation_dir) {
if !metadata.is_dir() {
return Err(anyhow!("Installation path is not a directory"));
}
} else {
fs::create_dir_all(&shim_data.installation_dir)?;
};
if shim_data.file_path.exists() && !install_flags_global.force {
return Err(anyhow!(
"Existing installation found. Aborting (Use -f to overwrite).",
));
};
generate_executable_file(&shim_data)?;
for (path, contents) in shim_data.extra_files {
fs::write(path, contents)?;
}
log::info!("✅ Successfully installed {}", shim_data.name);
log::info!("{}", shim_data.file_path.display());
if cfg!(windows) {
let display_path = shim_data.file_path.with_extension("");
log::info!("{} (shell)", display_path.display());
}
let installation_dir_str = shim_data.installation_dir.to_string_lossy();
if !is_in_path(&shim_data.installation_dir) {
log::info!(" Add {} to PATH", installation_dir_str);
if cfg!(windows) {
log::info!(" set PATH=%PATH%;{}", installation_dir_str);
} else {
log::info!(" export PATH=\"{}:$PATH\"", installation_dir_str);
}
}
Ok(())
}
struct ShimData {
name: String,
installation_dir: PathBuf,
file_path: PathBuf,
args: Vec<String>,
extra_files: Vec<(PathBuf, String)>,
}
async fn resolve_shim_data(
bin_name_resolver: &BinNameResolver<'_>,
cwd: &Path,
flags: &Flags,
install_flags_global: &InstallFlagsGlobal,
) -> Result<ShimData, AnyError> {
let installation_dir =
get_installer_bin_dir(cwd, install_flags_global.root.as_deref())?;
// Check if module_url is remote
let module_url = resolve_url_or_path(&install_flags_global.module_url, cwd)?;
let name = if install_flags_global.name.is_some() {
install_flags_global.name.clone()
} else {
bin_name_resolver.infer_name_from_url(&module_url).await
};
let name = match name {
Some(name) => name,
None => {
return Err(anyhow!(
"An executable name was not provided. One could not be inferred from the URL. Aborting.\n {} {}",
deno_runtime::colors::cyan("hint:"),
"provide one with the `--name` flag"
));
}
};
validate_name(name.as_str())?;
let mut file_path = installation_dir.join(&name);
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
let mut extra_files: Vec<(PathBuf, String)> = vec![];
let mut executable_args = vec!["run".to_string()];
executable_args.extend_from_slice(&flags.to_permission_args());
if let Some(url) = flags.location.as_ref() {
executable_args.push("--location".to_string());
executable_args.push(url.to_string());
}
if let Some(CaData::File(ca_file)) = &flags.ca_data {
executable_args.push("--cert".to_string());
executable_args.push(ca_file.to_owned())
}
if let Some(log_level) = flags.log_level {
if log_level == Level::Error {
executable_args.push("--quiet".to_string());
} else {
executable_args.push("--log-level".to_string());
let log_level = match log_level {
Level::Debug => "debug",
Level::Info => "info",
_ => return Err(anyhow!(format!("invalid log level {log_level}"))),
};
executable_args.push(log_level.to_string());
}
}
// we should avoid a default branch here to ensure we continue to cover any
// changes to this flag.
match flags.type_check_mode {
TypeCheckMode::All => executable_args.push("--check=all".to_string()),
TypeCheckMode::None => {}
TypeCheckMode::Local => executable_args.push("--check".to_string()),
}
for feature in &flags.unstable_config.features {
executable_args.push(format!("--unstable-{}", feature));
}
if flags.no_remote {
executable_args.push("--no-remote".to_string());
}
if flags.no_npm {
executable_args.push("--no-npm".to_string());
}
if flags.cached_only {
executable_args.push("--cached-only".to_string());
}
if flags.frozen_lockfile.unwrap_or(false) {
executable_args.push("--frozen".to_string());
}
if resolve_no_prompt(&flags.permissions) {
executable_args.push("--no-prompt".to_string());
}
if !flags.v8_flags.is_empty() {
executable_args.push(format!("--v8-flags={}", flags.v8_flags.join(",")));
}
if let Some(seed) = flags.seed {
executable_args.push("--seed".to_string());
executable_args.push(seed.to_string());
}
if let Some(inspect) = flags.inspect {
executable_args.push(format!("--inspect={inspect}"));
}
if let Some(inspect_brk) = flags.inspect_brk {
executable_args.push(format!("--inspect-brk={inspect_brk}"));
}
if let Some(import_map_path) = &flags.import_map_path {
let import_map_url = resolve_url_or_path(import_map_path, cwd)?;
executable_args.push("--import-map".to_string());
executable_args.push(import_map_url.to_string());
}
if let ConfigFlag::Path(config_path) = &flags.config_flag {
let copy_path = get_hidden_file_with_ext(&file_path, "deno.json");
executable_args.push("--config".to_string());
executable_args.push(copy_path.to_str().unwrap().to_string());
let mut config_text = fs::read_to_string(config_path)
.with_context(|| format!("error reading {config_path}"))?;
// always remove the import map field because when someone specifies `--import-map` we
// don't want that file to be attempted to be loaded and when they don't specify that
// (which is just something we haven't implemented yet)
if let Some(new_text) = remove_import_map_field_from_text(&config_text) {
if flags.import_map_path.is_none() {
log::warn!(
"{} \"importMap\" field in the specified config file we be ignored. Use the --import-map flag instead.",
crate::colors::yellow("Warning"),
);
}
config_text = new_text;
}
extra_files.push((copy_path, config_text));
} else {
executable_args.push("--no-config".to_string());
}
if flags.no_lock {
executable_args.push("--no-lock".to_string());
} else if flags.lock.is_some()
// always use a lockfile for an npm entrypoint unless --no-lock
|| NpmPackageReqReference::from_specifier(&module_url).is_ok()
{
let copy_path = get_hidden_file_with_ext(&file_path, "lock.json");
executable_args.push("--lock".to_string());
executable_args.push(copy_path.to_str().unwrap().to_string());
if let Some(lock_path) = &flags.lock {
extra_files.push((
copy_path,
fs::read_to_string(lock_path)
.with_context(|| format!("error reading {}", lock_path))?,
));
} else {
// Provide an empty lockfile so that this overwrites any existing lockfile
// from a previous installation. This will get populated on first run.
extra_files.push((copy_path, "{}".to_string()));
}
}
executable_args.push(module_url.into());
executable_args.extend_from_slice(&install_flags_global.args);
Ok(ShimData {
name,
installation_dir,
file_path,
args: executable_args,
extra_files,
})
}
fn remove_import_map_field_from_text(config_text: &str) -> Option<String> {
let value =
jsonc_parser::cst::CstRootNode::parse(config_text, &Default::default())
.ok()?;
let root_value = value.object_value()?;
let import_map_value = root_value.get("importMap")?;
import_map_value.remove();
Some(value.to_string())
}
fn get_hidden_file_with_ext(file_path: &Path, ext: &str) -> PathBuf {
// use a dot file to prevent the file from showing up in some
// users shell auto-complete since this directory is on the PATH
file_path
.with_file_name(format!(
".{}",
file_path.file_name().unwrap().to_string_lossy()
))
.with_extension(ext)
}
fn is_in_path(dir: &Path) -> bool {
if let Some(paths) = env::var_os("PATH") {
for p in env::split_paths(&paths) {
if *dir == p {
return true;
}
}
}
false
}
#[cfg(test)]
mod tests {
use std::process::Command;
use deno_lib::args::UnstableConfig;
use test_util::TempDir;
use test_util::testdata_path;
use super::*;
use crate::args::ConfigFlag;
use crate::args::PermissionFlags;
use crate::args::UninstallFlagsGlobal;
use crate::http_util::HttpClientProvider;
use crate::util::fs::canonicalize_path;
async fn create_install_shim(
flags: &Flags,
install_flags_global: InstallFlagsGlobal,
) -> Result<(), AnyError> {
let cwd = std::env::current_dir().unwrap();
let http_client = HttpClientProvider::new(None, None);
let registry_api = deno_npm::registry::TestNpmRegistryApi::default();
let resolver = BinNameResolver::new(&http_client, &registry_api);
super::create_install_shim(&resolver, &cwd, flags, install_flags_global)
.await
}
async fn resolve_shim_data(
flags: &Flags,
install_flags_global: &InstallFlagsGlobal,
) -> Result<ShimData, AnyError> {
let cwd = std::env::current_dir().unwrap();
let http_client = HttpClientProvider::new(None, None);
let registry_api = deno_npm::registry::TestNpmRegistryApi::default();
let resolver = BinNameResolver::new(&http_client, &registry_api);
super::resolve_shim_data(&resolver, &cwd, flags, install_flags_global).await
}
#[tokio::test]
async fn install_unstable() {
let temp_dir = TempDir::new();
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
create_install_shim(
&Flags::default(),
InstallFlagsGlobal {
module_url: "http://localhost:4545/echo_server.ts".to_string(),
args: vec![],
name: Some("echo_test".to_string()),
root: Some(temp_dir.path().to_string()),
force: false,
},
)
.await
.unwrap();
let mut file_path = bin_dir.join("echo_test");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
let content = fs::read_to_string(file_path).unwrap();
if cfg!(windows) {
assert!(content.contains(
r#""run" "--no-config" "http://localhost:4545/echo_server.ts""#
));
} else {
assert!(
content.contains(
r#"run --no-config 'http://localhost:4545/echo_server.ts'"#
)
);
}
}
#[tokio::test]
async fn install_inferred_name() {
let shim_data = resolve_shim_data(
&Flags::default(),
&InstallFlagsGlobal {
module_url: "http://localhost:4545/echo_server.ts".to_string(),
args: vec![],
name: None,
root: Some(env::temp_dir().to_string_lossy().into_owned()),
force: false,
},
)
.await
.unwrap();
assert_eq!(shim_data.name, "echo_server");
assert_eq!(
shim_data.args,
vec!["run", "--no-config", "http://localhost:4545/echo_server.ts",]
);
}
#[tokio::test]
async fn install_unstable_legacy() {
let shim_data = resolve_shim_data(
&Default::default(),
&InstallFlagsGlobal {
module_url: "http://localhost:4545/echo_server.ts".to_string(),
args: vec![],
name: None,
root: Some(env::temp_dir().to_string_lossy().into_owned()),
force: false,
},
)
.await
.unwrap();
assert_eq!(shim_data.name, "echo_server");
assert_eq!(
shim_data.args,
vec!["run", "--no-config", "http://localhost:4545/echo_server.ts",]
);
}
#[tokio::test]
async fn install_unstable_features() {
let shim_data = resolve_shim_data(
&Flags {
unstable_config: UnstableConfig {
features: vec!["kv".to_string(), "cron".to_string()],
..Default::default()
},
..Default::default()
},
&InstallFlagsGlobal {
module_url: "http://localhost:4545/echo_server.ts".to_string(),
args: vec![],
name: None,
root: Some(env::temp_dir().to_string_lossy().into_owned()),
force: false,
},
)
.await
.unwrap();
assert_eq!(shim_data.name, "echo_server");
assert_eq!(
shim_data.args,
vec![
"run",
"--unstable-kv",
"--unstable-cron",
"--no-config",
"http://localhost:4545/echo_server.ts",
]
);
}
#[tokio::test]
async fn install_inferred_name_from_parent() {
let shim_data = resolve_shim_data(
&Flags::default(),
&InstallFlagsGlobal {
module_url: "http://localhost:4545/subdir/main.ts".to_string(),
args: vec![],
name: None,
root: Some(env::temp_dir().to_string_lossy().into_owned()),
force: false,
},
)
.await
.unwrap();
assert_eq!(shim_data.name, "subdir");
assert_eq!(
shim_data.args,
vec!["run", "--no-config", "http://localhost:4545/subdir/main.ts",]
);
}
#[tokio::test]
async fn install_inferred_name_after_redirect_for_no_path_url() {
let _http_server_guard = test_util::http_server();
let shim_data = resolve_shim_data(
&Flags::default(),
&InstallFlagsGlobal {
module_url: "http://localhost:4550/?redirect_to=/subdir/redirects/a.ts"
.to_string(),
args: vec![],
name: None,
root: Some(env::temp_dir().to_string_lossy().into_owned()),
force: false,
},
)
.await
.unwrap();
assert_eq!(shim_data.name, "a");
assert_eq!(
shim_data.args,
vec![
"run",
"--no-config",
"http://localhost:4550/?redirect_to=/subdir/redirects/a.ts",
]
);
}
#[tokio::test]
async fn install_custom_dir_option() {
let shim_data = resolve_shim_data(
&Flags::default(),
&InstallFlagsGlobal {
module_url: "http://localhost:4545/echo_server.ts".to_string(),
args: vec![],
name: Some("echo_test".to_string()),
root: Some(env::temp_dir().to_string_lossy().into_owned()),
force: false,
},
)
.await
.unwrap();
assert_eq!(shim_data.name, "echo_test");
assert_eq!(
shim_data.args,
vec!["run", "--no-config", "http://localhost:4545/echo_server.ts",]
);
}
#[tokio::test]
async fn install_with_flags() {
let shim_data = resolve_shim_data(
&Flags {
permissions: PermissionFlags {
allow_net: Some(vec![]),
allow_read: Some(vec![]),
..Default::default()
},
type_check_mode: TypeCheckMode::None,
log_level: Some(Level::Error),
..Flags::default()
},
&InstallFlagsGlobal {
module_url: "http://localhost:4545/echo_server.ts".to_string(),
args: vec!["--foobar".to_string()],
name: Some("echo_test".to_string()),
root: Some(env::temp_dir().to_string_lossy().into_owned()),
force: false,
},
)
.await
.unwrap();
assert_eq!(shim_data.name, "echo_test");
assert_eq!(
shim_data.args,
vec![
"run",
"--allow-read",
"--allow-net",
"--quiet",
"--no-config",
"http://localhost:4545/echo_server.ts",
"--foobar",
]
);
}
#[tokio::test]
async fn install_prompt() {
let shim_data = resolve_shim_data(
&Flags {
permissions: PermissionFlags {
no_prompt: true,
..Default::default()
},
..Flags::default()
},
&InstallFlagsGlobal {
module_url: "http://localhost:4545/echo_server.ts".to_string(),
args: vec![],
name: Some("echo_test".to_string()),
root: Some(env::temp_dir().to_string_lossy().into_owned()),
force: false,
},
)
.await
.unwrap();
assert_eq!(
shim_data.args,
vec![
"run",
"--no-prompt",
"--no-config",
"http://localhost:4545/echo_server.ts",
]
);
}
#[tokio::test]
async fn install_allow_all() {
let shim_data = resolve_shim_data(
&Flags {
permissions: PermissionFlags {
allow_all: true,
..Default::default()
},
..Flags::default()
},
&InstallFlagsGlobal {
module_url: "http://localhost:4545/echo_server.ts".to_string(),
args: vec![],
name: Some("echo_test".to_string()),
root: Some(env::temp_dir().to_string_lossy().into_owned()),
force: false,
},
)
.await
.unwrap();
assert_eq!(
shim_data.args,
vec![
"run",
"--allow-all",
"--no-config",
"http://localhost:4545/echo_server.ts",
]
);
}
#[tokio::test]
async fn install_npm_lockfile_default() {
let temp_dir = canonicalize_path(&env::temp_dir()).unwrap();
let shim_data = resolve_shim_data(
&Flags {
permissions: PermissionFlags {
allow_all: true,
..Default::default()
},
..Flags::default()
},
&InstallFlagsGlobal {
module_url: "npm:cowsay".to_string(),
args: vec![],
name: None,
root: Some(temp_dir.to_string_lossy().into_owned()),
force: false,
},
)
.await
.unwrap();
let lock_path = temp_dir.join("bin").join(".cowsay.lock.json");
assert_eq!(
shim_data.args,
vec![
"run",
"--allow-all",
"--no-config",
"--lock",
&lock_path.to_string_lossy(),
"npm:cowsay"
]
);
assert_eq!(shim_data.extra_files, vec![(lock_path, "{}".to_string())]);
}
#[tokio::test]
async fn install_npm_no_lock() {
let shim_data = resolve_shim_data(
&Flags {
permissions: PermissionFlags {
allow_all: true,
..Default::default()
},
no_lock: true,
..Flags::default()
},
&InstallFlagsGlobal {
module_url: "npm:cowsay".to_string(),
args: vec![],
name: None,
root: Some(env::temp_dir().to_string_lossy().into_owned()),
force: false,
},
)
.await
.unwrap();
assert_eq!(
shim_data.args,
vec![
"run",
"--allow-all",
"--no-config",
"--no-lock",
"npm:cowsay"
]
);
assert_eq!(shim_data.extra_files, vec![]);
}
#[tokio::test]
async fn install_local_module() {
let temp_dir = TempDir::new();
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
let local_module = env::current_dir().unwrap().join("echo_server.ts");
let local_module_url = Url::from_file_path(&local_module).unwrap();
let local_module_str = local_module.to_string_lossy();
create_install_shim(
&Flags::default(),
InstallFlagsGlobal {
module_url: local_module_str.to_string(),
args: vec![],
name: Some("echo_test".to_string()),
root: Some(temp_dir.path().to_string()),
force: false,
},
)
.await
.unwrap();
let mut file_path = bin_dir.join("echo_test");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
let content = fs::read_to_string(file_path).unwrap();
assert!(content.contains(&local_module_url.to_string()));
}
#[tokio::test]
async fn install_force() {
let temp_dir = TempDir::new();
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
create_install_shim(
&Flags::default(),
InstallFlagsGlobal {
module_url: "http://localhost:4545/echo_server.ts".to_string(),
args: vec![],
name: Some("echo_test".to_string()),
root: Some(temp_dir.path().to_string()),
force: false,
},
)
.await
.unwrap();
let mut file_path = bin_dir.join("echo_test");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
// No force. Install failed.
let no_force_result = create_install_shim(
&Flags::default(),
InstallFlagsGlobal {
module_url: "http://localhost:4545/cat.ts".to_string(), // using a different URL
args: vec![],
name: Some("echo_test".to_string()),
root: Some(temp_dir.path().to_string()),
force: false,
},
)
.await;
assert!(no_force_result.is_err());
assert!(
no_force_result
.unwrap_err()
.to_string()
.contains("Existing installation found")
);
// Assert not modified
let file_content = fs::read_to_string(&file_path).unwrap();
assert!(file_content.contains("echo_server.ts"));
// Force. Install success.
let force_result = create_install_shim(
&Flags::default(),
InstallFlagsGlobal {
module_url: "http://localhost:4545/cat.ts".to_string(), // using a different URL
args: vec![],
name: Some("echo_test".to_string()),
root: Some(temp_dir.path().to_string()),
force: true,
},
)
.await;
assert!(force_result.is_ok());
// Assert modified
let file_content_2 = fs::read_to_string(&file_path).unwrap();
assert!(file_content_2.contains("cat.ts"));
}
#[tokio::test]
async fn install_with_config() {
let temp_dir = TempDir::new();
let bin_dir = temp_dir.path().join("bin");
let config_file_path = temp_dir.path().join("test_tsconfig.json");
let config = "{}";
let mut config_file = File::create(&config_file_path).unwrap();
let result = config_file.write_all(config.as_bytes());
assert!(result.is_ok());
let result = create_install_shim(
&Flags {
config_flag: ConfigFlag::Path(config_file_path.to_string()),
..Flags::default()
},
InstallFlagsGlobal {
module_url: "http://localhost:4545/cat.ts".to_string(),
args: vec![],
name: Some("echo_test".to_string()),
root: Some(temp_dir.path().to_string()),
force: true,
},
)
.await;
assert!(result.is_ok());
let config_file_name = ".echo_test.deno.json";
let file_path = bin_dir.join(config_file_name);
assert!(file_path.exists());
let content = fs::read_to_string(file_path).unwrap();
assert!(content == "{}");
}
// TODO: enable on Windows after fixing batch escaping
#[cfg(not(windows))]
#[tokio::test]
async fn install_shell_escaping() {
let temp_dir = TempDir::new();
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
create_install_shim(
&Flags::default(),
InstallFlagsGlobal {
module_url: "http://localhost:4545/echo_server.ts".to_string(),
args: vec!["\"".to_string()],
name: Some("echo_test".to_string()),
root: Some(temp_dir.path().to_string()),
force: false,
},
)
.await
.unwrap();
let mut file_path = bin_dir.join("echo_test");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
let content = fs::read_to_string(file_path).unwrap();
if cfg!(windows) {
// TODO: see comment above this test
} else {
assert!(content.contains(
r#"run --no-config 'http://localhost:4545/echo_server.ts' '"'"#
));
}
}
#[tokio::test]
async fn install_unicode() {
let temp_dir = TempDir::new();
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
let unicode_dir = temp_dir.path().join("Magnús");
std::fs::create_dir(&unicode_dir).unwrap();
let local_module = unicode_dir.join("echo_server.ts");
let local_module_str = local_module.to_string_lossy();
std::fs::write(&local_module, "// Some JavaScript I guess").unwrap();
create_install_shim(
&Flags::default(),
InstallFlagsGlobal {
module_url: local_module_str.to_string(),
args: vec![],
name: Some("echo_test".to_string()),
root: Some(temp_dir.path().to_string()),
force: false,
},
)
.await
.unwrap();
let mut file_path = bin_dir.join("echo_test");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
// We need to actually run it to make sure the URL is interpreted correctly
let status = Command::new(file_path)
.env_clear()
// use the deno binary in the target directory
.env("PATH", test_util::target_dir())
.env("RUST_BACKTRACE", "1")
.spawn()
.unwrap()
.wait()
.unwrap();
assert!(status.success());
}
#[tokio::test]
async fn install_with_import_map() {
let temp_dir = TempDir::new();
let bin_dir = temp_dir.path().join("bin");
let import_map_path = temp_dir.path().join("import_map.json");
let import_map_url = Url::from_file_path(&import_map_path).unwrap();
let import_map = "{ \"imports\": {} }";
let mut import_map_file = File::create(&import_map_path).unwrap();
let result = import_map_file.write_all(import_map.as_bytes());
assert!(result.is_ok());
let result = create_install_shim(
&Flags {
import_map_path: Some(import_map_path.to_string()),
..Flags::default()
},
InstallFlagsGlobal {
module_url: "http://localhost:4545/cat.ts".to_string(),
args: vec![],
name: Some("echo_test".to_string()),
root: Some(temp_dir.path().to_string()),
force: true,
},
)
.await;
assert!(result.is_ok());
let mut file_path = bin_dir.join("echo_test");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
let mut expected_string = format!(
"--import-map '{import_map_url}' --no-config 'http://localhost:4545/cat.ts'"
);
if cfg!(windows) {
expected_string = format!(
"\"--import-map\" \"{import_map_url}\" \"--no-config\" \"http://localhost:4545/cat.ts\""
);
}
let content = fs::read_to_string(file_path).unwrap();
assert!(content.contains(&expected_string));
}
// Regression test for https://github.com/denoland/deno/issues/10556.
#[tokio::test]
async fn install_file_url() {
let temp_dir = TempDir::new();
let bin_dir = temp_dir.path().join("bin");
let module_path = fs::canonicalize(testdata_path().join("cat.ts")).unwrap();
let file_module_string =
Url::from_file_path(module_path).unwrap().to_string();
assert!(file_module_string.starts_with("file:///"));
let result = create_install_shim(
&Flags::default(),
InstallFlagsGlobal {
module_url: file_module_string.to_string(),
args: vec![],
name: Some("echo_test".to_string()),
root: Some(temp_dir.path().to_string()),
force: true,
},
)
.await;
assert!(result.is_ok());
let mut file_path = bin_dir.join("echo_test");
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
}
assert!(file_path.exists());
let mut expected_string =
format!("run --no-config '{}'", &file_module_string);
if cfg!(windows) {
expected_string =
format!("\"run\" \"--no-config\" \"{}\"", &file_module_string);
}
let content = fs::read_to_string(file_path).unwrap();
assert!(content.contains(&expected_string));
}
#[tokio::test]
async fn uninstall_basic() {
let temp_dir = TempDir::new();
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
let mut file_path = bin_dir.join("echo_test");
File::create(&file_path).unwrap();
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
File::create(&file_path).unwrap();
}
// create extra files
{
let file_path = file_path.with_extension("deno.json");
File::create(file_path).unwrap();
}
{
// legacy tsconfig.json, make sure it's cleaned up for now
let file_path = file_path.with_extension("tsconfig.json");
File::create(file_path).unwrap();
}
{
let file_path = file_path.with_extension("lock.json");
File::create(file_path).unwrap();
}
uninstall(
Default::default(),
UninstallFlags {
kind: UninstallKind::Global(UninstallFlagsGlobal {
name: "echo_test".to_string(),
root: Some(temp_dir.path().to_string()),
}),
},
)
.await
.unwrap();
assert!(!file_path.exists());
assert!(!file_path.with_extension("tsconfig.json").exists());
assert!(!file_path.with_extension("deno.json").exists());
assert!(!file_path.with_extension("lock.json").exists());
if cfg!(windows) {
file_path = file_path.with_extension("cmd");
assert!(!file_path.exists());
}
}
#[test]
fn test_remove_import_map_field_from_text() {
assert_eq!(
remove_import_map_field_from_text(
r#"{
"importMap": "./value.json"
}"#,
)
.unwrap(),
"{}"
);
}
}