diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2023-12-06 16:36:06 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-06 16:36:06 -0500 |
commit | e372fc73e806faeeb3c67df2d5b10a63fe5e8213 (patch) | |
tree | 8231a04ab1ea19dc4aa9d8e6be502cbf6c136ede /cli/tools/task.rs | |
parent | 7fdc3c8f1fc27be2ca7d4ff62b9fd8ecb3d24e61 (diff) |
fix(task): handle node_modules/.bin directory with byonm (#21386)
A bit hacky, but it works. Essentially, this will check for all the
scripts in the node_modules/.bin directory then force them to run with
Deno via deno_task_shell.
Diffstat (limited to 'cli/tools/task.rs')
-rw-r--r-- | cli/tools/task.rs | 152 |
1 files changed, 149 insertions, 3 deletions
diff --git a/cli/tools/task.rs b/cli/tools/task.rs index d929dc666..78d09f0c7 100644 --- a/cli/tools/task.rs +++ b/cli/tools/task.rs @@ -5,6 +5,8 @@ use crate::args::Flags; use crate::args::TaskFlags; use crate::colors; use crate::factory::CliFactory; +use crate::npm::CliNpmResolver; +use crate::npm::InnerCliNpmResolverRef; use crate::npm::ManagedCliNpmResolver; use crate::util::fs::canonicalize_path; use deno_core::anyhow::bail; @@ -18,6 +20,8 @@ use deno_task_shell::ExecuteResult; use deno_task_shell::ShellCommand; use deno_task_shell::ShellCommandContext; use indexmap::IndexMap; +use lazy_regex::Lazy; +use regex::Regex; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -115,11 +119,15 @@ pub async fn execute_script( output_task(&task_name, &script); let seq_list = deno_task_shell::parser::parse(&script) .with_context(|| format!("Error parsing script '{task_name}'."))?; - let npx_commands = match npm_resolver.as_managed() { - Some(npm_resolver) => { + let npx_commands = match npm_resolver.as_inner() { + InnerCliNpmResolverRef::Managed(npm_resolver) => { resolve_npm_commands(npm_resolver, node_resolver)? } - None => Default::default(), + InnerCliNpmResolverRef::Byonm(npm_resolver) => { + let node_modules_dir = + npm_resolver.root_node_modules_path().unwrap(); + resolve_npm_commands_from_bin_dir(node_modules_dir)? + } }; let env_vars = match npm_resolver.root_node_modules_path() { Some(dir_path) => collect_env_vars_with_node_modules_dir(dir_path), @@ -294,6 +302,113 @@ impl ShellCommand for NpmPackageBinCommand { } } +/// Runs a module in the node_modules folder. +#[derive(Clone)] +struct NodeModulesFileRunCommand { + command_name: String, + path: PathBuf, +} + +impl ShellCommand for NodeModulesFileRunCommand { + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let mut args = vec![ + "run".to_string(), + "--ext=js".to_string(), + "-A".to_string(), + self.path.to_string_lossy().to_string(), + ]; + args.extend(context.args); + let executable_command = + deno_task_shell::ExecutableCommand::new("deno".to_string()); + // set this environment variable so that the launched process knows the npm command name + context + .state + .apply_env_var("DENO_INTERNAL_NPM_CMD_NAME", &self.command_name); + executable_command.execute(ShellCommandContext { args, ..context }) + } +} + +fn resolve_npm_commands_from_bin_dir( + node_modules_dir: &Path, +) -> Result<HashMap<String, Rc<dyn ShellCommand>>, AnyError> { + let mut result = HashMap::<String, Rc<dyn ShellCommand>>::new(); + let bin_dir = node_modules_dir.join(".bin"); + log::debug!("Resolving commands in '{}'.", bin_dir.display()); + match std::fs::read_dir(&bin_dir) { + Ok(entries) => { + for entry in entries { + let Ok(entry) = entry else { + continue; + }; + if let Some(command) = resolve_bin_dir_entry_command(entry) { + result.insert(command.command_name.clone(), Rc::new(command)); + } + } + } + Err(err) => { + log::debug!("Failed read_dir for '{}': {:#}", bin_dir.display(), err); + } + } + Ok(result) +} + +fn resolve_bin_dir_entry_command( + entry: std::fs::DirEntry, +) -> Option<NodeModulesFileRunCommand> { + if entry.path().extension().is_some() { + return None; // only look at files without extensions (even on Windows) + } + let file_type = entry.file_type().ok()?; + let path = if file_type.is_file() { + entry.path() + } else if file_type.is_symlink() { + entry.path().canonicalize().ok()? + } else { + return None; + }; + let text = std::fs::read_to_string(&path).ok()?; + let command_name = entry.file_name().to_string_lossy().to_string(); + if let Some(path) = resolve_execution_path_from_npx_shim(path, &text) { + log::debug!( + "Resolved npx command '{}' to '{}'.", + command_name, + path.display() + ); + Some(NodeModulesFileRunCommand { command_name, path }) + } else { + log::debug!("Failed resolving npx command '{}'.", command_name); + None + } +} + +/// This is not ideal, but it works ok because it allows us to bypass +/// the shebang and execute the script directly with Deno. +fn resolve_execution_path_from_npx_shim( + file_path: PathBuf, + text: &str, +) -> Option<PathBuf> { + static SCRIPT_PATH_RE: Lazy<Regex> = + lazy_regex::lazy_regex!(r#""\$basedir\/([^"]+)" "\$@""#); + + if text.starts_with("#!/usr/bin/env node") { + // launch this file itself because it's a JS file + Some(file_path) + } else { + // Search for... + // > "$basedir/../next/dist/bin/next" "$@" + // ...which is what it will look like on Windows + SCRIPT_PATH_RE + .captures(text) + .and_then(|c| c.get(1)) + .map(|relative_path| { + file_path.parent().unwrap().join(relative_path.as_str()) + }) + } +} + fn resolve_npm_commands( npm_resolver: &ManagedCliNpmResolver, node_resolver: &NodeResolver, @@ -351,4 +466,35 @@ mod test { HashMap::from([("PATH".to_string(), "/example".to_string())]) ); } + + #[test] + fn test_resolve_execution_path_from_npx_shim() { + // example shim on unix + let unix_shim = r#"#!/usr/bin/env node +"use strict"; +console.log('Hi!'); +"#; + let path = PathBuf::from("/node_modules/.bin/example"); + assert_eq!( + resolve_execution_path_from_npx_shim(path.clone(), unix_shim).unwrap(), + path + ); + // example shim on windows + let windows_shim = r#"#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../example/bin/example" "$@" +else + exec node "$basedir/../example/bin/example" "$@" +fi"#; + assert_eq!( + resolve_execution_path_from_npx_shim(path.clone(), windows_shim).unwrap(), + path.parent().unwrap().join("../example/bin/example") + ); + } } |