mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 20:38:55 +00:00
1592 lines
44 KiB
Rust
1592 lines
44 KiB
Rust
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||
|
||
use crate::args::resolve_no_prompt;
|
||
use crate::args::AddFlags;
|
||
use crate::args::CaData;
|
||
use crate::args::CacheSetting;
|
||
use crate::args::ConfigFlag;
|
||
use crate::args::Flags;
|
||
use crate::args::InstallFlags;
|
||
use crate::args::InstallFlagsGlobal;
|
||
use crate::args::InstallFlagsLocal;
|
||
use crate::args::InstallKind;
|
||
use crate::args::TypeCheckMode;
|
||
use crate::args::UninstallFlags;
|
||
use crate::args::UninstallKind;
|
||
use crate::factory::CliFactory;
|
||
use crate::file_fetcher::FileFetcher;
|
||
use crate::graph_container::ModuleGraphContainer;
|
||
use crate::http_util::HttpClientProvider;
|
||
use crate::jsr::JsrFetchResolver;
|
||
use crate::npm::NpmFetchResolver;
|
||
use crate::util::fs::canonicalize_path_maybe_not_exists;
|
||
|
||
use deno_core::anyhow::bail;
|
||
use deno_core::anyhow::Context;
|
||
use deno_core::error::generic_error;
|
||
use deno_core::error::AnyError;
|
||
use deno_core::resolve_url_or_path;
|
||
use deno_core::url::Url;
|
||
use deno_semver::npm::NpmPackageReqReference;
|
||
use log::Level;
|
||
use once_cell::sync::Lazy;
|
||
use regex::Regex;
|
||
use regex::RegexBuilder;
|
||
use std::env;
|
||
use std::fs;
|
||
use std::fs::File;
|
||
use std::io;
|
||
use std::io::Write;
|
||
use std::path::Path;
|
||
use std::path::PathBuf;
|
||
|
||
#[cfg(not(windows))]
|
||
use std::os::unix::fs::PermissionsExt;
|
||
use std::sync::Arc;
|
||
|
||
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(generic_error(format!(
|
||
"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_root() -> Result<PathBuf, io::Error> {
|
||
if let Ok(env_dir) = env::var("DENO_INSTALL_ROOT") {
|
||
if !env_dir.is_empty() {
|
||
return canonicalize_path_maybe_not_exists(&PathBuf::from(env_dir));
|
||
}
|
||
}
|
||
// 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 infer_name_from_url(
|
||
http_client_provider: &HttpClientProvider,
|
||
url: &Url,
|
||
) -> Option<String> {
|
||
// If there's an absolute url with no path, eg. https://my-cli.com
|
||
// perform a request, and see if it redirects another file instead.
|
||
let mut url = url.clone();
|
||
|
||
if url.path() == "/" {
|
||
if let Ok(client) = http_client_provider.get_or_create() {
|
||
if let Ok(redirected_url) =
|
||
client.get_redirected_url(url.clone(), None).await
|
||
{
|
||
url = redirected_url;
|
||
}
|
||
}
|
||
}
|
||
|
||
if let Ok(npm_ref) = NpmPackageReqReference::from_specifier(&url) {
|
||
let npm_ref = npm_ref.into_inner();
|
||
if let Some(sub_path) = npm_ref.sub_path {
|
||
if !sub_path.contains('/') {
|
||
return Some(sub_path);
|
||
}
|
||
}
|
||
if !npm_ref.req.name.contains('/') {
|
||
return Some(npm_ref.req.name);
|
||
}
|
||
return None;
|
||
}
|
||
|
||
let percent_decode = percent_encoding::percent_decode(url.path().as_bytes());
|
||
#[cfg(unix)]
|
||
let path = {
|
||
use std::os::unix::prelude::OsStringExt;
|
||
PathBuf::from(std::ffi::OsString::from_vec(
|
||
percent_decode.collect::<Vec<u8>>(),
|
||
))
|
||
};
|
||
#[cfg(windows)]
|
||
let path = PathBuf::from(percent_decode.decode_utf8_lossy().as_ref());
|
||
|
||
let mut stem = path.file_stem()?.to_string_lossy();
|
||
if matches!(stem.as_ref(), "main" | "mod" | "index" | "cli") {
|
||
if let Some(parent_name) = path.parent().and_then(|p| p.file_name()) {
|
||
stem = parent_name.to_string_lossy();
|
||
}
|
||
}
|
||
|
||
// if atmark symbol appears in the index other than 0 (e.g. `foo@bar`) we use
|
||
// the former part as the inferred name because the latter part is most likely
|
||
// a version number.
|
||
match stem.find('@') {
|
||
Some(at_index) if at_index > 0 => {
|
||
stem = stem.split_at(at_index).0.to_string().into();
|
||
}
|
||
_ => {}
|
||
}
|
||
|
||
Some(stem.to_string())
|
||
}
|
||
|
||
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::registry::remove(flags, remove_flags).await;
|
||
}
|
||
};
|
||
|
||
let cwd = std::env::current_dir().context("Unable to get CWD")?;
|
||
let root = if let Some(root) = uninstall_flags.root {
|
||
canonicalize_path_maybe_not_exists(&cwd.join(root))?
|
||
} else {
|
||
get_installer_root()?
|
||
};
|
||
let installation_dir = root.join("bin");
|
||
|
||
// ensure directory exists
|
||
if let Ok(metadata) = fs::metadata(&installation_dir) {
|
||
if !metadata.is_dir() {
|
||
return Err(generic_error("Installation path is not a directory"));
|
||
}
|
||
}
|
||
|
||
let file_path = installation_dir.join(&uninstall_flags.name);
|
||
|
||
let mut removed = false;
|
||
|
||
if file_path.exists() {
|
||
fs::remove_file(&file_path)?;
|
||
log::info!("deleted {}", file_path.to_string_lossy());
|
||
removed = true
|
||
};
|
||
|
||
if cfg!(windows) {
|
||
let file_path = file_path.with_extension("cmd");
|
||
if file_path.exists() {
|
||
fs::remove_file(&file_path)?;
|
||
log::info!("deleted {}", file_path.to_string_lossy());
|
||
removed = true
|
||
}
|
||
}
|
||
|
||
if !removed {
|
||
return Err(generic_error(format!(
|
||
"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);
|
||
if file_path.exists() {
|
||
fs::remove_file(&file_path)?;
|
||
log::info!("deleted {}", file_path.to_string_lossy());
|
||
}
|
||
}
|
||
|
||
log::info!("✅ Successfully uninstalled {}", uninstall_flags.name);
|
||
Ok(())
|
||
}
|
||
|
||
pub(crate) async fn install_from_entrypoints(
|
||
flags: Arc<Flags>,
|
||
entrypoints: &[String],
|
||
) -> Result<(), AnyError> {
|
||
let factory = CliFactory::from_flags(flags.clone());
|
||
let emitter = factory.emitter()?;
|
||
let main_graph_container = factory.main_module_graph_container().await?;
|
||
main_graph_container
|
||
.load_and_type_check_files(entrypoints)
|
||
.await?;
|
||
emitter
|
||
.cache_module_emits(&main_graph_container.graph())
|
||
.await
|
||
}
|
||
|
||
async fn install_local(
|
||
flags: Arc<Flags>,
|
||
install_flags: InstallFlagsLocal,
|
||
) -> Result<(), AnyError> {
|
||
match install_flags {
|
||
InstallFlagsLocal::Add(add_flags) => {
|
||
super::registry::add(
|
||
flags,
|
||
add_flags,
|
||
super::registry::AddCommandName::Install,
|
||
)
|
||
.await
|
||
}
|
||
InstallFlagsLocal::Entrypoints(entrypoints) => {
|
||
install_from_entrypoints(flags, &entrypoints).await
|
||
}
|
||
InstallFlagsLocal::TopLevel => {
|
||
let factory = CliFactory::from_flags(flags);
|
||
// surface any errors in the package.json
|
||
if let Some(npm_resolver) = factory.npm_resolver().await?.as_managed() {
|
||
npm_resolver.ensure_no_pkg_json_dep_errors()?;
|
||
}
|
||
crate::tools::registry::cache_top_level_deps(&factory, None).await?;
|
||
|
||
if let Some(lockfile) = factory.cli_options()?.maybe_lockfile() {
|
||
lockfile.write_if_changed()?;
|
||
}
|
||
|
||
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.kind {
|
||
InstallKind::Global(global_flags) => {
|
||
install_global(flags, global_flags).await
|
||
}
|
||
InstallKind::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 mut deps_file_fetcher = FileFetcher::new(
|
||
deps_http_cache.clone(),
|
||
CacheSetting::ReloadAll,
|
||
true,
|
||
http_client.clone(),
|
||
Default::default(),
|
||
None,
|
||
);
|
||
|
||
let npmrc = factory.cli_options().unwrap().npmrc();
|
||
|
||
deps_file_fetcher.set_download_log_level(log::Level::Trace);
|
||
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::registry::AddRmPackageReq::parse(entry_text)
|
||
{
|
||
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(&[install_flags_global.module_url.clone()])
|
||
.await?;
|
||
|
||
// create the install shim
|
||
create_install_shim(http_client, &flags, install_flags_global).await
|
||
}
|
||
|
||
async fn create_install_shim(
|
||
http_client_provider: &HttpClientProvider,
|
||
flags: &Flags,
|
||
install_flags_global: InstallFlagsGlobal,
|
||
) -> Result<(), AnyError> {
|
||
let shim_data =
|
||
resolve_shim_data(http_client_provider, flags, &install_flags_global)
|
||
.await?;
|
||
|
||
// ensure directory exists
|
||
if let Ok(metadata) = fs::metadata(&shim_data.installation_dir) {
|
||
if !metadata.is_dir() {
|
||
return Err(generic_error("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(generic_error(
|
||
"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(
|
||
http_client_provider: &HttpClientProvider,
|
||
flags: &Flags,
|
||
install_flags_global: &InstallFlagsGlobal,
|
||
) -> Result<ShimData, AnyError> {
|
||
let cwd = std::env::current_dir().context("Unable to get CWD")?;
|
||
let root = if let Some(root) = &install_flags_global.root {
|
||
canonicalize_path_maybe_not_exists(&cwd.join(root))?
|
||
} else {
|
||
get_installer_root()?
|
||
};
|
||
let installation_dir = root.join("bin");
|
||
|
||
// 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 {
|
||
infer_name_from_url(http_client_provider, &module_url).await
|
||
};
|
||
|
||
let name = match name {
|
||
Some(name) => name,
|
||
None => return Err(generic_error(
|
||
"An executable name was not provided. One could not be inferred from the URL. Aborting.",
|
||
)),
|
||
};
|
||
|
||
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(generic_error(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());
|
||
extra_files.push((
|
||
copy_path,
|
||
fs::read_to_string(config_path)
|
||
.with_context(|| format!("error reading {config_path}"))?,
|
||
));
|
||
} 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.to_string());
|
||
executable_args.extend_from_slice(&install_flags_global.args);
|
||
|
||
Ok(ShimData {
|
||
name,
|
||
installation_dir,
|
||
file_path,
|
||
args: executable_args,
|
||
extra_files,
|
||
})
|
||
}
|
||
|
||
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 super::*;
|
||
|
||
use crate::args::ConfigFlag;
|
||
use crate::args::PermissionFlags;
|
||
use crate::args::UninstallFlagsGlobal;
|
||
use crate::args::UnstableConfig;
|
||
use crate::util::fs::canonicalize_path;
|
||
use std::process::Command;
|
||
use test_util::testdata_path;
|
||
use test_util::TempDir;
|
||
|
||
#[tokio::test]
|
||
async fn install_infer_name_from_url() {
|
||
let http_client = HttpClientProvider::new(None, None);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("https://example.com/abc/server.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("server".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("https://example.com/abc/main.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("abc".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("https://example.com/abc/mod.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("abc".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("https://example.com/ab%20c/mod.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("ab c".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("https://example.com/abc/index.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("abc".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("https://example.com/abc/cli.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("abc".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("https://example.com/main.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("main".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("https://example.com").unwrap()
|
||
)
|
||
.await,
|
||
None
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("file:///abc/server.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("server".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("file:///abc/main.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("abc".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("file:///ab%20c/main.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("ab c".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("file:///main.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("main".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(&http_client, &Url::parse("file:///").unwrap()).await,
|
||
None
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("https://example.com/abc@0.1.0").unwrap()
|
||
)
|
||
.await,
|
||
Some("abc".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("https://example.com/abc@0.1.0/main.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("abc".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("https://example.com/abc@def@ghi").unwrap()
|
||
)
|
||
.await,
|
||
Some("abc".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("https://example.com/@abc.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("@abc".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("https://example.com/@abc/mod.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("@abc".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("file:///@abc.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("@abc".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("file:///@abc/cli.ts").unwrap()
|
||
)
|
||
.await,
|
||
Some("@abc".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("npm:cowsay@1.2/cowthink").unwrap()
|
||
)
|
||
.await,
|
||
Some("cowthink".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("npm:cowsay@1.2/cowthink/test").unwrap()
|
||
)
|
||
.await,
|
||
Some("cowsay".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(&http_client, &Url::parse("npm:cowsay@1.2").unwrap())
|
||
.await,
|
||
Some("cowsay".to_string())
|
||
);
|
||
assert_eq!(
|
||
infer_name_from_url(
|
||
&http_client,
|
||
&Url::parse("npm:@types/node@1.2").unwrap()
|
||
)
|
||
.await,
|
||
None
|
||
);
|
||
}
|
||
|
||
#[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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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().to_string()),
|
||
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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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().to_string()),
|
||
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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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().to_string()),
|
||
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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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().to_string()),
|
||
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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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().to_string()),
|
||
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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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().to_string()),
|
||
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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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().to_string()),
|
||
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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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().to_string()),
|
||
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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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().to_string()),
|
||
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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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().to_string()),
|
||
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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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().to_string()),
|
||
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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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(
|
||
&HttpClientProvider::new(None, None),
|
||
&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());
|
||
}
|
||
}
|
||
}
|