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.
This commit is contained in:
David Sherret 2023-12-06 16:36:06 -05:00 committed by GitHub
parent 7fdc3c8f1f
commit e372fc73e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 446 additions and 298 deletions

View file

@ -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")
);
}
}