mirror of
https://github.com/pnpm/pnpm.git
synced 2026-02-16 10:01:13 -05:00
feat: "Did you mean <option>?"
Print a "Did you mean" line under the unknown option error with any option that look similar to the typed one. close #2603 PR #2623
This commit is contained in:
7
.changeset/three-turtles-switch.md
Normal file
7
.changeset/three-turtles-switch.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@pnpm/parse-cli-args": major
|
||||
---
|
||||
|
||||
`unknownOptions` in the result object is a `Map` instead of an `Array`.
|
||||
|
||||
`unknownOptions` is a map of unknown options to options that are similar to the unknown options.
|
||||
5
.changeset/wise-shoes-shop.md
Normal file
5
.changeset/wise-shoes-shop.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Print a "Did you mean" line under the unknown option error with any option that look similar to the typed one.
|
||||
@@ -34,6 +34,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@pnpm/find-workspace-dir": "workspace:1.0.0",
|
||||
"didyoumean2": "^4.0.0",
|
||||
"nopt": "4.0.3"
|
||||
},
|
||||
"funding": "https://opencollective.com/pnpm"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import findWorkspaceDir from '@pnpm/find-workspace-dir'
|
||||
import didYouMean, { ReturnTypeEnums } from 'didyoumean2'
|
||||
import nopt = require('nopt')
|
||||
|
||||
const RECURSIVE_CMDS = new Set(['recursive', 'multi', 'm'])
|
||||
@@ -13,7 +14,7 @@ export interface ParsedCliArgs {
|
||||
// tslint:disable-next-line: no-any
|
||||
options: Record<string, any>
|
||||
cmd: string | null
|
||||
unknownOptions: string[]
|
||||
unknownOptions: Map<string, string[]>
|
||||
workspaceDir?: string
|
||||
}
|
||||
|
||||
@@ -50,7 +51,7 @@ export default async function parseCliArgs (
|
||||
cmd: 'help',
|
||||
options: {},
|
||||
params: noptExploratoryResults.argv.remain,
|
||||
unknownOptions: [] as string[],
|
||||
unknownOptions: new Map(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,13 +127,8 @@ export default async function parseCliArgs (
|
||||
cmd = 'recursive'
|
||||
}
|
||||
|
||||
const allowedOptions = new Set(Object.keys(types))
|
||||
const unknownOptions = [] as string[]
|
||||
for (const cliOption of Object.keys(options)) {
|
||||
if (!allowedOptions.has(cliOption) && !cliOption.startsWith('//')) {
|
||||
unknownOptions.push(cliOption)
|
||||
}
|
||||
}
|
||||
const knownOptions = new Set(Object.keys(types))
|
||||
const unknownOptions = getUnknownOptions(Object.keys(options), knownOptions)
|
||||
return {
|
||||
argv,
|
||||
cmd,
|
||||
@@ -142,3 +138,21 @@ export default async function parseCliArgs (
|
||||
workspaceDir,
|
||||
}
|
||||
}
|
||||
|
||||
function getUnknownOptions (usedOptions: string[], knownOptions: Set<string>) {
|
||||
const unknownOptions = new Map<string, string[]>()
|
||||
const closestMatches = getClosestOptionMatches.bind(null, Array.from(knownOptions))
|
||||
for (const usedOption of usedOptions) {
|
||||
if (knownOptions.has(usedOption) || usedOption.startsWith('//')) continue
|
||||
|
||||
unknownOptions.set(usedOption, closestMatches(usedOption))
|
||||
}
|
||||
return unknownOptions
|
||||
|
||||
}
|
||||
|
||||
function getClosestOptionMatches (knownOptions: string[], option: string) {
|
||||
return didYouMean(option, knownOptions, {
|
||||
returnType: ReturnTypeEnums.ALL_CLOSEST_MATCHES,
|
||||
}) as (string[] | null) ?? []
|
||||
}
|
||||
|
||||
@@ -120,6 +120,7 @@ test('detect unknown options', async (t) => {
|
||||
getTypesByCommandName: (commandName: string) => {
|
||||
if (commandName === 'install') {
|
||||
return {
|
||||
bar: Boolean,
|
||||
recursive: Boolean,
|
||||
registry: String,
|
||||
}
|
||||
@@ -128,7 +129,10 @@ test('detect unknown options', async (t) => {
|
||||
},
|
||||
universalOptionsTypes: { filter: [String, Array] },
|
||||
}, ['install', '--save-dev', '--registry=https://example.com', '--qar', '--filter=packages'])
|
||||
t.deepEqual(unknownOptions, ['save-dev', 'qar'])
|
||||
t.deepEqual(
|
||||
Array.from(unknownOptions.entries()),
|
||||
[['save-dev', []], ['qar', ['bar']]]
|
||||
)
|
||||
t.end()
|
||||
})
|
||||
|
||||
|
||||
16
packages/pnpm/src/formatError.ts
Normal file
16
packages/pnpm/src/formatError.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import chalk = require('chalk')
|
||||
|
||||
export function formatUnknownOptionsError (unknownOptions: Map<string, string[]>) {
|
||||
let output = chalk.bgRed.black('\u2009ERROR\u2009')
|
||||
const unknownOptionsArray = Array.from(unknownOptions.keys())
|
||||
if (unknownOptionsArray.length > 1) {
|
||||
return `${output} ${chalk.red(`Unknown options: ${unknownOptionsArray.map(unknownOption => `'${unknownOption}'`).join(', ')}`)}`
|
||||
}
|
||||
const unknownOption = unknownOptionsArray[0]
|
||||
output += ` ${chalk.red(`Unknown option: '${unknownOption}'`)}`
|
||||
const didYouMeanOptions = unknownOptions.get(unknownOption)
|
||||
if (!didYouMeanOptions?.length) {
|
||||
return output
|
||||
}
|
||||
return `${output}\nDid you mean '${didYouMeanOptions.join("', or '")}'?`
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import R = require('ramda')
|
||||
import which = require('which')
|
||||
import checkForUpdates from './checkForUpdates'
|
||||
import pnpmCmds, { getRCOptionsTypes } from './cmd'
|
||||
import { formatUnknownOptionsError } from './formatError'
|
||||
import './logging/fileLogger'
|
||||
import parseCliArgs from './parseCliArgs'
|
||||
import initReporter, { ReporterType } from './reporter'
|
||||
@@ -53,23 +54,18 @@ export default async function run (inputArgv: string[]) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (unknownOptions.length > 0) {
|
||||
if (unknownOptions.every((option) => DEPRECATED_OPTIONS.has(option))) {
|
||||
if (unknownOptions.size > 0) {
|
||||
const unknownOptionsArray = Array.from(unknownOptions.keys())
|
||||
if (unknownOptionsArray.every((option) => DEPRECATED_OPTIONS.has(option))) {
|
||||
let deprecationMsg = `${chalk.bgYellow.black('\u2009WARN\u2009')}`
|
||||
if (unknownOptions.length === 1) {
|
||||
deprecationMsg += ` ${chalk.yellow(`Deprecated option: '${unknownOptions[0]}'`)}`
|
||||
if (unknownOptionsArray.length === 1) {
|
||||
deprecationMsg += ` ${chalk.yellow(`Deprecated option: '${unknownOptionsArray[0]}'`)}`
|
||||
} else {
|
||||
deprecationMsg += ` ${chalk.yellow(`Deprecated options: ${unknownOptions.map(unknownOption => `'${unknownOption}'`).join(', ')}`)}`
|
||||
deprecationMsg += ` ${chalk.yellow(`Deprecated options: ${unknownOptionsArray.map(unknownOption => `'${unknownOption}'`).join(', ')}`)}`
|
||||
}
|
||||
console.log(deprecationMsg)
|
||||
} else {
|
||||
let errorMsg = `${chalk.bgRed.black('\u2009ERROR\u2009')}`
|
||||
if (unknownOptions.length === 1) {
|
||||
errorMsg += ` ${chalk.red(`Unknown option: '${unknownOptions[0]}'`)}`
|
||||
} else {
|
||||
errorMsg += ` ${chalk.red(`Unknown options: ${unknownOptions.map(unknownOption => `'${unknownOption}'`).join(', ')}`)}`
|
||||
}
|
||||
console.error(errorMsg)
|
||||
console.error(formatUnknownOptionsError(unknownOptions))
|
||||
console.log(`For help, run: pnpm help${cmd ? ` ${cmd}` : ''}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
22
packages/pnpm/test/formatError.test.ts
Normal file
22
packages/pnpm/test/formatError.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import chalk = require('chalk')
|
||||
import test = require('tape')
|
||||
import { formatUnknownOptionsError } from '../src/formatError'
|
||||
|
||||
let ERROR = chalk.bgRed.black('\u2009ERROR\u2009')
|
||||
|
||||
test('formatUnknownOptionsError()', async (t) => {
|
||||
t.equal(
|
||||
formatUnknownOptionsError(new Map([['foo', []]])),
|
||||
`${ERROR} ${chalk.red("Unknown option: 'foo'")}`
|
||||
)
|
||||
t.equal(
|
||||
formatUnknownOptionsError(new Map([['foo', ['foa', 'fob']]])),
|
||||
`${ERROR} ${chalk.red("Unknown option: 'foo'")}
|
||||
Did you mean 'foa', or 'fob'?`
|
||||
)
|
||||
t.equal(
|
||||
formatUnknownOptionsError(new Map([['foo', []], ['bar', []]])),
|
||||
`${ERROR} ${chalk.red("Unknown options: 'foo', 'bar'")}`
|
||||
)
|
||||
t.end()
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
///<reference path="../../../typings/index.d.ts" />
|
||||
import './cli'
|
||||
import './complete.test'
|
||||
import './formatError.test'
|
||||
import './getOptionType.test'
|
||||
import './help.spec'
|
||||
import './install'
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -1371,6 +1371,7 @@ importers:
|
||||
packages/parse-cli-args:
|
||||
dependencies:
|
||||
'@pnpm/find-workspace-dir': 'link:../find-workspace-dir'
|
||||
didyoumean2: 4.0.0
|
||||
nopt: 4.0.3
|
||||
devDependencies:
|
||||
'@pnpm/parse-cli-args': 'link:'
|
||||
@@ -1379,6 +1380,7 @@ importers:
|
||||
'@pnpm/find-workspace-dir': 'workspace:1.0.0'
|
||||
'@pnpm/parse-cli-args': 'link:'
|
||||
'@types/nopt': 3.0.29
|
||||
didyoumean2: ^4.0.0
|
||||
nopt: 4.0.3
|
||||
packages/parse-wanted-dependency:
|
||||
dependencies:
|
||||
@@ -5934,6 +5936,15 @@ packages:
|
||||
hasBin: true
|
||||
resolution:
|
||||
integrity: sha512-bOICKN2/UvsTjEnJpUld3/2SDahgHH6KvG7umRv9WNlYTXOlwplM5ZkJBQh72swRjI5D3iu2Aqde1STulCWUWg==
|
||||
/didyoumean2/4.0.0:
|
||||
dependencies:
|
||||
leven: 3.1.0
|
||||
lodash.deburr: 4.1.0
|
||||
dev: false
|
||||
engines:
|
||||
node: '>=10.13'
|
||||
resolution:
|
||||
integrity: sha512-7+OMIHqPDJ4uxeExQx8cSk26oD3KUloAQzi2R+3rmTU4IHvSDDmWZTQ6bmC4+MTw61DkYoh5ARxwS9MRoz0t2A==
|
||||
/diff-dates/1.0.12:
|
||||
dependencies:
|
||||
date-unit-ms: 1.1.13
|
||||
@@ -8218,6 +8229,12 @@ packages:
|
||||
node: '>=6'
|
||||
resolution:
|
||||
integrity: sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==
|
||||
/leven/3.1.0:
|
||||
dev: false
|
||||
engines:
|
||||
node: '>=6'
|
||||
resolution:
|
||||
integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
|
||||
/levn/0.3.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.1.2
|
||||
@@ -8340,6 +8357,10 @@ packages:
|
||||
dev: true
|
||||
resolution:
|
||||
integrity: sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=
|
||||
/lodash.deburr/4.1.0:
|
||||
dev: false
|
||||
resolution:
|
||||
integrity: sha1-3bG7s+8HRYwBd7oH3hRCLLAz/5s=
|
||||
/lodash.get/4.4.2:
|
||||
dev: true
|
||||
resolution:
|
||||
|
||||
Reference in New Issue
Block a user