Skip to content

Commit 143ea47

Browse files
authored
feat(task): run npm run commands with Deno more often (#23794)
Closes #23036
1 parent 0b8deca commit 143ea47

File tree

7 files changed

+143
-37
lines changed

7 files changed

+143
-37
lines changed

cli/tools/task.rs

+102-37
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ use std::path::PathBuf;
2828
use std::rc::Rc;
2929
use tokio::task::LocalSet;
3030

31+
// WARNING: Do not depend on this env var in user code. It's not stable API.
32+
const USE_PKG_JSON_HIDDEN_ENV_VAR_NAME: &str =
33+
"DENO_INTERNAL_TASK_USE_PKG_JSON";
34+
3135
pub async fn execute_script(
3236
flags: Flags,
3337
task_flags: TaskFlags,
@@ -55,13 +59,20 @@ pub async fn execute_script(
5559
let npm_resolver = factory.npm_resolver().await?;
5660
let node_resolver = factory.node_resolver().await?;
5761
let env_vars = real_env_vars();
62+
let force_use_pkg_json = std::env::var_os(USE_PKG_JSON_HIDDEN_ENV_VAR_NAME)
63+
.map(|v| {
64+
// always remove so sub processes don't inherit this env var
65+
std::env::remove_var(USE_PKG_JSON_HIDDEN_ENV_VAR_NAME);
66+
v == "1"
67+
})
68+
.unwrap_or(false);
5869

5970
if let Some(
6071
deno_config::Task::Definition(script)
6172
| deno_config::Task::Commented {
6273
definition: script, ..
6374
},
64-
) = tasks_config.get(task_name)
75+
) = tasks_config.get(task_name).filter(|_| !force_use_pkg_json)
6576
{
6677
let config_file_url = cli_options.maybe_config_file_specifier().unwrap();
6778
let config_file_path = if config_file_url.scheme() == "file" {
@@ -77,16 +88,18 @@ pub async fn execute_script(
7788

7889
let custom_commands =
7990
resolve_custom_commands(npm_resolver.as_ref(), node_resolver)?;
80-
run_task(
91+
run_task(RunTaskOptions {
8192
task_name,
8293
script,
83-
&cwd,
84-
cli_options.initial_cwd(),
94+
cwd: &cwd,
95+
init_cwd: cli_options.initial_cwd(),
8596
env_vars,
86-
cli_options.argv(),
97+
argv: cli_options.argv(),
8798
custom_commands,
88-
npm_resolver.root_node_modules_path().map(|p| p.as_path()),
89-
)
99+
root_node_modules_dir: npm_resolver
100+
.root_node_modules_path()
101+
.map(|p| p.as_path()),
102+
})
90103
.await
91104
} else if package_json_scripts.contains_key(task_name) {
92105
let package_json_deps_provider = factory.package_json_deps_provider();
@@ -134,18 +147,20 @@ pub async fn execute_script(
134147
];
135148
let custom_commands =
136149
resolve_custom_commands(npm_resolver.as_ref(), node_resolver)?;
137-
for task_name in task_names {
138-
if let Some(script) = package_json_scripts.get(&task_name) {
139-
let exit_code = run_task(
140-
&task_name,
150+
for task_name in &task_names {
151+
if let Some(script) = package_json_scripts.get(task_name) {
152+
let exit_code = run_task(RunTaskOptions {
153+
task_name,
141154
script,
142-
&cwd,
143-
cli_options.initial_cwd(),
144-
env_vars.clone(),
145-
cli_options.argv(),
146-
custom_commands.clone(),
147-
npm_resolver.root_node_modules_path().map(|p| p.as_path()),
148-
)
155+
cwd: &cwd,
156+
init_cwd: cli_options.initial_cwd(),
157+
env_vars: env_vars.clone(),
158+
argv: cli_options.argv(),
159+
custom_commands: custom_commands.clone(),
160+
root_node_modules_dir: npm_resolver
161+
.root_node_modules_path()
162+
.map(|p| p.as_path()),
163+
})
149164
.await?;
150165
if exit_code > 0 {
151166
return Ok(exit_code);
@@ -167,25 +182,31 @@ pub async fn execute_script(
167182
}
168183
}
169184

170-
#[allow(clippy::too_many_arguments)]
171-
async fn run_task(
172-
task_name: &str,
173-
script: &str,
174-
cwd: &Path,
175-
init_cwd: &Path,
185+
struct RunTaskOptions<'a> {
186+
task_name: &'a str,
187+
script: &'a str,
188+
cwd: &'a Path,
189+
init_cwd: &'a Path,
176190
env_vars: HashMap<String, String>,
177-
argv: &[String],
191+
argv: &'a [String],
178192
custom_commands: HashMap<String, Rc<dyn ShellCommand>>,
179-
root_node_modules_dir: Option<&Path>,
180-
) -> Result<i32, AnyError> {
181-
let script = get_script_with_args(script, argv);
182-
output_task(task_name, &script);
193+
root_node_modules_dir: Option<&'a Path>,
194+
}
195+
196+
async fn run_task(opts: RunTaskOptions<'_>) -> Result<i32, AnyError> {
197+
let script = get_script_with_args(opts.script, opts.argv);
198+
output_task(opts.task_name, &script);
183199
let seq_list = deno_task_shell::parser::parse(&script)
184-
.with_context(|| format!("Error parsing script '{}'.", task_name))?;
185-
let env_vars = prepare_env_vars(env_vars, init_cwd, root_node_modules_dir);
200+
.with_context(|| format!("Error parsing script '{}'.", opts.task_name))?;
201+
let env_vars =
202+
prepare_env_vars(opts.env_vars, opts.init_cwd, opts.root_node_modules_dir);
186203
let local = LocalSet::new();
187-
let future =
188-
deno_task_shell::execute(seq_list, env_vars, cwd, custom_commands);
204+
let future = deno_task_shell::execute(
205+
seq_list,
206+
env_vars,
207+
opts.cwd,
208+
opts.custom_commands,
209+
);
189210
Ok(local.run_until(future).await)
190211
}
191212

@@ -315,6 +336,48 @@ fn print_available_tasks(
315336
Ok(())
316337
}
317338

339+
struct NpmCommand;
340+
341+
impl ShellCommand for NpmCommand {
342+
fn execute(
343+
&self,
344+
mut context: ShellCommandContext,
345+
) -> LocalBoxFuture<'static, ExecuteResult> {
346+
if context.args.first().map(|s| s.as_str()) == Some("run")
347+
&& !context.args.iter().any(|s| s == "--")
348+
{
349+
if let Some(task_name) = context.args.get(1) {
350+
// run with deno task instead
351+
let mut args = vec!["task".to_string(), task_name.to_string()];
352+
args.extend(context.args.iter().skip(2).cloned());
353+
let mut state = context.state;
354+
state.apply_env_var(USE_PKG_JSON_HIDDEN_ENV_VAR_NAME, "1");
355+
return ExecutableCommand::new(
356+
"deno".to_string(),
357+
std::env::current_exe().unwrap(),
358+
)
359+
.execute(ShellCommandContext {
360+
args,
361+
state,
362+
..context
363+
});
364+
}
365+
}
366+
367+
// fallback to running the real npm command
368+
let npm_path = match context.resolve_command_path("npm") {
369+
Ok(path) => path,
370+
Err(err) => {
371+
let _ = context.stderr.write_line(&format!("{}", err));
372+
return Box::pin(futures::future::ready(
373+
ExecuteResult::from_exit_code(err.exit_code()),
374+
));
375+
}
376+
};
377+
ExecutableCommand::new("npm".to_string(), npm_path).execute(context)
378+
}
379+
}
380+
318381
struct NpxCommand;
319382

320383
impl ShellCommand for NpxCommand {
@@ -413,15 +476,17 @@ fn resolve_custom_commands(
413476
npm_resolver: &dyn CliNpmResolver,
414477
node_resolver: &NodeResolver,
415478
) -> Result<HashMap<String, Rc<dyn ShellCommand>>, AnyError> {
416-
match npm_resolver.as_inner() {
479+
let mut commands = match npm_resolver.as_inner() {
417480
InnerCliNpmResolverRef::Byonm(npm_resolver) => {
418481
let node_modules_dir = npm_resolver.root_node_modules_path().unwrap();
419-
Ok(resolve_npm_commands_from_bin_dir(node_modules_dir))
482+
resolve_npm_commands_from_bin_dir(node_modules_dir)
420483
}
421484
InnerCliNpmResolverRef::Managed(npm_resolver) => {
422-
resolve_managed_npm_commands(npm_resolver, node_resolver)
485+
resolve_managed_npm_commands(npm_resolver, node_resolver)?
423486
}
424-
}
487+
};
488+
commands.insert("npm".to_string(), Rc::new(NpmCommand));
489+
Ok(commands)
425490
}
426491

427492
fn resolve_npm_commands_from_bin_dir(
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"tests": {
3+
"uses_deno_no_flags": {
4+
"args": "task test",
5+
"output": "task_test.out"
6+
},
7+
"uses_npm_flags": {
8+
"args": "task test_using_npm",
9+
"output": "task_test_using_npm.out",
10+
"exitCode": 1
11+
},
12+
"npm_run": {
13+
"args": "task npm_run",
14+
"output": "task_npm_run.out"
15+
}
16+
}
17+
}

tests/specs/task/npm_run/deno.jsonc

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"tasks": {
3+
"echo": "echo 'Hello, World!'",
4+
// should use the task from package.json and not the one above
5+
"test": "npm run echo hi there",
6+
// currently this will execute using the actual `npm run` because we
7+
// haven't implemented the flags for `npm run` yet
8+
"test_using_npm": "npm run non_existent -- --ignore-scripts",
9+
"npm_run": "npm run"
10+
}
11+
}

tests/specs/task/npm_run/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"scripts": {
3+
"echo": "echo"
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Task npm_run npm run
2+
Scripts available [WILDCARD]
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Task test npm run echo hi there
2+
Task echo echo "hi" "there"
3+
hi there
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Task test_using_npm npm run non_existent -- --ignore-scripts
2+
npm [WILDLINE] Missing script: "non_existent"
3+
[WILDCARD]

0 commit comments

Comments
 (0)