summaryrefslogtreecommitdiff
path: root/cli/tools/task.rs
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2023-12-06 16:36:06 -0500
committerGitHub <noreply@github.com>2023-12-06 16:36:06 -0500
commite372fc73e806faeeb3c67df2d5b10a63fe5e8213 (patch)
tree8231a04ab1ea19dc4aa9d8e6be502cbf6c136ede /cli/tools/task.rs
parent7fdc3c8f1fc27be2ca7d4ff62b9fd8ecb3d24e61 (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.rs152
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")
+ );
+ }
}