Skip to content

Commit f24c7aa

Browse files
kisaragi-hiuSkn0tt
andauthored
fix(deps): replace string-similarity with fastest-levenshtein (#5759)
* fix(deps): replace string-similarity with fastest-levenshtein string-similarity is no longer maintained and deprecated, resulting in another warning when installing Netlify CLI. While the algorithm is different (Levenshtein distance instead of Dice's coefficient), the difference shouldn't be big enough to matter as this is only used for suggestion after a typo. * chore(main): add tests for typo suggestion * fix(recipes): suggest a replacement on typo again When the user specifies a recipe that doesn't exist, the recipes command is supposed to suggest a replacement. The way it detects this scenario is to catch the module not found error, but it was catching MODULE_NOT_FOUND, which is only thrown for CommonJS modules: > MODULE_NOT_FOUND > > A module file could not be resolved by the CommonJS modules loader > while attempting a require() operation or when loading the program entry > point. > > --- https://nodejs.org/api/errors.html#module_not_found For ESM modules, when a module isn't found, the ERR_MODULE_NOT_FOUND error is thrown instead: > ERR_MODULE_NOT_FOUND > > A module file could not be resolved by the ECMAScript modules loader > while attempting an import operation or when loading the program entry > point. > > --- https://nodejs.org/api/errors.html#err_module_not_found This commit makes it catch ERR_MODULE_NOT_FOUND instead. --------- Co-authored-by: Simon Knott <[email protected]>
1 parent d8debda commit f24c7aa

File tree

10 files changed

+80
-22
lines changed

10 files changed

+80
-22
lines changed

package-lock.json

Lines changed: 14 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
"express": "4.18.2",
118118
"express-logging": "1.1.1",
119119
"extract-zip": "2.0.1",
120+
"fastest-levenshtein": "1.0.16",
120121
"fastify": "4.17.0",
121122
"find-up": "6.3.0",
122123
"flush-write-stream": "2.0.0",
@@ -167,7 +168,6 @@
167168
"read-pkg-up": "9.1.0",
168169
"semver": "7.5.1",
169170
"source-map-support": "0.5.21",
170-
"string-similarity": "4.0.4",
171171
"strip-ansi-control-characters": "2.0.0",
172172
"tabtab": "3.0.2",
173173
"tempy": "3.0.0",

src/commands/main.mjs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import process from 'process'
33

44
import { Option } from 'commander'
5+
import { closest } from 'fastest-levenshtein'
56
import inquirer from 'inquirer'
6-
import { findBestMatch } from 'string-similarity'
77

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

120120
const allCommands = command.commands.map((cmd) => cmd.name())
121-
const {
122-
bestMatch: { target: suggestion },
123-
} = findBestMatch(command.args[0], allCommands)
121+
const suggestion = closest(command.args[0], allCommands)
124122

125123
const applySuggestion = await new Promise((resolve) => {
126124
const prompt = inquirer.prompt({

src/commands/recipes/recipes.mjs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// @ts-check
22
import { basename } from 'path'
33

4+
import { closest } from 'fastest-levenshtein'
45
import inquirer from 'inquirer'
5-
import { findBestMatch } from 'string-similarity'
66

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

@@ -28,17 +28,18 @@ const recipesCommand = async (recipeName, options, command) => {
2828
try {
2929
return await runRecipe({ config, recipeName: sanitizedRecipeName, repositoryRoot })
3030
} catch (error) {
31-
if (error.code !== 'MODULE_NOT_FOUND') {
31+
if (
32+
// The ESM loader throws this instead of MODULE_NOT_FOUND
33+
error.code !== 'ERR_MODULE_NOT_FOUND'
34+
) {
3235
throw error
3336
}
3437

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

3740
const recipes = await listRecipes()
3841
const recipeNames = recipes.map(({ name }) => name)
39-
const {
40-
bestMatch: { target: suggestion },
41-
} = findBestMatch(recipeName, recipeNames)
42+
const suggestion = closest(recipeName, recipeNames)
4243
const applySuggestion = await new Promise((resolve) => {
4344
const prompt = inquirer.prompt({
4445
type: 'confirm',

tests/integration/640.command.recipes.test.cjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,9 @@ test('Handles JSON with comments', async (t) => {
139139
t.deepEqual([...settings['deno.enablePaths']], ['/some/path', 'netlify/edge-functions'])
140140
})
141141
})
142+
143+
test('Suggests closest matching recipe on typo', async (t) => {
144+
const cliResponse = await callCli(['recipes', 'vsc'])
145+
146+
t.snapshot(normalize(cliResponse))
147+
})
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const test = require('ava')
2+
3+
const callCli = require('./utils/call-cli.cjs')
4+
const { normalize } = require('./utils/snapshots.cjs')
5+
6+
test('suggests closest matching command on typo', async (t) => {
7+
// failures are expected since we effectively quit out of the prompts
8+
const errors = await Promise.all([
9+
t.throwsAsync(() => callCli(['sta'])),
10+
t.throwsAsync(() => callCli(['opeen'])),
11+
t.throwsAsync(() => callCli(['hel'])),
12+
t.throwsAsync(() => callCli(['versio'])),
13+
])
14+
errors.forEach((error) => {
15+
t.snapshot(normalize(error.stdout, { duration: true, filePath: true }))
16+
})
17+
})

tests/integration/snapshots/640.command.recipes.test.cjs.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,10 @@ Generated by [AVA](https://avajs.dev).
1515
|--------|-------------------------------------------------------------------------|␊
1616
| vscode | Create VS Code settings for an optimal experience with Netlify projects |␊
1717
'----------------------------------------------------------------------------------'`
18+
19+
## Suggests closest matching recipe on typo
20+
21+
> Snapshot 1
22+
23+
`◈ vsc is not a valid recipe name.␊
24+
? Did you mean vscode (y/N) `
94 Bytes
Binary file not shown.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Snapshot report for `tests/integration/didyoumean.test.cjs`
2+
3+
The actual snapshot is saved in `didyoumean.test.cjs.snap`.
4+
5+
Generated by [AVA](https://avajs.dev).
6+
7+
## suggests closest matching command on typo
8+
9+
> Snapshot 1
10+
11+
`› Warning: sta is not a netlify command.␊
12+
? Did you mean api (y/N) `
13+
14+
> Snapshot 2
15+
16+
`› Warning: opeen is not a netlify command.␊
17+
? Did you mean open (y/N) `
18+
19+
> Snapshot 3
20+
21+
`› Warning: hel is not a netlify command.␊
22+
? Did you mean dev (y/N) `
23+
24+
> Snapshot 4
25+
26+
`› Warning: versio is not a netlify command.␊
27+
? Did you mean serve (y/N) `
287 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)