diff --git a/client-src/index.js b/client-src/index.js index 106ab1f8e..16b78f0cd 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -1,10 +1,9 @@ /* global __resourceQuery, __webpack_public_path__ */ -import stripAnsi from "strip-ansi"; - import configureOverlay from "./overlay.js"; import applyUpdate from "./process-update.js"; import { log, setLogLevel } from "./utils/log.js"; +import stripAnsi from "./utils/strip-ansi.js"; /** @typedef {import("./utils/log.js").LogLevel} LogLevel */ diff --git a/client-src/utils/strip-ansi.js b/client-src/utils/strip-ansi.js new file mode 100644 index 000000000..721aa41ba --- /dev/null +++ b/client-src/utils/strip-ansi.js @@ -0,0 +1,23 @@ +// Matches ANSI escape sequences. Ported from ansi-regex +// (https://github.com/chalk/ansi-regex), inlined so the browser client does +// not need the strip-ansi dependency. + +// Valid string terminator sequences are BEL, ESC\, and 0x9c +const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)"; + +// OSC sequences only: ESC ] ... ST (non-greedy until the first ST) +const OSC = `(?:\\u001B\\][\\s\\S]*?${ST})`; + +// CSI and related: ESC/C1, optional intermediates, optional params (supports ; and :) then final byte +const CSI = + "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]"; + +const ANSI_REGEX = new RegExp(`${OSC}|${CSI}`, "g"); + +/** + * @param {string} string string possibly containing ANSI escape sequences + * @returns {string} the string without ANSI escape sequences + */ +export default function stripAnsi(string) { + return string.replace(ANSI_REGEX, ""); +} diff --git a/cspell.config.json b/cspell.config.json index 46f70162e..4c86561d5 100644 --- a/cspell.config.json +++ b/cspell.config.json @@ -16,6 +16,8 @@ "eslintcache", "esmodules", "noopener", - "noreferrer" + "noreferrer", + "mred", + "mbold" ] } diff --git a/package-lock.json b/package-lock.json index 3a4ce53a4..f3bcd0f91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,7 @@ "mime-types": "^3.0.2", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "schema-utils": "^4.3.3", - "strip-ansi": "^6.0.1" + "schema-utils": "^4.3.3" }, "devDependencies": { "@babel/cli": "^7.16.7", @@ -17772,6 +17771,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -17808,6 +17808,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" diff --git a/package.json b/package.json index ab9d21e10..300d1d50e 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,7 @@ "mime-types": "^3.0.2", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "schema-utils": "^4.3.3", - "strip-ansi": "^6.0.1" + "schema-utils": "^4.3.3" }, "devDependencies": { "@babel/cli": "^7.16.7", diff --git a/test/strip-ansi.test.js b/test/strip-ansi.test.js new file mode 100644 index 000000000..64421fe5c --- /dev/null +++ b/test/strip-ansi.test.js @@ -0,0 +1,40 @@ +import stripAnsi from "../client-src/utils/strip-ansi"; + +const ESC = String.fromCharCode(27); +const BEL = String.fromCharCode(7); + +describe("stripAnsi", () => { + it("strips color sequences", () => { + expect(stripAnsi(`${ESC}[31mred${ESC}[39m plain`)).toBe("red plain"); + }); + + it("strips style and background sequences", () => { + expect(stripAnsi(`${ESC}[1m${ESC}[42mbold-bg${ESC}[0m`)).toBe("bold-bg"); + }); + + it("strips 256-color sequences", () => { + expect(stripAnsi(`${ESC}[38;5;196mred${ESC}[0m`)).toBe("red"); + }); + + it("strips cursor and erase sequences", () => { + expect(stripAnsi(`${ESC}[2K${ESC}[1A${ESC}[2K${ESC}[G`)).toBe(""); + }); + + it("strips terminal hyperlinks", () => { + expect( + stripAnsi(`${ESC}]8;;https://example.com${BEL}link${ESC}]8;;${BEL}`), + ).toBe("link"); + }); + + it("keeps plain strings untouched", () => { + expect(stripAnsi("no ansi at all")).toBe("no ansi at all"); + }); + + it("strips ansi from a webpack-style error line", () => { + expect( + stripAnsi( + `webpack ${ESC}[1m${ESC}[31mERROR${ESC}[39m${ESC}[22m in ./src/index.js 3:10`, + ), + ).toBe("webpack ERROR in ./src/index.js 3:10"); + }); +});