fix: keep workspace-root excluded when --recursive --filter uses only negative filters (#11480)

* fix: include workspace root only when --filter is positive

PR #10465 stopped the implicit workspace-root exclusion for `pnpm -r
run/exec/test/add` whenever any --filter was provided. That gate was
too broad: with a negative-only filter set (e.g. `--filter '!a'`) the
workspace root started showing up in the matched projects, contradicting
the documented default behavior.

Only suppress the implicit root exclusion when the user provided at
least one positive (non-`!`) filter that could have been intended to
select the root. Negative-only filter sets keep the documented default,
and `--include-workspace-root` continues to opt the root in explicitly.

Close #11341.

* test: fix root-exclusion assertion to match --stream prefix

The --stream reporter prefixes output lines with the project's relative
directory, so the workspace root appears as `. which$`, not
`root which$`. The original assertion would have passed even if the
regression were still present. Verified the corrected assertion fails
against the buggy code and passes against the fix.
This commit is contained in:
Zoltan Kochan
2026-05-06 01:51:37 +02:00
committed by GitHub
parent fcec623c00
commit 9018103edc
3 changed files with 119 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
---
"pnpm": patch
---
Fix a regression where `pnpm --recursive --filter '!<pkg>' run/exec/test/add` would include the workspace root in the matched projects. The workspace root is now correctly excluded by default when only negative `--filter` arguments are provided, matching the [documented behavior](https://pnpm.io/cli/recursive). To include the root, pass `--include-workspace-root` [#11341](https://github.com/pnpm/pnpm/issues/11341).

View File

@@ -249,7 +249,7 @@ export async function main (inputArgv: string[]): Promise<void> {
if (config.workspaceRoot) {
filters.push({ filter: `{${relativeWSDirPath()}}`, followProdDepsOnly: Boolean(config.filterProd.length) })
} else if (
filters.length === 0 &&
!filters.some(({ filter }) => !filter.startsWith('!')) &&
workspaceDir &&
config.workspacePackagePatterns &&
!isRootOnlyPatterns(config.workspacePackagePatterns) &&

View File

@@ -1,7 +1,7 @@
import fs from 'node:fs'
import { expect, test } from '@jest/globals'
import { prepare } from '@pnpm/prepare'
import { prepare, preparePackages } from '@pnpm/prepare'
import { execPnpmSync } from '../utils/index.js'
@@ -42,3 +42,115 @@ test('pnpm --filter . add <pkg> should work', async () => {
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
expect(pkg.dependencies['is-positive']).toBeTruthy()
})
// Regression test for https://github.com/pnpm/pnpm/issues/11341
test('pnpm --recursive --filter "!<pkg>" run should still exclude the workspace root', async () => {
preparePackages([
{
location: '.',
package: {
name: 'root',
version: '0.0.0',
private: true,
scripts: {
which: "node -e \"console.log('root')\"",
},
},
},
{
location: 'a',
package: {
name: 'a',
version: '1.0.0',
scripts: {
which: "node -e \"console.log('a')\"",
},
},
},
{
location: 'b',
package: {
name: 'b',
version: '1.0.0',
scripts: {
which: "node -e \"console.log('b')\"",
},
},
},
])
fs.writeFileSync('pnpm-workspace.yaml', 'packages:\n - "*"\n')
const result = execPnpmSync([
'--stream',
'--config.verify-deps-before-run=false',
'--recursive',
'--filter',
'!a',
'run',
'which',
])
expect(result.status).toBe(0)
const stdout = result.stdout.toString()
expect(stdout).toContain('b which$')
// The `--stream` reporter prefixes lines with the project's relative directory,
// so the workspace root (cwd === wsDir) would appear as `. which$` if included.
expect(stdout).not.toContain('. which$')
expect(stdout).not.toContain('a which$')
})
test('pnpm --recursive --filter "!<pkg>" --include-workspace-root run should include the workspace root', async () => {
preparePackages([
{
location: '.',
package: {
name: 'root',
version: '0.0.0',
private: true,
scripts: {
which: "node -e \"console.log('root')\"",
},
},
},
{
location: 'a',
package: {
name: 'a',
version: '1.0.0',
scripts: {
which: "node -e \"console.log('a')\"",
},
},
},
{
location: 'b',
package: {
name: 'b',
version: '1.0.0',
scripts: {
which: "node -e \"console.log('b')\"",
},
},
},
])
fs.writeFileSync('pnpm-workspace.yaml', 'packages:\n - "*"\n')
const result = execPnpmSync([
'--stream',
'--config.verify-deps-before-run=false',
'--recursive',
'--include-workspace-root',
'--filter',
'!a',
'run',
'which',
])
expect(result.status).toBe(0)
const stdout = result.stdout.toString()
expect(stdout).toContain('b which$')
expect(stdout).toContain('. which$')
expect(stdout).not.toContain('a which$')
})