feat: added colors helpers for CLI
Github Actions / lint (push) Waiting to run Details
Github Actions / validate-legacy-node (push) Waiting to run Details
Github Actions / benchmark (1/4) (push) Waiting to run Details
Github Actions / benchmark (2/4) (push) Waiting to run Details
Github Actions / benchmark (3/4) (push) Waiting to run Details
Github Actions / benchmark (4/4) (push) Waiting to run Details
Github Actions / basic (push) Waiting to run Details
Github Actions / unit (push) Waiting to run Details
Github Actions / integration (10.x, macos-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (10.x, macos-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (10.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (10.x, ubuntu-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (10.x, windows-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (10.x, windows-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (12.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (14.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (16.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (18.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (20.x, macos-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (20.x, macos-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (20.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (20.x, ubuntu-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (20.x, windows-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (20.x, windows-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (22.x, macos-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (22.x, macos-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (22.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (22.x, ubuntu-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (22.x, windows-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (22.x, windows-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (24.x, macos-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (24.x, macos-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (24.x, ubuntu-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (24.x, ubuntu-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (24.x, windows-latest, a) (push) Blocked by required conditions Details
Github Actions / integration (24.x, windows-latest, b) (push) Blocked by required conditions Details
Github Actions / integration (lts/*, ubuntu-latest, a, 1) (push) Blocked by required conditions Details
Github Actions / integration (lts/*, ubuntu-latest, b, 1) (push) Blocked by required conditions Details

This commit is contained in:
Alexander Akait 2025-07-17 16:16:32 +03:00 committed by GitHub
parent 3e5edf08f1
commit fbe43932ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 588 additions and 185 deletions

View File

@ -6,6 +6,7 @@
"use strict";
const path = require("path");
const tty = require("tty");
const webpackSchema = require("../schemas/WebpackOptions.json");
/** @typedef {import("json-schema").JSONSchema4} JSONSchema4 */
@ -712,5 +713,181 @@ const processArguments = (args, config, values) => {
return problems;
};
/**
* @returns {boolean} true when colors supported, otherwise false
*/
const isColorSupported = () => {
const { env = {}, argv = [], platform = "" } = process;
const isDisabled = "NO_COLOR" in env || argv.includes("--no-color");
const isForced = "FORCE_COLOR" in env || argv.includes("--color");
const isWindows = platform === "win32";
const isDumbTerminal = env.TERM === "dumb";
const isCompatibleTerminal = tty.isatty(1) && env.TERM && !isDumbTerminal;
const isCI =
"CI" in env &&
("GITHUB_ACTIONS" in env || "GITLAB_CI" in env || "CIRCLECI" in env);
return (
!isDisabled &&
(isForced || (isWindows && !isDumbTerminal) || isCompatibleTerminal || isCI)
);
};
/**
* @param {number} index index
* @param {string} string string
* @param {string} close close
* @param {string=} replace replace
* @param {string=} head head
* @param {string=} tail tail
* @param {number=} next next
* @returns {string} result
*/
const replaceClose = (
index,
string,
close,
replace,
head = string.slice(0, Math.max(0, index)) + replace,
tail = string.slice(Math.max(0, index + close.length)),
next = tail.indexOf(close)
) => head + (next < 0 ? tail : replaceClose(next, tail, close, replace));
/**
* @param {number} index index to replace
* @param {string} string string
* @param {string} open open string
* @param {string} close close string
* @param {string=} replace extra replace
* @returns {string} result
*/
const clearBleed = (index, string, open, close, replace) =>
index < 0
? open + string + close
: open + replaceClose(index, string, close, replace) + close;
/** @typedef {(value: EXPECTED_ANY) => string} PrintFunction */
/**
* @param {string} open open string
* @param {string} close close string
* @param {string=} replace extra replace
* @param {number=} at at
* @returns {PrintFunction} function to create color
*/
const filterEmpty =
(open, close, replace = open, at = open.length + 1) =>
(string) =>
string || !(string === "" || string === undefined)
? clearBleed(`${string}`.indexOf(close, at), string, open, close, replace)
: "";
/**
* @param {number} open open code
* @param {number} close close code
* @param {string=} replace extra replace
* @returns {PrintFunction} result
*/
const init = (open, close, replace) =>
filterEmpty(`\u001B[${open}m`, `\u001B[${close}m`, replace);
/**
* @typedef {{
* reset: PrintFunction
* bold: PrintFunction
* dim: PrintFunction
* italic: PrintFunction
* underline: PrintFunction
* inverse: PrintFunction
* hidden: PrintFunction
* strikethrough: PrintFunction
* black: PrintFunction
* red: PrintFunction
* green: PrintFunction
* yellow: PrintFunction
* blue: PrintFunction
* magenta: PrintFunction
* cyan: PrintFunction
* white: PrintFunction
* gray: PrintFunction
* bgBlack: PrintFunction
* bgRed: PrintFunction
* bgGreen: PrintFunction
* bgYellow: PrintFunction
* bgBlue: PrintFunction
* bgMagenta: PrintFunction
* bgCyan: PrintFunction
* bgWhite: PrintFunction
* blackBright: PrintFunction
* redBright: PrintFunction
* greenBright: PrintFunction
* yellowBright: PrintFunction
* blueBright: PrintFunction
* magentaBright: PrintFunction
* cyanBright: PrintFunction
* whiteBright: PrintFunction
* bgBlackBright: PrintFunction
* bgRedBright: PrintFunction
* bgGreenBright: PrintFunction
* bgYellowBright: PrintFunction
* bgBlueBright: PrintFunction
* bgMagentaBright: PrintFunction
* bgCyanBright: PrintFunction
* bgWhiteBright: PrintFunction
}} Colors */
/**
* @param {{ useColor?: boolean }=} options options
* @returns {Colors} colors
*/
const createColors = ({ useColor = isColorSupported() } = {}) => ({
reset: useColor ? init(0, 0) : String,
bold: useColor ? init(1, 22, "\u001B[22m\u001B[1m") : String,
dim: useColor ? init(2, 22, "\u001B[22m\u001B[2m") : String,
italic: useColor ? init(3, 23) : String,
underline: useColor ? init(4, 24) : String,
inverse: useColor ? init(7, 27) : String,
hidden: useColor ? init(8, 28) : String,
strikethrough: useColor ? init(9, 29) : String,
black: useColor ? init(30, 39) : String,
red: useColor ? init(31, 39) : String,
green: useColor ? init(32, 39) : String,
yellow: useColor ? init(33, 39) : String,
blue: useColor ? init(34, 39) : String,
magenta: useColor ? init(35, 39) : String,
cyan: useColor ? init(36, 39) : String,
white: useColor ? init(37, 39) : String,
gray: useColor ? init(90, 39) : String,
bgBlack: useColor ? init(40, 49) : String,
bgRed: useColor ? init(41, 49) : String,
bgGreen: useColor ? init(42, 49) : String,
bgYellow: useColor ? init(43, 49) : String,
bgBlue: useColor ? init(44, 49) : String,
bgMagenta: useColor ? init(45, 49) : String,
bgCyan: useColor ? init(46, 49) : String,
bgWhite: useColor ? init(47, 49) : String,
blackBright: useColor ? init(90, 39) : String,
redBright: useColor ? init(91, 39) : String,
greenBright: useColor ? init(92, 39) : String,
yellowBright: useColor ? init(93, 39) : String,
blueBright: useColor ? init(94, 39) : String,
magentaBright: useColor ? init(95, 39) : String,
cyanBright: useColor ? init(96, 39) : String,
whiteBright: useColor ? init(97, 39) : String,
bgBlackBright: useColor ? init(100, 49) : String,
bgRedBright: useColor ? init(101, 49) : String,
bgGreenBright: useColor ? init(102, 49) : String,
bgYellowBright: useColor ? init(103, 49) : String,
bgBlueBright: useColor ? init(104, 49) : String,
bgMagentaBright: useColor ? init(105, 49) : String,
bgCyanBright: useColor ? init(106, 49) : String,
bgWhiteBright: useColor ? init(107, 49) : String
});
module.exports.createColors = createColors;
module.exports.getArguments = getArguments;
module.exports.isColorSupported = isColorSupported;
module.exports.processArguments = processArguments;

View File

@ -133,7 +133,7 @@
"es6-promise-polyfill": "^1.2.0",
"eslint": "^9.29.0",
"eslint-config-prettier": "^10.1.1",
"eslint-config-webpack": "^4.3.0",
"eslint-config-webpack": "^4.4.1",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-jsdoc": "^51.2.3",

View File

@ -1,8 +1,10 @@
"use strict";
const { getArguments, processArguments } = require("../").cli;
const { createColors, getArguments, isColorSupported, processArguments } =
require("../").cli;
describe("Cli", () => {
describe("getArguments", () => {
it("should generate the correct cli flags", () => {
expect(getArguments()).toMatchSnapshot();
});
@ -50,7 +52,9 @@ describe("Cli", () => {
expect(getArguments(schema)).toMatchSnapshot();
});
});
describe("processArguments", () => {
const test = (name, values, config, fn) => {
it(`should correctly process arguments for ${name}`, () => {
const args = getArguments();
@ -417,3 +421,180 @@ describe("Cli", () => {
`)
);
});
describe("isColorSupported", () => {
const OLD_ENV = process.env;
beforeEach(() => {
// Most important - it clears the cache
jest.resetModules();
process.env = { ...OLD_ENV };
});
afterAll(() => {
process.env = OLD_ENV;
});
it("env NO_COLOR", () => {
process.env.NO_COLOR = "1";
expect(isColorSupported()).toBe(false);
});
it("env FORCE_COLOR", () => {
process.env.FORCE_COLOR = "1";
expect(isColorSupported()).toBe(true);
});
it("env TERM", () => {
const isCI =
"CI" in process.env &&
("GITHUB_ACTIONS" in process.env ||
"GITLAB_CI" in process.env ||
"CIRCLECI" in process.env);
process.env.TERM = "dumb";
expect(isColorSupported()).toBe(isCI);
});
it("env CI", () => {
process.env.CI = "1";
expect(isColorSupported()).toBe(true);
});
it("env GITHUB_ACTIONS", () => {
process.env.GITHUB_ACTIONS = "1";
expect(isColorSupported()).toBe(true);
});
it("env GITLAB_CI", () => {
process.env.GITLAB_CI = "1";
expect(isColorSupported()).toBe(true);
});
it("env CIRCLECI", () => {
process.env.CIRCLECI = "1";
expect(isColorSupported()).toBe(true);
});
});
describe("createColors", () => {
const colorsMap = [
["reset", "\u001B[0m", "\u001B[0m"],
["bold", "\u001B[1m", "\u001B[22m"],
["dim", "\u001B[2m", "\u001B[22m"],
["italic", "\u001B[3m", "\u001B[23m"],
["underline", "\u001B[4m", "\u001B[24m"],
["inverse", "\u001B[7m", "\u001B[27m"],
["hidden", "\u001B[8m", "\u001B[28m"],
["strikethrough", "\u001B[9m", "\u001B[29m"],
["black", "\u001B[30m", "\u001B[39m"],
["red", "\u001B[31m", "\u001B[39m"],
["green", "\u001B[32m", "\u001B[39m"],
["yellow", "\u001B[33m", "\u001B[39m"],
["blue", "\u001B[34m", "\u001B[39m"],
["magenta", "\u001B[35m", "\u001B[39m"],
["cyan", "\u001B[36m", "\u001B[39m"],
["white", "\u001B[37m", "\u001B[39m"],
["gray", "\u001B[90m", "\u001B[39m"],
["bgBlack", "\u001B[40m", "\u001B[49m"],
["bgRed", "\u001B[41m", "\u001B[49m"],
["bgGreen", "\u001B[42m", "\u001B[49m"],
["bgYellow", "\u001B[43m", "\u001B[49m"],
["bgBlue", "\u001B[44m", "\u001B[49m"],
["bgMagenta", "\u001B[45m", "\u001B[49m"],
["bgCyan", "\u001B[46m", "\u001B[49m"],
["bgWhite", "\u001B[47m", "\u001B[49m"],
["blackBright", "\u001B[90m", "\u001B[39m"],
["redBright", "\u001B[91m", "\u001B[39m"],
["greenBright", "\u001B[92m", "\u001B[39m"],
["yellowBright", "\u001B[93m", "\u001B[39m"],
["blueBright", "\u001B[94m", "\u001B[39m"],
["magentaBright", "\u001B[95m", "\u001B[39m"],
["cyanBright", "\u001B[96m", "\u001B[39m"],
["whiteBright", "\u001B[97m", "\u001B[39m"],
["bgBlackBright", "\u001B[100m", "\u001B[49m"],
["bgRedBright", "\u001B[101m", "\u001B[49m"],
["bgGreenBright", "\u001B[102m", "\u001B[49m"],
["bgYellowBright", "\u001B[103m", "\u001B[49m"],
["bgBlueBright", "\u001B[104m", "\u001B[49m"],
["bgMagentaBright", "\u001B[105m", "\u001B[49m"],
["bgCyanBright", "\u001B[106m", "\u001B[49m"],
["bgWhiteBright", "\u001B[107m", "\u001B[49m"]
];
const colors = createColors({ useColor: true });
it("simple", () => {
for (const [name, open, close] of colorsMap) {
expect(colors[name](name)).toBe(open + name + close);
}
});
it("nesting", () => {
expect(
colors.bold(`bold ${colors.red(`red ${colors.dim("dim")} red`)} bold`)
).toBe(
/* cspell:disable-next-line */
"\u001B[1mbold \u001B[31mred \u001B[2mdim\u001B[22m\u001B[1m red\u001B[39m bold\u001B[22m"
);
expect(
colors.magenta(
`magenta ${colors.yellow(
`yellow ${colors.cyan("cyan")} ${colors.red("red")} ${colors.green(
"green"
)} yellow`
)} magenta`
)
).toBe(
/* cspell:disable-next-line */
"\u001B[35mmagenta \u001B[33myellow \u001B[36mcyan\u001B[33m \u001B[31mred\u001B[33m \u001B[32mgreen\u001B[33m yellow\u001B[35m magenta\u001B[39m"
);
});
it("numbers & others", () => {
for (const n of [new Date(), -1e10, -1, -0.1, 0, 0.1, 1, 1e10]) {
expect(colors.red(n)).toBe(`\u001B[31m${n}\u001B[39m`);
}
});
it("empty & falsy values", () => {
expect(colors.blue()).toBe("");
expect(colors.blue("")).toBe("");
expect(colors.blue(undefined)).toBe("");
expect(colors.blue(0)).toBe("\u001B[34m0\u001B[39m");
// eslint-disable-next-line unicorn/prefer-number-properties
expect(colors.blue(NaN)).toBe("\u001B[34mNaN\u001B[39m");
expect(colors.blue(Number.NaN)).toBe("\u001B[34mNaN\u001B[39m");
/* cspell:disable-next-line */
expect(colors.blue(null)).toBe("\u001B[34mnull\u001B[39m");
/* cspell:disable-next-line */
expect(colors.blue(true)).toBe("\u001B[34mtrue\u001B[39m");
/* cspell:disable-next-line */
expect(colors.blue(false)).toBe("\u001B[34mfalse\u001B[39m");
expect(colors.blue(Infinity)).toBe("\u001B[34mInfinity\u001B[39m");
});
const noColors = createColors({ useColor: false });
it("simple (no colors)", () => {
for (const [name] of colorsMap) {
expect(noColors[name](name)).toBe(name);
}
});
const defaultColors = createColors();
it("simple (colors by default)", () => {
for (const [name, open, close] of colorsMap) {
expect(defaultColors[name](name)).toBe(open + name + close);
}
});
});
});

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Cli should generate the correct cli flags 1`] = `
exports[`Cli getArguments should generate the correct cli flags 1`] = `
Object {
"amd": Object {
"configs": Array [
@ -10385,7 +10385,7 @@ Object {
}
`;
exports[`Cli should generate the correct cli flags with custom schema 1`] = `
exports[`Cli getArguments should generate the correct cli flags with custom schema 1`] = `
Object {
"with-both-cli-and-negative-description": Object {
"configs": Array [

45
types.d.ts vendored
View File

@ -1951,6 +1951,49 @@ type CodeValuePrimitive =
| boolean
| Function
| RegExp;
declare interface Colors {
reset: (value?: any) => string;
bold: (value?: any) => string;
dim: (value?: any) => string;
italic: (value?: any) => string;
underline: (value?: any) => string;
inverse: (value?: any) => string;
hidden: (value?: any) => string;
strikethrough: (value?: any) => string;
black: (value?: any) => string;
red: (value?: any) => string;
green: (value?: any) => string;
yellow: (value?: any) => string;
blue: (value?: any) => string;
magenta: (value?: any) => string;
cyan: (value?: any) => string;
white: (value?: any) => string;
gray: (value?: any) => string;
bgBlack: (value?: any) => string;
bgRed: (value?: any) => string;
bgGreen: (value?: any) => string;
bgYellow: (value?: any) => string;
bgBlue: (value?: any) => string;
bgMagenta: (value?: any) => string;
bgCyan: (value?: any) => string;
bgWhite: (value?: any) => string;
blackBright: (value?: any) => string;
redBright: (value?: any) => string;
greenBright: (value?: any) => string;
yellowBright: (value?: any) => string;
blueBright: (value?: any) => string;
magentaBright: (value?: any) => string;
cyanBright: (value?: any) => string;
whiteBright: (value?: any) => string;
bgBlackBright: (value?: any) => string;
bgRedBright: (value?: any) => string;
bgGreenBright: (value?: any) => string;
bgYellowBright: (value?: any) => string;
bgBlueBright: (value?: any) => string;
bgMagentaBright: (value?: any) => string;
bgCyanBright: (value?: any) => string;
bgWhiteBright: (value?: any) => string;
}
declare interface Comparator<T> {
(a: T, b: T): 0 | 1 | -1;
}
@ -17412,6 +17455,7 @@ declare namespace exports {
) => void;
export const version: string;
export namespace cli {
export let createColors: (__0?: { useColor?: boolean }) => Colors;
export let getArguments: (
schema?:
| (JSONSchema4 & {
@ -17448,6 +17492,7 @@ declare namespace exports {
};
})
) => Flags;
export let isColorSupported: () => boolean;
export let processArguments: (
args: Flags,
config: ObjectConfiguration,

View File

@ -3319,10 +3319,10 @@ eslint-config-prettier@^10.1.1:
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782"
integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==
eslint-config-webpack@^4.3.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/eslint-config-webpack/-/eslint-config-webpack-4.4.0.tgz#56fae0c217c1226b3ce9f3e00f4ce62d8f1cc6b1"
integrity sha512-W0hMYVayDR4Sk+owcKtJDNEoiFDTHNzQJk/wnIBOxh6xjgOVj9MnHPtIP6AB3Ru2Suc+T8juIjfxyn3vuM0ptg==
eslint-config-webpack@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/eslint-config-webpack/-/eslint-config-webpack-4.4.1.tgz#cfcb77c3295c8f1c3fcbd523d71e2ccc7092e16a"
integrity sha512-IPerJYT5ErPUbrVUCNVQF5RmCUrnA1Am8D1wJufetmEu4hsZXzigy4wP6uroLv8s9GBpiEPM5NZ0PHmao4tUMw==
dependencies:
detect-indent "^7.0.1"
jsonc-eslint-parser "^2.4.0"