feat: run arbitrary commands via pnpm CLI (#3478)

ref #3191
This commit is contained in:
Zoltan Kochan
2021-05-27 23:56:48 +03:00
committed by GitHub
parent 618f7455ca
commit 209c142358
7 changed files with 50 additions and 10 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/plugin-commands-script-runners": minor
"pnpm": minor
---
`pnpm run` is passed through to `pnpm exec` when it detects a command that is not in the scripts.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/parse-cli-args": minor
---
A new property is returned in the result: fallbackCommandUsed. It is true when an unknown command was used, so the fallback command had to be used instead.

View File

@@ -16,6 +16,7 @@ export interface ParsedCliArgs {
options: Record<string, any>
cmd: string | null
unknownOptions: Map<string, string[]>
fallbackCommandUsed: boolean
workspaceDir?: string
}
@@ -54,16 +55,18 @@ export default async function parseCliArgs (
options: {},
params: noptExploratoryResults.argv.remain,
unknownOptions: new Map(),
fallbackCommandUsed: false,
}
}
const recursiveCommandUsed = RECURSIVE_CMDS.has(noptExploratoryResults.argv.remain[0])
let commandName = getCommandName(noptExploratoryResults.argv.remain)
let cmd = commandName ? opts.getCommandLongName(commandName) : null
if (commandName && !cmd && opts.fallbackCommand) {
cmd = opts.fallbackCommand
commandName = opts.fallbackCommand
inputArgv.unshift(opts.fallbackCommand)
const fallbackCommandUsed = Boolean(commandName && !cmd && opts.fallbackCommand)
if (fallbackCommandUsed) {
cmd = opts.fallbackCommand!
commandName = opts.fallbackCommand!
inputArgv.unshift(opts.fallbackCommand!)
}
const types = {
...opts.universalOptionsTypes,
@@ -141,6 +144,7 @@ export default async function parseCliArgs (
cmd,
params,
workspaceDir,
fallbackCommandUsed,
...normalizeOptions(options, knownOptions),
}
}

View File

@@ -131,7 +131,7 @@ test('allow any option that starts with "config."', async () => {
})
test('do not incorrectly change "install" command to "add"', async () => {
const { cmd } = await parseCliArgs({
const { cmd, fallbackCommandUsed } = await parseCliArgs({
...DEFAULT_OPTS,
getTypesByCommandName: (commandName: string) => {
switch (commandName) {
@@ -148,6 +148,7 @@ test('do not incorrectly change "install" command to "add"', async () => {
},
}, ['install', '-C', os.homedir(), '--network-concurrency', '1'])
expect(cmd).toBe('install')
expect(fallbackCommandUsed).toBeFalsy()
})
test('if a help option is used, set cmd to "help"', async () => {
@@ -183,7 +184,7 @@ test('use command-specific shorthands', async () => {
})
test('any unknown command is treated as a script', async () => {
const { options, cmd, params } = await parseCliArgs({
const { options, cmd, params, fallbackCommandUsed } = await parseCliArgs({
...DEFAULT_OPTS,
fallbackCommand: 'run',
getCommandLongName: () => null,
@@ -192,6 +193,7 @@ test('any unknown command is treated as a script', async () => {
expect(cmd).toBe('run')
expect(params).toStrictEqual(['foo'])
expect(options).toHaveProperty(['recursive'])
expect(fallbackCommandUsed).toBeTruthy()
})
test("don't use the fallback command if no command is present", async () => {

View File

@@ -18,6 +18,7 @@ import realpathMissing from 'realpath-missing'
import renderHelp from 'render-help'
import runRecursive, { RecursiveRunOpts } from './runRecursive'
import existsInDir from './existsInDir'
import { handler as exec } from './exec'
export const IF_PRESENT_OPTION = {
'if-present': Boolean,
@@ -111,13 +112,19 @@ For options that may be used with `-r`, see "pnpm help recursive"',
export type RunOpts =
& Omit<RecursiveRunOpts, 'allProjects' | 'selectedProjectsGraph' | 'workspaceDir'>
& { recursive?: boolean }
& Pick<Config, 'dir' | 'engineStrict' | 'reporter' | 'scriptShell' | 'shellEmulator' | 'enablePrePostScripts'>
& Pick<Config, 'dir' | 'engineStrict' | 'extraBinPaths' | 'reporter' | 'scriptShell' | 'shellEmulator' | 'enablePrePostScripts'>
& (
& { recursive?: false }
& Partial<Pick<Config, 'allProjects' | 'selectedProjectsGraph' | 'workspaceDir'>>
| { recursive: true }
& Required<Pick<Config, 'allProjects' | 'selectedProjectsGraph' | 'workspaceDir'>>
)
& {
argv?: {
original: string[]
}
fallbackCommandUsed?: boolean
}
export async function handler (
opts: RunOpts,
@@ -143,6 +150,14 @@ export async function handler (
}
if (scriptName !== 'start' && !manifest.scripts?.[scriptName]) {
if (opts.ifPresent) return
if (opts.fallbackCommandUsed) {
if (opts.argv == null) throw new Error('Could not fallback because opts.argv.original was not passed to the script runner')
await exec({
selectedProjectsGraph: {},
...opts,
}, opts.argv.original.slice(1))
return
}
if (opts.workspaceDir) {
const { manifest: rootManifest } = await tryReadProjectManifest(opts.workspaceDir, opts)
if (rootManifest?.scripts?.[scriptName]) {

View File

@@ -54,6 +54,7 @@ export default async function run (inputArgv: string[]) {
params: cliParams,
options: cliOptions,
cmd,
fallbackCommandUsed,
unknownOptions,
workspaceDir,
} = parsedCliArgs
@@ -62,7 +63,7 @@ export default async function run (inputArgv: string[]) {
process.exit(1)
}
if (unknownOptions.size > 0) {
if (unknownOptions.size > 0 && !fallbackCommandUsed) {
const unknownOptionsArray = Array.from(unknownOptions.keys())
if (unknownOptionsArray.every((option) => DEPRECATED_OPTIONS.has(option))) {
let deprecationMsg = `${chalk.bgYellow.black('\u2009WARN\u2009')}`
@@ -82,6 +83,7 @@ export default async function run (inputArgv: string[]) {
let config: Config & {
forceSharedLockfile: boolean
argv: { remain: string[], cooked: string[], original: string[] }
fallbackCommandUsed: boolean
}
try {
// When we just want to print the location of the global bin directory,
@@ -96,6 +98,7 @@ export default async function run (inputArgv: string[]) {
}) as typeof config
config.forceSharedLockfile = typeof config.workspaceDir === 'string' && config.sharedWorkspaceLockfile === true
config.argv = argv
config.fallbackCommandUsed = fallbackCommandUsed
} catch (err) {
// Reporting is not initialized at this point, so just printing the error
const hint = err['hint'] ? err['hint'] : `For help, run: pnpm help${cmd ? ` ${cmd}` : ''}`

View File

@@ -67,10 +67,9 @@ test('pass through to npm with all the args', async () => {
test('pnpm fails when an unsupported command is used', async () => {
prepare()
const { status, stdout } = execPnpmSync(['unsupported-command'])
const { status } = execPnpmSync(['unsupported-command'])
expect(status).toBe(1)
expect(stdout.toString()).toMatch(/Missing script: unsupported-command/)
})
test('pnpm fails when no command is specified', async () => {
@@ -181,3 +180,9 @@ test('use the specified Node.js version for running scripts', async () => {
await execPnpm(['run', 'test'])
expect(await fs.readFile('version', 'utf8')).toBe('v14.0.0')
})
test('if an unknown command is executed, run it', async () => {
prepare({})
await execPnpm(['node', '-e', "require('fs').writeFileSync('foo','','utf8')"])
expect(await fs.readFile('foo', 'utf8')).toBe('')
})