Visitar URL original
Improve performance · chalk/chalk@5729845 · GitHub
Skip to content

Commit 5729845

Browse files
committed
Improve performance
1 parent 4c304dd commit 5729845

10 files changed

Lines changed: 177 additions & 68 deletions

File tree

‎readme.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ const customChalk = new Chalk({level: 0});
140140
| `2` | 256 color support |
141141
| `3` | Truecolor support (16 million colors) |
142142

143+
Both the `level` option and the `level` property throw for anything that is not an integer from 0 to 3. Omit the option, or pass `undefined`, to have the level detected instead.
144+
143145
### supportsColor
144146

145147
Detect whether the terminal [supports color](https://github.com/chalk/supports-color). Used internally and handled for you, but exposed for convenience.

‎source/index.d.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ export interface Options {
2020
- `1` - Basic 16 colors support.
2121
- `2` - ANSI 256 colors support.
2222
- `3` - Truecolor 16 million colors support.
23+
24+
Omit this option, or pass `undefined`, to have the level detected instead.
25+
26+
@throws If the value is neither `undefined` nor an integer from 0 to 3.
2327
*/
24-
readonly level?: ColorSupportLevel;
28+
readonly level?: ColorSupportLevel | undefined;
2529
}
2630

2731
/**
@@ -42,6 +46,8 @@ export interface ChalkInstance {
4246
- `1` - Basic 16 colors support.
4347
- `2` - ANSI 256 colors support.
4448
- `3` - Truecolor 16 million colors support.
49+
50+
@throws If the assigned value is not an integer from 0 to 3.
4551
*/
4652
level: ColorSupportLevel;
4753

‎source/index.js‎

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,36 @@ const {stdout: stdoutColor, stderr: stderrColor} = supportsColor;
1010
const GENERATOR = Symbol('GENERATOR');
1111
const STYLER = Symbol('STYLER');
1212
const IS_EMPTY = Symbol('IS_EMPTY');
13-
14-
// `supportsColor.level` → `ansiStyles.color[name]` mapping
15-
const levelMapping = [
16-
'ansi',
17-
'ansi',
18-
'ansi256',
19-
'ansi16m',
20-
];
13+
const LEVEL = Symbol('LEVEL');
2114

2215
const styles = Object.create(null);
2316

17+
const assertValidLevel = level => {
18+
if (!Number.isSafeInteger(level) || level < 0 || level > 3) {
19+
throw new Error('The `level` should be an integer from 0 to 3');
20+
}
21+
};
22+
23+
// The level is stored under a symbol so the hot path can read it as a plain property, while `level` itself is an accessor that rejects values the rest of the code could not handle.
24+
const levelDescriptor = {
25+
enumerable: true,
26+
get() {
27+
return this[LEVEL];
28+
},
29+
set(level) {
30+
assertValidLevel(level);
31+
this[LEVEL] = level;
32+
},
33+
};
34+
2435
const applyOptions = (object, options = {}) => {
25-
if (options.level && !(Number.isSafeInteger(options.level) && options.level >= 0 && options.level <= 3)) {
26-
throw new Error('The `level` option should be an integer from 0 to 3');
36+
if (options.level !== undefined) {
37+
assertValidLevel(options.level);
2738
}
2839

29-
// Detect level if not set manually
40+
// Detect level if not set manually. Written under the symbol rather than through `level`, as the prototype carrying that accessor is not installed until after this runs.
3041
const colorLevel = stdoutColor ? stdoutColor.level : 0;
31-
object.level = options.level === undefined ? colorLevel : options.level;
42+
object[LEVEL] = options.level === undefined ? colorLevel : options.level;
3243
};
3344

3445
export class Chalk {
@@ -72,28 +83,25 @@ styles.visible = {
7283
},
7384
};
7485

75-
const getModelAnsi = (model, level, type, ...arguments_) => {
76-
if (model === 'rgb') {
77-
if (level === 'ansi16m') {
78-
return ansiStyles[type].ansi16m(...arguments_);
79-
}
86+
// Resolve a color model to one converter per `level`, so that a call only has to look up the converter for the current level instead of re-deciding the model and level every time.
87+
const createModelConverters = (model, type) => {
88+
const style = ansiStyles[type];
8089

81-
if (level === 'ansi256') {
82-
return ansiStyles[type].ansi256(ansiStyles.rgbToAnsi256(...arguments_));
83-
}
84-
85-
return ansiStyles[type].ansi(ansiStyles.rgbToAnsi(...arguments_));
90+
if (model === 'rgb') {
91+
const ansi = (red, green, blue) => style.ansi(ansiStyles.rgbToAnsi(red, green, blue));
92+
const ansi256 = (red, green, blue) => style.ansi256(ansiStyles.rgbToAnsi256(red, green, blue));
93+
return [ansi, ansi, ansi256, style.ansi16m];
8694
}
8795

8896
if (model === 'hex') {
89-
return getModelAnsi('rgb', level, type, ...ansiStyles.hexToRgb(...arguments_));
97+
const ansi = hex => style.ansi(ansiStyles.hexToAnsi(hex));
98+
const ansi256 = hex => style.ansi256(ansiStyles.hexToAnsi256(hex));
99+
return [ansi, ansi, ansi256, hex => style.ansi16m(...ansiStyles.hexToRgb(hex))];
90100
}
91101

92-
if (model === 'ansi256' && level === 'ansi') {
93-
return ansiStyles[type].ansi(ansiStyles.ansi256ToAnsi(...arguments_));
94-
}
95-
96-
return ansiStyles[type][model](...arguments_);
102+
// `ansi256` is already the native form, so only the 16-color levels need converting.
103+
const ansi = code => style.ansi(ansiStyles.ansi256ToAnsi(code));
104+
return [ansi, ansi, style.ansi256, style.ansi256];
97105
};
98106

99107
const usedModels = ['rgb', 'hex', 'ansi256'];
@@ -107,13 +115,19 @@ for (const model of usedModels) {
107115
['underline' + capitalizedModel, 'underlineColor'],
108116
]) {
109117
const {close} = ansiStyles[type];
118+
const converters = createModelConverters(model, type);
119+
110120
styles[styleName] = {
111121
get() {
112-
const {level} = this;
113-
return function (...arguments_) {
114-
const styler = createStyler(getModelAnsi(model, levelMapping[level], type, ...arguments_), close, this[STYLER]);
115-
return createBuilder(this, styler, this[IS_EMPTY]);
122+
// The level is read on call rather than captured here so the function can be cached on the instance instead of being reallocated on every property access.
123+
// `rgb` is the widest model, so naming the three parameters avoids a rest array.
124+
const styleFunction = function (first, second, third) {
125+
const open = converters[this.level](first, second, third);
126+
return createBuilder(this, createStyler(open, close, this[STYLER]), this[IS_EMPTY]);
116127
};
128+
129+
Object.defineProperty(this, styleName, {value: styleFunction});
130+
return styleFunction;
117131
},
118132
};
119133
}
@@ -174,7 +188,8 @@ const createBuilder = (self, _styler, _isEmpty) => {
174188
// no way to create a function with a different prototype
175189
Object.setPrototypeOf(builder, proto);
176190

177-
builder[GENERATOR] = self;
191+
// Point every builder at the root generator instead of its immediate parent, so reading the level costs one property load rather than walking a `level` getter per link of the chain.
192+
builder[GENERATOR] = self[GENERATOR] ?? self;
178193
builder[STYLER] = _styler;
179194
builder[IS_EMPTY] = _isEmpty;
180195

@@ -183,7 +198,7 @@ const createBuilder = (self, _styler, _isEmpty) => {
183198

184199
const applyStyle = (self, string) => {
185200
// Read the level directly off the generator to skip the `level` getter dispatch on this hot path
186-
if (self[GENERATOR].level <= 0 || !string) {
201+
if (self[GENERATOR][LEVEL] <= 0 || !string) {
187202
// eslint-disable-next-line unicorn/no-computed-property-existence-check -- Reads the boolean value, not a property existence check.
188203
return self[IS_EMPTY] ? '' : string;
189204
}
@@ -217,8 +232,9 @@ const applyStyle = (self, string) => {
217232
return openAll + string + closeAll;
218233
};
219234

235+
// `level` lives on the prototype rather than on each instance, so it costs nothing to construct an instance and matches how builders already expose it. It is inherited rather than own, so it does not show up in `Object.keys()`, same as for a builder.
220236
// eslint-disable-next-line unicorn/no-top-level-side-effects -- The style getters must be installed at module load.
221-
Object.defineProperties(createChalk.prototype, styles);
237+
Object.defineProperties(createChalk.prototype, {...styles, level: levelDescriptor});
222238

223239
const chalk = createChalk();
224240
export const chalkStderr = createChalk({level: stderrColor ? stderrColor.level : 0});

‎source/utilities.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// TODO: When targeting Node.js 16, use `String.prototype.replaceAll`.
2-
export function stringReplaceAll(string, substring, replacer) {
1+
// Note: Each match is kept and `postfix` is inserted after it. `String#replaceAll(substring, substring + postfix)` does the same, but it has to scan the replacement for `$` patterns and it is several times slower on the no-match path that most calls take.
2+
export function stringReplaceAll(string, substring, postfix) {
33
let index = string.indexOf(substring);
44
if (index === -1) {
55
return string;
@@ -9,7 +9,7 @@ export function stringReplaceAll(string, substring, replacer) {
99
let endIndex = 0;
1010
let returnValue = '';
1111
do {
12-
returnValue += string.slice(endIndex, index) + substring + replacer;
12+
returnValue += string.slice(endIndex, index) + substring + postfix;
1313
endIndex = index + substringLength;
1414
index = string.indexOf(substring, endIndex);
1515
} while (index !== -1);

‎source/vendor/ansi-styles/index.d.ts‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export interface CSPair { // eslint-disable-line @typescript-eslint/naming-convention
1+
export type CSPair = { // eslint-disable-line @typescript-eslint/naming-convention
22
/**
33
The ANSI terminal control sequence for starting this style.
44
*/
@@ -8,9 +8,9 @@ export interface CSPair { // eslint-disable-line @typescript-eslint/naming-conve
88
The ANSI terminal control sequence for ending this style.
99
*/
1010
readonly close: string;
11-
}
11+
};
1212

13-
export interface ColorBase {
13+
export type ColorBase = {
1414
/**
1515
The ANSI terminal control sequence for ending this color.
1616
*/
@@ -21,9 +21,9 @@ export interface ColorBase {
2121
ansi256(code: number): string;
2222

2323
ansi16m(red: number, green: number, blue: number): string;
24-
}
24+
};
2525

26-
export interface Modifier {
26+
export type Modifier = {
2727
/**
2828
Resets the current color chain.
2929
*/
@@ -90,9 +90,9 @@ export interface Modifier {
9090
Puts a horizontal line through the center of the text. (Not widely supported)
9191
*/
9292
readonly strikethrough: CSPair;
93-
}
93+
};
9494

95-
export interface ForegroundColor {
95+
export type ForegroundColor = {
9696
readonly black: CSPair;
9797
readonly red: CSPair;
9898
readonly green: CSPair;
@@ -120,9 +120,9 @@ export interface ForegroundColor {
120120
readonly cyanBright: CSPair;
121121
readonly magentaBright: CSPair;
122122
readonly whiteBright: CSPair;
123-
}
123+
};
124124

125-
export interface BackgroundColor {
125+
export type BackgroundColor = {
126126
readonly bgBlack: CSPair;
127127
readonly bgRed: CSPair;
128128
readonly bgGreen: CSPair;
@@ -150,9 +150,9 @@ export interface BackgroundColor {
150150
readonly bgCyanBright: CSPair;
151151
readonly bgMagentaBright: CSPair;
152152
readonly bgWhiteBright: CSPair;
153-
}
153+
};
154154

155-
export interface UnderlineColor {
155+
export type UnderlineColor = {
156156
readonly underlineBlack: CSPair;
157157
readonly underlineRed: CSPair;
158158
readonly underlineGreen: CSPair;
@@ -180,9 +180,9 @@ export interface UnderlineColor {
180180
readonly underlineCyanBright: CSPair;
181181
readonly underlineMagentaBright: CSPair;
182182
readonly underlineWhiteBright: CSPair;
183-
}
183+
};
184184

185-
export interface ConvertColor {
185+
export type ConvertColor = {
186186
/**
187187
Convert from the RGB color space to the ANSI 256 color space.
188188
@@ -228,7 +228,7 @@ export interface ConvertColor {
228228
@param hex - A hexadecimal string containing RGB data.
229229
*/
230230
hexToAnsi(hex: string): number;
231-
}
231+
};
232232

233233
/**
234234
Basic modifier names.

‎source/vendor/supports-color/browser.js‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ const level = (() => {
33
return 0;
44
}
55

6-
if (navigator.userAgentData) {
7-
const brand = navigator.userAgentData.brands.find(({brand}) => brand === 'Chromium');
8-
if (brand && brand.version > 93) {
6+
if (globalThis.navigator.userAgentData) {
7+
const brand = globalThis.navigator.userAgentData.brands.find(({brand}) => brand === 'Chromium');
8+
if (brand?.version > 93) {
99
return 3;
1010
}
1111
}
1212

13-
if (/\b(?:Chrome|Chromium)\//.test(navigator.userAgent)) {
13+
if (/\b(?:Chrome|Chromium)\//.test(globalThis.navigator.userAgent)) {
1414
return 1;
1515
}
1616

‎source/vendor/supports-color/index.js‎

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,30 +30,29 @@ if (
3030
flagForceColor = 1;
3131
}
3232

33+
// Whether `FORCE_COLOR` names a level. Shared with the exact-level check below so that the two cannot disagree on what counts as numeric.
34+
function hasNumericForceColor() {
35+
return /^\d+$/.test(env.FORCE_COLOR);
36+
}
37+
3338
function envForceColor() {
3439
if (!('FORCE_COLOR' in env)) {
3540
return;
3641
}
3742

38-
if (env.FORCE_COLOR === 'true') {
39-
return 1;
40-
}
41-
4243
if (env.FORCE_COLOR === 'false') {
4344
return 0;
4445
}
4546

46-
if (env.FORCE_COLOR.length === 0) {
47+
if (env.FORCE_COLOR === 'true' || env.FORCE_COLOR.length === 0) {
4748
return 1;
4849
}
4950

50-
const level = Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
51-
52-
if (![0, 1, 2, 3].includes(level)) {
51+
if (!hasNumericForceColor()) {
5352
return;
5453
}
5554

56-
return level;
55+
return Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
5756
}
5857

5958
function translateLevel(level) {
@@ -94,8 +93,8 @@ function _supportsColor(haveStream, {streamIsTTY, sniffFlags = true} = {}) {
9493
}
9594

9695
// A numeric `FORCE_COLOR` requests an exact level, while `FORCE_COLOR=true` and `FORCE_COLOR=` only enable color and let the level be detected.
97-
if (noFlagForceColor !== undefined && !Number.isNaN(Number.parseInt(env.FORCE_COLOR, 10))) {
98-
return noFlagForceColor;
96+
if (forceColor !== undefined && hasNumericForceColor()) {
97+
return forceColor;
9998
}
10099

101100
// Check for Azure DevOps pipelines.

‎test/chalk.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,17 @@ test('line breaks should open and close colors with CRLF', t => {
102102
});
103103

104104
test('properly convert RGB to 16 colors on basic color terminals', t => {
105+
t.is(new Chalk({level: 1}).rgb(255, 0, 0)('hello'), '\u{1B}[91mhello\u{1B}[39m');
106+
t.is(new Chalk({level: 1}).bgRgb(255, 0, 0)('hello'), '\u{1B}[101mhello\u{1B}[49m');
105107
t.is(new Chalk({level: 1}).hex('#FF0000')('hello'), '\u{1B}[91mhello\u{1B}[39m');
106108
t.is(new Chalk({level: 1}).bgHex('#FF0000')('hello'), '\u{1B}[101mhello\u{1B}[49m');
107109
});
108110

109111
test('properly convert RGB to 256 colors on basic color terminals', t => {
112+
t.is(new Chalk({level: 2}).rgb(255, 0, 0)('hello'), '\u{1B}[38;5;196mhello\u{1B}[39m');
113+
t.is(new Chalk({level: 2}).bgRgb(255, 0, 0)('hello'), '\u{1B}[48;5;196mhello\u{1B}[49m');
114+
t.is(new Chalk({level: 3}).rgb(255, 0, 0)('hello'), '\u{1B}[38;2;255;0;0mhello\u{1B}[39m');
115+
t.is(new Chalk({level: 3}).bgRgb(255, 0, 0)('hello'), '\u{1B}[48;2;255;0;0mhello\u{1B}[49m');
110116
t.is(new Chalk({level: 2}).hex('#FF0000')('hello'), '\u{1B}[38;5;196mhello\u{1B}[39m');
111117
t.is(new Chalk({level: 2}).bgHex('#FF0000')('hello'), '\u{1B}[48;5;196mhello\u{1B}[49m');
112118
t.is(new Chalk({level: 3}).bgHex('#FF0000')('hello'), '\u{1B}[48;2;255;0;0mhello\u{1B}[49m');

‎test/force-color.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,16 @@ test('a non-numeric `FORCE_COLOR` is treated as unset, not as disabled', async t
6464
t.is(await detectLevel({FORCE_COLOR: 'unicorn', TF_BUILD: '1', AGENT_NAME: 'agent'}), '1');
6565
t.is(await detectLevel({FORCE_COLOR: '0', TF_BUILD: '1', AGENT_NAME: 'agent'}), '0');
6666
});
67+
68+
test('a partly numeric `FORCE_COLOR` is treated as unset, not as a level', async t => {
69+
for (const forceColor of [' 2', '2 ', '2abc', '+2', '1e1', '0x2']) {
70+
// eslint-disable-next-line no-await-in-loop
71+
const [piped, azure] = await Promise.all([
72+
detectLevel({FORCE_COLOR: forceColor, COLORTERM: 'truecolor'}),
73+
detectLevel({FORCE_COLOR: forceColor, TF_BUILD: '1', AGENT_NAME: 'agent'}),
74+
]);
75+
76+
t.is(piped, '0', `FORCE_COLOR=${forceColor}`);
77+
t.is(azure, '1', `FORCE_COLOR=${forceColor}`);
78+
}
79+
});

0 commit comments

Comments
 (0)