From afb7f0959794e65d578afca7db8d5d861cb1d440 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 25 Feb 2022 16:22:53 -0800 Subject: [PATCH 1/4] Add CLI command typo detection --- 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..1a4cbed9585eb67 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( + `"${dir}" seems to be a typo, 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..a8e9d04314b3f65 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( + `"${check[0]}" seems to be a typo, did you mean: next ${check[1]}` + ) + } + }) }) describe('build', () => { From 64d311d5b7e2cf4e60291076708a7f8adea18ee3 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 25 Feb 2022 20:16:13 -0800 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Steven --- packages/next/lib/get-project-dir.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/lib/get-project-dir.ts b/packages/next/lib/get-project-dir.ts index 1a4cbed9585eb67..f132c846d0ce5d0 100644 --- a/packages/next/lib/get-project-dir.ts +++ b/packages/next/lib/get-project-dir.ts @@ -26,7 +26,7 @@ export function getProjectDir(dir?: string) { if (detectedTypo) { Log.error( - `"${dir}" seems to be a typo, did you mean: next ${detectedTypo}` + `"next ${dir}" does not exist. Did you mean "next ${detectedTypo}"?` ) process.exit(1) } From feb4348caded7be3eed18ea79534f532f02529f3 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 25 Feb 2022 20:18:10 -0800 Subject: [PATCH 3/4] Apply suggestions from code review --- test/integration/cli/test/index.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/cli/test/index.test.js b/test/integration/cli/test/index.test.js index a8e9d04314b3f65..7cb3c5ed579ce2e 100644 --- a/test/integration/cli/test/index.test.js +++ b/test/integration/cli/test/index.test.js @@ -73,7 +73,7 @@ describe('CLI Usage', () => { stderr: true, }) expect(output.stderr).toContain( - `"${check[0]}" seems to be a typo, did you mean: next ${check[1]}` + `"next ${check[0]}" does not exist. Did you mean "next ${check[1]}"? ` ) } }) From b71fd101b07d4d7fe5b5cf31bb561ebbd95e860b Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 25 Feb 2022 20:37:46 -0800 Subject: [PATCH 4/4] Apply suggestions from code review --- test/integration/cli/test/index.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/cli/test/index.test.js b/test/integration/cli/test/index.test.js index 7cb3c5ed579ce2e..2fbae8c6ce2c1af 100644 --- a/test/integration/cli/test/index.test.js +++ b/test/integration/cli/test/index.test.js @@ -73,7 +73,7 @@ describe('CLI Usage', () => { stderr: true, }) expect(output.stderr).toContain( - `"next ${check[0]}" does not exist. Did you mean "next ${check[1]}"? ` + `"next ${check[0]}" does not exist. Did you mean "next ${check[1]}"?` ) } })