fix(run): treat empty regexp scripts as missing (#14237)
RegExp script selectors included matching names even when their script bodies were empty, unlike exact-name selection. Recursive task orchestration could therefore dispatch tasks pulled in through dependsOn before eventually reporting that the requested script did not exist. Filter empty bodies at the shared script-selection boundary in both the TypeScript CLI and pacquet, so pre-dispatch validation observes the same missing-script semantics for exact and RegExp selectors. Related to pnpm/pnpm#14211.
This commit is contained in:
1 parent
e322d08690
commit
dcf3657187
5 files changed
+79
-8
No files matched your search
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@pnpm/exec.commands": patch
|
||||
"pnpm": patch
|
||||
"pacquet": patch
|
||||
---
|
||||
|
||||
Treat empty scripts selected by a regular expression as missing before running dependent tasks.
|
||||
@@ -566,7 +566,13 @@ impl<'a> ScriptSelector<'a> {
|
||||
let (Some(pattern), Some(scripts)) = (self.pattern.as_ref(), scripts) else {
|
||||
return Vec::new();
|
||||
};
|
||||
scripts.keys().filter(|script| pattern.is_match(script)).cloned().collect()
|
||||
scripts
|
||||
.iter()
|
||||
.filter(|(script, body)| {
|
||||
body.as_str().is_some_and(|body| !body.is_empty()) && pattern.is_match(script)
|
||||
})
|
||||
.map(|(script, _)| script.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// [`Self::select`] plus single-project `run`'s `start` fallback:
|
||||
|
||||
@@ -2105,11 +2105,8 @@ fn recursive_run_keeps_a_failure_when_a_later_selected_script_passes() {
|
||||
drop(root);
|
||||
}
|
||||
|
||||
/// A selector can match a script with an empty body alongside a real
|
||||
/// one. The no-op says nothing about the script that did run, so it must
|
||||
/// not overwrite the project's recorded status.
|
||||
#[test]
|
||||
fn recursive_run_keeps_a_pass_when_a_later_selected_script_is_a_no_op() {
|
||||
fn recursive_run_keeps_a_pass_when_a_later_matching_script_is_empty() {
|
||||
let CommandTempCwd { pacquet, root, workspace, .. } = CommandTempCwd::init();
|
||||
write_workspace(
|
||||
&workspace,
|
||||
@@ -2120,8 +2117,6 @@ fn recursive_run_keeps_a_pass_when_a_later_selected_script_is_a_no_op() {
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"check:a": "true",
|
||||
// Sorts after `check:a`, so a regression reports the
|
||||
// project as skipped rather than passed.
|
||||
"check:b": "",
|
||||
},
|
||||
}),
|
||||
@@ -2940,6 +2935,40 @@ fn missing_requested_script_errors_before_upstream_tasks_run() {
|
||||
drop(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regexp_selected_empty_script_errors_before_upstream_tasks_run() {
|
||||
let CommandTempCwd { pacquet, root, workspace, .. } = CommandTempCwd::init();
|
||||
write_workspace(
|
||||
&workspace,
|
||||
&[(
|
||||
"project-a",
|
||||
json!({
|
||||
"name": "project-a",
|
||||
"version": "1.0.0",
|
||||
"scripts": { "build:empty": "", "codegen": "echo codegen >> ../order.log" },
|
||||
}),
|
||||
)],
|
||||
);
|
||||
fs::write(
|
||||
workspace.join("pnpm-workspace.yaml"),
|
||||
"packages:\n - project-a\ntasks:\n '/^build:/':\n dependsOn: ['codegen']\n",
|
||||
)
|
||||
.expect("write workspace settings");
|
||||
|
||||
let output =
|
||||
pacquet.with_args(["-r", "run", "/^build:/"]).output().expect("run recursive script");
|
||||
eprintln!("STATUS: {}", output.status);
|
||||
assert!(!output.status.success(), "an empty selected script must fail the run");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
eprintln!("STDERR:\n{stderr}\n");
|
||||
assert!(stderr.contains("RECURSIVE_RUN_NO_SCRIPT"));
|
||||
let order_log_exists = workspace.join("order.log").exists();
|
||||
eprintln!("ORDER LOG EXISTS: {order_log_exists}");
|
||||
assert!(!order_log_exists, "the pulled-in task must not have run");
|
||||
|
||||
drop(root);
|
||||
}
|
||||
|
||||
/// `ignoreWorkspaceCycles: true` downgrades the task-cycle error to a
|
||||
/// warning: the cycle's members run in an arbitrary order relative to each
|
||||
/// other and the run completes.
|
||||
|
||||
@@ -364,7 +364,7 @@ export function getSpecifiedScripts (scripts: PackageScripts, scriptName: string
|
||||
|
||||
// if scriptName which a user passes is RegExp (like /build:.*/), multiple scripts to execute will be selected with RegExp
|
||||
if (scriptSelector) {
|
||||
return Object.keys(scripts).filter(script => script.match(scriptSelector))
|
||||
return Object.keys(scripts).filter(script => Boolean(scripts[script]) && scriptSelector.test(script))
|
||||
}
|
||||
|
||||
return []
|
||||
|
||||
@@ -577,6 +577,35 @@ test('a missing requested script errors before upstream tasks run', async () =>
|
||||
expect(server.getLines()).toStrictEqual([])
|
||||
})
|
||||
|
||||
test('an empty requested script matched by a RegExp errors before upstream tasks run', async () => {
|
||||
await using server = await createTestIpcServer()
|
||||
|
||||
preparePackages([
|
||||
{
|
||||
name: 'project-a',
|
||||
version: '1.0.0',
|
||||
scripts: {
|
||||
'build:empty': '',
|
||||
codegen: server.sendLineScript('codegen'),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await expect(run.handler({
|
||||
...DEFAULT_OPTS,
|
||||
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
tasks: {
|
||||
'/^build:/': { dependsOn: ['codegen'] },
|
||||
},
|
||||
workspaceDir: process.cwd(),
|
||||
}, ['/^build:/'])).rejects.toMatchObject({
|
||||
code: 'ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT',
|
||||
})
|
||||
expect(server.getLines()).toStrictEqual([])
|
||||
})
|
||||
|
||||
test('a failed upstream task is reported as the failure, not as a missing script', async () => {
|
||||
preparePackages([
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user