Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
"express": "4.18.2",
"express-logging": "1.1.1",
"extract-zip": "2.0.1",
"fastest-levenshtein": "1.0.16",
"fastify": "4.17.0",
"find-up": "6.3.0",
"flush-write-stream": "2.0.0",
Expand Down Expand Up @@ -167,7 +168,6 @@
"read-pkg-up": "9.1.0",
"semver": "7.5.1",
"source-map-support": "0.5.21",
"string-similarity": "4.0.4",
"strip-ansi-control-characters": "2.0.0",
"tabtab": "3.0.2",
"tempy": "3.0.0",
Expand Down
6 changes: 2 additions & 4 deletions src/commands/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import process from 'process'

import { Option } from 'commander'
import { closest } from 'fastest-levenshtein'
import inquirer from 'inquirer'
import { findBestMatch } from 'string-similarity'

import { BANG, chalk, error, exit, log, NETLIFY_CYAN, USER_AGENT, warn } from '../utils/command-helpers.mjs'
import execa from '../utils/execa.mjs'
Expand Down Expand Up @@ -118,9 +118,7 @@ const mainCommand = async function (options, command) {
warn(`${chalk.yellow(command.args[0])} is not a ${command.name()} command.`)

const allCommands = command.commands.map((cmd) => cmd.name())
const {
bestMatch: { target: suggestion },
} = findBestMatch(command.args[0], allCommands)
const suggestion = closest(command.args[0], allCommands)

const applySuggestion = await new Promise((resolve) => {
const prompt = inquirer.prompt({
Expand Down
11 changes: 6 additions & 5 deletions src/commands/recipes/recipes.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// @ts-check
import { basename } from 'path'

import { closest } from 'fastest-levenshtein'
import inquirer from 'inquirer'
import { findBestMatch } from 'string-similarity'

import { NETLIFYDEVERR, chalk, log } from '../../utils/command-helpers.mjs'

Expand All @@ -28,17 +28,18 @@ const recipesCommand = async (recipeName, options, command) => {
try {
return await runRecipe({ config, recipeName: sanitizedRecipeName, repositoryRoot })
} catch (error) {
if (error.code !== 'MODULE_NOT_FOUND') {
if (
// The ESM loader throws this instead of MODULE_NOT_FOUND
error.code !== 'ERR_MODULE_NOT_FOUND'
) {
throw error
}

log(`${NETLIFYDEVERR} ${chalk.yellow(recipeName)} is not a valid recipe name.`)

const recipes = await listRecipes()
const recipeNames = recipes.map(({ name }) => name)
const {
bestMatch: { target: suggestion },
} = findBestMatch(recipeName, recipeNames)
const suggestion = closest(recipeName, recipeNames)
const applySuggestion = await new Promise((resolve) => {
const prompt = inquirer.prompt({
type: 'confirm',
Expand Down
6 changes: 6 additions & 0 deletions tests/integration/640.command.recipes.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,9 @@ test('Handles JSON with comments', async (t) => {
t.deepEqual([...settings['deno.enablePaths']], ['/some/path', 'netlify/edge-functions'])
})
})

test('Suggests closest matching recipe on typo', async (t) => {
const cliResponse = await callCli(['recipes', 'vsc'])

t.snapshot(normalize(cliResponse))
})
17 changes: 17 additions & 0 deletions tests/integration/didyoumean.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const test = require('ava')

const callCli = require('./utils/call-cli.cjs')
const { normalize } = require('./utils/snapshots.cjs')

test('suggests closest matching command on typo', async (t) => {
// failures are expected since we effectively quit out of the prompts
const errors = await Promise.all([
t.throwsAsync(() => callCli(['sta'])),
t.throwsAsync(() => callCli(['opeen'])),
t.throwsAsync(() => callCli(['hel'])),
t.throwsAsync(() => callCli(['versio'])),
])
errors.forEach((error) => {
t.snapshot(normalize(error.stdout, { duration: true, filePath: true }))
})
})
7 changes: 7 additions & 0 deletions tests/integration/snapshots/640.command.recipes.test.cjs.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,10 @@ Generated by [AVA](https://avajs.dev).
|--------|-------------------------------------------------------------------------|␊
| vscode | Create VS Code settings for an optimal experience with Netlify projects |␊
'----------------------------------------------------------------------------------'`

## Suggests closest matching recipe on typo

> Snapshot 1

`β—ˆ vsc is not a valid recipe name.␊
? Did you mean vscode (y/N) `
Binary file modified tests/integration/snapshots/640.command.recipes.test.cjs.snap
Binary file not shown.
27 changes: 27 additions & 0 deletions tests/integration/snapshots/didyoumean.test.cjs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Snapshot report for `tests/integration/didyoumean.test.cjs`

The actual snapshot is saved in `didyoumean.test.cjs.snap`.

Generated by [AVA](https://avajs.dev).

## suggests closest matching command on typo

> Snapshot 1

`β€Ί Warning: sta is not a netlify command.␊
? Did you mean api (y/N) `

> Snapshot 2

`β€Ί Warning: opeen is not a netlify command.␊
? Did you mean open (y/N) `

> Snapshot 3

`β€Ί Warning: hel is not a netlify command.␊
? Did you mean dev (y/N) `

> Snapshot 4

`β€Ί Warning: versio is not a netlify command.␊
? Did you mean serve (y/N) `
Binary file not shown.