Skip to content

Commit

Permalink
feat(plugin-commands-script-runners): support --resume-from for pnpm …
Browse files Browse the repository at this point in the history
…exec command (#5856)

close #4690
  • Loading branch information
await-ovo committed Jan 2, 2023
1 parent 37c818d commit da15828
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 3 deletions.
6 changes: 6 additions & 0 deletions .changeset/silly-ties-arrive.md
@@ -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).
40 changes: 38 additions & 2 deletions exec/plugin-commands-script-runners/src/exec.ts
Expand Up @@ -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'
Expand All @@ -16,6 +16,7 @@ import {
PARALLEL_OPTION_HELP,
shorthands as runShorthands,
} from './run'
import { PnpmError } from '@pnpm/error'

export const shorthands = {
parallel: runShorthands.parallel,
Expand All @@ -34,6 +35,7 @@ export function rcOptionsTypes () {
'workspace-concurrency',
], types),
'shell-mode': Boolean,
'resume-from': String,
}
}

Expand Down Expand Up @@ -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',
},
],
},
],
Expand All @@ -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
Expand All @@ -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[]
) {
Expand Down Expand Up @@ -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)

Expand All @@ -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'),
Expand Down
1 change: 1 addition & 0 deletions exec/plugin-commands-script-runners/src/run.ts
Expand Up @@ -69,6 +69,7 @@ export function cliOptionsTypes () {
...IF_PRESENT_OPTION,
recursive: Boolean,
reverse: Boolean,
'resume-from': String,
}
}

Expand Down
12 changes: 11 additions & 1 deletion exec/plugin-commands-script-runners/src/runRecursive.ts
Expand Up @@ -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'
Expand All @@ -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 (
Expand All @@ -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: [],
Expand Down
109 changes: 109 additions & 0 deletions exec/plugin-commands-script-runners/test/exec.e2e.ts
Expand Up @@ -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')
}
})
58 changes: 58 additions & 0 deletions exec/plugin-commands-script-runners/test/runRecursive.ts
Expand Up @@ -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')
})

0 comments on commit da15828

Please sign in to comment.