/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
v2.7.13
cli/tools/x.rs
700 строк
20 KB
David Sherret
refactor: reduce cwd lookups and enforce `canonicalize_path` (#32250)
23 фев 2026, 06:04
Не верифицирован
23 фев 2026, 06:04
38f3ffe
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::collections::BTreeMap; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use deno_cache_dir::file_fetcher::CacheSetting; use deno_core::anyhow; use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_npm_installer::PackagesAllowedScripts; use deno_runtime::deno_permissions::PathQueryDescriptor; use deno_semver::jsr::JsrPackageReqReference; use deno_semver::npm::NpmPackageReqReference; use deno_semver::package::PackageReq; use node_resolver::BinValue; use crate::args::DenoXShimName; use crate::args::Flags; use crate::args::XFlags; use crate::args::XFlagsKind; use crate::factory::CliFactory; use crate::node::CliNodeResolver; use crate::npm::CliManagedNpmResolver; use crate::npm::CliNpmResolver; use crate::tools::pm::CacheTopLevelDepsOptions; use crate::util::console::ConfirmOptions; use crate::util::console::confirm; use crate::util::draw_thread::DrawThread; use crate::util::fs::canonicalize_path; async fn resolve_local_bins( node_resolver: &CliNodeResolver, npm_resolver: &CliNpmResolver, factory: &CliFactory, ) -> Result<BTreeMap<String, BinValue>, AnyError> { match &npm_resolver { deno_resolver::npm::NpmResolver::Byonm(npm_resolver) => { let node_modules_dir = npm_resolver.root_node_modules_path().unwrap(); let bin_dir = node_modules_dir.join(".bin"); Ok(node_resolver.resolve_npm_commands_from_bin_dir(&bin_dir)) } deno_resolver::npm::NpmResolver::Managed(npm_resolver) => { let mut all_bins = BTreeMap::new(); for id in npm_resolver.resolution().top_level_packages() { let package_folder = npm_resolver.resolve_pkg_folder_from_pkg_id(&id)?; let bins = match node_resolver .resolve_npm_binary_commands_for_package(&package_folder) { Ok(bins) => bins, Err(_) => { crate::tools::pm::cache_top_level_deps( factory, None, CacheTopLevelDepsOptions { lockfile_only: false, }, ) .await?; node_resolver .resolve_npm_binary_commands_for_package(&package_folder)? } }; for (command, bin_value) in bins { all_bins.insert(command.clone(), bin_value.clone()); } } Ok(all_bins) } } } fn run_js_file( main_module: &deno_core::url::Url, deno_args: &[String], argv: &[String], npm_process_state: Option<String>, npm: bool, ) -> Result<i32, AnyError> { use deno_runtime::deno_io::FromRawIoHandle; let deno_exe = std::env::current_exe() .and_then(|p| canonicalize_path(&p)) .context("Failed to get current executable path")?; let mut args: Vec<std::ffi::OsString> = vec!["run".into()]; args.extend(deno_args.iter().map(|s| s.into())); args.push(main_module.as_str().into()); args.extend(argv.iter().map(|s| s.into())); let mut command = std::process::Command::new(deno_exe); command.args(&args); let _temp_file = if let Some(state) = &npm_process_state { let fd = deno_runtime::deno_process::npm_process_state_tempfile(state.as_bytes()) .context("Failed to create npm process state tempfile")?; command.env( deno_runtime::deno_process::NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME, (fd as usize).to_string(), ); // Keep the file alive until the subprocess completes // SAFETY: fd is valid Some(unsafe { std::fs::File::from_raw_io_handle(fd) }) } else { None }; if npm { crate::tools::run::set_npm_user_agent(); } #[cfg(unix)] { use std::os::unix::process::CommandExt; Err(command.exec().into()) } #[cfg(not(unix))] { let mut child = command.spawn().context("Failed to spawn deno subprocess")?; let status = child.wait().context("Failed to wait for deno subprocess")?; Ok(status.code().unwrap_or(1)) } } fn get_npm_process_state(npm_resolver: &CliNpmResolver) -> Option<String> { match npm_resolver { deno_resolver::npm::NpmResolver::Managed(managed) => Some( deno_npm_installer::process_state::NpmProcessState::new_managed( managed.resolution().serialized_valid_snapshot(), managed.root_node_modules_path(), ) .as_serialized(), ), deno_resolver::npm::NpmResolver::Byonm(_) => None, } } fn run_bin_value( factory: &CliFactory, flags: &Flags, bin_value: BinValue, npm_process_state: Option<String>, unstable_args: &[String], ) -> Result<i32, AnyError> { match bin_value { BinValue::JsFile(path_buf) => { let path = deno_path_util::url_from_file_path(path_buf.as_ref())?; let mut deno_args = flags.to_permission_args(); deno_args.extend(unstable_args.iter().cloned()); run_js_file(&path, &deno_args, &flags.argv, npm_process_state, true) } BinValue::Executable(mut path_buf) => { if cfg!(windows) && path_buf.extension().is_none() { // prefer cmd shim over sh path_buf.set_extension("cmd"); if !path_buf.exists() { // just fall back to original path path_buf.set_extension(""); } } let permissions = factory.root_permissions_container()?; permissions.check_run( &deno_runtime::deno_permissions::RunQueryDescriptor::Path( PathQueryDescriptor::new( &factory.sys(), std::borrow::Cow::Borrowed(path_buf.as_ref()), )?, ), "entrypoint", )?; let mut child = std::process::Command::new(path_buf) .args(&flags.argv) .spawn() .context("Failed to spawn command")?; let status = child.wait()?; Ok(status.code().unwrap_or(1)) } } } /// Try to find a bin value from a map of bins, with fallbacks for scoped package names /// and single-bin packages. fn find_bin_value( bins: &BTreeMap<String, BinValue>, bin_name: &str, ) -> Option<BinValue> { bins .get(bin_name) .or_else(|| { // Try the package name without scope as fallback if bin_name.starts_with('@') && bin_name.contains('/') { bin_name .split('/') .next_back() .and_then(|name| bins.get(name)) } else { None } }) .or_else(|| { // If there's only one bin, use it if bins.len() == 1 { bins.values().next() } else { None } }) .cloned() } async fn maybe_run_local_npm_bin( factory: &CliFactory, flags: &Flags, node_resolver: &CliNodeResolver, npm_resolver: &CliNpmResolver, command: &str, unstable_args: &[String], ) -> Result<Option<i32>, AnyError> { let mut bins = resolve_local_bins(node_resolver, npm_resolver, factory).await?; let bin_value = if let Some(bin_value) = bins.remove(command) { bin_value } else if let Some(bin_value) = { let command = if command.starts_with("@") && command.contains("/") { command.split("/").last().unwrap() } else { command }; bins.remove(command) } { bin_value } else { return Ok(None); }; let npm_process_state = get_npm_process_state(npm_resolver); run_bin_value(factory, flags, bin_value, npm_process_state, unstable_args) .map(Some) } enum XTempDir { Existing(PathBuf), New(PathBuf), } impl XTempDir { fn path(&self) -> &PathBuf { match self { XTempDir::Existing(path) => path, XTempDir::New(path) => path, } } } fn create_package_temp_dir( prefix: Option<&str>, package_req: &PackageReq, reload: bool, deno_dir: &Path, ) -> Result<XTempDir, AnyError> { let mut package_req_folder = String::from(prefix.unwrap_or("")); package_req_folder.push_str( &package_req .to_string() .replace("/", "__") .replace(">", "gt") .replace("<", "lt"), ); let temp_dir = deno_dir.join("deno_x_cache").join(package_req_folder); if temp_dir.exists() { if reload || !temp_dir.join("deno.lock").exists() { std::fs::remove_dir_all(&temp_dir)?; } else { let temp_dir = canonicalize_path(&temp_dir).unwrap_or(temp_dir); return Ok(XTempDir::Existing(temp_dir)); } } std::fs::create_dir_all(&temp_dir)?; let package_json_path = temp_dir.join("package.json"); std::fs::write(&package_json_path, "{}")?; let deno_json_path = temp_dir.join("deno.json"); std::fs::write(&deno_json_path, r#"{"nodeModulesDir": "auto"}"#)?; let temp_dir = canonicalize_path(&temp_dir).unwrap_or(temp_dir); Ok(XTempDir::New(temp_dir)) } fn write_shim( out_dir: &Path, shim_name: DenoXShimName, ) -> Result<(), AnyError> { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let out_path = out_dir.join(shim_name.name()); if let DenoXShimName::Other(_) = shim_name { std::fs::write( &out_path, r##"#!/bin/sh SCRIPT_DIR="$(dirname -- "$(readlink -f -- "$0")")" exec "$SCRIPT_DIR/deno" x "$@" "## .as_bytes(), )?; let mut permissions = std::fs::metadata(&out_path)?.permissions(); permissions.set_mode(0o755); std::fs::set_permissions(&out_path, permissions)?; } else { match std::os::unix::fs::symlink("./deno", &out_path) { Ok(_) => {} Err(e) => match e.kind() { std::io::ErrorKind::AlreadyExists => { std::fs::remove_file(&out_path)?; std::os::unix::fs::symlink("./deno", &out_path)?; } _ => return Err(e.into()), }, } } } #[cfg(windows)] { let out_path = out_dir.join(format!("{}.cmd", shim_name.name())); std::fs::write( out_path, r##"@echo off "%~dp0deno.exe" x %* exit /b %ERRORLEVEL% "##, )?; } Ok(()) } pub async fn run(flags: Arc<Flags>, x_flags: XFlags) -> Result<i32, AnyError> { let command_flags = match x_flags.kind { XFlagsKind::InstallAlias(shim_name) => { let exe = std::env::current_exe()?; let out_dir = exe.parent().unwrap(); write_shim(out_dir, shim_name)?; return Ok(0); } XFlagsKind::Command(command) => command, XFlagsKind::Print => { let factory = CliFactory::from_flags(flags.clone()); let npm_resolver = factory.npm_resolver().await?; let node_resolver = factory.node_resolver().await?; let bins = resolve_local_bins(node_resolver, npm_resolver, &factory).await?; if bins.is_empty() { log::info!("No local commands found"); return Ok(0); } log::info!("Available (local) commands:"); for command in bins.keys() { log::info!(" {}", command); } return Ok(0); } }; let factory = CliFactory::from_flags(flags.clone()); let cli_options = factory.cli_options()?; let unstable_args = cli_options.unstable_args(); let npm_resolver = factory.npm_resolver().await?; let node_resolver = factory.node_resolver().await?; let result = maybe_run_local_npm_bin( &factory, &flags, node_resolver, npm_resolver, &command_flags.command, &unstable_args, ) .await?; if let Some(exit_code) = result { return Ok(exit_code); } let is_file_like = command_flags.command.starts_with('.') || command_flags.command.starts_with('/') || command_flags.command.starts_with('~') || command_flags.command.starts_with('\\') || Path::new(&command_flags.command).extension().is_some(); if is_file_like && Path::new(&command_flags.command).is_file() { return Err(anyhow::anyhow!( "Use 'deno run' to run a local file directly, 'deno x' is intended for running commands from packages." )); } let thing_to_run = match deno_core::url::Url::parse(&command_flags.command) { Ok(url) => { if url.scheme() == "npm" { let req_ref = NpmPackageReqReference::from_specifier(&url)?; ReqRefOrUrl::Npm(req_ref) } else if url.scheme() == "jsr" { let req_ref = JsrPackageReqReference::from_specifier(&url)?; ReqRefOrUrl::Jsr(req_ref) } else { ReqRefOrUrl::Url(url) } } Err(deno_core::url::ParseError::RelativeUrlWithoutBase) => { let new_command = format!("npm:{}", command_flags.command); let req_ref = NpmPackageReqReference::from_str(&new_command)?; ReqRefOrUrl::Npm(req_ref) } Err(e) => { return Err(e.into()); } }; let cache_setting = cli_options.cache_setting(); let reload = matches!(cache_setting, CacheSetting::ReloadAll); match thing_to_run { ReqRefOrUrl::Npm(npm_package_req_reference) => { // First try to resolve from the local project if let Ok(package_folder) = npm_resolver .resolve_pkg_folder_from_deno_module_req( npm_package_req_reference.req(), &deno_path_util::url_from_directory_path(cli_options.initial_cwd()) .unwrap(), ) { let bin_name = if let Some(sub_path) = npm_package_req_reference.sub_path() { sub_path } else { npm_package_req_reference.req().name.as_str() }; let bins = node_resolver .resolve_npm_binary_commands_for_package(&package_folder)?; if let Some(bin_value) = find_bin_value(&bins, bin_name) { let npm_process_state = get_npm_process_state(npm_resolver); return run_bin_value( &factory, &flags, bin_value, npm_process_state, &unstable_args, ); } } // Fall back to autoinstall let (managed_flags, managed_factory) = autoinstall_package( ReqRef::Npm(&npm_package_req_reference), &flags, reload, command_flags.yes, &factory.deno_dir()?.root, ) .await?; let mut runner_flags = (*managed_flags).clone(); runner_flags.node_modules_dir = Some(deno_config::deno_json::NodeModulesDirMode::Manual); let runner_flags = Arc::new(runner_flags); let runner_factory = CliFactory::from_flags(runner_flags.clone()); let runner_node_resolver = runner_factory.node_resolver().await?; let runner_npm_resolver = runner_factory.npm_resolver().await?; let bin_name = if let Some(sub_path) = npm_package_req_reference.sub_path() { sub_path } else { npm_package_req_reference.req().name.as_str() }; let res = maybe_run_local_npm_bin( &runner_factory, &runner_flags, runner_node_resolver, runner_npm_resolver, bin_name, &unstable_args, ) .await?; if let Some(exit_code) = res { Ok(exit_code) } else { let managed_npm_resolver = managed_factory.npm_resolver().await?.as_managed().unwrap(); let bin_commands = bin_commands_for_package( runner_node_resolver, managed_npm_resolver, npm_package_req_reference.req(), )?; let fallback_name = if bin_commands.len() == 1 { Some(bin_commands.keys().next().unwrap()) } else { None }; if let Some(fallback_name) = fallback_name && let Some(exit_code) = maybe_run_local_npm_bin( &runner_factory, &runner_flags, runner_node_resolver, runner_npm_resolver, fallback_name.as_ref(), &unstable_args, ) .await? { return Ok(exit_code); } Err(anyhow::anyhow!( "Unable to choose binary for {}\n Available bins:\n{}", command_flags.command, bin_commands .keys() .map(|k| format!(" {}", k)) .collect::<Vec<_>>() .join("\n") )) } } ReqRefOrUrl::Jsr(jsr_package_req_reference) => { let (_new_flags, new_factory) = autoinstall_package( ReqRef::Jsr(&jsr_package_req_reference), &flags, reload, command_flags.yes, &factory.deno_dir()?.root, ) .await?; let npm_resolver = new_factory.npm_resolver().await?; let npm_process_state = get_npm_process_state(npm_resolver); let permission_args = flags.to_permission_args(); let url = deno_core::url::Url::parse(&jsr_package_req_reference.to_string())?; run_js_file( &url, &permission_args, &flags.argv, npm_process_state, false, ) } ReqRefOrUrl::Url(url) => { let permission_args = flags.to_permission_args(); run_js_file(&url, &permission_args, &flags.argv, None, false) } } } fn bin_commands_for_package( node_resolver: &CliNodeResolver, managed_npm_resolver: &CliManagedNpmResolver, package_req: &PackageReq, ) -> Result<BTreeMap<String, BinValue>, AnyError> { let pkg_id = managed_npm_resolver.resolve_pkg_id_from_deno_module_req(package_req)?; let package_folder = managed_npm_resolver.resolve_pkg_folder_from_pkg_id(&pkg_id)?; node_resolver .resolve_npm_binary_commands_for_package(&package_folder) .map_err(Into::into) } async fn autoinstall_package( req_ref: ReqRef<'_>, old_flags: &Flags, reload: bool, yes: bool, deno_dir: &Path, ) -> Result<(Arc<Flags>, CliFactory), AnyError> { fn make_new_flags(old_flags: &Flags, temp_dir: &Path) -> Arc<Flags> { let mut new_flags = (*old_flags).clone(); new_flags.node_modules_dir = Some(deno_config::deno_json::NodeModulesDirMode::Auto); let temp_node_modules = temp_dir.join("node_modules"); new_flags.internal.root_node_modules_dir_override = Some(temp_node_modules); new_flags.config_flag = crate::args::ConfigFlag::Path( temp_dir.join("deno.json").to_string_lossy().into_owned(), ); new_flags.allow_scripts = PackagesAllowedScripts::All; log::debug!("new_flags: {:?}", new_flags); Arc::new(new_flags) } let temp_dir = create_package_temp_dir( Some(req_ref.prefix()), req_ref.req(), reload, deno_dir, )?; let new_flags = make_new_flags(old_flags, temp_dir.path()); let new_factory = CliFactory::from_flags(new_flags.clone()); match temp_dir { XTempDir::Existing(_) => Ok((new_flags, new_factory)), XTempDir::New(temp_dir) => { let confirmed = yes || match confirm(ConfirmOptions { default: true, message: format!("Install {}?", req_ref), }) { Some(true) => true, Some(false) => false, None if !DrawThread::is_supported() => { log::warn!( "Unable to prompt, installing {} without confirmation", req_ref.req() ); true } None => false, }; if !confirmed { return Err(anyhow::anyhow!("Installation rejected")); } match req_ref { ReqRef::Npm(req_ref) => { let pkg_json = temp_dir.join("package.json"); std::fs::write( &pkg_json, format!( "{{\"dependencies\": {{\"{}\": \"{}\"}} }}", req_ref.req().name, req_ref.req().version_req ), )?; } ReqRef::Jsr(req_ref) => { let deno_json = temp_dir.join("deno.json"); std::fs::write( &deno_json, format!( "{{ \"nodeModulesDir\": \"manual\", \"imports\": {{ \"{}\": \"{}\" }} }}", req_ref.req().name, format_args!("jsr:{}", req_ref.req()) ), )?; } } crate::tools::pm::cache_top_level_deps( &new_factory, None, CacheTopLevelDepsOptions { lockfile_only: false, }, ) .await?; if let Some(lockfile) = new_factory.maybe_lockfile().await? { lockfile.write_if_changed()?; } Ok((new_flags, new_factory)) } } } #[derive(Debug, Clone, Copy)] enum ReqRef<'a> { Npm(&'a NpmPackageReqReference), Jsr(&'a JsrPackageReqReference), } impl<'a> std::fmt::Display for ReqRef<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ReqRef::Npm(req) => write!(f, "{}", req), ReqRef::Jsr(req) => write!(f, "{}", req), } } } impl<'a> ReqRef<'a> { fn req(&self) -> &PackageReq { match self { ReqRef::Npm(req) => req.req(), ReqRef::Jsr(req) => req.req(), } } fn prefix(&self) -> &str { match self { ReqRef::Npm(_) => "npm-", ReqRef::Jsr(_) => "jsr-", } } } enum ReqRefOrUrl { Npm(NpmPackageReqReference), Jsr(JsrPackageReqReference), Url(deno_core::url::Url), }