mirror of
https://github.com/pnpm/pnpm.git
synced 2026-01-11 00:18:32 -05:00
feat(plugin-commands-script-runners): support --resume-from for pnpm exec command (#5856)
close #4690
This commit is contained in:
6
.changeset/silly-ties-arrive.md
Normal file
6
.changeset/silly-ties-arrive.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-script-runners": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
`pnpm exec` and `pnpm run` command support `--resume-from` option. When used, the command will executed from given package [#4690](https://github.com/pnpm/pnpm/issues/4690).
|
||||
@@ -5,7 +5,7 @@ import { makeNodeRequireOption } from '@pnpm/lifecycle'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { tryReadProjectManifest } from '@pnpm/read-project-manifest'
|
||||
import { sortPackages } from '@pnpm/sort-packages'
|
||||
import { Project } from '@pnpm/types'
|
||||
import { Project, ProjectsGraph } from '@pnpm/types'
|
||||
import execa from 'execa'
|
||||
import pLimit from 'p-limit'
|
||||
import pick from 'ramda/src/pick'
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
PARALLEL_OPTION_HELP,
|
||||
shorthands as runShorthands,
|
||||
} from './run'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
|
||||
export const shorthands = {
|
||||
parallel: runShorthands.parallel,
|
||||
@@ -34,6 +35,7 @@ export function rcOptionsTypes () {
|
||||
'workspace-concurrency',
|
||||
], types),
|
||||
'shell-mode': Boolean,
|
||||
'resume-from': String,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +68,10 @@ The shell should understand the -c switch on UNIX or /d /s /c on Windows.',
|
||||
name: '--shell-mode',
|
||||
shortAlias: '-c',
|
||||
},
|
||||
{
|
||||
description: 'command executed from given package',
|
||||
name: '--resume-from',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -74,6 +80,26 @@ The shell should understand the -c switch on UNIX or /d /s /c on Windows.',
|
||||
})
|
||||
}
|
||||
|
||||
export function getResumedPackageChunks ({
|
||||
resumeFrom,
|
||||
chunks,
|
||||
selectedProjectsGraph,
|
||||
}: {
|
||||
resumeFrom: string
|
||||
chunks: string[][]
|
||||
selectedProjectsGraph: ProjectsGraph
|
||||
}) {
|
||||
const resumeFromPackagePrefix = Object.keys(selectedProjectsGraph)
|
||||
.find((prefix) => selectedProjectsGraph[prefix]?.package.manifest.name === resumeFrom)
|
||||
|
||||
if (!resumeFromPackagePrefix) {
|
||||
throw new PnpmError('RESUME_FROM_NOT_FOUND', `Cannot find package ${resumeFrom}. Could not determine where to resume from.`)
|
||||
}
|
||||
|
||||
const chunkPosition = chunks.findIndex(chunk => chunk.includes(resumeFromPackagePrefix))
|
||||
return chunks.slice(chunkPosition)
|
||||
}
|
||||
|
||||
export async function handler (
|
||||
opts: Required<Pick<Config, 'selectedProjectsGraph'>> & {
|
||||
bail?: boolean
|
||||
@@ -83,6 +109,7 @@ export async function handler (
|
||||
sort?: boolean
|
||||
workspaceConcurrency?: number
|
||||
shellMode?: boolean
|
||||
resumeFrom?: string
|
||||
} & Pick<Config, 'extraBinPaths' | 'extraEnv' | 'lockfileDir' | 'dir' | 'userAgent' | 'recursive' | 'workspaceDir'>,
|
||||
params: string[]
|
||||
) {
|
||||
@@ -120,6 +147,15 @@ export async function handler (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.resumeFrom) {
|
||||
chunks = getResumedPackageChunks({
|
||||
resumeFrom: opts.resumeFrom,
|
||||
chunks,
|
||||
selectedProjectsGraph: opts.selectedProjectsGraph,
|
||||
})
|
||||
}
|
||||
|
||||
const existsPnp = existsInDir.bind(null, '.pnp.cjs')
|
||||
const workspacePnpPath = opts.workspaceDir && await existsPnp(opts.workspaceDir)
|
||||
|
||||
@@ -136,7 +172,7 @@ export async function handler (
|
||||
const env = makeEnv({
|
||||
extraEnv: {
|
||||
...extraEnv,
|
||||
PNPM_PACKAGE_NAME: opts.selectedProjectsGraph?.[prefix]?.package.manifest.name,
|
||||
PNPM_PACKAGE_NAME: opts.selectedProjectsGraph[prefix]?.package.manifest.name,
|
||||
},
|
||||
prependPaths: [
|
||||
path.join(prefix, 'node_modules/.bin'),
|
||||
|
||||
@@ -69,6 +69,7 @@ export function cliOptionsTypes () {
|
||||
...IF_PRESENT_OPTION,
|
||||
recursive: Boolean,
|
||||
reverse: Boolean,
|
||||
'resume-from': String,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { sortPackages } from '@pnpm/sort-packages'
|
||||
import pLimit from 'p-limit'
|
||||
import realpathMissing from 'realpath-missing'
|
||||
import { existsInDir } from './existsInDir'
|
||||
import { getResumedPackageChunks } from './exec'
|
||||
|
||||
export type RecursiveRunOpts = Pick<Config,
|
||||
| 'enablePrePostScripts'
|
||||
@@ -26,6 +27,7 @@ export type RecursiveRunOpts = Pick<Config,
|
||||
Partial<Pick<Config, 'extraBinPaths' | 'extraEnv' | 'bail' | 'reverse' | 'sort' | 'workspaceConcurrency'>> &
|
||||
{
|
||||
ifPresent?: boolean
|
||||
resumeFrom?: string
|
||||
}
|
||||
|
||||
export async function runRecursive (
|
||||
@@ -41,7 +43,15 @@ export async function runRecursive (
|
||||
const sortedPackageChunks = opts.sort
|
||||
? sortPackages(opts.selectedProjectsGraph)
|
||||
: [Object.keys(opts.selectedProjectsGraph).sort()]
|
||||
const packageChunks = opts.reverse ? sortedPackageChunks.reverse() : sortedPackageChunks
|
||||
let packageChunks = opts.reverse ? sortedPackageChunks.reverse() : sortedPackageChunks
|
||||
|
||||
if (opts.resumeFrom) {
|
||||
packageChunks = getResumedPackageChunks({
|
||||
resumeFrom: opts.resumeFrom,
|
||||
chunks: packageChunks,
|
||||
selectedProjectsGraph: opts.selectedProjectsGraph,
|
||||
})
|
||||
}
|
||||
|
||||
const result = {
|
||||
fails: [],
|
||||
|
||||
@@ -552,3 +552,112 @@ testOnPosixOnly('pnpm recursive exec works with PnP', async () => {
|
||||
expect(outputs1).toStrictEqual(['project-1', 'project-2-prebuild', 'project-2', 'project-2-postbuild'])
|
||||
expect(outputs2).toStrictEqual(['project-1', 'project-3'])
|
||||
})
|
||||
|
||||
test('pnpm recursive exec --resume-from should work', async () => {
|
||||
preparePackages([
|
||||
{
|
||||
name: 'project-1',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
'json-append': '1',
|
||||
},
|
||||
scripts: {
|
||||
build: 'node -e "process.stdout.write(\'project-1\')" | json-append ../output1.json',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'project-2',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
'json-append': '1',
|
||||
'project-1': '1',
|
||||
},
|
||||
scripts: {
|
||||
build: 'node -e "process.stdout.write(\'project-2\')" | json-append ../output1.json',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'project-3',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
'json-append': '1',
|
||||
'project-1': '1',
|
||||
},
|
||||
scripts: {
|
||||
build: 'node -e "process.stdout.write(\'project-3\')" | json-append ../output1.json',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'project-4',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
'json-append': '1',
|
||||
},
|
||||
scripts: {
|
||||
build: 'node -e "process.stdout.write(\'project-4\')" | json-append ../output1.json',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const { selectedProjectsGraph } = await readProjects(process.cwd(), [])
|
||||
await execa(pnpmBin, [
|
||||
'install',
|
||||
'-r',
|
||||
'--registry',
|
||||
REGISTRY_URL,
|
||||
'--store-dir',
|
||||
path.resolve(DEFAULT_OPTS.storeDir),
|
||||
])
|
||||
await exec.handler({
|
||||
...DEFAULT_OPTS,
|
||||
dir: process.cwd(),
|
||||
selectedProjectsGraph,
|
||||
recursive: true,
|
||||
sort: true,
|
||||
resumeFrom: 'project-3',
|
||||
}, ['npm', 'run', 'build'])
|
||||
|
||||
const { default: outputs1 } = await import(path.resolve('output1.json'))
|
||||
expect(outputs1).not.toContain('project-1')
|
||||
expect(outputs1).not.toContain('project-4')
|
||||
expect(outputs1).toContain('project-2')
|
||||
expect(outputs1).toContain('project-3')
|
||||
})
|
||||
|
||||
test('should throw error when the package specified by resume-from does not exist', async () => {
|
||||
preparePackages([
|
||||
{
|
||||
name: 'foo',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
'json-append': '1',
|
||||
},
|
||||
scripts: {
|
||||
build: 'echo foo',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const { selectedProjectsGraph } = await readProjects(process.cwd(), [])
|
||||
await execa(pnpmBin, [
|
||||
'install',
|
||||
'-r',
|
||||
'--registry',
|
||||
REGISTRY_URL,
|
||||
'--store-dir',
|
||||
path.resolve(DEFAULT_OPTS.storeDir),
|
||||
])
|
||||
|
||||
try {
|
||||
await exec.handler({
|
||||
...DEFAULT_OPTS,
|
||||
dir: process.cwd(),
|
||||
selectedProjectsGraph,
|
||||
recursive: true,
|
||||
sort: true,
|
||||
resumeFrom: 'project-2',
|
||||
}, ['npm', 'run', 'build'])
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
expect(err.code).toBe('ERR_PNPM_RESUME_FROM_NOT_FOUND')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -832,3 +832,61 @@ test('`pnpm recursive run` should fail when no script in package with requiredSc
|
||||
expect(err.message).toContain('Missing script "build" in packages: project-1, project-3')
|
||||
expect(err.code).toBe('ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT')
|
||||
})
|
||||
|
||||
test('`pnpm -r --resume-from run` should executed from given package', async () => {
|
||||
preparePackages([
|
||||
{
|
||||
name: 'project-1',
|
||||
version: '1.0.0',
|
||||
scripts: {
|
||||
build: 'node -e "process.stdout.write(\'project-1\')" | json-append ../output1.json',
|
||||
},
|
||||
dependencies: {
|
||||
'json-append': '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'project-2',
|
||||
version: '1.0.0',
|
||||
scripts: {
|
||||
build: 'node -e "process.stdout.write(\'project-2\')" | json-append ../output1.json',
|
||||
},
|
||||
dependencies: {
|
||||
'project-1': '1',
|
||||
'json-append': '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'project-3',
|
||||
version: '1.0.0',
|
||||
scripts: {
|
||||
build: 'node -e "process.stdout.write(\'project-3\')" | json-append ../output1.json',
|
||||
},
|
||||
dependencies: {
|
||||
'project-1': '1',
|
||||
'json-append': '1',
|
||||
},
|
||||
},
|
||||
])
|
||||
await execa(pnpmBin, [
|
||||
'install',
|
||||
'-r',
|
||||
'--registry',
|
||||
REGISTRY_URL,
|
||||
'--store-dir',
|
||||
path.resolve(DEFAULT_OPTS.storeDir),
|
||||
])
|
||||
await run.handler({
|
||||
...DEFAULT_OPTS,
|
||||
...await readProjects(process.cwd(), [{ namePattern: '*' }]),
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
resumeFrom: 'project-3',
|
||||
workspaceDir: process.cwd(),
|
||||
}, ['build'])
|
||||
|
||||
const { default: output1 } = await import(path.resolve('output1.json'))
|
||||
expect(output1).not.toContain('project-1')
|
||||
expect(output1).toContain('project-2')
|
||||
expect(output1).toContain('project-3')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user