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>
1006 lines
26 KiB
TypeScript
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
|
|
}
|