Files
pnpm/pnpm11/exec/commands/test/runTasks.e2e.ts
01e7232d7f fix(run): honor tasks dependsOn for each script a RegExp selector matches (#16203)
With `tasks` declared, a recursive run with a RegExp selector now seeds
one task per script the selector matches in each project, so each
resolves the `dependsOn` declared for its own name, as a run of that
script by name does. The selector string itself matched no `tasks`
entry, so its task fell back to `^<selector>` and the matched scripts'
declared dependencies never ran.

Resolving the union of the matched scripts' entries onto one
per-project task was rejected. That task runs its scripts concurrently,
so a matched `test` could not wait for a matched `build`. A `^build`
entry also pulled in a `build` task that the dependency project's
selector task ran again.

A project the selector matches nothing in keeps a requested
pass-through task under the selector name, with no dependencies.
Without `tasks`, or with an exact `tasks` entry under the selector
string, the selector stays a single task per project.

The hidden-script check runs per project over the requested tasks. A
selector that matches only hidden scripts in a project still fails, but
a hidden script alongside visible ones is filtered as before. A
requested task that a `dependsOn` declaration targets is exempt, like
any `dependsOn` reference. The exempt set is every dependency of a task
with a `tasks` entry, since a task without one only has the default
`^<name>` edge. It is read from the full graph before sequencing, in
declaration direction when `--reverse` inverted the edges.

`--resume-from` anchors on every task requested in the anchor project
when the invocation's own task is absent, which is the case for an
expanded selector. Summary keys qualify every task but the one
named by the invocation, so expanded tasks do not share a key.

Closes pnpm/pnpm#15596

Co-authored-by: Dex <4175097+Dextheking1@users.noreply.github.com>

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:35:12 +02:00

1006 lines
26 KiB
TypeScript

import fs from 'node:fs'
import path from 'node:path'
import { expect, test } from '@jest/globals'
import type { PnpmError } from '@pnpm/error'
import { run } from '@pnpm/exec.commands'
import { preparePackages } from '@pnpm/prepare'
import { createTestIpcServer } from '@pnpm/test-ipc-server'
import { filterProjectsBySelectorObjectsFromDir } from '@pnpm/workspace.projects-filter'
import { safeExeca as execa } from 'execa'
import { writeYamlFileSync } from 'write-yaml-file'
import { DEFAULT_OPTS } from './utils/index.js'
const pnpmBin = path.join(import.meta.dirname, '../../../pnpm/bin/pnpm.mjs')
test('a task starts as soon as its dependencies finish, without waiting for unrelated tasks', async () => {
await using server = await createTestIpcServer()
// slow waits for a marker only mid writes, and mid may start only once
// dep is done — so the run completes only if mid is dispatched while the
// unrelated slow is still in flight. Any barrier between
// dependency-independent tasks deadlocks this fixture.
preparePackages([
{
name: 'dep',
version: '1.0.0',
scripts: {
build: server.sendLineScript('dep'),
},
},
{
name: 'mid',
version: '1.0.0',
dependencies: {
dep: 'workspace:*',
},
scripts: {
build: `${server.sendLineScript('mid')} && node -e "require('fs').writeFileSync('../slow-marker', '')"`,
},
},
{
name: 'slow',
version: '1.0.0',
scripts: {
build: `node -e "const fs = require('fs'); const started = Date.now(); (function poll () { if (fs.existsSync('../slow-marker')) process.exit(0); if (Date.now() - started > 30000) process.exit(1); setTimeout(poll, 50) })()" && ${server.sendLineScript('slow')}`,
},
},
])
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
workspaceConcurrency: 2,
workspaceDir: process.cwd(),
}, ['build'])
expect(server.getLines()).toStrictEqual(['dep', 'mid', 'slow'])
})
test('dependsOn runs the tasks a task depends on, in dependency order', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
dependencies: {
'project-b': 'workspace:*',
},
scripts: {
build: server.sendLineScript('a-build'),
test: server.sendLineScript('a-test'),
},
},
{
name: 'project-b',
version: '1.0.0',
scripts: {
build: server.sendLineScript('b-build'),
test: server.sendLineScript('b-test'),
},
},
])
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
tasks: {
build: { dependsOn: ['^build'] },
test: { dependsOn: ['build'] },
},
workspaceDir: process.cwd(),
}, ['test'])
const order = server.getLines()
expect([...order].sort()).toStrictEqual(['a-build', 'a-test', 'b-build', 'b-test'])
expect(order.indexOf('b-build')).toBeLessThan(order.indexOf('a-build'))
expect(order.indexOf('a-build')).toBeLessThan(order.indexOf('a-test'))
expect(order.indexOf('b-build')).toBeLessThan(order.indexOf('b-test'))
})
test('a RegExp selector runs the dependsOn of the scripts it matches', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
dependencies: {
'project-b': 'workspace:*',
},
scripts: {
build: server.sendLineScript('a-build'),
test: server.sendLineScript('a-test'),
},
},
{
name: 'project-b',
version: '1.0.0',
scripts: {
build: server.sendLineScript('b-build'),
test: server.sendLineScript('b-test'),
},
},
])
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
tasks: {
build: { dependsOn: ['^build'] },
test: { dependsOn: ['build'] },
},
workspaceDir: process.cwd(),
}, ['/test/'])
const order = server.getLines()
expect([...order].sort()).toStrictEqual(['a-build', 'a-test', 'b-build', 'b-test'])
expect(order.indexOf('b-build')).toBeLessThan(order.indexOf('a-build'))
expect(order.indexOf('a-build')).toBeLessThan(order.indexOf('a-test'))
expect(order.indexOf('b-build')).toBeLessThan(order.indexOf('b-test'))
})
test('a RegExp selector orders the matched scripts by their dependsOn', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
dependencies: {
'project-b': 'workspace:*',
},
scripts: {
build: server.sendLineScript('a-build'),
test: server.sendLineScript('a-test'),
},
},
{
name: 'project-b',
version: '1.0.0',
scripts: {
build: server.sendLineScript('b-build'),
test: server.sendLineScript('b-test'),
},
},
])
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
reportSummary: true,
tasks: {
build: { dependsOn: ['^build'] },
test: { dependsOn: ['build'] },
},
workspaceDir: process.cwd(),
}, ['/^(build|test)$/'])
const order = server.getLines()
expect([...order].sort()).toStrictEqual(['a-build', 'a-test', 'b-build', 'b-test'])
expect(order.indexOf('b-build')).toBeLessThan(order.indexOf('a-build'))
expect(order.indexOf('a-build')).toBeLessThan(order.indexOf('a-test'))
expect(order.indexOf('b-build')).toBeLessThan(order.indexOf('b-test'))
const executionStatus = readSummary()
for (const project of ['project-a', 'project-b']) {
expect(executionStatus[`${path.resolve(project)}#build`].status).toBe('passed')
expect(executionStatus[`${path.resolve(project)}#test`].status).toBe('passed')
expect(executionStatus[path.resolve(project)]).toBeUndefined()
}
})
test('a task with an explicitly empty dependsOn starts without waiting for anything', async () => {
await using server = await createTestIpcServer()
// dependency's lint waits for the marker dependent's lint writes: only
// possible when lint tasks are not ordered by the project graph.
preparePackages([
{
name: 'dependency',
version: '1.0.0',
scripts: {
lint: `node -e "const fs = require('fs'); const started = Date.now(); (function poll () { if (fs.existsSync('../lint-marker')) process.exit(0); if (Date.now() - started > 30000) process.exit(1); setTimeout(poll, 50) })()" && ${server.sendLineScript('dependency')}`,
},
},
{
name: 'dependent',
version: '1.0.0',
dependencies: {
dependency: 'workspace:*',
},
scripts: {
lint: `${server.sendLineScript('dependent')} && node -e "require('fs').writeFileSync('../lint-marker', '')"`,
},
},
])
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
tasks: {
lint: {},
},
workspaceConcurrency: 2,
workspaceDir: process.cwd(),
}, ['lint'])
expect(server.getLines()).toStrictEqual(['dependent', 'dependency'])
})
test('a task concurrency limit applies across workspace projects', async () => {
preparePackages(['project-a', 'project-b', 'project-c'].map((name) => ({
name,
version: '1.0.0',
scripts: {
build: `node ../concurrency-probe.cjs ${name}`,
},
})))
fs.writeFileSync(path.join(process.cwd(), 'concurrency-probe.cjs'), `const fs = require('fs')
const path = require('path')
const lock = path.join(__dirname, 'build-active')
let ownsLock = false
try {
fs.mkdirSync(lock)
ownsLock = true
} catch (error) {
if (error.code !== 'EEXIST') throw error
fs.writeFileSync(path.join(__dirname, 'exceeded-task-concurrency'), '')
}
setTimeout(() => {
if (ownsLock) fs.rmdirSync(lock)
fs.writeFileSync(path.join(__dirname, 'ran-' + process.argv[2]), '')
}, 200)
`)
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
tasks: {
build: { concurrency: 1 },
},
workspaceConcurrency: 4,
workspaceDir: process.cwd(),
}, ['build'])
expect(fs.existsSync(path.join(process.cwd(), 'exceeded-task-concurrency'))).toBe(false)
for (const name of ['project-a', 'project-b', 'project-c']) {
expect(fs.existsSync(path.join(process.cwd(), `ran-${name}`))).toBe(true)
}
})
test('a project without the script is reported skipped and does not sever the chain', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
dependencies: {
'project-b': 'workspace:*',
},
scripts: {
build: server.sendLineScript('project-a'),
},
},
{
name: 'project-b',
version: '1.0.0',
dependencies: {
'project-c': 'workspace:*',
},
},
{
name: 'project-c',
version: '1.0.0',
scripts: {
build: server.sendLineScript('project-c'),
},
},
])
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
reportSummary: true,
workspaceDir: process.cwd(),
}, ['build'])
expect(server.getLines()).toStrictEqual(['project-c', 'project-a'])
const executionStatus = readSummary()
expect(executionStatus[path.resolve('project-a')].status).toBe('passed')
expect(executionStatus[path.resolve('project-b')].status).toBe('skipped')
expect(executionStatus[path.resolve('project-c')].status).toBe('passed')
})
test('without --bail, dependents of a failed task are skipped and unrelated tasks still run', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
dependencies: {
'project-b': 'workspace:*',
},
scripts: {
build: server.sendLineScript('project-a'),
},
},
{
name: 'project-b',
version: '1.0.0',
scripts: {
build: 'exit 1',
},
},
{
name: 'project-c',
version: '1.0.0',
scripts: {
build: server.sendLineScript('project-c'),
},
},
])
let err!: PnpmError
try {
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
bail: false,
dir: process.cwd(),
recursive: true,
reportSummary: true,
workspaceDir: process.cwd(),
}, ['build'])
} catch (_err: any) { // eslint-disable-line
err = _err
}
// The failure that blocked project-a is already counted; the skipped
// dependent must not turn one failure into two.
expect(err.code).toBe('ERR_PNPM_RECURSIVE_FAIL')
expect(err.message).toContain('failed in 1 packages')
expect(server.getLines()).toStrictEqual(['project-c'])
const executionStatus = readSummary()
expect(executionStatus[path.resolve('project-a')].status).toBe('skipped')
expect(executionStatus[path.resolve('project-b')].status).toBe('failure')
expect(executionStatus[path.resolve('project-c')].status).toBe('passed')
})
test('a workspace dependency cycle is an error naming the participating tasks', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
dependencies: {
'project-b': 'workspace:*',
},
scripts: {
build: server.sendLineScript('project-a'),
},
},
{
name: 'project-b',
version: '1.0.0',
dependencies: {
'project-a': 'workspace:*',
},
scripts: {
build: server.sendLineScript('project-b'),
},
},
])
let err!: PnpmError
try {
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
workspaceDir: process.cwd(),
}, ['build'])
} catch (_err: any) { // eslint-disable-line
err = _err
}
expect(err.code).toBe('ERR_PNPM_TASK_CYCLE')
expect(err.message).toContain('project-a#build')
expect(err.message).toContain('project-b#build')
expect(server.getLines()).toStrictEqual([])
})
test('a cycle declared through dependsOn is an error', async () => {
preparePackages([
{
name: 'project-a',
version: '1.0.0',
scripts: {
build: 'echo build',
test: 'echo test',
},
},
])
let err!: PnpmError
try {
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
tasks: {
build: { dependsOn: ['test'] },
test: { dependsOn: ['build'] },
},
workspaceDir: process.cwd(),
}, ['test'])
} catch (_err: any) { // eslint-disable-line
err = _err
}
expect(err.code).toBe('ERR_PNPM_TASK_CYCLE')
})
test('--dry-run prints one stable linearization and runs nothing', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
dependencies: {
'project-b': 'workspace:*',
},
scripts: {
build: server.sendLineScript('project-a'),
},
},
{
name: 'project-b',
version: '1.0.0',
dependencies: {
'project-c': 'workspace:*',
},
},
{
name: 'project-c',
version: '1.0.0',
scripts: {
build: server.sendLineScript('project-c'),
},
},
])
const output = await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
dryRun: true,
recursive: true,
workspaceDir: process.cwd(),
}, ['build'])
expect(output).toBe([
'project-c#build',
'project-b#build (skipped: no such script)',
'project-a#build',
].join('\n'))
expect(server.getLines()).toStrictEqual([])
})
test('--dry-run --json emits the tasks and their resolved edges', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
dependencies: {
'project-b': 'workspace:*',
},
scripts: {
build: server.sendLineScript('project-a'),
test: server.sendLineScript('project-a'),
},
},
{
name: 'project-b',
version: '1.0.0',
scripts: {
build: server.sendLineScript('project-b'),
},
},
])
const output = await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
dryRun: true,
json: true,
recursive: true,
tasks: {
build: { dependsOn: ['^build'] },
test: { dependsOn: ['build'] },
},
workspaceDir: process.cwd(),
}, ['test'])
expect(JSON.parse(output as string)).toStrictEqual({
tasks: [
{
project: 'project-a',
script: 'build',
missingScript: false,
dependsOn: [{ project: 'project-b', script: 'build' }],
},
{
project: 'project-a',
script: 'test',
missingScript: false,
dependsOn: [{ project: 'project-a', script: 'build' }],
},
{
project: 'project-b',
script: 'build',
missingScript: false,
dependsOn: [],
},
{
project: 'project-b',
script: 'test',
missingScript: true,
dependsOn: [{ project: 'project-b', script: 'build' }],
},
],
})
expect(server.getLines()).toStrictEqual([])
})
test('--dry-run outside a recursive run is an error', async () => {
preparePackages([
{
name: 'project-a',
version: '1.0.0',
scripts: {
build: 'echo build',
},
},
])
let err!: PnpmError
try {
await run.handler({
...DEFAULT_OPTS,
dir: path.resolve('project-a'),
dryRun: true,
} as never, ['build'])
} catch (_err: any) { // eslint-disable-line
err = _err
}
expect(err.code).toBe('ERR_PNPM_DRY_RUN_NOT_RECURSIVE')
})
test('ignoreWorkspaceCycles downgrades the task-cycle error to a warning', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
dependencies: {
'project-b': 'workspace:*',
},
scripts: {
build: server.sendLineScript('project-a'),
},
},
{
name: 'project-b',
version: '1.0.0',
dependencies: {
'project-a': 'workspace:*',
},
scripts: {
build: server.sendLineScript('project-b'),
},
},
])
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
ignoreWorkspaceCycles: true,
recursive: true,
workspaceDir: process.cwd(),
}, ['build'])
expect([...server.getLines()].sort()).toStrictEqual(['project-a', 'project-b'])
})
test('a missing requested script errors before upstream tasks run', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
scripts: {
codegen: server.sendLineScript('codegen'),
},
},
])
let err!: PnpmError
try {
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
tasks: {
build: { dependsOn: ['codegen'] },
},
workspaceDir: process.cwd(),
}, ['build'])
} catch (_err: any) { // eslint-disable-line
err = _err
}
expect(err.code).toBe('ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT')
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 RegExp selector filters hidden scripts when a visible script also matches', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
scripts: {
'build:visible': server.sendLineScript('visible'),
'.build:hidden': server.sendLineScript('hidden'),
},
},
])
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
workspaceDir: process.cwd(),
}, ['/build/'])
expect(server.getLines()).toStrictEqual(['visible'])
})
test('a RegExp selector with tasks declared filters a matched hidden script', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
scripts: {
'build:visible': server.sendLineScript('visible'),
'.build:hidden': server.sendLineScript('hidden'),
},
},
])
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
tasks: {
lint: { dependsOn: [] },
},
workspaceDir: process.cwd(),
}, ['/build/'])
expect(server.getLines()).toStrictEqual(['visible'])
})
test('a RegExp selector runs a matched hidden script that another matched script depends on', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
scripts: {
test: server.sendLineScript('test'),
'.test-setup': server.sendLineScript('setup'),
},
},
])
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
tasks: {
test: { dependsOn: ['.test-setup'] },
},
workspaceDir: process.cwd(),
}, ['/test/'])
expect(server.getLines()).toStrictEqual(['setup', 'test'])
})
test('a dependsOn reference exempts only the hidden task it targets', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
scripts: {
test: server.sendLineScript('a-test'),
'.setup': server.sendLineScript('a-setup'),
},
},
{
name: 'project-b',
version: '1.0.0',
scripts: {
lint: server.sendLineScript('b-lint'),
'.setup': server.sendLineScript('b-setup'),
},
},
])
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
tasks: {
test: { dependsOn: ['.setup'] },
},
workspaceDir: process.cwd(),
}, ['/test|lint|setup/'])
expect([...server.getLines()].sort()).toStrictEqual(['a-setup', 'a-test', 'b-lint'])
})
test('an explicit same-name dependsOn exempts the hidden task it targets', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
dependencies: {
'project-b': 'workspace:*',
},
scripts: {
test: server.sendLineScript('a-test'),
'.setup': server.sendLineScript('a-setup'),
},
},
{
name: 'project-b',
version: '1.0.0',
scripts: {
lint: server.sendLineScript('b-lint'),
'.setup': server.sendLineScript('b-setup'),
},
},
])
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
tasks: {
test: { dependsOn: ['.setup'] },
'.setup': { dependsOn: ['^.setup'] },
},
workspaceDir: process.cwd(),
}, ['/test|lint|setup/'])
const order = server.getLines()
expect([...order].sort()).toStrictEqual(['a-setup', 'a-test', 'b-lint', 'b-setup'])
expect(order.indexOf('b-setup')).toBeLessThan(order.indexOf('a-setup'))
})
test('--reverse still rejects a hidden script name that has dependsOn', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
scripts: {
build: server.sendLineScript('build'),
'.secret': server.sendLineScript('secret'),
},
},
])
await expect(run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
reverse: true,
tasks: {
'.secret': { dependsOn: ['build'] },
},
workspaceDir: process.cwd(),
}, ['.secret'])).rejects.toMatchObject({
code: 'ERR_PNPM_HIDDEN_SCRIPT',
})
expect(server.getLines()).toStrictEqual([])
})
test('a failed upstream task is reported as the failure, not as a missing script', async () => {
preparePackages([
{
name: 'project-a',
version: '1.0.0',
scripts: {
build: 'exit 1',
test: 'echo test',
},
},
])
let err!: PnpmError
try {
await run.handler({
...DEFAULT_OPTS,
...await filterProjectsBySelectorObjectsFromDir(process.cwd(), []),
bail: false,
dir: process.cwd(),
recursive: true,
reportSummary: true,
tasks: {
test: { dependsOn: ['build'] },
},
workspaceDir: process.cwd(),
}, ['test'])
} catch (_err: any) { // eslint-disable-line
err = _err
}
// Every requested task was skipped because its build dependency failed;
// the run must report that failure, not RECURSIVE_RUN_NO_SCRIPT.
expect(err.code).toBe('ERR_PNPM_RECURSIVE_FAIL')
expect(err.message).toContain('failed in 1 packages')
const executionStatus = readSummary()
expect(executionStatus[path.resolve('project-a')].status).toBe('skipped')
expect(executionStatus[`${path.resolve('project-a')}#build`].status).toBe('failure')
})
test('the tolerated cycle warning reaches the CLI output', async () => {
preparePackages([
{
name: 'project-a',
version: '1.0.0',
dependencies: {
'project-b': 'workspace:*',
},
scripts: {
build: 'echo project-a',
},
},
{
name: 'project-b',
version: '1.0.0',
dependencies: {
'project-a': 'workspace:*',
},
scripts: {
build: 'echo project-b',
},
},
])
writeYamlFileSync('pnpm-workspace.yaml', {
packages: ['**'],
ignoreWorkspaceCycles: true,
})
const { stdout } = await execa(pnpmBin, ['run', '-r', 'build'])
expect(stdout).toContain('The tasks form a dependency cycle')
})
test('the tasks section of pnpm-workspace.yaml reaches the CLI run', async () => {
await using server = await createTestIpcServer()
preparePackages([
{
name: 'project-a',
version: '1.0.0',
dependencies: {
'project-b': 'workspace:*',
},
scripts: {
build: server.sendLineScript('a-build'),
test: server.sendLineScript('a-test'),
},
},
{
name: 'project-b',
version: '1.0.0',
scripts: {
build: server.sendLineScript('b-build'),
},
},
])
writeYamlFileSync('pnpm-workspace.yaml', {
packages: ['**'],
tasks: {
build: { dependsOn: ['^build'] },
test: { dependsOn: ['build'] },
},
})
await execa(pnpmBin, ['run', '-r', 'test'])
const order = server.getLines()
expect([...order].sort()).toStrictEqual(['a-build', 'a-test', 'b-build'])
expect(order.indexOf('b-build')).toBeLessThan(order.indexOf('a-build'))
expect(order.indexOf('a-build')).toBeLessThan(order.indexOf('a-test'))
})
function readSummary (): Record<string, { status: string }> {
return JSON.parse(fs.readFileSync('pnpm-exec-summary.json', 'utf8')).executionStatus
}