Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin-commands-script-runners): support --resume-from for pnpm exec command #5856

Merged
merged 5 commits into from
Jan 2, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/silly-ties-arrive.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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[0]).toBe('project-2')
expect(output1[1]).toBe('project-3')
})