Skip to content

Commit 2bbb639

Browse files
refactor(webpack-cli): replace fastest-levenshtein with in-tree implementation (#4762)
1 parent a467d6e commit 2bbb639

6 files changed

Lines changed: 215 additions & 12 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack-cli": patch
3+
---
4+
5+
Replace the `fastest-levenshtein` dependency with a small in-tree implementation used for command/option "did you mean" suggestions.

package-lock.json

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

packages/webpack-cli/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
"commander": "^14.0.3",
3636
"cross-spawn": "^7.0.6",
3737
"envinfo": "^7.14.0",
38-
"fastest-levenshtein": "^1.0.12",
3938
"import-local": "^3.0.2",
4039
"interpret": "^3.1.1",
4140
"rechoir": "^0.8.0",
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Levenshtein distance via Myers' bit-parallel algorithm.
2+
// Inspired by fastest-levenshtein (MIT, https://github.com/ka-weihe/fastest-levenshtein).
3+
4+
const peq = new Uint32Array(0x10000);
5+
6+
function myers32(a: string, b: string): number {
7+
const n = a.length;
8+
const m = b.length;
9+
const lst = 1 << (n - 1);
10+
let pv = -1;
11+
let mv = 0;
12+
let sc = n;
13+
let i = n;
14+
15+
while (i--) {
16+
peq[a.charCodeAt(i)] |= 1 << i;
17+
}
18+
19+
for (i = 0; i < m; i++) {
20+
let eq = peq[b.charCodeAt(i)];
21+
const xv = eq | mv;
22+
23+
eq |= ((eq & pv) + pv) ^ pv;
24+
mv |= ~(eq | pv);
25+
pv &= eq;
26+
27+
if (mv & lst) {
28+
sc++;
29+
}
30+
31+
if (pv & lst) {
32+
sc--;
33+
}
34+
35+
mv = (mv << 1) | 1;
36+
pv = (pv << 1) | ~(xv | mv);
37+
mv &= xv;
38+
}
39+
40+
i = n;
41+
42+
while (i--) {
43+
peq[a.charCodeAt(i)] = 0;
44+
}
45+
46+
return sc;
47+
}
48+
49+
function myersX(longer: string, shorter: string): number {
50+
const n = shorter.length;
51+
const m = longer.length;
52+
const mhc: number[] = [];
53+
const phc: number[] = [];
54+
const horizontalSize = Math.ceil(n / 32);
55+
const verticalSize = Math.ceil(m / 32);
56+
57+
for (let i = 0; i < horizontalSize; i++) {
58+
phc[i] = -1;
59+
mhc[i] = 0;
60+
}
61+
62+
let j = 0;
63+
64+
for (; j < verticalSize - 1; j++) {
65+
let mv = 0;
66+
let pv = -1;
67+
const start = j * 32;
68+
const verticalLen = Math.min(32, m) + start;
69+
70+
for (let k = start; k < verticalLen; k++) {
71+
peq[longer.charCodeAt(k)] |= 1 << k;
72+
}
73+
74+
for (let i = 0; i < n; i++) {
75+
const eq = peq[shorter.charCodeAt(i)];
76+
const pb = (phc[(i / 32) | 0] >>> i) & 1;
77+
const mb = (mhc[(i / 32) | 0] >>> i) & 1;
78+
const xv = eq | mv;
79+
const xh = ((((eq | mb) & pv) + pv) ^ pv) | eq | mb;
80+
let ph = mv | ~(xh | pv);
81+
let mh = pv & xh;
82+
83+
if ((ph >>> 31) ^ pb) {
84+
phc[(i / 32) | 0] ^= 1 << i;
85+
}
86+
87+
if ((mh >>> 31) ^ mb) {
88+
mhc[(i / 32) | 0] ^= 1 << i;
89+
}
90+
91+
ph = (ph << 1) | pb;
92+
mh = (mh << 1) | mb;
93+
pv = mh | ~(xv | ph);
94+
mv = ph & xv;
95+
}
96+
97+
for (let k = start; k < verticalLen; k++) {
98+
peq[longer.charCodeAt(k)] = 0;
99+
}
100+
}
101+
102+
let mv = 0;
103+
let pv = -1;
104+
const start = j * 32;
105+
const verticalLen = Math.min(32, m - start) + start;
106+
107+
for (let k = start; k < verticalLen; k++) {
108+
peq[longer.charCodeAt(k)] |= 1 << k;
109+
}
110+
111+
let score = m;
112+
113+
for (let i = 0; i < n; i++) {
114+
const eq = peq[shorter.charCodeAt(i)];
115+
const pb = (phc[(i / 32) | 0] >>> i) & 1;
116+
const mb = (mhc[(i / 32) | 0] >>> i) & 1;
117+
const xv = eq | mv;
118+
const xh = ((((eq | mb) & pv) + pv) ^ pv) | eq | mb;
119+
let ph = mv | ~(xh | pv);
120+
let mh = pv & xh;
121+
122+
score += (ph >>> (m - 1)) & 1;
123+
score -= (mh >>> (m - 1)) & 1;
124+
125+
if ((ph >>> 31) ^ pb) {
126+
phc[(i / 32) | 0] ^= 1 << i;
127+
}
128+
129+
if ((mh >>> 31) ^ mb) {
130+
mhc[(i / 32) | 0] ^= 1 << i;
131+
}
132+
133+
ph = (ph << 1) | pb;
134+
mh = (mh << 1) | mb;
135+
pv = mh | ~(xv | ph);
136+
mv = ph & xv;
137+
}
138+
139+
for (let k = start; k < verticalLen; k++) {
140+
peq[longer.charCodeAt(k)] = 0;
141+
}
142+
143+
return score;
144+
}
145+
146+
/**
147+
* Returns the Levenshtein edit distance between two strings.
148+
*/
149+
export function distance(first: string, second: string): number {
150+
let a = first;
151+
let b = second;
152+
153+
if (a.length < b.length) {
154+
const tmp = b;
155+
156+
b = a;
157+
a = tmp;
158+
}
159+
160+
if (b.length === 0) {
161+
return a.length;
162+
}
163+
164+
return a.length <= 32 ? myers32(a, b) : myersX(a, b);
165+
}

packages/webpack-cli/src/webpack-cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
program,
1414
} from "commander";
1515
import { type Config as EnvinfoConfig, type Options as EnvinfoOptions } from "envinfo";
16-
import { distance } from "fastest-levenshtein";
1716
import { type prepare } from "rechoir";
1817
import {
1918
type Argument as WebpackArgument,
@@ -31,6 +30,7 @@ import {
3130
default as webpack,
3231
} from "webpack";
3332
import { type Configuration as DevServerConfiguration } from "webpack-dev-server";
33+
import { distance } from "./levenshtein.js";
3434

3535
const WEBPACK_PACKAGE_IS_CUSTOM = Boolean(process.env.WEBPACK_PACKAGE);
3636
const WEBPACK_PACKAGE = WEBPACK_PACKAGE_IS_CUSTOM

test/api/levenshtein.test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const { distance } = require("../../packages/webpack-cli/lib/levenshtein");
2+
3+
describe("distance", () => {
4+
it("should return 0 for equal strings", () => {
5+
expect(distance("", "")).toBe(0);
6+
expect(distance("webpack", "webpack")).toBe(0);
7+
});
8+
9+
it("should return the length of the other string when one is empty", () => {
10+
expect(distance("", "abc")).toBe(3);
11+
expect(distance("abc", "")).toBe(3);
12+
});
13+
14+
it("should not depend on argument order", () => {
15+
expect(distance("kitten", "sitting")).toBe(distance("sitting", "kitten"));
16+
});
17+
18+
it("should count single edits", () => {
19+
expect(distance("server", "serve")).toBe(1);
20+
expect(distance("test", "tests")).toBe(1);
21+
expect(distance("cat", "car")).toBe(1);
22+
});
23+
24+
it("should compute classic distances", () => {
25+
expect(distance("kitten", "sitting")).toBe(3);
26+
expect(distance("flying", "sailing")).toBe(4);
27+
});
28+
29+
it("should handle strings longer than 32 characters", () => {
30+
const a = "a".repeat(40);
31+
const b = `${"a".repeat(39)}b`;
32+
33+
expect(distance(a, b)).toBe(1);
34+
expect(distance("a".repeat(40), "b".repeat(40))).toBe(40);
35+
});
36+
37+
it("should handle a long string against a much shorter one", () => {
38+
const long = "abcdefghijklmnopqrstuvwxyz0123456789ABCD";
39+
40+
expect(long).toHaveLength(40);
41+
expect(distance(long, "abcde")).toBe(35);
42+
expect(distance("abcde", long)).toBe(35);
43+
});
44+
});

0 commit comments

Comments
 (0)