feat: warn on unknown options and suggest the intended one · nuxt/cli@07e7981 · GitHub
Skip to content

Commit 07e7981

Browse files
committed
feat: warn on unknown options and suggest the intended one
1 parent 967f709 commit 07e7981

3 files changed

Lines changed: 214 additions & 0 deletions

File tree

‎packages/nuxt-cli/src/main.ts‎

Lines changed: 40 additions & 0 deletions
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { ArgsDef } from 'citty'
2+
3+
/** Always accepted by citty, so never reported as unknown. */
4+
const BUILTIN_FLAGS = ['help', 'version']
5+
6+
/** Low enough to catch a wrong case (`--loglevel`) or a dropped letter, high enough not to invent a match. */
7+
const SUGGESTION_THRESHOLD = 0.3
8+
9+
const LEADING_DASHES_RE = /^--/
10+
const NEGATION_RE = /^no-/
11+
12+
export interface UnknownFlags {
13+
flags: string[]
14+
known: string[]
15+
}
16+
17+
/**
18+
* Long flags in `rawArgs` that `argsDef` does not declare.
19+
*
20+
* Only `--long` forms are considered. A bundle of short flags (`-abc`) cannot be
21+
* told apart from a single misspelled one, and guessing wrong is worse than
22+
* staying quiet.
23+
*/
24+
export function findUnknownFlags(argsDef: ArgsDef, rawArgs: string[]): UnknownFlags {
25+
const known = new Set<string>(BUILTIN_FLAGS)
26+
for (const [name, def] of Object.entries(argsDef)) {
27+
known.add(name)
28+
const alias = (def as { alias?: string | string[] }).alias
29+
for (const entry of Array.isArray(alias) ? alias : alias ? [alias] : []) {
30+
known.add(entry)
31+
}
32+
}
33+
34+
const separator = rawArgs.indexOf('--')
35+
const argv = separator === -1 ? rawArgs : rawArgs.slice(0, separator)
36+
37+
const flags: string[] = []
38+
for (const arg of argv) {
39+
if (!arg.startsWith('--') || arg === '--') {
40+
continue
41+
}
42+
const equals = arg.indexOf('=')
43+
const name = (equals === -1 ? arg : arg.slice(0, equals)).replace(LEADING_DASHES_RE, '')
44+
if (name && !isKnown(known, name) && !flags.includes(name)) {
45+
flags.push(name)
46+
}
47+
}
48+
return { flags, known: [...known] }
49+
}
50+
51+
/**
52+
* The declared flag each unknown flag was most likely meant to be. Resolved
53+
* separately so the fuzzy matcher is only loaded once something is wrong.
54+
*/
55+
export async function suggestFlags({ flags, known }: UnknownFlags): Promise<Array<{ flag: string, suggestion?: string }>> {
56+
const { default: fuzzysort } = await import('fuzzysort')
57+
return flags.map((flag) => {
58+
const [match] = fuzzysort.go(flag.replace(NEGATION_RE, ''), known, { limit: 1, threshold: SUGGESTION_THRESHOLD })
59+
return { flag: `--${flag}`, suggestion: match && `--${match.target}` }
60+
})
61+
}
62+
63+
/**
64+
* Dotted flags are declared either whole (`https.cert`) or as the object that
65+
* holds them (`https`), and a boolean may be negated with a `no-` prefix.
66+
*/
67+
function isKnown(known: Set<string>, name: string): boolean {
68+
for (const candidate of new Set([name, name.replace(NEGATION_RE, '')])) {
69+
if (known.has(candidate)) {
70+
return true
71+
}
72+
for (let dot = candidate.lastIndexOf('.'); dot !== -1; dot = candidate.lastIndexOf('.', dot - 1)) {
73+
if (known.has(candidate.slice(0, dot))) {
74+
return true
75+
}
76+
}
77+
}
78+
return false
79+
}
Lines changed: 95 additions & 0 deletions

0 commit comments

Comments
 (0)