mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-28 01:45:30 -04:00
fix: parse bare color as boolean flag (#12162)
* fix: parse bare color as boolean flag Bare --color was treated as an enum-only option, so nopt could consume the following flag as its value. This prevented command-specific shorthands such as run --parallel from expanding when --color preceded them. Accept boolean values for the color option, and add parser/config regression coverage for the shorthand case. Co-authored-by: OpenAI Codex <codex@openai.com> * test: remove tautological color type metadata test Per review feedback, the assertion only restated the type metadata; the behavior is already covered by the parser regression test and the color-normalization test. * fix: keep with current dispatch working after a boolean global flag Bare `--color` now parses as a boolean flag, which enables forms like `pnpm --color with current <cmd>`. The `with current` rewrite skipped any occurrence whose preceding token was a long flag without `=`, assuming it consumed `with` as its value. For boolean flags that is wrong, so the lookup returned -1 and pnpm threw MISSING_WITH_CURRENT_CMD. Skip only when the preceding long option actually takes a value, based on the option type metadata; boolean flags and `--no-` negations no longer hide the command. --------- Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
6
.changeset/fuzzy-color-flags.md
Normal file
6
.changeset/fuzzy-color-flags.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/config.reader": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Fixed bare `--color` so it does not consume the following CLI flag, allowing command shorthands like `--parallel` to expand correctly and forms like `pnpm --color with current <command>` to dispatch the inner command instead of failing with `MISSING_WITH_CURRENT_CMD`.
|
||||
@@ -263,6 +263,40 @@ test('use command-specific shorthands', async () => {
|
||||
expect(options).toHaveProperty(['dev'])
|
||||
})
|
||||
|
||||
test('bare --color does not consume a following command-specific shorthand', async () => {
|
||||
const { cmd, options, params } = await parseCliArgs({
|
||||
...DEFAULT_OPTS,
|
||||
getTypesByCommandName: (commandName: string) => {
|
||||
if (commandName === 'run') {
|
||||
return {
|
||||
recursive: Boolean,
|
||||
sort: Boolean,
|
||||
stream: Boolean,
|
||||
'workspace-concurrency': Number,
|
||||
}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
shorthandsByCommandName: {
|
||||
run: {
|
||||
parallel: ['--workspace-concurrency=Infinity', '--no-sort', '--stream', '--recursive'],
|
||||
},
|
||||
},
|
||||
universalOptionsTypes: {
|
||||
color: [Boolean, 'always', 'auto', 'never'],
|
||||
recursive: Boolean,
|
||||
},
|
||||
}, ['--recursive', '--color', '--parallel', 'run', 'dev'])
|
||||
|
||||
expect(cmd).toBe('run')
|
||||
expect(params).toStrictEqual(['dev'])
|
||||
expect(options.color).toBe(true)
|
||||
expect(options.recursive).toBe(true)
|
||||
expect(options['workspace-concurrency']).toBe(Infinity)
|
||||
expect(options.sort).toBe(false)
|
||||
expect(options.stream).toBe(true)
|
||||
})
|
||||
|
||||
test('command-specific shorthands override universal shorthands', async () => {
|
||||
const { options } = await parseCliArgs({
|
||||
...DEFAULT_OPTS,
|
||||
|
||||
@@ -13,7 +13,7 @@ export const pnpmTypes = {
|
||||
'child-concurrency': Number,
|
||||
'merge-git-branch-lockfiles': Boolean,
|
||||
'merge-git-branch-lockfiles-branch-pattern': Array,
|
||||
color: ['always', 'auto', 'never'],
|
||||
color: [Boolean, 'always', 'auto', 'never'],
|
||||
'config-dir': String,
|
||||
'dangerously-allow-all-builds': Boolean,
|
||||
'deploy-all-files': Boolean,
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function parseCliArgs (inputArgv: string[]): Promise<ParsedCliArgsW
|
||||
// any global flags the user put BEFORE `with` (e.g. `--dir`, `--filter`)
|
||||
// are preserved.
|
||||
if (result.cmd === 'with' && result.params[0] === 'current') {
|
||||
const withIdx = findWithCurrentIndex(inputArgv)
|
||||
const withIdx = findWithCurrentIndex(inputArgv, GLOBAL_OPTIONS)
|
||||
if (withIdx < 0 || inputArgv.length - withIdx - 2 === 0) {
|
||||
throw new PnpmError('MISSING_WITH_CURRENT_CMD',
|
||||
'Missing command after "current". Usage: pnpm with current <command> [args...]')
|
||||
@@ -63,14 +63,26 @@ export async function parseCliArgs (inputArgv: string[]): Promise<ParsedCliArgsW
|
||||
* is the one. Good enough for realistic CLI usage — no pnpm option is
|
||||
* expected to take the literal value `with`.
|
||||
*/
|
||||
function findWithCurrentIndex (argv: string[]): number {
|
||||
function findWithCurrentIndex (argv: string[], optionTypes: Record<string, unknown>): number {
|
||||
for (let i = 0; i < argv.length - 1; i++) {
|
||||
if (argv[i] !== 'with' || argv[i + 1] !== 'current') continue
|
||||
const prev = argv[i - 1]
|
||||
// If the previous token is a long flag without an `=value` form, it may
|
||||
// be consuming `with` as its value — skip this occurrence in that case.
|
||||
if (prev != null && prev.startsWith('--') && !prev.includes('=')) continue
|
||||
// A preceding long option that takes a value would consume `with` as its
|
||||
// value, so this `with current` pair isn't the command — skip it. Boolean
|
||||
// flags (e.g. `--color`) and `--no-` negations don't consume a value.
|
||||
if (prev != null && longOptionConsumesValue(prev, optionTypes)) continue
|
||||
return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function longOptionConsumesValue (token: string, optionTypes: Record<string, unknown>): boolean {
|
||||
if (!token.startsWith('--') || token.includes('=')) return false
|
||||
const name = token.slice(2)
|
||||
if (name.startsWith('no-')) return false
|
||||
return !isBooleanType(optionTypes[name])
|
||||
}
|
||||
|
||||
function isBooleanType (type: unknown): boolean {
|
||||
return type === Boolean || (Array.isArray(type) && type.includes(Boolean))
|
||||
}
|
||||
|
||||
@@ -67,6 +67,21 @@ test('pnpm with forwards subsequent args to the child pnpm', () => {
|
||||
expect(stdout.toString().trim()).toMatch(/^\d+\.\d+\.\d+/)
|
||||
})
|
||||
|
||||
test('pnpm with current dispatches the inner command after a global boolean flag', () => {
|
||||
prepare()
|
||||
writeJsonFileSync('package.json', {
|
||||
name: 'project',
|
||||
version: '1.0.0',
|
||||
})
|
||||
|
||||
for (const flag of ['--color', '--yes']) {
|
||||
const { status, stdout } = execPnpmSync([flag, 'with', 'current', '--version'])
|
||||
|
||||
expect(status).toBe(0)
|
||||
expect(stdout.toString().trim()).toMatch(/^\d+\.\d+\.\d+/)
|
||||
}
|
||||
})
|
||||
|
||||
test('pnpm with fails when no spec is provided', () => {
|
||||
prepare()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user