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
5 changes: 5 additions & 0 deletions .changeset/replace-fastest-levenshtein.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"webpack-cli": patch
---

Replace the `fastest-levenshtein` dependency with a small in-tree implementation used for command/option "did you mean" suggestions.
10 changes: 0 additions & 10 deletions package-lock.json

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

1 change: 0 additions & 1 deletion packages/webpack-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"commander": "^14.0.3",
"cross-spawn": "^7.0.6",
"envinfo": "^7.14.0",
"fastest-levenshtein": "^1.0.12",
"import-local": "^3.0.2",
"interpret": "^3.1.1",
"rechoir": "^0.8.0",
Expand Down
165 changes: 165 additions & 0 deletions packages/webpack-cli/src/levenshtein.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Levenshtein distance via Myers' bit-parallel algorithm.
// Inspired by fastest-levenshtein (MIT, https://github.com/ka-weihe/fastest-levenshtein).

const peq = new Uint32Array(0x10000);

function myers32(a: string, b: string): number {
const n = a.length;
const m = b.length;
const lst = 1 << (n - 1);
let pv = -1;
let mv = 0;
let sc = n;
let i = n;

while (i--) {
peq[a.charCodeAt(i)] |= 1 << i;
}

for (i = 0; i < m; i++) {
let eq = peq[b.charCodeAt(i)];
const xv = eq | mv;

eq |= ((eq & pv) + pv) ^ pv;
mv |= ~(eq | pv);
pv &= eq;

if (mv & lst) {
sc++;
}

if (pv & lst) {
sc--;
}

mv = (mv << 1) | 1;
pv = (pv << 1) | ~(xv | mv);
mv &= xv;
}

i = n;

while (i--) {
peq[a.charCodeAt(i)] = 0;
}

return sc;
}

function myersX(longer: string, shorter: string): number {
const n = shorter.length;
const m = longer.length;
const mhc: number[] = [];
const phc: number[] = [];
const horizontalSize = Math.ceil(n / 32);
const verticalSize = Math.ceil(m / 32);

for (let i = 0; i < horizontalSize; i++) {
phc[i] = -1;
mhc[i] = 0;
}

let j = 0;

for (; j < verticalSize - 1; j++) {
let mv = 0;
let pv = -1;
const start = j * 32;
const verticalLen = Math.min(32, m) + start;

for (let k = start; k < verticalLen; k++) {
peq[longer.charCodeAt(k)] |= 1 << k;
}

for (let i = 0; i < n; i++) {
const eq = peq[shorter.charCodeAt(i)];
const pb = (phc[(i / 32) | 0] >>> i) & 1;
const mb = (mhc[(i / 32) | 0] >>> i) & 1;
const xv = eq | mv;
const xh = ((((eq | mb) & pv) + pv) ^ pv) | eq | mb;
let ph = mv | ~(xh | pv);
let mh = pv & xh;

if ((ph >>> 31) ^ pb) {
phc[(i / 32) | 0] ^= 1 << i;
}

if ((mh >>> 31) ^ mb) {
mhc[(i / 32) | 0] ^= 1 << i;
}

ph = (ph << 1) | pb;
mh = (mh << 1) | mb;
pv = mh | ~(xv | ph);
mv = ph & xv;
}

for (let k = start; k < verticalLen; k++) {
peq[longer.charCodeAt(k)] = 0;
}
}

let mv = 0;
let pv = -1;
const start = j * 32;
const verticalLen = Math.min(32, m - start) + start;

for (let k = start; k < verticalLen; k++) {
peq[longer.charCodeAt(k)] |= 1 << k;
}

let score = m;

for (let i = 0; i < n; i++) {
const eq = peq[shorter.charCodeAt(i)];
const pb = (phc[(i / 32) | 0] >>> i) & 1;
const mb = (mhc[(i / 32) | 0] >>> i) & 1;
const xv = eq | mv;
const xh = ((((eq | mb) & pv) + pv) ^ pv) | eq | mb;
let ph = mv | ~(xh | pv);
let mh = pv & xh;

score += (ph >>> (m - 1)) & 1;
score -= (mh >>> (m - 1)) & 1;

if ((ph >>> 31) ^ pb) {
phc[(i / 32) | 0] ^= 1 << i;
}

if ((mh >>> 31) ^ mb) {
mhc[(i / 32) | 0] ^= 1 << i;
}

ph = (ph << 1) | pb;
mh = (mh << 1) | mb;
pv = mh | ~(xv | ph);
mv = ph & xv;
}

for (let k = start; k < verticalLen; k++) {
peq[longer.charCodeAt(k)] = 0;
}

return score;
}

/**
* Returns the Levenshtein edit distance between two strings.
*/
export function distance(first: string, second: string): number {
let a = first;
let b = second;

if (a.length < b.length) {
const tmp = b;

b = a;
a = tmp;
}

if (b.length === 0) {
return a.length;
}

return a.length <= 32 ? myers32(a, b) : myersX(a, b);
}
2 changes: 1 addition & 1 deletion packages/webpack-cli/src/webpack-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
program,
} from "commander";
import { type Config as EnvinfoConfig, type Options as EnvinfoOptions } from "envinfo";
import { distance } from "fastest-levenshtein";
import { type prepare } from "rechoir";
import {
type Argument as WebpackArgument,
Expand All @@ -31,6 +30,7 @@ import {
default as webpack,
} from "webpack";
import { type Configuration as DevServerConfiguration } from "webpack-dev-server";
import { distance } from "./levenshtein.js";

const WEBPACK_PACKAGE_IS_CUSTOM = Boolean(process.env.WEBPACK_PACKAGE);
const WEBPACK_PACKAGE = WEBPACK_PACKAGE_IS_CUSTOM
Expand Down
44 changes: 44 additions & 0 deletions test/api/levenshtein.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const { distance } = require("../../packages/webpack-cli/lib/levenshtein");

describe("distance", () => {
it("should return 0 for equal strings", () => {
expect(distance("", "")).toBe(0);
expect(distance("webpack", "webpack")).toBe(0);
});

it("should return the length of the other string when one is empty", () => {
expect(distance("", "abc")).toBe(3);
expect(distance("abc", "")).toBe(3);
});

it("should not depend on argument order", () => {
expect(distance("kitten", "sitting")).toBe(distance("sitting", "kitten"));
});

it("should count single edits", () => {
expect(distance("server", "serve")).toBe(1);
expect(distance("test", "tests")).toBe(1);
expect(distance("cat", "car")).toBe(1);
});

it("should compute classic distances", () => {
expect(distance("kitten", "sitting")).toBe(3);
expect(distance("flying", "sailing")).toBe(4);
});

it("should handle strings longer than 32 characters", () => {
const a = "a".repeat(40);
const b = `${"a".repeat(39)}b`;

expect(distance(a, b)).toBe(1);
expect(distance("a".repeat(40), "b".repeat(40))).toBe(40);
Comment on lines +31 to +34
});

it("should handle a long string against a much shorter one", () => {
const long = "abcdefghijklmnopqrstuvwxyz0123456789ABCD";

expect(long).toHaveLength(40);
expect(distance(long, "abcde")).toBe(35);
expect(distance("abcde", long)).toBe(35);
});
});
Loading