use crate::core::runner;
use crate::core::utils::resolved_command;
use anyhow::Result;
const NPM_SUBCOMMANDS: &[&str] = &[
"install",
"i",
"ci",
"uninstall",
"remove",
"rm",
"update",
"up",
"list",
"ls",
"outdated",
"init",
"create",
"publish",
"pack",
"link",
"audit",
"fund",
"exec",
"explain",
"why",
"search",
"view",
"info",
"show",
"config",
"set",
"get",
"cache",
"prune",
"dedupe",
"doctor",
"help",
"version",
"prefix",
"root",
"bin",
"bugs",
"docs",
"home",
"repo",
"ping",
"whoami",
"token",
"profile",
"team",
"access",
"owner",
"deprecate",
"dist-tag",
"star",
"stars",
"login",
"logout",
"adduser",
"unpublish",
"pkg",
"diff",
"rebuild",
"test",
"t",
"start",
"stop",
"restart",
];
pub fn run(args: &[String], verbose: u8, skip_env: bool) -> Result<i32> {
let first_arg = args.first().map(|s| s.as_str());
let is_run_explicit = first_arg == Some("run");
let is_npm_subcommand = first_arg
.map(|a| NPM_SUBCOMMANDS.contains(&a) || a.starts_with('-'))
.unwrap_or(false);
let mut effective_args: Vec<String> = Vec::with_capacity(args.len() + 1);
if is_run_explicit || is_npm_subcommand {
effective_args.extend_from_slice(args);
} else {
effective_args.push("run".to_string());
effective_args.extend_from_slice(args);
}
run_filtered("npm", &effective_args, verbose, skip_env)
}
pub fn exec(args: &[String], verbose: u8, skip_env: bool) -> Result<i32> {
run_filtered("npx", args, verbose, skip_env)
}
fn run_filtered(name: &str, args: &[String], verbose: u8, skip_env: bool) -> Result<i32> {
let mut cmd = resolved_command(name);
for arg in args {
cmd.arg(arg);
}
if skip_env {
cmd.env("SKIP_ENV_VALIDATION", "1");
}
let args_display = args.join(" ");
if verbose > 0 {
eprintln!("Running: {} {}", name, args_display);
}
runner::run_filtered(
cmd,
name,
&args_display,
filter_npm_output,
runner::RunOptions::default(),
)
}
fn filter_npm_output(output: &str) -> String {
let mut result = Vec::new();
for line in output.lines() {
if line.starts_with('>') && line.contains('@') {
continue;
}
if line.trim_start().starts_with("npm WARN") {
continue;
}
if line.trim_start().starts_with("npm notice") {
continue;
}
if line.contains("⸩") || line.contains("⸨") || line.contains("...") && line.len() < 10 {
continue;
}
if line.trim().is_empty() {
continue;
}
result.push(line.to_string());
}
if result.is_empty() {
"ok".to_string()
} else {
result.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_filter_npm_output() {
let output = r#"
> project@1.0.0 build
> next build
npm WARN deprecated inflight@1.0.6: This module is not supported
npm notice
Creating an optimized production build...
✓ Build completed
"#;
let result = filter_npm_output(output);
assert!(!result.contains("npm WARN"));
assert!(!result.contains("npm notice"));
assert!(!result.contains("> project@"));
assert!(result.contains("Build completed"));
}
#[test]
fn test_npm_subcommand_routing() {
fn needs_run_injection(args: &[&str]) -> bool {
let first = args.first().copied();
let is_run_explicit = first == Some("run");
let is_subcommand = first
.map(|a| NPM_SUBCOMMANDS.contains(&a) || a.starts_with('-'))
.unwrap_or(false);
!is_run_explicit && !is_subcommand
}
for subcmd in NPM_SUBCOMMANDS {
assert!(
!needs_run_injection(&[subcmd]),
"'npm {}' should NOT inject 'run'",
subcmd
);
}
for script in &["build", "dev", "lint", "typecheck", "deploy"] {
assert!(
needs_run_injection(&[script]),
"'npm {}' SHOULD inject 'run'",
script
);
}
assert!(!needs_run_injection(&["--version"]));
assert!(!needs_run_injection(&["-h"]));
assert!(!needs_run_injection(&["run", "build"]));
}
#[test]
fn test_filter_npm_output_empty() {
let output = "\n\n\n";
let result = filter_npm_output(output);
assert_eq!(result, "ok");
}
}