diff --git a/.changeset/recursive-filter-root-exclusion.md b/.changeset/recursive-filter-root-exclusion.md new file mode 100644 index 0000000000..a4af404d96 --- /dev/null +++ b/.changeset/recursive-filter-root-exclusion.md @@ -0,0 +1,5 @@ +--- +"pnpm": patch +--- + +Fix a regression where `pnpm --recursive --filter '!' 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). diff --git a/pnpm/src/main.ts b/pnpm/src/main.ts index 2337a4dfd2..02457fc2e5 100644 --- a/pnpm/src/main.ts +++ b/pnpm/src/main.ts @@ -249,7 +249,7 @@ export async function main (inputArgv: string[]): Promise { 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) && diff --git a/pnpm/test/recursive/filter.ts b/pnpm/test/recursive/filter.ts index 881025f9e3..3470c76f82 100644 --- a/pnpm/test/recursive/filter.ts +++ b/pnpm/test/recursive/filter.ts @@ -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 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 "!" 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 "!" --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$') +})