From ed43c03ebb164d88657c933be569b499f8e1e074 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Sat, 26 Feb 2022 07:05:26 -0800 Subject: [PATCH] Add CLI command typo detection (#34836) * Add CLI command typo detection * Apply suggestions from code review Co-authored-by: Steven * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Steven --- packages/next/bin/next.ts | 2 +- packages/next/lib/detect-typo.ts | 44 +++++++++++++++++++++++++ packages/next/lib/get-project-dir.ts | 13 ++++++++ test/integration/cli/test/index.test.js | 20 +++++++++++ 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 packages/next/lib/detect-typo.ts diff --git a/packages/next/bin/next.ts b/packages/next/bin/next.ts index bba1a9c9a00995d..dead0052077ffc2 100755 --- a/packages/next/bin/next.ts +++ b/packages/next/bin/next.ts @@ -15,7 +15,7 @@ import { NON_STANDARD_NODE_ENV } from '../lib/constants' const defaultCommand = 'dev' export type cliCommand = (argv?: string[]) => void -const commands: { [command: string]: () => Promise } = { +export const commands: { [command: string]: () => Promise } = { build: () => Promise.resolve(require('../cli/next-build').nextBuild), start: () => Promise.resolve(require('../cli/next-start').nextStart), export: () => Promise.resolve(require('../cli/next-export').nextExport), diff --git a/packages/next/lib/detect-typo.ts b/packages/next/lib/detect-typo.ts new file mode 100644 index 000000000000000..b94200482dbc937 --- /dev/null +++ b/packages/next/lib/detect-typo.ts @@ -0,0 +1,44 @@ +// the minimum number of operations required to convert string a to string b. +function minDistance(a: string, b: string, threshold: number): number { + const m = a.length + const n = b.length + + if (m < n) { + return minDistance(b, a, threshold) + } + + if (n === 0) { + return m + } + + let previousRow = Array.from({ length: n + 1 }, (_, i) => i) + + for (let i = 0; i < m; i++) { + const s1 = a[i] + let currentRow = [i + 1] + for (let j = 0; j < n; j++) { + const s2 = b[j] + const insertions = previousRow[j + 1] + 1 + const deletions = currentRow[j] + 1 + const substitutions = previousRow[j] + Number(s1 !== s2) + currentRow.push(Math.min(insertions, deletions, substitutions)) + } + previousRow = currentRow + } + return previousRow[previousRow.length - 1] +} + +export function detectTypo(input: string, options: string[], threshold = 2) { + const potentialTypos = options + .map((o) => ({ + option: o, + distance: minDistance(o, input, threshold), + })) + .filter(({ distance }) => distance <= threshold && distance > 0) + .sort((a, b) => a.distance - b.distance) + + if (potentialTypos.length) { + return potentialTypos[0].option + } + return null +} diff --git a/packages/next/lib/get-project-dir.ts b/packages/next/lib/get-project-dir.ts index 24b4c32070ab396..f132c846d0ce5d0 100644 --- a/packages/next/lib/get-project-dir.ts +++ b/packages/next/lib/get-project-dir.ts @@ -1,6 +1,8 @@ import fs from 'fs' import path from 'path' +import { commands } from '../bin/next' import * as Log from '../build/output/log' +import { detectTypo } from './detect-typo' export function getProjectDir(dir?: string) { try { @@ -19,6 +21,17 @@ export function getProjectDir(dir?: string) { return realDir } catch (err: any) { if (err.code === 'ENOENT') { + if (typeof dir === 'string') { + const detectedTypo = detectTypo(dir, Object.keys(commands)) + + if (detectedTypo) { + Log.error( + `"next ${dir}" does not exist. Did you mean "next ${detectedTypo}"?` + ) + process.exit(1) + } + } + Log.error( `Invalid project directory provided, no such directory: ${path.resolve( dir || '.' diff --git a/test/integration/cli/test/index.test.js b/test/integration/cli/test/index.test.js index bf7e39e892eadee..2fbae8c6ce2c1af 100644 --- a/test/integration/cli/test/index.test.js +++ b/test/integration/cli/test/index.test.js @@ -57,6 +57,26 @@ describe('CLI Usage', () => { 'Invalid project directory provided, no such directory' ) }) + + test('detects command typos', async () => { + const typos = [ + ['buidl', 'build'], + ['buill', 'build'], + ['biild', 'build'], + ['exporr', 'export'], + ['starr', 'start'], + ['dee', 'dev'], + ] + + for (const check of typos) { + const output = await runNextCommand([check[0]], { + stderr: true, + }) + expect(output.stderr).toContain( + `"next ${check[0]}" does not exist. Did you mean "next ${check[1]}"?` + ) + } + }) }) describe('build', () => {