@zealakit/css-doctor-cli 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +362 -10
- package/README_CN.md +269 -10
- package/README_VI.md +277 -10
- package/dist/index.js +3107 -461
- package/dist/index.js.map +1 -1
- package/package.json +24 -2
package/dist/index.js
CHANGED
|
@@ -3052,80 +3052,6 @@ var require_commander = __commonJS({
|
|
|
3052
3052
|
}
|
|
3053
3053
|
});
|
|
3054
3054
|
|
|
3055
|
-
// ../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js
|
|
3056
|
-
var require_picocolors = __commonJS({
|
|
3057
|
-
"../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js"(exports, module) {
|
|
3058
|
-
"use strict";
|
|
3059
|
-
init_esm_shims();
|
|
3060
|
-
var p = process || {};
|
|
3061
|
-
var argv = p.argv || [];
|
|
3062
|
-
var env = p.env || {};
|
|
3063
|
-
var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
3064
|
-
var formatter = (open, close, replace = open) => (input) => {
|
|
3065
|
-
let string = "" + input, index = string.indexOf(close, open.length);
|
|
3066
|
-
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
3067
|
-
};
|
|
3068
|
-
var replaceClose = (string, close, replace, index) => {
|
|
3069
|
-
let result = "", cursor = 0;
|
|
3070
|
-
do {
|
|
3071
|
-
result += string.substring(cursor, index) + replace;
|
|
3072
|
-
cursor = index + close.length;
|
|
3073
|
-
index = string.indexOf(close, cursor);
|
|
3074
|
-
} while (~index);
|
|
3075
|
-
return result + string.substring(cursor);
|
|
3076
|
-
};
|
|
3077
|
-
var createColors = (enabled = isColorSupported) => {
|
|
3078
|
-
let f = enabled ? formatter : () => String;
|
|
3079
|
-
return {
|
|
3080
|
-
isColorSupported: enabled,
|
|
3081
|
-
reset: f("\x1B[0m", "\x1B[0m"),
|
|
3082
|
-
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
3083
|
-
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
3084
|
-
italic: f("\x1B[3m", "\x1B[23m"),
|
|
3085
|
-
underline: f("\x1B[4m", "\x1B[24m"),
|
|
3086
|
-
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
3087
|
-
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
3088
|
-
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
3089
|
-
black: f("\x1B[30m", "\x1B[39m"),
|
|
3090
|
-
red: f("\x1B[31m", "\x1B[39m"),
|
|
3091
|
-
green: f("\x1B[32m", "\x1B[39m"),
|
|
3092
|
-
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
3093
|
-
blue: f("\x1B[34m", "\x1B[39m"),
|
|
3094
|
-
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
3095
|
-
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
3096
|
-
white: f("\x1B[37m", "\x1B[39m"),
|
|
3097
|
-
gray: f("\x1B[90m", "\x1B[39m"),
|
|
3098
|
-
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
3099
|
-
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
3100
|
-
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
3101
|
-
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
3102
|
-
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
3103
|
-
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
3104
|
-
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
3105
|
-
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
3106
|
-
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
3107
|
-
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
3108
|
-
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
3109
|
-
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
3110
|
-
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
3111
|
-
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
3112
|
-
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
3113
|
-
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
3114
|
-
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
3115
|
-
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
3116
|
-
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
3117
|
-
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
3118
|
-
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
3119
|
-
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
3120
|
-
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
3121
|
-
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
3122
|
-
};
|
|
3123
|
-
};
|
|
3124
|
-
module.exports = createColors();
|
|
3125
|
-
module.exports.createColors = createColors;
|
|
3126
|
-
}
|
|
3127
|
-
});
|
|
3128
|
-
|
|
3129
3055
|
// ../../node_modules/.pnpm/fast-glob@3.3.3/node_modules/fast-glob/out/utils/array.js
|
|
3130
3056
|
var require_array = __commonJS({
|
|
3131
3057
|
"../../node_modules/.pnpm/fast-glob@3.3.3/node_modules/fast-glob/out/utils/array.js"(exports) {
|
|
@@ -3803,7 +3729,7 @@ var require_fill_range = __commonJS({
|
|
|
3803
3729
|
}
|
|
3804
3730
|
return options.stringify === true;
|
|
3805
3731
|
};
|
|
3806
|
-
var
|
|
3732
|
+
var pad4 = (input, maxLength, toNumber) => {
|
|
3807
3733
|
if (maxLength > 0) {
|
|
3808
3734
|
let dash = input[0] === "-" ? "-" : "";
|
|
3809
3735
|
if (dash) input = input.slice(1);
|
|
@@ -3905,7 +3831,7 @@ var require_fill_range = __commonJS({
|
|
|
3905
3831
|
if (options.toRegex === true && step > 1) {
|
|
3906
3832
|
push(a);
|
|
3907
3833
|
} else {
|
|
3908
|
-
range.push(
|
|
3834
|
+
range.push(pad4(format(a, index), maxLen, toNumber));
|
|
3909
3835
|
}
|
|
3910
3836
|
a = descending ? a - step : a + step;
|
|
3911
3837
|
index++;
|
|
@@ -6583,7 +6509,7 @@ var require_pattern = __commonJS({
|
|
|
6583
6509
|
const absolute = [];
|
|
6584
6510
|
const relative2 = [];
|
|
6585
6511
|
for (const pattern of patterns) {
|
|
6586
|
-
if (
|
|
6512
|
+
if (isAbsolute3(pattern)) {
|
|
6587
6513
|
absolute.push(pattern);
|
|
6588
6514
|
} else {
|
|
6589
6515
|
relative2.push(pattern);
|
|
@@ -6592,10 +6518,10 @@ var require_pattern = __commonJS({
|
|
|
6592
6518
|
return [absolute, relative2];
|
|
6593
6519
|
}
|
|
6594
6520
|
exports.partitionAbsoluteAndRelative = partitionAbsoluteAndRelative;
|
|
6595
|
-
function
|
|
6521
|
+
function isAbsolute3(pattern) {
|
|
6596
6522
|
return path2.isAbsolute(pattern);
|
|
6597
6523
|
}
|
|
6598
|
-
exports.isAbsolute =
|
|
6524
|
+
exports.isAbsolute = isAbsolute3;
|
|
6599
6525
|
}
|
|
6600
6526
|
});
|
|
6601
6527
|
|
|
@@ -6899,7 +6825,7 @@ var require_async = __commonJS({
|
|
|
6899
6825
|
callSuccessCallback(callback, lstat);
|
|
6900
6826
|
return;
|
|
6901
6827
|
}
|
|
6902
|
-
settings.fs.stat(path2, (statError,
|
|
6828
|
+
settings.fs.stat(path2, (statError, stat3) => {
|
|
6903
6829
|
if (statError !== null) {
|
|
6904
6830
|
if (settings.throwErrorOnBrokenSymbolicLink) {
|
|
6905
6831
|
callFailureCallback(callback, statError);
|
|
@@ -6909,9 +6835,9 @@ var require_async = __commonJS({
|
|
|
6909
6835
|
return;
|
|
6910
6836
|
}
|
|
6911
6837
|
if (settings.markSymbolicLink) {
|
|
6912
|
-
|
|
6838
|
+
stat3.isSymbolicLink = () => true;
|
|
6913
6839
|
}
|
|
6914
|
-
callSuccessCallback(callback,
|
|
6840
|
+
callSuccessCallback(callback, stat3);
|
|
6915
6841
|
});
|
|
6916
6842
|
});
|
|
6917
6843
|
}
|
|
@@ -6938,11 +6864,11 @@ var require_sync = __commonJS({
|
|
|
6938
6864
|
return lstat;
|
|
6939
6865
|
}
|
|
6940
6866
|
try {
|
|
6941
|
-
const
|
|
6867
|
+
const stat3 = settings.fs.statSync(path2);
|
|
6942
6868
|
if (settings.markSymbolicLink) {
|
|
6943
|
-
|
|
6869
|
+
stat3.isSymbolicLink = () => true;
|
|
6944
6870
|
}
|
|
6945
|
-
return
|
|
6871
|
+
return stat3;
|
|
6946
6872
|
} catch (error) {
|
|
6947
6873
|
if (!settings.throwErrorOnBrokenSymbolicLink) {
|
|
6948
6874
|
return lstat;
|
|
@@ -7012,14 +6938,14 @@ var require_out = __commonJS({
|
|
|
7012
6938
|
var sync = require_sync();
|
|
7013
6939
|
var settings_1 = require_settings();
|
|
7014
6940
|
exports.Settings = settings_1.default;
|
|
7015
|
-
function
|
|
6941
|
+
function stat3(path2, optionsOrSettingsOrCallback, callback) {
|
|
7016
6942
|
if (typeof optionsOrSettingsOrCallback === "function") {
|
|
7017
6943
|
async.read(path2, getSettings(), optionsOrSettingsOrCallback);
|
|
7018
6944
|
return;
|
|
7019
6945
|
}
|
|
7020
6946
|
async.read(path2, getSettings(optionsOrSettingsOrCallback), callback);
|
|
7021
6947
|
}
|
|
7022
|
-
exports.stat =
|
|
6948
|
+
exports.stat = stat3;
|
|
7023
6949
|
function statSync(path2, optionsOrSettings) {
|
|
7024
6950
|
const settings = getSettings(optionsOrSettings);
|
|
7025
6951
|
return sync.read(path2, settings);
|
|
@@ -7705,41 +7631,41 @@ var require_queue = __commonJS({
|
|
|
7705
7631
|
queue.drained = drained;
|
|
7706
7632
|
return queue;
|
|
7707
7633
|
function push(value) {
|
|
7708
|
-
var p = new Promise(function(
|
|
7634
|
+
var p = new Promise(function(resolve9, reject) {
|
|
7709
7635
|
pushCb(value, function(err, result) {
|
|
7710
7636
|
if (err) {
|
|
7711
7637
|
reject(err);
|
|
7712
7638
|
return;
|
|
7713
7639
|
}
|
|
7714
|
-
|
|
7640
|
+
resolve9(result);
|
|
7715
7641
|
});
|
|
7716
7642
|
});
|
|
7717
7643
|
p.catch(noop);
|
|
7718
7644
|
return p;
|
|
7719
7645
|
}
|
|
7720
7646
|
function unshift(value) {
|
|
7721
|
-
var p = new Promise(function(
|
|
7647
|
+
var p = new Promise(function(resolve9, reject) {
|
|
7722
7648
|
unshiftCb(value, function(err, result) {
|
|
7723
7649
|
if (err) {
|
|
7724
7650
|
reject(err);
|
|
7725
7651
|
return;
|
|
7726
7652
|
}
|
|
7727
|
-
|
|
7653
|
+
resolve9(result);
|
|
7728
7654
|
});
|
|
7729
7655
|
});
|
|
7730
7656
|
p.catch(noop);
|
|
7731
7657
|
return p;
|
|
7732
7658
|
}
|
|
7733
7659
|
function drained() {
|
|
7734
|
-
var p = new Promise(function(
|
|
7660
|
+
var p = new Promise(function(resolve9) {
|
|
7735
7661
|
process.nextTick(function() {
|
|
7736
7662
|
if (queue.idle()) {
|
|
7737
|
-
|
|
7663
|
+
resolve9();
|
|
7738
7664
|
} else {
|
|
7739
7665
|
var previousDrain = queue.drain;
|
|
7740
7666
|
queue.drain = function() {
|
|
7741
7667
|
if (typeof previousDrain === "function") previousDrain();
|
|
7742
|
-
|
|
7668
|
+
resolve9();
|
|
7743
7669
|
queue.drain = previousDrain;
|
|
7744
7670
|
};
|
|
7745
7671
|
}
|
|
@@ -8236,9 +8162,9 @@ var require_stream3 = __commonJS({
|
|
|
8236
8162
|
});
|
|
8237
8163
|
}
|
|
8238
8164
|
_getStat(filepath) {
|
|
8239
|
-
return new Promise((
|
|
8165
|
+
return new Promise((resolve9, reject) => {
|
|
8240
8166
|
this._stat(filepath, this._fsStatSettings, (error, stats) => {
|
|
8241
|
-
return error === null ?
|
|
8167
|
+
return error === null ? resolve9(stats) : reject(error);
|
|
8242
8168
|
});
|
|
8243
8169
|
});
|
|
8244
8170
|
}
|
|
@@ -8263,10 +8189,10 @@ var require_async5 = __commonJS({
|
|
|
8263
8189
|
this._readerStream = new stream_1.default(this._settings);
|
|
8264
8190
|
}
|
|
8265
8191
|
dynamic(root, options) {
|
|
8266
|
-
return new Promise((
|
|
8192
|
+
return new Promise((resolve9, reject) => {
|
|
8267
8193
|
this._walkAsync(root, options, (error, entries) => {
|
|
8268
8194
|
if (error === null) {
|
|
8269
|
-
|
|
8195
|
+
resolve9(entries);
|
|
8270
8196
|
} else {
|
|
8271
8197
|
reject(error);
|
|
8272
8198
|
}
|
|
@@ -8276,10 +8202,10 @@ var require_async5 = __commonJS({
|
|
|
8276
8202
|
async static(patterns, options) {
|
|
8277
8203
|
const entries = [];
|
|
8278
8204
|
const stream = this._readerStream.static(patterns, options);
|
|
8279
|
-
return new Promise((
|
|
8205
|
+
return new Promise((resolve9, reject) => {
|
|
8280
8206
|
stream.once("error", reject);
|
|
8281
8207
|
stream.on("data", (entry) => entries.push(entry));
|
|
8282
|
-
stream.once("end", () =>
|
|
8208
|
+
stream.once("end", () => resolve9(entries));
|
|
8283
8209
|
});
|
|
8284
8210
|
}
|
|
8285
8211
|
};
|
|
@@ -8949,6 +8875,80 @@ var require_out4 = __commonJS({
|
|
|
8949
8875
|
}
|
|
8950
8876
|
});
|
|
8951
8877
|
|
|
8878
|
+
// ../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js
|
|
8879
|
+
var require_picocolors = __commonJS({
|
|
8880
|
+
"../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js"(exports, module) {
|
|
8881
|
+
"use strict";
|
|
8882
|
+
init_esm_shims();
|
|
8883
|
+
var p = process || {};
|
|
8884
|
+
var argv = p.argv || [];
|
|
8885
|
+
var env = p.env || {};
|
|
8886
|
+
var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
8887
|
+
var formatter = (open, close, replace = open) => (input) => {
|
|
8888
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
8889
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
8890
|
+
};
|
|
8891
|
+
var replaceClose = (string, close, replace, index) => {
|
|
8892
|
+
let result = "", cursor = 0;
|
|
8893
|
+
do {
|
|
8894
|
+
result += string.substring(cursor, index) + replace;
|
|
8895
|
+
cursor = index + close.length;
|
|
8896
|
+
index = string.indexOf(close, cursor);
|
|
8897
|
+
} while (~index);
|
|
8898
|
+
return result + string.substring(cursor);
|
|
8899
|
+
};
|
|
8900
|
+
var createColors = (enabled = isColorSupported) => {
|
|
8901
|
+
let f = enabled ? formatter : () => String;
|
|
8902
|
+
return {
|
|
8903
|
+
isColorSupported: enabled,
|
|
8904
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
8905
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
8906
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
8907
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
8908
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
8909
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
8910
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
8911
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
8912
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
8913
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
8914
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
8915
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
8916
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
8917
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
8918
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
8919
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
8920
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
8921
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
8922
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
8923
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
8924
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
8925
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
8926
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
8927
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
8928
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
8929
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
8930
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
8931
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
8932
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
8933
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
8934
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
8935
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
8936
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
8937
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
8938
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
8939
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
8940
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
8941
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
8942
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
8943
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
8944
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
8945
|
+
};
|
|
8946
|
+
};
|
|
8947
|
+
module.exports = createColors();
|
|
8948
|
+
module.exports.createColors = createColors;
|
|
8949
|
+
}
|
|
8950
|
+
});
|
|
8951
|
+
|
|
8952
8952
|
// src/index.ts
|
|
8953
8953
|
init_esm_shims();
|
|
8954
8954
|
|
|
@@ -8973,8 +8973,30 @@ var {
|
|
|
8973
8973
|
// package.json
|
|
8974
8974
|
var package_default = {
|
|
8975
8975
|
name: "@zealakit/css-doctor-cli",
|
|
8976
|
-
version: "1.
|
|
8976
|
+
version: "1.1.0",
|
|
8977
8977
|
description: "Command line interface for css-doctor: CSS diagnostics from the terminal",
|
|
8978
|
+
keywords: [
|
|
8979
|
+
"css",
|
|
8980
|
+
"css-doctor",
|
|
8981
|
+
"cli",
|
|
8982
|
+
"command-line",
|
|
8983
|
+
"css-analysis",
|
|
8984
|
+
"css-optimizer",
|
|
8985
|
+
"dead-code-elimination",
|
|
8986
|
+
"unused-css",
|
|
8987
|
+
"purgecss",
|
|
8988
|
+
"css-health",
|
|
8989
|
+
"css-baseline",
|
|
8990
|
+
"css-regression",
|
|
8991
|
+
"css-splitting",
|
|
8992
|
+
"page-coverage",
|
|
8993
|
+
"devtools",
|
|
8994
|
+
"ci",
|
|
8995
|
+
"performance",
|
|
8996
|
+
"web-performance",
|
|
8997
|
+
"stylesheet",
|
|
8998
|
+
"frontend"
|
|
8999
|
+
],
|
|
8978
9000
|
author: "nqhed - Huy Edward Nguyen",
|
|
8979
9001
|
type: "module",
|
|
8980
9002
|
license: "MIT",
|
|
@@ -9027,11 +9049,9 @@ var package_default = {
|
|
|
9027
9049
|
homepage: "https://github.com/zealakit/css-doctor#readme"
|
|
9028
9050
|
};
|
|
9029
9051
|
|
|
9030
|
-
// src/commands/
|
|
9052
|
+
// src/commands/baseline.ts
|
|
9031
9053
|
init_esm_shims();
|
|
9032
|
-
import {
|
|
9033
|
-
import { performance } from "perf_hooks";
|
|
9034
|
-
import { resolve as resolve2 } from "path";
|
|
9054
|
+
import { resolve as resolve3 } from "path";
|
|
9035
9055
|
|
|
9036
9056
|
// src/formatters/json.ts
|
|
9037
9057
|
init_esm_shims();
|
|
@@ -9039,309 +9059,2734 @@ function formatJsonReport(report) {
|
|
|
9039
9059
|
return JSON.stringify(report, null, 2);
|
|
9040
9060
|
}
|
|
9041
9061
|
|
|
9042
|
-
// src/
|
|
9062
|
+
// src/utils/baseline-io.ts
|
|
9043
9063
|
init_esm_shims();
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9064
|
+
import { mkdir, readFile as readFile2, unlink, writeFile } from "fs/promises";
|
|
9065
|
+
import { dirname } from "path";
|
|
9066
|
+
|
|
9067
|
+
// src/utils/binding.ts
|
|
9068
|
+
init_esm_shims();
|
|
9069
|
+
import { createRequire } from "module";
|
|
9070
|
+
|
|
9071
|
+
// src/utils/file.ts
|
|
9072
|
+
init_esm_shims();
|
|
9073
|
+
var import_fast_glob = __toESM(require_out4(), 1);
|
|
9074
|
+
import { readFile, stat } from "fs/promises";
|
|
9075
|
+
import { isAbsolute, relative, resolve, sep } from "path";
|
|
9076
|
+
var DEFAULT_IGNORES = ["**/node_modules/**", "**/.git/**"];
|
|
9077
|
+
async function isFile(filePath) {
|
|
9078
|
+
try {
|
|
9079
|
+
return (await stat(filePath)).isFile();
|
|
9080
|
+
} catch {
|
|
9081
|
+
return false;
|
|
9082
|
+
}
|
|
9048
9083
|
}
|
|
9049
|
-
function
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
const separator = colors.dim("\u2500".repeat(64));
|
|
9055
|
-
lines.push(
|
|
9056
|
-
`${colors.bold(colors.cyan("css-doctor"))} ${colors.dim(`v${report.version}`)} ${colors.dim("\xB7")} ${colors.dim("check")}`
|
|
9057
|
-
);
|
|
9058
|
-
lines.push(
|
|
9059
|
-
`${colors.dim("patterns")} ${report.patterns.length > 0 ? report.patterns.join(", ") : "(none)"}`
|
|
9060
|
-
);
|
|
9061
|
-
lines.push(separator);
|
|
9062
|
-
if (report.errors.length > 0) {
|
|
9063
|
-
lines.push(colors.red(colors.bold("ERRORS")));
|
|
9064
|
-
for (const error of report.errors) {
|
|
9065
|
-
lines.push(` ${colors.red("\u2716")} ${error}`);
|
|
9066
|
-
}
|
|
9067
|
-
lines.push("");
|
|
9084
|
+
async function isDirectory(directoryPath) {
|
|
9085
|
+
try {
|
|
9086
|
+
return (await stat(directoryPath)).isDirectory();
|
|
9087
|
+
} catch {
|
|
9088
|
+
return false;
|
|
9068
9089
|
}
|
|
9069
|
-
|
|
9070
|
-
|
|
9090
|
+
}
|
|
9091
|
+
async function resolveCssFiles(patterns, cwd) {
|
|
9092
|
+
const files = /* @__PURE__ */ new Set();
|
|
9093
|
+
const globPatterns = [];
|
|
9094
|
+
for (const pattern of patterns) {
|
|
9095
|
+
const absolutePath = isAbsolute(pattern) ? pattern : resolve(cwd, pattern);
|
|
9096
|
+
if (await isFile(absolutePath)) {
|
|
9097
|
+
files.add(absolutePath);
|
|
9098
|
+
} else {
|
|
9099
|
+
globPatterns.push(pattern);
|
|
9100
|
+
}
|
|
9071
9101
|
}
|
|
9072
|
-
if (
|
|
9073
|
-
|
|
9074
|
-
|
|
9102
|
+
if (globPatterns.length > 0) {
|
|
9103
|
+
const matches = await (0, import_fast_glob.default)(globPatterns, {
|
|
9104
|
+
cwd,
|
|
9105
|
+
absolute: true,
|
|
9106
|
+
onlyFiles: true,
|
|
9107
|
+
unique: true,
|
|
9108
|
+
dot: false,
|
|
9109
|
+
followSymbolicLinks: true,
|
|
9110
|
+
ignore: DEFAULT_IGNORES
|
|
9111
|
+
});
|
|
9112
|
+
for (const match of matches) {
|
|
9113
|
+
files.add(resolve(match));
|
|
9114
|
+
}
|
|
9075
9115
|
}
|
|
9076
|
-
|
|
9077
|
-
lines.push(...formatSummary(report, colors));
|
|
9078
|
-
return lines.join("\n");
|
|
9116
|
+
return [...files].sort((left, right) => left.localeCompare(right));
|
|
9079
9117
|
}
|
|
9080
|
-
function
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
return
|
|
9118
|
+
async function readCssFile(filePath) {
|
|
9119
|
+
try {
|
|
9120
|
+
const buffer = await readFile(filePath);
|
|
9121
|
+
return {
|
|
9122
|
+
ok: true,
|
|
9123
|
+
content: buffer.toString("utf8"),
|
|
9124
|
+
sizeBytes: buffer.byteLength
|
|
9125
|
+
};
|
|
9126
|
+
} catch (error) {
|
|
9127
|
+
return {
|
|
9128
|
+
ok: false,
|
|
9129
|
+
error: `Failed to read "${filePath}": ${describeError(error)}`
|
|
9130
|
+
};
|
|
9090
9131
|
}
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9132
|
+
}
|
|
9133
|
+
function toDisplayPath(filePath, cwd) {
|
|
9134
|
+
const relativePath = relative(cwd, filePath);
|
|
9135
|
+
const displayPath = relativePath.length > 0 ? relativePath : filePath;
|
|
9136
|
+
return displayPath.split(sep).join("/");
|
|
9137
|
+
}
|
|
9138
|
+
function describeError(error) {
|
|
9139
|
+
if (error instanceof Error) {
|
|
9140
|
+
return error.message;
|
|
9141
|
+
}
|
|
9142
|
+
return String(error);
|
|
9143
|
+
}
|
|
9144
|
+
|
|
9145
|
+
// src/utils/binding.ts
|
|
9146
|
+
var require2 = createRequire(import.meta.url);
|
|
9147
|
+
var cachedBinding;
|
|
9148
|
+
function loadBinding() {
|
|
9149
|
+
if (cachedBinding) {
|
|
9150
|
+
return cachedBinding;
|
|
9151
|
+
}
|
|
9152
|
+
try {
|
|
9153
|
+
cachedBinding = require2("@zealakit/css-doctor-binding");
|
|
9154
|
+
return cachedBinding;
|
|
9155
|
+
} catch (error) {
|
|
9156
|
+
throw new Error(
|
|
9157
|
+
"Unable to load @zealakit/css-doctor-binding. Build it first with `pnpm --filter @zealakit/css-doctor-binding build`.\n" + describeError(error)
|
|
9158
|
+
);
|
|
9159
|
+
}
|
|
9160
|
+
}
|
|
9161
|
+
|
|
9162
|
+
// src/utils/git-metadata.ts
|
|
9163
|
+
init_esm_shims();
|
|
9164
|
+
import { spawnSync } from "child_process";
|
|
9165
|
+
function readGitMetadata(cwd) {
|
|
9166
|
+
try {
|
|
9167
|
+
const commit = runGit(cwd, ["rev-parse", "HEAD"]);
|
|
9168
|
+
const branch = runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
9169
|
+
const status = runGit(cwd, ["status", "--porcelain"]);
|
|
9170
|
+
if (!commit && !branch) {
|
|
9171
|
+
return {};
|
|
9172
|
+
}
|
|
9173
|
+
return {
|
|
9174
|
+
commit: commit || void 0,
|
|
9175
|
+
branch: branch || void 0,
|
|
9176
|
+
dirty: status === void 0 ? void 0 : status.length > 0
|
|
9177
|
+
};
|
|
9178
|
+
} catch {
|
|
9179
|
+
return {};
|
|
9180
|
+
}
|
|
9181
|
+
}
|
|
9182
|
+
function runGit(cwd, args) {
|
|
9183
|
+
const result = spawnSync("git", args, {
|
|
9184
|
+
cwd,
|
|
9185
|
+
encoding: "utf8",
|
|
9186
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
9187
|
+
});
|
|
9188
|
+
if (result.status !== 0 || typeof result.stdout !== "string") {
|
|
9189
|
+
return void 0;
|
|
9190
|
+
}
|
|
9191
|
+
return result.stdout.trim();
|
|
9192
|
+
}
|
|
9193
|
+
|
|
9194
|
+
// src/utils/baseline-io.ts
|
|
9195
|
+
async function createBaselineSnapshot(options) {
|
|
9196
|
+
const cwd = options.cwd;
|
|
9197
|
+
const errors = [];
|
|
9198
|
+
if (options.patterns.length === 0) {
|
|
9199
|
+
return {
|
|
9200
|
+
ok: false,
|
|
9201
|
+
errors: [
|
|
9202
|
+
'No CSS files or glob patterns were provided. Example: css-doctor baseline create "src/**/*.css"'
|
|
9203
|
+
]
|
|
9204
|
+
};
|
|
9205
|
+
}
|
|
9206
|
+
if (!await isDirectory(cwd)) {
|
|
9207
|
+
return {
|
|
9208
|
+
ok: false,
|
|
9209
|
+
errors: [
|
|
9210
|
+
`Working directory does not exist or is not a directory: ${cwd}`
|
|
9211
|
+
]
|
|
9212
|
+
};
|
|
9213
|
+
}
|
|
9214
|
+
let files = [];
|
|
9215
|
+
try {
|
|
9216
|
+
files = await resolveCssFiles(options.patterns, cwd);
|
|
9217
|
+
} catch (error) {
|
|
9218
|
+
return {
|
|
9219
|
+
ok: false,
|
|
9220
|
+
errors: [`Failed to resolve CSS files: ${describeError(error)}`]
|
|
9221
|
+
};
|
|
9222
|
+
}
|
|
9223
|
+
if (files.length === 0) {
|
|
9224
|
+
return {
|
|
9225
|
+
ok: false,
|
|
9226
|
+
errors: [`No CSS files matched: ${options.patterns.join(", ")}`]
|
|
9227
|
+
};
|
|
9228
|
+
}
|
|
9229
|
+
try {
|
|
9230
|
+
const binding = loadBinding();
|
|
9231
|
+
const batchFiles = [];
|
|
9232
|
+
for (const file of files) {
|
|
9233
|
+
const read = await readCssFile(file);
|
|
9234
|
+
if (!read.ok) {
|
|
9235
|
+
errors.push(`${toDisplayPath(file, cwd)}: ${read.error}`);
|
|
9236
|
+
continue;
|
|
9237
|
+
}
|
|
9238
|
+
batchFiles.push({
|
|
9239
|
+
path: toDisplayPath(file, cwd),
|
|
9240
|
+
cssContent: read.content,
|
|
9241
|
+
sizeBytes: read.sizeBytes
|
|
9242
|
+
});
|
|
9243
|
+
}
|
|
9244
|
+
if (batchFiles.length === 0) {
|
|
9245
|
+
return {
|
|
9246
|
+
ok: false,
|
|
9247
|
+
errors: errors.length > 0 ? errors : ["No CSS files could be read for baseline creation."]
|
|
9248
|
+
};
|
|
9249
|
+
}
|
|
9250
|
+
const git = options.includeGit === false ? void 0 : readGitMetadata(cwd);
|
|
9251
|
+
const json = binding.createBaseline({
|
|
9252
|
+
files: batchFiles,
|
|
9253
|
+
toolVersion: package_default.version,
|
|
9254
|
+
createdAt: options.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
9255
|
+
patterns: options.patterns,
|
|
9256
|
+
threshold: options.threshold ?? void 0,
|
|
9257
|
+
project: {
|
|
9258
|
+
root: "."
|
|
9259
|
+
},
|
|
9260
|
+
git
|
|
9261
|
+
});
|
|
9262
|
+
const document = JSON.parse(json);
|
|
9263
|
+
return {
|
|
9264
|
+
ok: true,
|
|
9265
|
+
json,
|
|
9266
|
+
document,
|
|
9267
|
+
files: batchFiles.length
|
|
9268
|
+
};
|
|
9269
|
+
} catch (error) {
|
|
9270
|
+
return {
|
|
9271
|
+
ok: false,
|
|
9272
|
+
errors: [...errors, describeError(error)]
|
|
9273
|
+
};
|
|
9274
|
+
}
|
|
9275
|
+
}
|
|
9276
|
+
async function readBaselineFile(absolutePath) {
|
|
9277
|
+
try {
|
|
9278
|
+
const json = await readFile2(absolutePath, "utf8");
|
|
9279
|
+
return { ok: true, json };
|
|
9280
|
+
} catch (error) {
|
|
9281
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
9282
|
+
return {
|
|
9283
|
+
ok: false,
|
|
9284
|
+
error: `Baseline not found at ${absolutePath}. Create one with \`css-doctor baseline create\`.`
|
|
9285
|
+
};
|
|
9286
|
+
}
|
|
9287
|
+
return {
|
|
9288
|
+
ok: false,
|
|
9289
|
+
error: `Failed to read baseline ${absolutePath}: ${describeError(error)}`
|
|
9290
|
+
};
|
|
9291
|
+
}
|
|
9292
|
+
}
|
|
9293
|
+
async function writeBaselineFile(absolutePath, json) {
|
|
9294
|
+
try {
|
|
9295
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
9296
|
+
await writeFile(absolutePath, `${json.trimEnd()}
|
|
9297
|
+
`, "utf8");
|
|
9298
|
+
return { ok: true };
|
|
9299
|
+
} catch (error) {
|
|
9300
|
+
return {
|
|
9301
|
+
ok: false,
|
|
9302
|
+
error: `Failed to write baseline ${absolutePath}: ${describeError(error)}`
|
|
9303
|
+
};
|
|
9304
|
+
}
|
|
9305
|
+
}
|
|
9306
|
+
async function removeBaselineFile(absolutePath) {
|
|
9307
|
+
try {
|
|
9308
|
+
await unlink(absolutePath);
|
|
9309
|
+
return { ok: true };
|
|
9310
|
+
} catch (error) {
|
|
9311
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
9312
|
+
return {
|
|
9313
|
+
ok: false,
|
|
9314
|
+
error: `Baseline not found at ${absolutePath}.`
|
|
9315
|
+
};
|
|
9316
|
+
}
|
|
9317
|
+
return {
|
|
9318
|
+
ok: false,
|
|
9319
|
+
error: `Failed to remove baseline ${absolutePath}: ${describeError(error)}`
|
|
9320
|
+
};
|
|
9321
|
+
}
|
|
9322
|
+
}
|
|
9323
|
+
function validateBaselineJson(json, toolVersion = package_default.version) {
|
|
9324
|
+
const binding = loadBinding();
|
|
9325
|
+
return binding.validateBaseline(json, toolVersion);
|
|
9326
|
+
}
|
|
9327
|
+
|
|
9328
|
+
// src/utils/config.ts
|
|
9329
|
+
init_esm_shims();
|
|
9330
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
9331
|
+
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
9332
|
+
var DEFAULT_BASELINE_PATH = ".css-doctor/baseline.json";
|
|
9333
|
+
var DEFAULT_CONFIG_PATH = ".css-doctor/config.json";
|
|
9334
|
+
var DEFAULT_CONFIG = {
|
|
9335
|
+
baseline: DEFAULT_BASELINE_PATH,
|
|
9336
|
+
patterns: [],
|
|
9337
|
+
threshold: null,
|
|
9338
|
+
regression: {
|
|
9339
|
+
enabled: true,
|
|
9340
|
+
scoreTolerance: 1,
|
|
9341
|
+
sizeIncreasePercent: 10,
|
|
9342
|
+
sizeAbsoluteToleranceBytes: 1024,
|
|
9343
|
+
maxNewErrors: null,
|
|
9344
|
+
maxNewWarnings: null
|
|
9345
|
+
},
|
|
9346
|
+
qualityGates: {
|
|
9347
|
+
minScore: null,
|
|
9348
|
+
maxCssSize: null,
|
|
9349
|
+
maxSizeIncreasePercent: null,
|
|
9350
|
+
maxNewErrors: null,
|
|
9351
|
+
maxNewWarnings: null,
|
|
9352
|
+
failOnRegression: false,
|
|
9353
|
+
failOnNewErrors: false,
|
|
9354
|
+
failOnNewDiagnostics: false
|
|
9355
|
+
}
|
|
9356
|
+
};
|
|
9357
|
+
async function loadProjectConfig(cwd, configPath) {
|
|
9358
|
+
const absoluteConfigPath = resolve2(cwd, configPath ?? DEFAULT_CONFIG_PATH);
|
|
9359
|
+
const errors = [];
|
|
9360
|
+
try {
|
|
9361
|
+
const raw = await readFile3(absoluteConfigPath, "utf8");
|
|
9362
|
+
const parsed = JSON.parse(raw);
|
|
9363
|
+
const config = normalizeConfig(parsed, errors);
|
|
9364
|
+
return {
|
|
9365
|
+
config,
|
|
9366
|
+
path: absoluteConfigPath,
|
|
9367
|
+
errors
|
|
9368
|
+
};
|
|
9369
|
+
} catch (error) {
|
|
9370
|
+
if (isMissingFileError(error)) {
|
|
9371
|
+
return {
|
|
9372
|
+
config: structuredClone(DEFAULT_CONFIG),
|
|
9373
|
+
path: null,
|
|
9374
|
+
errors: []
|
|
9375
|
+
};
|
|
9376
|
+
}
|
|
9377
|
+
return {
|
|
9378
|
+
config: structuredClone(DEFAULT_CONFIG),
|
|
9379
|
+
path: absoluteConfigPath,
|
|
9380
|
+
errors: [
|
|
9381
|
+
`Failed to load config ${absoluteConfigPath}: ${describeError(error)}`
|
|
9382
|
+
]
|
|
9383
|
+
};
|
|
9384
|
+
}
|
|
9385
|
+
}
|
|
9386
|
+
function resolveBaselinePath(cwd, baselinePath) {
|
|
9387
|
+
return isAbsolute2(baselinePath) ? baselinePath : resolve2(cwd, baselinePath);
|
|
9388
|
+
}
|
|
9389
|
+
function normalizeConfig(value, errors) {
|
|
9390
|
+
const config = structuredClone(DEFAULT_CONFIG);
|
|
9391
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
9392
|
+
errors.push("Config root must be a JSON object.");
|
|
9393
|
+
return config;
|
|
9394
|
+
}
|
|
9395
|
+
const root = value;
|
|
9396
|
+
if (root.baseline !== void 0) {
|
|
9397
|
+
if (typeof root.baseline !== "string" || root.baseline.trim() === "") {
|
|
9398
|
+
errors.push("`baseline` must be a non-empty string path.");
|
|
9399
|
+
} else {
|
|
9400
|
+
config.baseline = root.baseline;
|
|
9401
|
+
}
|
|
9402
|
+
}
|
|
9403
|
+
if (root.patterns !== void 0) {
|
|
9404
|
+
if (!Array.isArray(root.patterns) || root.patterns.some((pattern) => typeof pattern !== "string")) {
|
|
9405
|
+
errors.push("`patterns` must be an array of strings.");
|
|
9406
|
+
} else {
|
|
9407
|
+
config.patterns = root.patterns;
|
|
9408
|
+
}
|
|
9409
|
+
}
|
|
9410
|
+
if (root.threshold !== void 0) {
|
|
9411
|
+
if (typeof root.threshold !== "number" || !Number.isInteger(root.threshold) || root.threshold < 0) {
|
|
9412
|
+
errors.push("`threshold` must be a non-negative integer.");
|
|
9413
|
+
} else {
|
|
9414
|
+
config.threshold = root.threshold;
|
|
9415
|
+
}
|
|
9416
|
+
}
|
|
9417
|
+
if (root.regression !== void 0) {
|
|
9418
|
+
Object.assign(
|
|
9419
|
+
config.regression,
|
|
9420
|
+
normalizeRegression(root.regression, errors)
|
|
9421
|
+
);
|
|
9422
|
+
}
|
|
9423
|
+
if (root.qualityGates !== void 0) {
|
|
9424
|
+
Object.assign(
|
|
9425
|
+
config.qualityGates,
|
|
9426
|
+
normalizeQualityGates(root.qualityGates, errors)
|
|
9427
|
+
);
|
|
9428
|
+
}
|
|
9429
|
+
return config;
|
|
9430
|
+
}
|
|
9431
|
+
function normalizeRegression(value, errors) {
|
|
9432
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
9433
|
+
errors.push("`regression` must be an object.");
|
|
9434
|
+
return {};
|
|
9435
|
+
}
|
|
9436
|
+
const source = value;
|
|
9437
|
+
const result = {};
|
|
9438
|
+
if (source.enabled !== void 0) {
|
|
9439
|
+
if (typeof source.enabled !== "boolean") {
|
|
9440
|
+
errors.push("`regression.enabled` must be a boolean.");
|
|
9441
|
+
} else {
|
|
9442
|
+
result.enabled = source.enabled;
|
|
9443
|
+
}
|
|
9444
|
+
}
|
|
9445
|
+
assignNumber(
|
|
9446
|
+
source,
|
|
9447
|
+
"scoreTolerance",
|
|
9448
|
+
result,
|
|
9449
|
+
"scoreTolerance",
|
|
9450
|
+
errors,
|
|
9451
|
+
"`regression.scoreTolerance`",
|
|
9452
|
+
{ integer: true, min: 0, max: 100 }
|
|
9453
|
+
);
|
|
9454
|
+
assignNumber(
|
|
9455
|
+
source,
|
|
9456
|
+
"sizeIncreasePercent",
|
|
9457
|
+
result,
|
|
9458
|
+
"sizeIncreasePercent",
|
|
9459
|
+
errors,
|
|
9460
|
+
"`regression.sizeIncreasePercent`",
|
|
9461
|
+
{ min: 0 }
|
|
9462
|
+
);
|
|
9463
|
+
assignNumber(
|
|
9464
|
+
source,
|
|
9465
|
+
"sizeAbsoluteToleranceBytes",
|
|
9466
|
+
result,
|
|
9467
|
+
"sizeAbsoluteToleranceBytes",
|
|
9468
|
+
errors,
|
|
9469
|
+
"`regression.sizeAbsoluteToleranceBytes`",
|
|
9470
|
+
{ integer: true, min: 0 }
|
|
9471
|
+
);
|
|
9472
|
+
assignNullableInteger(
|
|
9473
|
+
source,
|
|
9474
|
+
"maxNewErrors",
|
|
9475
|
+
result,
|
|
9476
|
+
"maxNewErrors",
|
|
9477
|
+
errors,
|
|
9478
|
+
"`regression.maxNewErrors`"
|
|
9479
|
+
);
|
|
9480
|
+
assignNullableInteger(
|
|
9481
|
+
source,
|
|
9482
|
+
"maxNewWarnings",
|
|
9483
|
+
result,
|
|
9484
|
+
"maxNewWarnings",
|
|
9485
|
+
errors,
|
|
9486
|
+
"`regression.maxNewWarnings`"
|
|
9487
|
+
);
|
|
9488
|
+
return result;
|
|
9489
|
+
}
|
|
9490
|
+
function normalizeQualityGates(value, errors) {
|
|
9491
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
9492
|
+
errors.push("`qualityGates` must be an object.");
|
|
9493
|
+
return {};
|
|
9494
|
+
}
|
|
9495
|
+
const source = value;
|
|
9496
|
+
const result = {};
|
|
9497
|
+
assignNullableInteger(
|
|
9498
|
+
source,
|
|
9499
|
+
"minScore",
|
|
9500
|
+
result,
|
|
9501
|
+
"minScore",
|
|
9502
|
+
errors,
|
|
9503
|
+
"`qualityGates.minScore`",
|
|
9504
|
+
{ max: 100 }
|
|
9505
|
+
);
|
|
9506
|
+
assignNullableInteger(
|
|
9507
|
+
source,
|
|
9508
|
+
"maxCssSize",
|
|
9509
|
+
result,
|
|
9510
|
+
"maxCssSize",
|
|
9511
|
+
errors,
|
|
9512
|
+
"`qualityGates.maxCssSize`"
|
|
9513
|
+
);
|
|
9514
|
+
assignNumber(
|
|
9515
|
+
source,
|
|
9516
|
+
"maxSizeIncreasePercent",
|
|
9517
|
+
result,
|
|
9518
|
+
"maxSizeIncreasePercent",
|
|
9519
|
+
errors,
|
|
9520
|
+
"`qualityGates.maxSizeIncreasePercent`",
|
|
9521
|
+
{ min: 0, nullable: true }
|
|
9522
|
+
);
|
|
9523
|
+
assignNullableInteger(
|
|
9524
|
+
source,
|
|
9525
|
+
"maxNewErrors",
|
|
9526
|
+
result,
|
|
9527
|
+
"maxNewErrors",
|
|
9528
|
+
errors,
|
|
9529
|
+
"`qualityGates.maxNewErrors`"
|
|
9530
|
+
);
|
|
9531
|
+
assignNullableInteger(
|
|
9532
|
+
source,
|
|
9533
|
+
"maxNewWarnings",
|
|
9534
|
+
result,
|
|
9535
|
+
"maxNewWarnings",
|
|
9536
|
+
errors,
|
|
9537
|
+
"`qualityGates.maxNewWarnings`"
|
|
9538
|
+
);
|
|
9539
|
+
for (const key of [
|
|
9540
|
+
"failOnRegression",
|
|
9541
|
+
"failOnNewErrors",
|
|
9542
|
+
"failOnNewDiagnostics"
|
|
9543
|
+
]) {
|
|
9544
|
+
if (source[key] !== void 0) {
|
|
9545
|
+
if (typeof source[key] !== "boolean") {
|
|
9546
|
+
errors.push(`\`qualityGates.${key}\` must be a boolean.`);
|
|
9547
|
+
} else {
|
|
9548
|
+
result[key] = source[key];
|
|
9549
|
+
}
|
|
9550
|
+
}
|
|
9551
|
+
}
|
|
9552
|
+
return result;
|
|
9553
|
+
}
|
|
9554
|
+
function assignNumber(source, sourceKey, target, targetKey, errors, label3, options = {}) {
|
|
9555
|
+
if (source[sourceKey] === void 0) {
|
|
9556
|
+
return;
|
|
9557
|
+
}
|
|
9558
|
+
if (options.nullable && source[sourceKey] === null) {
|
|
9559
|
+
target[targetKey] = null;
|
|
9560
|
+
return;
|
|
9561
|
+
}
|
|
9562
|
+
const value = source[sourceKey];
|
|
9563
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
9564
|
+
errors.push(`${label3} must be a number.`);
|
|
9565
|
+
return;
|
|
9566
|
+
}
|
|
9567
|
+
if (options.integer && !Number.isInteger(value)) {
|
|
9568
|
+
errors.push(`${label3} must be an integer.`);
|
|
9569
|
+
return;
|
|
9570
|
+
}
|
|
9571
|
+
if (options.min !== void 0 && value < options.min) {
|
|
9572
|
+
errors.push(`${label3} must be >= ${options.min}.`);
|
|
9573
|
+
return;
|
|
9574
|
+
}
|
|
9575
|
+
if (options.max !== void 0 && value > options.max) {
|
|
9576
|
+
errors.push(`${label3} must be <= ${options.max}.`);
|
|
9577
|
+
return;
|
|
9578
|
+
}
|
|
9579
|
+
target[targetKey] = value;
|
|
9580
|
+
}
|
|
9581
|
+
function assignNullableInteger(source, sourceKey, target, targetKey, errors, label3, options = {}) {
|
|
9582
|
+
if (source[sourceKey] === void 0) {
|
|
9583
|
+
return;
|
|
9584
|
+
}
|
|
9585
|
+
if (source[sourceKey] === null) {
|
|
9586
|
+
target[targetKey] = null;
|
|
9587
|
+
return;
|
|
9588
|
+
}
|
|
9589
|
+
const value = source[sourceKey];
|
|
9590
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
9591
|
+
errors.push(`${label3} must be a non-negative integer or null.`);
|
|
9592
|
+
return;
|
|
9593
|
+
}
|
|
9594
|
+
if (options.max !== void 0 && value > options.max) {
|
|
9595
|
+
errors.push(`${label3} must be <= ${options.max}.`);
|
|
9596
|
+
return;
|
|
9597
|
+
}
|
|
9598
|
+
target[targetKey] = value;
|
|
9599
|
+
}
|
|
9600
|
+
function isMissingFileError(error) {
|
|
9601
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
9602
|
+
}
|
|
9603
|
+
|
|
9604
|
+
// src/commands/baseline.ts
|
|
9605
|
+
function registerBaselineCommand(program3) {
|
|
9606
|
+
const baseline = program3.command("baseline").description("Create, inspect, validate, or remove a CSS quality baseline");
|
|
9607
|
+
baseline.command("create").description("Create a baseline from the current CSS Health Score").argument(
|
|
9608
|
+
"[patterns...]",
|
|
9609
|
+
"CSS files or glob patterns (falls back to config.patterns)"
|
|
9610
|
+
).option(
|
|
9611
|
+
"-o, --output <path>",
|
|
9612
|
+
`Baseline output path (default: ${DEFAULT_BASELINE_PATH})`
|
|
9613
|
+
).option("--config <path>", "Project config path").option(
|
|
9614
|
+
"--threshold <number>",
|
|
9615
|
+
"Selector-count threshold stored with the baseline",
|
|
9616
|
+
parseNonNegativeInteger
|
|
9617
|
+
).option("-j, --json", "Emit JSON instead of a human summary").action(async (patterns, options) => {
|
|
9618
|
+
const report = await runBaselineCreate(patterns ?? [], options);
|
|
9619
|
+
writeBaselineReport(report, options.json === true);
|
|
9620
|
+
process.exitCode = report.success ? 0 : 1;
|
|
9621
|
+
});
|
|
9622
|
+
baseline.command("show").description("Display baseline metadata and scores").option(
|
|
9623
|
+
"-b, --baseline <path>",
|
|
9624
|
+
`Baseline path (default: ${DEFAULT_BASELINE_PATH})`
|
|
9625
|
+
).option("--config <path>", "Project config path").option("-j, --json", "Emit JSON instead of a human summary").action(async (options) => {
|
|
9626
|
+
const report = await runBaselineShow(options);
|
|
9627
|
+
writeBaselineReport(report, options.json === true);
|
|
9628
|
+
process.exitCode = report.success ? 0 : 1;
|
|
9629
|
+
});
|
|
9630
|
+
baseline.command("validate").description("Validate baseline schema and compatibility").option(
|
|
9631
|
+
"-b, --baseline <path>",
|
|
9632
|
+
`Baseline path (default: ${DEFAULT_BASELINE_PATH})`
|
|
9633
|
+
).option("--config <path>", "Project config path").option("-j, --json", "Emit JSON instead of a human summary").action(async (options) => {
|
|
9634
|
+
const report = await runBaselineValidate(options);
|
|
9635
|
+
writeBaselineReport(report, options.json === true);
|
|
9636
|
+
process.exitCode = report.success ? 0 : 1;
|
|
9637
|
+
});
|
|
9638
|
+
baseline.command("remove").description("Delete the baseline file").option(
|
|
9639
|
+
"-b, --baseline <path>",
|
|
9640
|
+
`Baseline path (default: ${DEFAULT_BASELINE_PATH})`
|
|
9641
|
+
).option("--config <path>", "Project config path").option("-j, --json", "Emit JSON instead of a human summary").action(async (options) => {
|
|
9642
|
+
const report = await runBaselineRemove(options);
|
|
9643
|
+
writeBaselineReport(report, options.json === true);
|
|
9644
|
+
process.exitCode = report.success ? 0 : 1;
|
|
9645
|
+
});
|
|
9646
|
+
}
|
|
9647
|
+
async function runBaselineCreate(patterns, options = {}) {
|
|
9648
|
+
const cwd = resolve3(options.cwd ?? process.cwd());
|
|
9649
|
+
const loaded = await loadProjectConfig(cwd, options.config);
|
|
9650
|
+
const errors = [...loaded.errors];
|
|
9651
|
+
const effectivePatterns = patterns.length > 0 ? patterns : loaded.config.patterns;
|
|
9652
|
+
const baselinePath = resolveBaselinePath(
|
|
9653
|
+
cwd,
|
|
9654
|
+
options.output ?? loaded.config.baseline
|
|
9655
|
+
);
|
|
9656
|
+
const snapshot = await createBaselineSnapshot({
|
|
9657
|
+
cwd,
|
|
9658
|
+
patterns: effectivePatterns,
|
|
9659
|
+
threshold: options.threshold ?? loaded.config.threshold
|
|
9660
|
+
});
|
|
9661
|
+
if (!snapshot.ok) {
|
|
9662
|
+
return {
|
|
9663
|
+
schemaVersion: 1,
|
|
9664
|
+
command: "baseline",
|
|
9665
|
+
action: "create",
|
|
9666
|
+
success: false,
|
|
9667
|
+
errors: [...errors, ...snapshot.errors],
|
|
9668
|
+
warnings: [],
|
|
9669
|
+
baselinePath: toDisplayPath(baselinePath, cwd),
|
|
9670
|
+
baseline: null
|
|
9671
|
+
};
|
|
9672
|
+
}
|
|
9673
|
+
const written = await writeBaselineFile(baselinePath, snapshot.json);
|
|
9674
|
+
if (!written.ok) {
|
|
9675
|
+
return {
|
|
9676
|
+
schemaVersion: 1,
|
|
9677
|
+
command: "baseline",
|
|
9678
|
+
action: "create",
|
|
9679
|
+
success: false,
|
|
9680
|
+
errors: [...errors, written.error],
|
|
9681
|
+
warnings: [],
|
|
9682
|
+
baselinePath: toDisplayPath(baselinePath, cwd),
|
|
9683
|
+
baseline: null
|
|
9684
|
+
};
|
|
9685
|
+
}
|
|
9686
|
+
return {
|
|
9687
|
+
schemaVersion: 1,
|
|
9688
|
+
command: "baseline",
|
|
9689
|
+
action: "create",
|
|
9690
|
+
success: errors.length === 0,
|
|
9691
|
+
errors,
|
|
9692
|
+
warnings: [],
|
|
9693
|
+
baselinePath: toDisplayPath(baselinePath, cwd),
|
|
9694
|
+
baseline: snapshot.document
|
|
9695
|
+
};
|
|
9696
|
+
}
|
|
9697
|
+
async function runBaselineShow(options = {}) {
|
|
9698
|
+
return readAndReport("show", options);
|
|
9699
|
+
}
|
|
9700
|
+
async function runBaselineValidate(options = {}) {
|
|
9701
|
+
const cwd = resolve3(options.cwd ?? process.cwd());
|
|
9702
|
+
const loaded = await loadProjectConfig(cwd, options.config);
|
|
9703
|
+
const errors = [...loaded.errors];
|
|
9704
|
+
const warnings = [];
|
|
9705
|
+
const baselinePath = resolveBaselinePath(
|
|
9706
|
+
cwd,
|
|
9707
|
+
options.baseline ?? loaded.config.baseline
|
|
9708
|
+
);
|
|
9709
|
+
const displayPath = toDisplayPath(baselinePath, cwd);
|
|
9710
|
+
const read = await readBaselineFile(baselinePath);
|
|
9711
|
+
if (!read.ok) {
|
|
9712
|
+
return {
|
|
9713
|
+
schemaVersion: 1,
|
|
9714
|
+
command: "baseline",
|
|
9715
|
+
action: "validate",
|
|
9716
|
+
success: false,
|
|
9717
|
+
errors: [...errors, read.error],
|
|
9718
|
+
warnings,
|
|
9719
|
+
baselinePath: displayPath,
|
|
9720
|
+
baseline: null
|
|
9721
|
+
};
|
|
9722
|
+
}
|
|
9723
|
+
try {
|
|
9724
|
+
const validation = validateBaselineJson(read.json);
|
|
9725
|
+
if (validation.status === "incompatible") {
|
|
9726
|
+
errors.push(validation.message ?? "Baseline is incompatible.");
|
|
9727
|
+
} else if (validation.status === "softWarning" && validation.message) {
|
|
9728
|
+
warnings.push(validation.message);
|
|
9729
|
+
}
|
|
9730
|
+
return {
|
|
9731
|
+
schemaVersion: 1,
|
|
9732
|
+
command: "baseline",
|
|
9733
|
+
action: "validate",
|
|
9734
|
+
success: errors.length === 0,
|
|
9735
|
+
errors,
|
|
9736
|
+
warnings,
|
|
9737
|
+
baselinePath: displayPath,
|
|
9738
|
+
baseline: JSON.parse(read.json)
|
|
9739
|
+
};
|
|
9740
|
+
} catch (error) {
|
|
9741
|
+
return {
|
|
9742
|
+
schemaVersion: 1,
|
|
9743
|
+
command: "baseline",
|
|
9744
|
+
action: "validate",
|
|
9745
|
+
success: false,
|
|
9746
|
+
errors: [
|
|
9747
|
+
...errors,
|
|
9748
|
+
error instanceof Error ? error.message : String(error)
|
|
9749
|
+
],
|
|
9750
|
+
warnings,
|
|
9751
|
+
baselinePath: displayPath,
|
|
9752
|
+
baseline: null
|
|
9753
|
+
};
|
|
9754
|
+
}
|
|
9755
|
+
}
|
|
9756
|
+
async function runBaselineRemove(options = {}) {
|
|
9757
|
+
const cwd = resolve3(options.cwd ?? process.cwd());
|
|
9758
|
+
const loaded = await loadProjectConfig(cwd, options.config);
|
|
9759
|
+
const errors = [...loaded.errors];
|
|
9760
|
+
const baselinePath = resolveBaselinePath(
|
|
9761
|
+
cwd,
|
|
9762
|
+
options.baseline ?? loaded.config.baseline
|
|
9763
|
+
);
|
|
9764
|
+
const displayPath = toDisplayPath(baselinePath, cwd);
|
|
9765
|
+
const removed = await removeBaselineFile(baselinePath);
|
|
9766
|
+
if (!removed.ok) {
|
|
9767
|
+
errors.push(removed.error);
|
|
9768
|
+
}
|
|
9769
|
+
return {
|
|
9770
|
+
schemaVersion: 1,
|
|
9771
|
+
command: "baseline",
|
|
9772
|
+
action: "remove",
|
|
9773
|
+
success: errors.length === 0,
|
|
9774
|
+
errors,
|
|
9775
|
+
warnings: [],
|
|
9776
|
+
baselinePath: displayPath,
|
|
9777
|
+
baseline: null
|
|
9778
|
+
};
|
|
9779
|
+
}
|
|
9780
|
+
async function readAndReport(action, options) {
|
|
9781
|
+
const cwd = resolve3(options.cwd ?? process.cwd());
|
|
9782
|
+
const loaded = await loadProjectConfig(cwd, options.config);
|
|
9783
|
+
const errors = [...loaded.errors];
|
|
9784
|
+
const baselinePath = resolveBaselinePath(
|
|
9785
|
+
cwd,
|
|
9786
|
+
options.baseline ?? loaded.config.baseline
|
|
9787
|
+
);
|
|
9788
|
+
const displayPath = toDisplayPath(baselinePath, cwd);
|
|
9789
|
+
const read = await readBaselineFile(baselinePath);
|
|
9790
|
+
if (!read.ok) {
|
|
9791
|
+
return {
|
|
9792
|
+
schemaVersion: 1,
|
|
9793
|
+
command: "baseline",
|
|
9794
|
+
action,
|
|
9795
|
+
success: false,
|
|
9796
|
+
errors: [...errors, read.error],
|
|
9797
|
+
warnings: [],
|
|
9798
|
+
baselinePath: displayPath,
|
|
9799
|
+
baseline: null
|
|
9800
|
+
};
|
|
9801
|
+
}
|
|
9802
|
+
try {
|
|
9803
|
+
return {
|
|
9804
|
+
schemaVersion: 1,
|
|
9805
|
+
command: "baseline",
|
|
9806
|
+
action,
|
|
9807
|
+
success: errors.length === 0,
|
|
9808
|
+
errors,
|
|
9809
|
+
warnings: [],
|
|
9810
|
+
baselinePath: displayPath,
|
|
9811
|
+
baseline: JSON.parse(read.json)
|
|
9812
|
+
};
|
|
9813
|
+
} catch (error) {
|
|
9814
|
+
return {
|
|
9815
|
+
schemaVersion: 1,
|
|
9816
|
+
command: "baseline",
|
|
9817
|
+
action,
|
|
9818
|
+
success: false,
|
|
9819
|
+
errors: [
|
|
9820
|
+
...errors,
|
|
9821
|
+
`Corrupted baseline JSON at ${displayPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
9822
|
+
],
|
|
9823
|
+
warnings: [],
|
|
9824
|
+
baselinePath: displayPath,
|
|
9825
|
+
baseline: null
|
|
9826
|
+
};
|
|
9827
|
+
}
|
|
9828
|
+
}
|
|
9829
|
+
function writeBaselineReport(report, json) {
|
|
9830
|
+
if (json) {
|
|
9831
|
+
process.stdout.write(`${formatJsonReport(report)}
|
|
9832
|
+
`);
|
|
9833
|
+
return;
|
|
9834
|
+
}
|
|
9835
|
+
if (!report.success) {
|
|
9836
|
+
process.stdout.write(
|
|
9837
|
+
`css-doctor baseline ${report.action} failed:
|
|
9838
|
+
${report.errors.map((error) => ` - ${error}`).join("\n")}
|
|
9839
|
+
`
|
|
9840
|
+
);
|
|
9841
|
+
return;
|
|
9842
|
+
}
|
|
9843
|
+
if (report.action === "remove") {
|
|
9844
|
+
process.stdout.write(`Removed baseline ${report.baselinePath}
|
|
9845
|
+
`);
|
|
9846
|
+
return;
|
|
9847
|
+
}
|
|
9848
|
+
if (report.action === "create") {
|
|
9849
|
+
process.stdout.write(
|
|
9850
|
+
`Created baseline ${report.baselinePath}` + (report.baseline ? ` \xB7 score ${report.baseline.health.overall}/100 \xB7 ${report.baseline.files.length} files
|
|
9851
|
+
` : "\n")
|
|
9852
|
+
);
|
|
9853
|
+
} else if (report.baseline) {
|
|
9854
|
+
const health = report.baseline.health;
|
|
9855
|
+
process.stdout.write(
|
|
9856
|
+
[
|
|
9857
|
+
`Baseline ${report.baselinePath}`,
|
|
9858
|
+
` schema ${report.baseline.schemaVersion} \xB7 tool ${report.baseline.toolVersion}`,
|
|
9859
|
+
` created ${report.baseline.createdAt}`,
|
|
9860
|
+
` score ${health.overall}/100 ${health.grade} (${health.status})`,
|
|
9861
|
+
` ${health.files} files \xB7 ${health.rules} rules \xB7 ${health.sizeBytes} bytes`,
|
|
9862
|
+
...report.warnings.map((warning) => ` warning: ${warning}`),
|
|
9863
|
+
""
|
|
9864
|
+
].join("\n")
|
|
9865
|
+
);
|
|
9866
|
+
}
|
|
9867
|
+
for (const warning of report.warnings) {
|
|
9868
|
+
if (report.action === "create") {
|
|
9869
|
+
process.stderr.write(`css-doctor: ${warning}
|
|
9870
|
+
`);
|
|
9871
|
+
}
|
|
9872
|
+
}
|
|
9873
|
+
}
|
|
9874
|
+
function parseNonNegativeInteger(value) {
|
|
9875
|
+
const parsed = Number(value);
|
|
9876
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
9877
|
+
throw new InvalidArgumentError("must be a non-negative integer");
|
|
9878
|
+
}
|
|
9879
|
+
return parsed;
|
|
9880
|
+
}
|
|
9881
|
+
|
|
9882
|
+
// src/commands/check.ts
|
|
9883
|
+
init_esm_shims();
|
|
9884
|
+
import { createRequire as createRequire2 } from "module";
|
|
9885
|
+
import { performance } from "perf_hooks";
|
|
9886
|
+
import { resolve as resolve4 } from "path";
|
|
9887
|
+
|
|
9888
|
+
// src/formatters/health-terminal.ts
|
|
9889
|
+
init_esm_shims();
|
|
9890
|
+
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
9891
|
+
var DEFAULT_TOP_ISSUES = 8;
|
|
9892
|
+
var BAR_WIDTH = 36;
|
|
9893
|
+
function formatHealthTerminalReport(report, options = {}) {
|
|
9894
|
+
const colors = import_picocolors.default.createColors(
|
|
9895
|
+
options.color ?? import_picocolors.default.isColorSupported
|
|
9896
|
+
);
|
|
9897
|
+
const columns = Math.max(
|
|
9898
|
+
48,
|
|
9899
|
+
Math.min(options.columns ?? detectColumns(), 88)
|
|
9900
|
+
);
|
|
9901
|
+
const topIssues = options.topIssues ?? DEFAULT_TOP_ISSUES;
|
|
9902
|
+
const lines = [];
|
|
9903
|
+
const rule = colors.dim("\u2500".repeat(Math.min(columns, 64)));
|
|
9904
|
+
lines.push("");
|
|
9905
|
+
lines.push(
|
|
9906
|
+
` ${colors.bold(colors.cyan("CSS DOCTOR"))}${pad(
|
|
9907
|
+
columns - 22,
|
|
9908
|
+
""
|
|
9909
|
+
)}${colors.dim("HEALTH REPORT")}`
|
|
9910
|
+
);
|
|
9911
|
+
lines.push("");
|
|
9912
|
+
lines.push(` ${colors.bold("Overall Health")}`);
|
|
9913
|
+
lines.push("");
|
|
9914
|
+
lines.push(
|
|
9915
|
+
` ${scoreColor(report.overall, colors)(colors.bold(`${report.overall}/100`))} ${colors.bold(report.grade)} ${statusLabel(report.status, colors)}`
|
|
9916
|
+
);
|
|
9917
|
+
lines.push("");
|
|
9918
|
+
lines.push(` ${scoreBar(report.overall, colors)}`);
|
|
9919
|
+
lines.push("");
|
|
9920
|
+
lines.push(
|
|
9921
|
+
` ${colors.dim(
|
|
9922
|
+
`Analyzed ${report.summary.files} file${report.summary.files === 1 ? "" : "s"} \xB7 ${formatNumber(report.summary.rules)} rules \xB7 ${formatNumber(report.summary.declarations)} declarations`
|
|
9923
|
+
)}`
|
|
9924
|
+
);
|
|
9925
|
+
lines.push(
|
|
9926
|
+
` ${colors.dim(
|
|
9927
|
+
`Total size ${formatBytes(report.summary.sizeBytes)} \xB7 Analysis completed in ${formatDuration(report.summary.analysisMs)}`
|
|
9928
|
+
)}`
|
|
9929
|
+
);
|
|
9930
|
+
lines.push("");
|
|
9931
|
+
lines.push(rule);
|
|
9932
|
+
lines.push("");
|
|
9933
|
+
lines.push(` ${colors.bold("HEALTH BREAKDOWN")}`);
|
|
9934
|
+
lines.push("");
|
|
9935
|
+
for (const dimension of report.dimensions) {
|
|
9936
|
+
const label3 = dimensionLabel(dimension.id).padEnd(24);
|
|
9937
|
+
const scoreText = dimension.score === void 0 || dimension.score === null ? " n/a" : `${String(dimension.score).padStart(3, " ")}/100`;
|
|
9938
|
+
const bar = dimension.score === void 0 || dimension.score === null ? colors.dim("\u2591".repeat(20)) : miniBar(dimension.score, colors);
|
|
9939
|
+
lines.push(` ${label3}${scoreText} ${bar}`);
|
|
9940
|
+
}
|
|
9941
|
+
lines.push("");
|
|
9942
|
+
lines.push(rule);
|
|
9943
|
+
lines.push("");
|
|
9944
|
+
lines.push(` ${colors.bold("DIAGNOSTICS")}`);
|
|
9945
|
+
lines.push("");
|
|
9946
|
+
lines.push(
|
|
9947
|
+
` ${colors.red("\u2716")} ${report.summary.errors} Error${report.summary.errors === 1 ? "" : "s"}`
|
|
9948
|
+
);
|
|
9949
|
+
lines.push(
|
|
9950
|
+
` ${colors.yellow("\u25B2")} ${report.summary.warnings} Warning${report.summary.warnings === 1 ? "" : "s"}`
|
|
9951
|
+
);
|
|
9952
|
+
lines.push(
|
|
9953
|
+
` ${colors.cyan("\u25CF")} ${report.summary.suggestions} Suggestion${report.summary.suggestions === 1 ? "" : "s"}`
|
|
9954
|
+
);
|
|
9955
|
+
if (report.diagnostics.length > 0) {
|
|
9956
|
+
lines.push("");
|
|
9957
|
+
lines.push(` ${colors.bold("TOP ISSUES")}`);
|
|
9958
|
+
lines.push("");
|
|
9959
|
+
const shown = report.diagnostics.slice(0, topIssues);
|
|
9960
|
+
for (const diagnostic of shown) {
|
|
9961
|
+
const marker = diagnostic.severity === "error" ? colors.red("\u2716") : diagnostic.severity === "warning" ? colors.yellow("\u25B2") : colors.cyan("\u25CF");
|
|
9962
|
+
lines.push(` ${marker} ${colors.bold(titleFromCode(diagnostic.code))}`);
|
|
9963
|
+
const where = [
|
|
9964
|
+
diagnostic.filePath,
|
|
9965
|
+
diagnostic.location ? `${diagnostic.location.line}:${diagnostic.location.column}` : void 0
|
|
9966
|
+
].filter(Boolean).join(":");
|
|
9967
|
+
if (where) {
|
|
9968
|
+
lines.push(` ${colors.dim(where)}`);
|
|
9969
|
+
}
|
|
9970
|
+
if (diagnostic.selector) {
|
|
9971
|
+
lines.push(` ${colors.magenta(diagnostic.selector)}`);
|
|
9972
|
+
}
|
|
9973
|
+
lines.push(` ${colors.dim(diagnostic.remediation)}`);
|
|
9974
|
+
lines.push("");
|
|
9975
|
+
}
|
|
9976
|
+
if (report.diagnostics.length > shown.length) {
|
|
9977
|
+
lines.push(
|
|
9978
|
+
` ${colors.dim(
|
|
9979
|
+
`\u2026 and ${report.diagnostics.length - shown.length} more (see --json for full list)`
|
|
9980
|
+
)}`
|
|
9981
|
+
);
|
|
9982
|
+
lines.push("");
|
|
9983
|
+
}
|
|
9984
|
+
}
|
|
9985
|
+
if (report.recommendations.length > 0) {
|
|
9986
|
+
lines.push(` ${colors.bold("RECOMMENDATIONS")}`);
|
|
9987
|
+
lines.push("");
|
|
9988
|
+
report.recommendations.forEach((recommendation, index) => {
|
|
9989
|
+
lines.push(` ${index + 1}. ${recommendation}`);
|
|
9990
|
+
});
|
|
9991
|
+
lines.push("");
|
|
9992
|
+
}
|
|
9993
|
+
lines.push(rule);
|
|
9994
|
+
lines.push("");
|
|
9995
|
+
lines.push(` ${finalStatusLine(report, colors)}`);
|
|
9996
|
+
lines.push("");
|
|
9997
|
+
lines.push(
|
|
9998
|
+
` ${colors.dim("Run `css-doctor health --help` for more options.")}`
|
|
9999
|
+
);
|
|
10000
|
+
lines.push("");
|
|
10001
|
+
return lines.join("\n");
|
|
10002
|
+
}
|
|
10003
|
+
function formatCompactHealthSummary(report, options = {}) {
|
|
10004
|
+
const colors = import_picocolors.default.createColors(
|
|
10005
|
+
options.color ?? import_picocolors.default.isColorSupported
|
|
10006
|
+
);
|
|
10007
|
+
return `${colors.bold("Health")} ${scoreColor(
|
|
10008
|
+
report.overall,
|
|
10009
|
+
colors
|
|
10010
|
+
)(
|
|
10011
|
+
`${report.overall}/100`
|
|
10012
|
+
)} ${colors.bold(report.grade)} ${colors.dim(`(${report.status})`)} ${colors.dim("\xB7")} ${report.summary.errors} err ${colors.dim("\xB7")} ${report.summary.warnings} warn ${colors.dim("\xB7")} ${report.summary.suggestions} info`;
|
|
10013
|
+
}
|
|
10014
|
+
function finalStatusLine(report, colors) {
|
|
10015
|
+
if (report.overall >= 90) {
|
|
10016
|
+
return `${colors.green(colors.bold("HEALTHY"))} CSS quality is within recommended limits.`;
|
|
10017
|
+
}
|
|
10018
|
+
if (report.overall >= 70) {
|
|
10019
|
+
return `${colors.yellow(colors.bold("NEEDS ATTENTION"))} Address the top warnings before release.`;
|
|
10020
|
+
}
|
|
10021
|
+
return `${colors.red(colors.bold("CRITICAL"))} CSS quality is below recommended limits.`;
|
|
10022
|
+
}
|
|
10023
|
+
function scoreColor(score, colors) {
|
|
10024
|
+
if (score >= 90) {
|
|
10025
|
+
return colors.green;
|
|
10026
|
+
}
|
|
10027
|
+
if (score >= 70) {
|
|
10028
|
+
return colors.yellow;
|
|
10029
|
+
}
|
|
10030
|
+
return colors.red;
|
|
10031
|
+
}
|
|
10032
|
+
function statusLabel(status, colors) {
|
|
10033
|
+
const label3 = status.charAt(0).toUpperCase() + status.slice(1);
|
|
10034
|
+
if (status === "excellent" || status === "good") {
|
|
10035
|
+
return colors.green(label3);
|
|
10036
|
+
}
|
|
10037
|
+
if (status === "fair") {
|
|
10038
|
+
return colors.yellow(label3);
|
|
10039
|
+
}
|
|
10040
|
+
return colors.red(label3);
|
|
10041
|
+
}
|
|
10042
|
+
function scoreBar(score, colors) {
|
|
10043
|
+
const filled = Math.round(score / 100 * BAR_WIDTH);
|
|
10044
|
+
const empty = Math.max(0, BAR_WIDTH - filled);
|
|
10045
|
+
return `${scoreColor(score, colors)("\u2588".repeat(filled))}${colors.dim("\u2591".repeat(empty))}`;
|
|
10046
|
+
}
|
|
10047
|
+
function miniBar(score, colors) {
|
|
10048
|
+
const width = 20;
|
|
10049
|
+
const filled = Math.round(score / 100 * width);
|
|
10050
|
+
return `${scoreColor(score, colors)("\u2588".repeat(filled))}${colors.dim("\u2591".repeat(width - filled))}`;
|
|
10051
|
+
}
|
|
10052
|
+
function dimensionLabel(id) {
|
|
10053
|
+
switch (id) {
|
|
10054
|
+
case "selectorComplexity":
|
|
10055
|
+
return "Selector Complexity";
|
|
10056
|
+
case "duplicateRules":
|
|
10057
|
+
return "Duplicate Rules";
|
|
10058
|
+
case "cssSizeEfficiency":
|
|
10059
|
+
return "CSS Size Efficiency";
|
|
10060
|
+
case "deadCss":
|
|
10061
|
+
return "Dead CSS";
|
|
10062
|
+
case "variableConsistency":
|
|
10063
|
+
return "Variable Consistency";
|
|
10064
|
+
default:
|
|
10065
|
+
return id;
|
|
10066
|
+
}
|
|
10067
|
+
}
|
|
10068
|
+
function titleFromCode(code) {
|
|
10069
|
+
const leaf = code.split(".").pop() ?? code;
|
|
10070
|
+
return leaf.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
10071
|
+
}
|
|
10072
|
+
function detectColumns() {
|
|
10073
|
+
const columns = process.stdout.columns;
|
|
10074
|
+
return typeof columns === "number" && columns > 0 ? columns : 80;
|
|
10075
|
+
}
|
|
10076
|
+
function pad(width, text) {
|
|
10077
|
+
return " ".repeat(Math.max(1, width - text.length));
|
|
10078
|
+
}
|
|
10079
|
+
function formatBytes(bytes) {
|
|
10080
|
+
if (bytes < 1024) {
|
|
10081
|
+
return `${bytes} B`;
|
|
10082
|
+
}
|
|
10083
|
+
if (bytes < 1024 * 1024) {
|
|
10084
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
10085
|
+
}
|
|
10086
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
10087
|
+
}
|
|
10088
|
+
function formatDuration(ms) {
|
|
10089
|
+
if (ms < 1) {
|
|
10090
|
+
return `${ms.toFixed(2)} ms`;
|
|
10091
|
+
}
|
|
10092
|
+
if (ms < 1e3) {
|
|
10093
|
+
return `${ms.toFixed(1)} ms`;
|
|
10094
|
+
}
|
|
10095
|
+
return `${(ms / 1e3).toFixed(2)} s`;
|
|
10096
|
+
}
|
|
10097
|
+
function formatNumber(value) {
|
|
10098
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
10099
|
+
}
|
|
10100
|
+
|
|
10101
|
+
// src/formatters/terminal.ts
|
|
10102
|
+
init_esm_shims();
|
|
10103
|
+
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
10104
|
+
var TOP_SELECTOR_LIMIT = 5;
|
|
10105
|
+
function terminalExitCode(report) {
|
|
10106
|
+
return report.success ? 0 : 1;
|
|
10107
|
+
}
|
|
10108
|
+
function formatTerminalReport(report, options = {}) {
|
|
10109
|
+
const colors = import_picocolors2.default.createColors(
|
|
10110
|
+
options.color ?? import_picocolors2.default.isColorSupported
|
|
10111
|
+
);
|
|
10112
|
+
const lines = [];
|
|
10113
|
+
const separator = colors.dim("\u2500".repeat(64));
|
|
10114
|
+
lines.push(
|
|
10115
|
+
`${colors.bold(colors.cyan("css-doctor"))} ${colors.dim(`v${report.version}`)} ${colors.dim("\xB7")} ${colors.dim("check")}`
|
|
10116
|
+
);
|
|
10117
|
+
lines.push(
|
|
10118
|
+
`${colors.dim("patterns")} ${report.patterns.length > 0 ? report.patterns.join(", ") : "(none)"}`
|
|
10119
|
+
);
|
|
10120
|
+
lines.push(separator);
|
|
10121
|
+
if (report.errors.length > 0) {
|
|
10122
|
+
lines.push(colors.red(colors.bold("ERRORS")));
|
|
10123
|
+
for (const error of report.errors) {
|
|
10124
|
+
lines.push(` ${colors.red("\u2716")} ${error}`);
|
|
10125
|
+
}
|
|
10126
|
+
lines.push("");
|
|
10127
|
+
}
|
|
10128
|
+
for (const file of report.files) {
|
|
10129
|
+
lines.push(...formatFile(file, colors));
|
|
10130
|
+
}
|
|
10131
|
+
if (report.files.length === 0 && report.errors.length === 0) {
|
|
10132
|
+
lines.push(colors.yellow("No files were analyzed."));
|
|
10133
|
+
lines.push("");
|
|
10134
|
+
}
|
|
10135
|
+
lines.push(separator);
|
|
10136
|
+
lines.push(...formatSummary(report, colors));
|
|
10137
|
+
return lines.join("\n");
|
|
10138
|
+
}
|
|
10139
|
+
function formatFile(file, colors) {
|
|
10140
|
+
const status = fileStatus(file);
|
|
10141
|
+
const lines = [];
|
|
10142
|
+
lines.push(
|
|
10143
|
+
`${statusBadge(status, colors)} ${colors.bold(file.path)} ${colors.dim(`\xB7 ${formatBytes2(file.sizeBytes)}`)}`
|
|
10144
|
+
);
|
|
10145
|
+
if (!file.ok) {
|
|
10146
|
+
lines.push(` ${colors.red(file.error ?? "Unknown error")}`);
|
|
10147
|
+
lines.push("");
|
|
9095
10148
|
return lines;
|
|
9096
10149
|
}
|
|
9097
|
-
const
|
|
10150
|
+
const result = file.result;
|
|
10151
|
+
if (!result) {
|
|
10152
|
+
lines.push(` ${colors.red("No analysis result was returned.")}`);
|
|
10153
|
+
lines.push("");
|
|
10154
|
+
return lines;
|
|
10155
|
+
}
|
|
10156
|
+
const { stats } = result;
|
|
10157
|
+
lines.push(
|
|
10158
|
+
` ${colors.dim("selectors")} ${stats.selectorCount} ${colors.dim("\xB7")} ${colors.dim("rules")} ${stats.ruleCount} ${colors.dim("\xB7")} ${colors.dim("declarations")} ${stats.declarationCount + stats.importantDeclarationCount}`
|
|
10159
|
+
);
|
|
10160
|
+
lines.push(
|
|
10161
|
+
` ${colors.dim("analysis")} ${formatDuration2(file.analysisMs)} ${colors.dim("\xB7")} ${colors.dim("read")} ${formatDuration2(file.readMs)} ${colors.dim("\xB7")} ${colors.dim("total")} ${formatDuration2(file.totalMs)}`
|
|
10162
|
+
);
|
|
10163
|
+
if (file.issues.length > 0) {
|
|
10164
|
+
lines.push("");
|
|
10165
|
+
lines.push(
|
|
10166
|
+
` ${colors.yellow("\u26A0")} ${colors.bold("Diagnostics")} ${colors.dim(`(${file.issues.length})`)}`
|
|
10167
|
+
);
|
|
10168
|
+
for (const issue of file.issues) {
|
|
10169
|
+
lines.push(` ${formatIssue(issue, colors)}`);
|
|
10170
|
+
}
|
|
10171
|
+
}
|
|
10172
|
+
if (result.selectors.length > 0) {
|
|
10173
|
+
lines.push("");
|
|
10174
|
+
lines.push(...formatTopSelectors(result, colors));
|
|
10175
|
+
}
|
|
10176
|
+
lines.push("");
|
|
10177
|
+
return lines;
|
|
10178
|
+
}
|
|
10179
|
+
function formatTopSelectors(result, colors) {
|
|
10180
|
+
const top = topSelectors(result.selectors, TOP_SELECTOR_LIMIT);
|
|
10181
|
+
const total = result.selectors.length;
|
|
10182
|
+
const heading = total > top.length ? `Top selectors (${top.length} of ${total})` : `Top selectors (${total})`;
|
|
10183
|
+
const lines = [` ${colors.bold(heading)}`];
|
|
10184
|
+
top.forEach((selector, index) => {
|
|
10185
|
+
const frequency = selector.count > 1 ? ` ${colors.yellow(`\xD7${selector.count}`)}` : "";
|
|
10186
|
+
lines.push(
|
|
10187
|
+
` ${colors.dim(`${String(index + 1).padStart(2, " ")}.`)} ${selector.raw}${frequency} ${colors.dim(`(${selector.specificity})`)}`
|
|
10188
|
+
);
|
|
10189
|
+
});
|
|
10190
|
+
return lines;
|
|
10191
|
+
}
|
|
10192
|
+
function topSelectors(selectors, limit) {
|
|
10193
|
+
const entries = /* @__PURE__ */ new Map();
|
|
10194
|
+
selectors.forEach((selector, index) => {
|
|
10195
|
+
const existing = entries.get(selector.raw);
|
|
10196
|
+
if (existing) {
|
|
10197
|
+
existing.count += 1;
|
|
10198
|
+
return;
|
|
10199
|
+
}
|
|
10200
|
+
entries.set(selector.raw, {
|
|
10201
|
+
raw: selector.raw,
|
|
10202
|
+
count: 1,
|
|
10203
|
+
firstIndex: index,
|
|
10204
|
+
specificity: `${selector.specificity.ids},${selector.specificity.classes},${selector.specificity.types}`
|
|
10205
|
+
});
|
|
10206
|
+
});
|
|
10207
|
+
return [...entries.values()].sort(
|
|
10208
|
+
(left, right) => right.count - left.count || left.firstIndex - right.firstIndex
|
|
10209
|
+
).slice(0, limit);
|
|
10210
|
+
}
|
|
10211
|
+
function formatIssue(issue, colors) {
|
|
10212
|
+
const location = issue.location ? `${issue.location.line}:${issue.location.column}` : "\u2014";
|
|
10213
|
+
const tag = issue.kind === "syntax" ? colors.yellow("syntax") : issue.kind === "threshold" ? colors.yellow("threshold") : issue.kind === "empty-rule" ? colors.magenta("dead code") : colors.magenta("duplicate");
|
|
10214
|
+
return `${colors.dim(location.padEnd(7))} ${tag} ${issue.message}`;
|
|
10215
|
+
}
|
|
10216
|
+
function formatSummary(report, colors) {
|
|
10217
|
+
const { summary } = report;
|
|
10218
|
+
const status = reportStatus(report);
|
|
10219
|
+
const lines = [];
|
|
10220
|
+
lines.push(
|
|
10221
|
+
`${statusBadge(status, colors)} ${summary.files} file${summary.files === 1 ? "" : "s"} ${colors.dim("\xB7")} ${formatBytes2(summary.totalSizeBytes)} ${colors.dim("\xB7")} ${summary.selectors} selectors ${colors.dim("\xB7")} ${formatDuration2(summary.totalMs)}`
|
|
10222
|
+
);
|
|
10223
|
+
lines.push(
|
|
10224
|
+
` ${colors.dim("analyzed")} ${summary.analyzed} ${colors.dim("\xB7")} ${colors.dim("failed")} ${summary.failed} ${colors.dim("\xB7")} ${colors.dim("warnings")} ${summary.warnings} ${colors.dim("\xB7")} ${colors.dim("hints")} ${summary.hints}`
|
|
10225
|
+
);
|
|
10226
|
+
lines.push(
|
|
10227
|
+
` ${colors.dim("rules")} ${summary.rules} ${colors.dim("\xB7")} ${colors.dim("declarations")} ${summary.declarations} ${colors.dim("\xB7")} ${colors.dim("analysis")} ${formatDuration2(summary.analysisMs)}`
|
|
10228
|
+
);
|
|
10229
|
+
if (report.threshold !== null) {
|
|
10230
|
+
lines.push(` ${colors.dim("selector threshold")} ${report.threshold}`);
|
|
10231
|
+
}
|
|
10232
|
+
return lines;
|
|
10233
|
+
}
|
|
10234
|
+
function fileStatus(file) {
|
|
10235
|
+
if (!file.ok) {
|
|
10236
|
+
return "ERROR";
|
|
10237
|
+
}
|
|
10238
|
+
return file.issues.length > 0 ? "WARNING" : "SUCCESS";
|
|
10239
|
+
}
|
|
10240
|
+
function reportStatus(report) {
|
|
10241
|
+
if (!report.success || report.errors.length > 0 || report.summary.failed > 0) {
|
|
10242
|
+
return "ERROR";
|
|
10243
|
+
}
|
|
10244
|
+
return report.summary.issues > 0 ? "WARNING" : "SUCCESS";
|
|
10245
|
+
}
|
|
10246
|
+
function statusBadge(status, colors) {
|
|
10247
|
+
switch (status) {
|
|
10248
|
+
case "SUCCESS":
|
|
10249
|
+
return colors.bgGreen(colors.black(" SUCCESS "));
|
|
10250
|
+
case "WARNING":
|
|
10251
|
+
return colors.bgYellow(colors.black(" WARNING "));
|
|
10252
|
+
case "ERROR":
|
|
10253
|
+
return colors.bgRed(colors.white(" ERROR "));
|
|
10254
|
+
}
|
|
10255
|
+
}
|
|
10256
|
+
function formatBytes2(bytes) {
|
|
10257
|
+
if (bytes < 1024) {
|
|
10258
|
+
return `${bytes} B`;
|
|
10259
|
+
}
|
|
10260
|
+
if (bytes < 1024 * 1024) {
|
|
10261
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
10262
|
+
}
|
|
10263
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
10264
|
+
}
|
|
10265
|
+
function formatDuration2(milliseconds) {
|
|
10266
|
+
if (!Number.isFinite(milliseconds)) {
|
|
10267
|
+
return "\u2014";
|
|
10268
|
+
}
|
|
10269
|
+
return milliseconds < 1 ? `${milliseconds.toFixed(2)} ms` : `${milliseconds.toFixed(1)} ms`;
|
|
10270
|
+
}
|
|
10271
|
+
|
|
10272
|
+
// src/commands/check.ts
|
|
10273
|
+
var require3 = createRequire2(import.meta.url);
|
|
10274
|
+
var cachedBinding2;
|
|
10275
|
+
function loadBinding2() {
|
|
10276
|
+
if (cachedBinding2) {
|
|
10277
|
+
return cachedBinding2;
|
|
10278
|
+
}
|
|
10279
|
+
try {
|
|
10280
|
+
cachedBinding2 = require3("@zealakit/css-doctor-binding");
|
|
10281
|
+
return cachedBinding2;
|
|
10282
|
+
} catch (error) {
|
|
10283
|
+
throw new Error(
|
|
10284
|
+
"Unable to load @zealakit/css-doctor-binding. Build it first with `pnpm --filter @zealakit/css-doctor-binding build`.\n" + describeError(error)
|
|
10285
|
+
);
|
|
10286
|
+
}
|
|
10287
|
+
}
|
|
10288
|
+
function registerCheckCommand(program3) {
|
|
10289
|
+
program3.command("check").description("Analyze CSS files and report diagnostics").argument(
|
|
10290
|
+
"[patterns...]",
|
|
10291
|
+
"CSS files or glob patterns (quote globs to let css-doctor expand them)"
|
|
10292
|
+
).option("-j, --json", "Output raw JSON for piping and CI/CD").option(
|
|
10293
|
+
"--threshold <number>",
|
|
10294
|
+
"Warn when a file contains more than <number> selectors",
|
|
10295
|
+
parseThreshold
|
|
10296
|
+
).option(
|
|
10297
|
+
"--fail-under <number>",
|
|
10298
|
+
"Exit with code 1 when overall health score is below <number> (0-100)",
|
|
10299
|
+
parseScore
|
|
10300
|
+
).action(async (patterns, options) => {
|
|
10301
|
+
const report = await runCheck(patterns ?? [], {
|
|
10302
|
+
threshold: options.threshold,
|
|
10303
|
+
failUnder: options.failUnder
|
|
10304
|
+
});
|
|
10305
|
+
if (options.json === true) {
|
|
10306
|
+
process.stdout.write(`${formatJsonReport(report)}
|
|
10307
|
+
`);
|
|
10308
|
+
} else {
|
|
10309
|
+
let output = formatTerminalReport(report);
|
|
10310
|
+
if (report.health) {
|
|
10311
|
+
output += `
|
|
10312
|
+
${formatCompactHealthSummary(report.health)}
|
|
10313
|
+
`;
|
|
10314
|
+
}
|
|
10315
|
+
process.stdout.write(`${output}
|
|
10316
|
+
`);
|
|
10317
|
+
}
|
|
10318
|
+
process.exitCode = terminalExitCode(report);
|
|
10319
|
+
});
|
|
10320
|
+
}
|
|
10321
|
+
function parseThreshold(value) {
|
|
10322
|
+
const parsed = Number(value);
|
|
10323
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
10324
|
+
throw new InvalidArgumentError("threshold must be a non-negative integer");
|
|
10325
|
+
}
|
|
10326
|
+
return parsed;
|
|
10327
|
+
}
|
|
10328
|
+
function parseScore(value) {
|
|
10329
|
+
const parsed = Number(value);
|
|
10330
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) {
|
|
10331
|
+
throw new InvalidArgumentError("must be an integer between 0 and 100");
|
|
10332
|
+
}
|
|
10333
|
+
return parsed;
|
|
10334
|
+
}
|
|
10335
|
+
async function runCheck(patterns, options = {}) {
|
|
10336
|
+
const startedAt = performance.now();
|
|
10337
|
+
const cwd = resolve4(options.cwd ?? process.cwd());
|
|
10338
|
+
const threshold = options.threshold ?? null;
|
|
10339
|
+
const failUnder = options.failUnder ?? null;
|
|
10340
|
+
const errors = [];
|
|
10341
|
+
const results = [];
|
|
10342
|
+
let files = [];
|
|
10343
|
+
if (threshold !== null && (!Number.isInteger(threshold) || threshold < 0)) {
|
|
10344
|
+
errors.push("Threshold must be a non-negative integer.");
|
|
10345
|
+
}
|
|
10346
|
+
if (failUnder !== null && (failUnder < 0 || failUnder > 100)) {
|
|
10347
|
+
errors.push("--fail-under must be an integer between 0 and 100.");
|
|
10348
|
+
}
|
|
10349
|
+
if (patterns.length === 0) {
|
|
10350
|
+
errors.push(
|
|
10351
|
+
'No CSS files or glob patterns were provided. Example: css-doctor check "src/**/*.css"'
|
|
10352
|
+
);
|
|
10353
|
+
} else if (!await isDirectory(cwd)) {
|
|
10354
|
+
errors.push(
|
|
10355
|
+
`Working directory does not exist or is not a directory: ${cwd}`
|
|
10356
|
+
);
|
|
10357
|
+
} else {
|
|
10358
|
+
try {
|
|
10359
|
+
files = await resolveCssFiles(patterns, cwd);
|
|
10360
|
+
if (files.length === 0) {
|
|
10361
|
+
errors.push(`No CSS files matched: ${patterns.join(", ")}`);
|
|
10362
|
+
}
|
|
10363
|
+
} catch (error) {
|
|
10364
|
+
errors.push(`Failed to resolve CSS files: ${describeError(error)}`);
|
|
10365
|
+
}
|
|
10366
|
+
}
|
|
10367
|
+
let health = null;
|
|
10368
|
+
if (errors.length === 0) {
|
|
10369
|
+
try {
|
|
10370
|
+
const binding = loadBinding2();
|
|
10371
|
+
const analyze = binding.analyzeCss;
|
|
10372
|
+
for (const file of files) {
|
|
10373
|
+
results.push(await checkFile(file, cwd, threshold, analyze));
|
|
10374
|
+
}
|
|
10375
|
+
const healthFiles = [];
|
|
10376
|
+
for (const file of files) {
|
|
10377
|
+
const read = await readCssFile(file);
|
|
10378
|
+
if (!read.ok) {
|
|
10379
|
+
continue;
|
|
10380
|
+
}
|
|
10381
|
+
healthFiles.push({
|
|
10382
|
+
cssContent: read.content,
|
|
10383
|
+
filePath: toDisplayPath(file, cwd),
|
|
10384
|
+
sizeBytes: read.sizeBytes
|
|
10385
|
+
});
|
|
10386
|
+
}
|
|
10387
|
+
if (healthFiles.length > 0) {
|
|
10388
|
+
health = binding.evaluateCssHealthBatch({
|
|
10389
|
+
files: healthFiles,
|
|
10390
|
+
thresholds: threshold === null ? void 0 : {
|
|
10391
|
+
manySelectors: threshold
|
|
10392
|
+
}
|
|
10393
|
+
});
|
|
10394
|
+
}
|
|
10395
|
+
} catch (error) {
|
|
10396
|
+
errors.push(describeError(error));
|
|
10397
|
+
}
|
|
10398
|
+
}
|
|
10399
|
+
const issues = results.flatMap((file) => file.issues);
|
|
10400
|
+
const warnings = issues.filter((issue) => issue.kind === "syntax").length;
|
|
10401
|
+
const analyzed = results.filter((file) => file.ok).length;
|
|
10402
|
+
const failed = results.length - analyzed;
|
|
10403
|
+
const summary = {
|
|
10404
|
+
files: results.length,
|
|
10405
|
+
analyzed,
|
|
10406
|
+
failed,
|
|
10407
|
+
filesWithIssues: results.filter((file) => file.issues.length > 0).length,
|
|
10408
|
+
totalSizeBytes: results.reduce((total, file) => total + file.sizeBytes, 0),
|
|
10409
|
+
rules: sum(results, (file) => file.result?.stats.ruleCount ?? 0),
|
|
10410
|
+
selectors: sum(results, (file) => file.result?.stats.selectorCount ?? 0),
|
|
10411
|
+
declarations: sum(
|
|
10412
|
+
results,
|
|
10413
|
+
(file) => (file.result?.stats.declarationCount ?? 0) + (file.result?.stats.importantDeclarationCount ?? 0)
|
|
10414
|
+
),
|
|
10415
|
+
warnings,
|
|
10416
|
+
hints: issues.length - warnings,
|
|
10417
|
+
issues: issues.length,
|
|
10418
|
+
analysisMs: roundMs(
|
|
10419
|
+
results.reduce((total, file) => total + file.analysisMs, 0)
|
|
10420
|
+
),
|
|
10421
|
+
totalMs: elapsedMs(startedAt)
|
|
10422
|
+
};
|
|
10423
|
+
const belowFloor = failUnder !== null && health !== null && health.overall < failUnder ? [
|
|
10424
|
+
`Overall health score ${health.overall} is below --fail-under ${failUnder}.`
|
|
10425
|
+
] : [];
|
|
10426
|
+
return {
|
|
10427
|
+
schemaVersion: 1,
|
|
10428
|
+
command: "check",
|
|
10429
|
+
version: package_default.version,
|
|
10430
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10431
|
+
cwd,
|
|
10432
|
+
patterns,
|
|
10433
|
+
threshold,
|
|
10434
|
+
failUnder,
|
|
10435
|
+
success: errors.length === 0 && failed === 0 && belowFloor.length === 0,
|
|
10436
|
+
errors: [...errors, ...belowFloor],
|
|
10437
|
+
files: results,
|
|
10438
|
+
summary,
|
|
10439
|
+
health
|
|
10440
|
+
};
|
|
10441
|
+
}
|
|
10442
|
+
async function checkFile(filePath, cwd, threshold, analyze) {
|
|
10443
|
+
const path2 = toDisplayPath(filePath, cwd);
|
|
10444
|
+
const readStartedAt = performance.now();
|
|
10445
|
+
const read = await readCssFile(filePath);
|
|
10446
|
+
const readMs = elapsedMs(readStartedAt);
|
|
10447
|
+
if (!read.ok) {
|
|
10448
|
+
return {
|
|
10449
|
+
path: path2,
|
|
10450
|
+
absolutePath: filePath,
|
|
10451
|
+
sizeBytes: 0,
|
|
10452
|
+
ok: false,
|
|
10453
|
+
totalMs: readMs,
|
|
10454
|
+
readMs,
|
|
10455
|
+
analysisMs: 0,
|
|
10456
|
+
issues: [],
|
|
10457
|
+
error: read.error
|
|
10458
|
+
};
|
|
10459
|
+
}
|
|
10460
|
+
const analysisStartedAt = performance.now();
|
|
10461
|
+
try {
|
|
10462
|
+
const result = analyze(read.content);
|
|
10463
|
+
const analysisMs = elapsedMs(analysisStartedAt);
|
|
10464
|
+
return {
|
|
10465
|
+
path: path2,
|
|
10466
|
+
absolutePath: filePath,
|
|
10467
|
+
sizeBytes: read.sizeBytes,
|
|
10468
|
+
ok: true,
|
|
10469
|
+
totalMs: roundMs(readMs + analysisMs),
|
|
10470
|
+
readMs,
|
|
10471
|
+
analysisMs,
|
|
10472
|
+
issues: collectIssues(result, threshold),
|
|
10473
|
+
result
|
|
10474
|
+
};
|
|
10475
|
+
} catch (error) {
|
|
10476
|
+
const analysisMs = elapsedMs(analysisStartedAt);
|
|
10477
|
+
return {
|
|
10478
|
+
path: path2,
|
|
10479
|
+
absolutePath: filePath,
|
|
10480
|
+
sizeBytes: read.sizeBytes,
|
|
10481
|
+
ok: false,
|
|
10482
|
+
totalMs: roundMs(readMs + analysisMs),
|
|
10483
|
+
readMs,
|
|
10484
|
+
analysisMs,
|
|
10485
|
+
issues: [],
|
|
10486
|
+
error: `Failed to analyze file: ${describeError(error)}`
|
|
10487
|
+
};
|
|
10488
|
+
}
|
|
10489
|
+
}
|
|
10490
|
+
function collectIssues(result, threshold) {
|
|
10491
|
+
const issues = [];
|
|
10492
|
+
for (const warning of result.warnings) {
|
|
10493
|
+
issues.push({
|
|
10494
|
+
kind: "syntax",
|
|
10495
|
+
severity: "warning",
|
|
10496
|
+
message: warning.message,
|
|
10497
|
+
...warning.location ? { location: warning.location } : {}
|
|
10498
|
+
});
|
|
10499
|
+
}
|
|
10500
|
+
const parentRuleIndices = new Set(
|
|
10501
|
+
result.rules.map((rule) => rule.parentRuleIndex).filter((index) => index !== void 0)
|
|
10502
|
+
);
|
|
10503
|
+
for (const rule of result.rules) {
|
|
10504
|
+
const hasNoDeclarations = rule.declarationCount === 0 && rule.importantDeclarationCount === 0;
|
|
10505
|
+
const hasNoNestedRules = !parentRuleIndices.has(rule.index);
|
|
10506
|
+
if (hasNoDeclarations && hasNoNestedRules) {
|
|
10507
|
+
const selectorText = rule.selectorIndices.map((index) => result.selectors[index]?.raw).filter((value) => value !== void 0).join(", ");
|
|
10508
|
+
issues.push({
|
|
10509
|
+
kind: "empty-rule",
|
|
10510
|
+
severity: "warning",
|
|
10511
|
+
message: selectorText.length > 0 ? `Rule "${selectorText}" has no declarations and no nested rules (dead code candidate).` : "Rule has no declarations and no nested rules (dead code candidate).",
|
|
10512
|
+
location: rule.location,
|
|
10513
|
+
...selectorText.length > 0 ? { selector: selectorText } : {}
|
|
10514
|
+
});
|
|
10515
|
+
}
|
|
10516
|
+
}
|
|
10517
|
+
const seenSelectors = /* @__PURE__ */ new Map();
|
|
10518
|
+
for (const selector of result.selectors) {
|
|
10519
|
+
const first = seenSelectors.get(selector.raw);
|
|
10520
|
+
if (!first) {
|
|
10521
|
+
seenSelectors.set(selector.raw, selector);
|
|
10522
|
+
continue;
|
|
10523
|
+
}
|
|
10524
|
+
const firstRule = result.rules[first.ruleIndex];
|
|
10525
|
+
const currentRule = result.rules[selector.ruleIndex];
|
|
10526
|
+
issues.push({
|
|
10527
|
+
kind: "duplicate-selector",
|
|
10528
|
+
severity: "warning",
|
|
10529
|
+
message: `Selector "${selector.raw}" is defined in multiple rules` + (firstRule ? ` (first at line ${firstRule.location.line})` : "") + "; verify whether this is intentional.",
|
|
10530
|
+
...currentRule ? { location: currentRule.location } : {},
|
|
10531
|
+
selector: selector.raw
|
|
10532
|
+
});
|
|
10533
|
+
}
|
|
10534
|
+
if (threshold !== null && result.selectors.length > threshold) {
|
|
10535
|
+
issues.push({
|
|
10536
|
+
kind: "threshold",
|
|
10537
|
+
severity: "warning",
|
|
10538
|
+
message: `Selector threshold exceeded: ${result.selectors.length} selectors found (threshold: ${threshold}).`
|
|
10539
|
+
});
|
|
10540
|
+
}
|
|
10541
|
+
return issues.sort(compareIssues);
|
|
10542
|
+
}
|
|
10543
|
+
function compareIssues(left, right) {
|
|
10544
|
+
const leftLine = left.location?.line ?? Number.MAX_SAFE_INTEGER;
|
|
10545
|
+
const rightLine = right.location?.line ?? Number.MAX_SAFE_INTEGER;
|
|
10546
|
+
const lineDifference = leftLine - rightLine;
|
|
10547
|
+
if (lineDifference !== 0) {
|
|
10548
|
+
return lineDifference;
|
|
10549
|
+
}
|
|
10550
|
+
const columnDifference = (left.location?.column ?? 0) - (right.location?.column ?? 0);
|
|
10551
|
+
if (columnDifference !== 0) {
|
|
10552
|
+
return columnDifference;
|
|
10553
|
+
}
|
|
10554
|
+
return left.kind.localeCompare(right.kind);
|
|
10555
|
+
}
|
|
10556
|
+
function sum(files, select) {
|
|
10557
|
+
return files.reduce((total, file) => total + select(file), 0);
|
|
10558
|
+
}
|
|
10559
|
+
function elapsedMs(startedAt) {
|
|
10560
|
+
return roundMs(performance.now() - startedAt);
|
|
10561
|
+
}
|
|
10562
|
+
function roundMs(value) {
|
|
10563
|
+
return Math.round(value * 1e3) / 1e3;
|
|
10564
|
+
}
|
|
10565
|
+
|
|
10566
|
+
// src/commands/diff.ts
|
|
10567
|
+
init_esm_shims();
|
|
10568
|
+
import { resolve as resolve5 } from "path";
|
|
10569
|
+
|
|
10570
|
+
// src/formatters/regression-terminal.ts
|
|
10571
|
+
init_esm_shims();
|
|
10572
|
+
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
10573
|
+
function formatRegressionTerminalReport(report, options = {}) {
|
|
10574
|
+
const colors = import_picocolors3.default.createColors(
|
|
10575
|
+
options.color ?? import_picocolors3.default.isColorSupported
|
|
10576
|
+
);
|
|
10577
|
+
const columns = Math.max(
|
|
10578
|
+
48,
|
|
10579
|
+
Math.min(options.columns ?? detectColumns2(), 88)
|
|
10580
|
+
);
|
|
10581
|
+
const verbose = options.verbose === true;
|
|
10582
|
+
const lines = [];
|
|
10583
|
+
const rule = colors.dim("\u2500".repeat(Math.min(columns, 64)));
|
|
10584
|
+
lines.push("");
|
|
10585
|
+
lines.push(
|
|
10586
|
+
` ${colors.bold(colors.cyan("CSS DOCTOR"))}${pad2(
|
|
10587
|
+
columns - 28,
|
|
10588
|
+
""
|
|
10589
|
+
)}${colors.dim("REGRESSION REPORT")}`
|
|
10590
|
+
);
|
|
10591
|
+
lines.push("");
|
|
10592
|
+
lines.push(` ${colors.bold("Overall Health")}`);
|
|
10593
|
+
lines.push("");
|
|
10594
|
+
lines.push(
|
|
10595
|
+
` ${scoreColor2(
|
|
10596
|
+
report.current.score,
|
|
10597
|
+
colors
|
|
10598
|
+
)(
|
|
10599
|
+
colors.bold(`${report.current.score}/100`)
|
|
10600
|
+
)} ${colors.bold(report.current.grade)} ${statusWord(
|
|
10601
|
+
report.current.status,
|
|
10602
|
+
colors
|
|
10603
|
+
)}`
|
|
10604
|
+
);
|
|
10605
|
+
lines.push("");
|
|
10606
|
+
lines.push(` ${label("Baseline").padEnd(14)}${report.baseline.score}/100`);
|
|
10607
|
+
lines.push(` ${label("Current").padEnd(14)}${report.current.score}/100`);
|
|
10608
|
+
lines.push(
|
|
10609
|
+
` ${label("Change").padEnd(14)}${formatSigned(
|
|
10610
|
+
report.changes.score,
|
|
10611
|
+
"points",
|
|
10612
|
+
colors
|
|
10613
|
+
)}`
|
|
10614
|
+
);
|
|
10615
|
+
lines.push("");
|
|
10616
|
+
lines.push(` ${statusBanner(report.status, colors)}`);
|
|
10617
|
+
lines.push("");
|
|
10618
|
+
lines.push(
|
|
10619
|
+
` ${colors.dim(
|
|
10620
|
+
`Analyzed ${report.current.files} files \xB7 ${formatNumber2(report.current.rules)} rules \xB7 ${formatNumber2(report.current.declarations)} declarations`
|
|
10621
|
+
)}`
|
|
10622
|
+
);
|
|
10623
|
+
if (report.warnings.length > 0) {
|
|
10624
|
+
lines.push("");
|
|
10625
|
+
for (const warning of report.warnings) {
|
|
10626
|
+
lines.push(` ${colors.yellow("!")} ${colors.dim(warning)}`);
|
|
10627
|
+
}
|
|
10628
|
+
}
|
|
10629
|
+
lines.push("");
|
|
10630
|
+
lines.push(rule);
|
|
10631
|
+
lines.push("");
|
|
10632
|
+
lines.push(` ${colors.bold("DIMENSION COMPARISON")}`);
|
|
10633
|
+
lines.push("");
|
|
9098
10634
|
lines.push(
|
|
9099
|
-
` ${
|
|
10635
|
+
` ${"Dimension".padEnd(26)}${"Baseline".padStart(8)} ${"Current".padStart(8)} ${"Change".padStart(7)}`
|
|
9100
10636
|
);
|
|
10637
|
+
lines.push("");
|
|
10638
|
+
for (const dimension of report.changes.dimensions) {
|
|
10639
|
+
lines.push(` ${formatDimensionRow(dimension, colors)}`);
|
|
10640
|
+
}
|
|
10641
|
+
lines.push("");
|
|
10642
|
+
lines.push(rule);
|
|
10643
|
+
lines.push("");
|
|
10644
|
+
lines.push(` ${colors.bold("CSS SIZE ANALYSIS")}`);
|
|
10645
|
+
lines.push("");
|
|
9101
10646
|
lines.push(
|
|
9102
|
-
` ${
|
|
10647
|
+
` ${formatMetricLine(
|
|
10648
|
+
"Total size",
|
|
10649
|
+
formatBytes3(report.changes.cssSize.baseline),
|
|
10650
|
+
formatBytes3(report.changes.cssSize.current),
|
|
10651
|
+
report.changes.cssSize.changePercent,
|
|
10652
|
+
colors
|
|
10653
|
+
)}`
|
|
9103
10654
|
);
|
|
9104
|
-
|
|
10655
|
+
lines.push(
|
|
10656
|
+
` ${formatMetricLine(
|
|
10657
|
+
"Rules",
|
|
10658
|
+
formatNumber2(report.changes.rules.baseline),
|
|
10659
|
+
formatNumber2(report.changes.rules.current),
|
|
10660
|
+
report.changes.rules.changePercent,
|
|
10661
|
+
colors
|
|
10662
|
+
)}`
|
|
10663
|
+
);
|
|
10664
|
+
lines.push(
|
|
10665
|
+
` ${formatMetricLine(
|
|
10666
|
+
"Declarations",
|
|
10667
|
+
formatNumber2(report.changes.declarations.baseline),
|
|
10668
|
+
formatNumber2(report.changes.declarations.current),
|
|
10669
|
+
report.changes.declarations.changePercent,
|
|
10670
|
+
colors
|
|
10671
|
+
)}`
|
|
10672
|
+
);
|
|
10673
|
+
const largest = [...report.files.modified].filter((file) => (file.sizeChangePercent ?? 0) > 0).sort(
|
|
10674
|
+
(left, right) => (right.sizeChangePercent ?? 0) - (left.sizeChangePercent ?? 0)
|
|
10675
|
+
).slice(0, verbose ? 10 : 3);
|
|
10676
|
+
if (largest.length > 0) {
|
|
10677
|
+
lines.push("");
|
|
10678
|
+
lines.push(` ${colors.bold("LARGEST INCREASES")}`);
|
|
10679
|
+
lines.push("");
|
|
10680
|
+
for (const file of largest) {
|
|
10681
|
+
lines.push(` ${file.path}`);
|
|
10682
|
+
lines.push(
|
|
10683
|
+
` ${formatBytes3(file.baselineSizeBytes ?? 0)} \u2192 ${formatBytes3(
|
|
10684
|
+
file.currentSizeBytes ?? 0
|
|
10685
|
+
)} ${formatPercent(file.sizeChangePercent, colors)}`
|
|
10686
|
+
);
|
|
10687
|
+
lines.push("");
|
|
10688
|
+
}
|
|
10689
|
+
}
|
|
10690
|
+
lines.push(rule);
|
|
10691
|
+
lines.push("");
|
|
10692
|
+
lines.push(` ${colors.bold("DIAGNOSTIC CHANGES")}`);
|
|
10693
|
+
lines.push("");
|
|
10694
|
+
lines.push(
|
|
10695
|
+
` ${colors.red("+")} ${report.diagnostics.new.length} New issues`
|
|
10696
|
+
);
|
|
10697
|
+
lines.push(
|
|
10698
|
+
` ${colors.green("-")} ${report.diagnostics.resolved.length} Resolved issues`
|
|
10699
|
+
);
|
|
10700
|
+
lines.push(
|
|
10701
|
+
` ${colors.dim("=")} ${report.diagnostics.persistent.length} Persistent issues`
|
|
10702
|
+
);
|
|
10703
|
+
const newIssues = report.diagnostics.new.slice(0, verbose ? 20 : 5);
|
|
10704
|
+
if (newIssues.length > 0) {
|
|
10705
|
+
lines.push("");
|
|
10706
|
+
lines.push(` ${colors.bold("NEW ISSUES")}`);
|
|
10707
|
+
lines.push("");
|
|
10708
|
+
for (const diagnostic of newIssues) {
|
|
10709
|
+
const marker = diagnostic.severity === "error" ? colors.red("\u2716") : colors.yellow("\u25B2");
|
|
10710
|
+
lines.push(` ${marker} ${colors.bold(titleFromCode2(diagnostic.code))}`);
|
|
10711
|
+
const where = [diagnostic.filePath, diagnostic.selector].filter(Boolean).join(" \xB7 ");
|
|
10712
|
+
if (where) {
|
|
10713
|
+
lines.push(` ${colors.dim(where)}`);
|
|
10714
|
+
}
|
|
10715
|
+
}
|
|
10716
|
+
if (report.diagnostics.new.length > newIssues.length) {
|
|
10717
|
+
lines.push("");
|
|
10718
|
+
lines.push(
|
|
10719
|
+
` ${colors.dim(
|
|
10720
|
+
`\u2026 and ${report.diagnostics.new.length - newIssues.length} more`
|
|
10721
|
+
)}`
|
|
10722
|
+
);
|
|
10723
|
+
}
|
|
10724
|
+
}
|
|
10725
|
+
if (report.files.added.length > 0 || report.files.removed.length > 0 || report.files.regressed.length > 0) {
|
|
10726
|
+
lines.push("");
|
|
10727
|
+
lines.push(rule);
|
|
10728
|
+
lines.push("");
|
|
10729
|
+
lines.push(` ${colors.bold("FILE CHANGES")}`);
|
|
9105
10730
|
lines.push("");
|
|
9106
10731
|
lines.push(
|
|
9107
|
-
` ${
|
|
10732
|
+
` + ${report.files.added.length} added \xB7 - ${report.files.removed.length} removed \xB7 ~ ${report.files.modified.length} modified`
|
|
9108
10733
|
);
|
|
9109
|
-
|
|
9110
|
-
|
|
10734
|
+
if (verbose) {
|
|
10735
|
+
for (const file of report.files.added.slice(0, 10)) {
|
|
10736
|
+
lines.push(` ${colors.green("+")} ${file.path}`);
|
|
10737
|
+
}
|
|
10738
|
+
for (const file of report.files.removed.slice(0, 10)) {
|
|
10739
|
+
lines.push(` ${colors.red("-")} ${file.path}`);
|
|
10740
|
+
}
|
|
10741
|
+
for (const file of report.files.regressed.slice(0, 10)) {
|
|
10742
|
+
lines.push(` ${colors.yellow("~")} ${file.path} (size growth)`);
|
|
10743
|
+
}
|
|
10744
|
+
}
|
|
10745
|
+
}
|
|
10746
|
+
if (report.qualityGates && report.qualityGates.results.length > 0) {
|
|
10747
|
+
lines.push("");
|
|
10748
|
+
lines.push(rule);
|
|
10749
|
+
lines.push("");
|
|
10750
|
+
lines.push(...formatQualityGates(report.qualityGates, colors));
|
|
10751
|
+
}
|
|
10752
|
+
lines.push("");
|
|
10753
|
+
lines.push(rule);
|
|
10754
|
+
lines.push("");
|
|
10755
|
+
lines.push(` ${finalStatusLine2(report.status, colors)}`);
|
|
10756
|
+
lines.push("");
|
|
10757
|
+
return lines.join("\n");
|
|
10758
|
+
}
|
|
10759
|
+
function formatQualityGates(gates, colors) {
|
|
10760
|
+
const lines = [` ${colors.bold("QUALITY GATES")}`, ""];
|
|
10761
|
+
for (const result of gates.results) {
|
|
10762
|
+
const marker = result.passed ? colors.green("\u2713") : colors.red("\u2717");
|
|
10763
|
+
lines.push(
|
|
10764
|
+
` ${marker} ${result.label.padEnd(26)} ${colors.dim(result.detail)}`
|
|
10765
|
+
);
|
|
10766
|
+
}
|
|
10767
|
+
lines.push("");
|
|
10768
|
+
lines.push(
|
|
10769
|
+
` RESULT: ${gates.passed ? colors.green(colors.bold("PASSED")) : colors.red(colors.bold("FAILED"))}`
|
|
10770
|
+
);
|
|
10771
|
+
if (!gates.passed && gates.failureSummary) {
|
|
10772
|
+
lines.push("");
|
|
10773
|
+
lines.push(` ${gates.failureSummary}`);
|
|
10774
|
+
}
|
|
10775
|
+
return lines;
|
|
10776
|
+
}
|
|
10777
|
+
function formatDimensionRow(dimension, colors) {
|
|
10778
|
+
const labelText = dimensionLabel2(dimension.id).padEnd(26);
|
|
10779
|
+
const baseline = dimension.baseline === null || dimension.baseline === void 0 ? "n/a".padStart(8) : String(dimension.baseline).padStart(8);
|
|
10780
|
+
const current = dimension.current === null || dimension.current === void 0 ? "n/a".padStart(8) : String(dimension.current).padStart(8);
|
|
10781
|
+
const changeText = dimension.change === null || dimension.change === void 0 ? "n/a" : dimension.change > 0 ? `+${dimension.change}` : `${dimension.change}`;
|
|
10782
|
+
const meaningful = dimension.change !== null && dimension.change !== void 0 && Math.abs(dimension.change) > 1;
|
|
10783
|
+
const changeColored = dimension.change === null || dimension.change === void 0 ? colors.dim(changeText.padStart(7)) : dimension.change > 0 ? colors.green(changeText.padStart(7)) : dimension.change < 0 ? colors.red(changeText.padStart(7)) : colors.dim(changeText.padStart(7));
|
|
10784
|
+
const row = `${labelText}${baseline} ${current} ${changeColored}`;
|
|
10785
|
+
return meaningful ? row : colors.dim(
|
|
10786
|
+
`${labelText}${baseline} ${current} ${changeText.padStart(7)}`
|
|
10787
|
+
);
|
|
10788
|
+
}
|
|
10789
|
+
function formatMetricLine(labelText, baseline, current, percent, colors) {
|
|
10790
|
+
return `${labelText.padEnd(14)}${baseline} \u2192 ${current} ${formatPercent(
|
|
10791
|
+
percent,
|
|
10792
|
+
colors
|
|
10793
|
+
)}`;
|
|
10794
|
+
}
|
|
10795
|
+
function statusBanner(status, colors) {
|
|
10796
|
+
const text = `STATUS: ${status.toUpperCase()}`;
|
|
10797
|
+
switch (status) {
|
|
10798
|
+
case "healthy":
|
|
10799
|
+
case "improved":
|
|
10800
|
+
return colors.green(colors.bold(text));
|
|
10801
|
+
case "mixed":
|
|
10802
|
+
case "inconclusive":
|
|
10803
|
+
return colors.yellow(colors.bold(text));
|
|
10804
|
+
case "regressed":
|
|
10805
|
+
return colors.red(colors.bold(text));
|
|
10806
|
+
default:
|
|
10807
|
+
return colors.bold(text);
|
|
10808
|
+
}
|
|
10809
|
+
}
|
|
10810
|
+
function finalStatusLine2(status, colors) {
|
|
10811
|
+
switch (status) {
|
|
10812
|
+
case "healthy":
|
|
10813
|
+
return `${colors.green(colors.bold("HEALTHY"))} No meaningful CSS regression detected.`;
|
|
10814
|
+
case "improved":
|
|
10815
|
+
return `${colors.green(colors.bold("IMPROVED"))} CSS quality improved relative to baseline.`;
|
|
10816
|
+
case "mixed":
|
|
10817
|
+
return `${colors.yellow(colors.bold("MIXED"))} Improvements and regressions coexist.`;
|
|
10818
|
+
case "inconclusive":
|
|
10819
|
+
return `${colors.yellow(colors.bold("INCONCLUSIVE"))} Insufficient comparable data.`;
|
|
10820
|
+
case "regressed":
|
|
10821
|
+
return `${colors.red(colors.bold("REGRESSED"))} CSS quality deteriorated relative to baseline.`;
|
|
10822
|
+
default:
|
|
10823
|
+
return status;
|
|
10824
|
+
}
|
|
10825
|
+
}
|
|
10826
|
+
function scoreColor2(score, colors) {
|
|
10827
|
+
if (score >= 90) {
|
|
10828
|
+
return colors.green;
|
|
10829
|
+
}
|
|
10830
|
+
if (score >= 70) {
|
|
10831
|
+
return colors.yellow;
|
|
10832
|
+
}
|
|
10833
|
+
return colors.red;
|
|
10834
|
+
}
|
|
10835
|
+
function statusWord(status, colors) {
|
|
10836
|
+
const labelText = status.charAt(0).toUpperCase() + status.slice(1);
|
|
10837
|
+
if (status === "excellent" || status === "good") {
|
|
10838
|
+
return colors.green(labelText);
|
|
10839
|
+
}
|
|
10840
|
+
if (status === "fair") {
|
|
10841
|
+
return colors.yellow(labelText);
|
|
10842
|
+
}
|
|
10843
|
+
return colors.red(labelText);
|
|
10844
|
+
}
|
|
10845
|
+
function formatSigned(value, unit, colors) {
|
|
10846
|
+
return `${formatSignedNumber(value, colors)} ${unit}`;
|
|
10847
|
+
}
|
|
10848
|
+
function formatSignedNumber(value, colors) {
|
|
10849
|
+
const text = value > 0 ? `+${value}` : `${value}`;
|
|
10850
|
+
if (value > 0) {
|
|
10851
|
+
return colors.green(text);
|
|
10852
|
+
}
|
|
10853
|
+
if (value < 0) {
|
|
10854
|
+
return colors.red(text);
|
|
10855
|
+
}
|
|
10856
|
+
return colors.dim(text);
|
|
10857
|
+
}
|
|
10858
|
+
function formatPercent(value, colors) {
|
|
10859
|
+
if (value === null || value === void 0) {
|
|
10860
|
+
return colors.dim("n/a");
|
|
10861
|
+
}
|
|
10862
|
+
const text = `${value > 0 ? "+" : ""}${value.toFixed(1)}%`;
|
|
10863
|
+
if (value > 0) {
|
|
10864
|
+
return colors.red(text);
|
|
10865
|
+
}
|
|
10866
|
+
if (value < 0) {
|
|
10867
|
+
return colors.green(text);
|
|
10868
|
+
}
|
|
10869
|
+
return colors.dim(text);
|
|
10870
|
+
}
|
|
10871
|
+
function dimensionLabel2(id) {
|
|
10872
|
+
switch (id) {
|
|
10873
|
+
case "selectorComplexity":
|
|
10874
|
+
return "Selector Complexity";
|
|
10875
|
+
case "duplicateRules":
|
|
10876
|
+
return "Duplicate Rules";
|
|
10877
|
+
case "cssSizeEfficiency":
|
|
10878
|
+
return "CSS Size Efficiency";
|
|
10879
|
+
case "deadCss":
|
|
10880
|
+
return "Dead CSS";
|
|
10881
|
+
case "variableConsistency":
|
|
10882
|
+
return "Variable Consistency";
|
|
10883
|
+
default:
|
|
10884
|
+
return id;
|
|
10885
|
+
}
|
|
10886
|
+
}
|
|
10887
|
+
function titleFromCode2(code) {
|
|
10888
|
+
const leaf = code.split(".").pop() ?? code;
|
|
10889
|
+
return leaf.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
10890
|
+
}
|
|
10891
|
+
function detectColumns2() {
|
|
10892
|
+
const columns = process.stdout.columns;
|
|
10893
|
+
return typeof columns === "number" && columns > 0 ? columns : 80;
|
|
10894
|
+
}
|
|
10895
|
+
function pad2(width, text) {
|
|
10896
|
+
return " ".repeat(Math.max(1, width - text.length));
|
|
10897
|
+
}
|
|
10898
|
+
function label(text) {
|
|
10899
|
+
return text;
|
|
10900
|
+
}
|
|
10901
|
+
function formatBytes3(bytes) {
|
|
10902
|
+
if (bytes < 1024) {
|
|
10903
|
+
return `${bytes} B`;
|
|
10904
|
+
}
|
|
10905
|
+
if (bytes < 1024 * 1024) {
|
|
10906
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
10907
|
+
}
|
|
10908
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
10909
|
+
}
|
|
10910
|
+
function formatNumber2(value) {
|
|
10911
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
10912
|
+
}
|
|
10913
|
+
|
|
10914
|
+
// src/commands/diff.ts
|
|
10915
|
+
function registerDiffCommand(program3) {
|
|
10916
|
+
program3.command("diff").description("Compare current CSS quality against a committed baseline").argument(
|
|
10917
|
+
"[patterns...]",
|
|
10918
|
+
"CSS files or glob patterns (falls back to config.patterns or baseline patterns)"
|
|
10919
|
+
).option(
|
|
10920
|
+
"-b, --baseline <path>",
|
|
10921
|
+
`Baseline path (default: ${DEFAULT_BASELINE_PATH})`
|
|
10922
|
+
).option("--config <path>", "Project config path").option(
|
|
10923
|
+
"--threshold <number>",
|
|
10924
|
+
"Selector-count threshold for current analysis",
|
|
10925
|
+
parseNonNegativeInteger2
|
|
10926
|
+
).option("--fail-on-regression", "Exit 1 when status is regressed or mixed").option("--fail-on-new-errors", "Exit 1 when new error diagnostics appear").option(
|
|
10927
|
+
"--fail-on-new-diagnostics",
|
|
10928
|
+
"Exit 1 when any new diagnostics appear"
|
|
10929
|
+
).option(
|
|
10930
|
+
"--max-size-increase <value>",
|
|
10931
|
+
"Fail when CSS size grows by more than this percent (e.g. 10 or 10%)",
|
|
10932
|
+
parsePercent
|
|
10933
|
+
).option(
|
|
10934
|
+
"--max-new-errors <number>",
|
|
10935
|
+
"Fail when new error count exceeds this value",
|
|
10936
|
+
parseNonNegativeInteger2
|
|
10937
|
+
).option(
|
|
10938
|
+
"--max-new-warnings <number>",
|
|
10939
|
+
"Fail when new warning count exceeds this value",
|
|
10940
|
+
parseNonNegativeInteger2
|
|
10941
|
+
).option(
|
|
10942
|
+
"--min-score <number>",
|
|
10943
|
+
"Fail when current overall score is below this value",
|
|
10944
|
+
parseScore2
|
|
10945
|
+
).option(
|
|
10946
|
+
"--score-tolerance <number>",
|
|
10947
|
+
"Ignore overall/dimension score deltas within this absolute tolerance",
|
|
10948
|
+
parseNonNegativeInteger2
|
|
10949
|
+
).option("-v, --verbose", "Show detailed file and diagnostic lists").option("-j, --json", "Output raw JSON for piping and CI/CD").action(async (patterns, options) => {
|
|
10950
|
+
const report = await runDiff(patterns ?? [], options);
|
|
10951
|
+
if (options.json === true) {
|
|
10952
|
+
process.stdout.write(`${formatJsonReport(report)}
|
|
10953
|
+
`);
|
|
10954
|
+
} else if (report.regression) {
|
|
10955
|
+
process.stdout.write(
|
|
10956
|
+
`${formatRegressionTerminalReport(report.regression, {
|
|
10957
|
+
verbose: options.verbose === true
|
|
10958
|
+
})}
|
|
10959
|
+
`
|
|
10960
|
+
);
|
|
10961
|
+
if (report.errors.length > 0) {
|
|
10962
|
+
process.stderr.write(
|
|
10963
|
+
`css-doctor: ${report.errors.join("\ncss-doctor: ")}
|
|
10964
|
+
`
|
|
10965
|
+
);
|
|
10966
|
+
}
|
|
10967
|
+
} else {
|
|
10968
|
+
process.stdout.write(
|
|
10969
|
+
`css-doctor diff failed:
|
|
10970
|
+
${report.errors.map((error) => ` - ${error}`).join("\n")}
|
|
10971
|
+
`
|
|
10972
|
+
);
|
|
10973
|
+
}
|
|
10974
|
+
process.exitCode = report.success ? 0 : 1;
|
|
10975
|
+
});
|
|
10976
|
+
}
|
|
10977
|
+
async function runDiff(patterns, options = {}) {
|
|
10978
|
+
const cwd = resolve5(options.cwd ?? process.cwd());
|
|
10979
|
+
const loaded = await loadProjectConfig(cwd, options.config);
|
|
10980
|
+
const errors = [...loaded.errors];
|
|
10981
|
+
const warnings = [];
|
|
10982
|
+
const baselinePath = resolveBaselinePath(
|
|
10983
|
+
cwd,
|
|
10984
|
+
options.baseline ?? loaded.config.baseline
|
|
10985
|
+
);
|
|
10986
|
+
const displayPath = toDisplayPath(baselinePath, cwd);
|
|
10987
|
+
const stored = await readBaselineFile(baselinePath);
|
|
10988
|
+
if (!stored.ok) {
|
|
10989
|
+
return {
|
|
10990
|
+
schemaVersion: 1,
|
|
10991
|
+
command: "diff",
|
|
10992
|
+
success: false,
|
|
10993
|
+
errors: [...errors, stored.error],
|
|
10994
|
+
warnings,
|
|
10995
|
+
baselinePath: displayPath,
|
|
10996
|
+
regression: null
|
|
10997
|
+
};
|
|
10998
|
+
}
|
|
10999
|
+
try {
|
|
11000
|
+
const validation = validateBaselineJson(stored.json);
|
|
11001
|
+
if (validation.status === "incompatible") {
|
|
11002
|
+
return {
|
|
11003
|
+
schemaVersion: 1,
|
|
11004
|
+
command: "diff",
|
|
11005
|
+
success: false,
|
|
11006
|
+
errors: [
|
|
11007
|
+
...errors,
|
|
11008
|
+
validation.message ?? "Baseline is incompatible with this tool version."
|
|
11009
|
+
],
|
|
11010
|
+
warnings,
|
|
11011
|
+
baselinePath: displayPath,
|
|
11012
|
+
regression: null
|
|
11013
|
+
};
|
|
11014
|
+
}
|
|
11015
|
+
if (validation.status === "softWarning" && validation.message) {
|
|
11016
|
+
warnings.push(validation.message);
|
|
11017
|
+
}
|
|
11018
|
+
} catch (error) {
|
|
11019
|
+
return {
|
|
11020
|
+
schemaVersion: 1,
|
|
11021
|
+
command: "diff",
|
|
11022
|
+
success: false,
|
|
11023
|
+
errors: [
|
|
11024
|
+
...errors,
|
|
11025
|
+
error instanceof Error ? error.message : String(error)
|
|
11026
|
+
],
|
|
11027
|
+
warnings,
|
|
11028
|
+
baselinePath: displayPath,
|
|
11029
|
+
regression: null
|
|
11030
|
+
};
|
|
11031
|
+
}
|
|
11032
|
+
let baselinePatterns = [];
|
|
11033
|
+
try {
|
|
11034
|
+
const parsed = JSON.parse(stored.json);
|
|
11035
|
+
baselinePatterns = parsed.config?.patterns ?? [];
|
|
11036
|
+
} catch {
|
|
11037
|
+
baselinePatterns = [];
|
|
11038
|
+
}
|
|
11039
|
+
const effectivePatterns = patterns.length > 0 ? patterns : options.patterns && options.patterns.length > 0 ? options.patterns : loaded.config.patterns.length > 0 ? loaded.config.patterns : baselinePatterns;
|
|
11040
|
+
const current = await createBaselineSnapshot({
|
|
11041
|
+
cwd,
|
|
11042
|
+
patterns: effectivePatterns,
|
|
11043
|
+
threshold: options.threshold ?? loaded.config.threshold
|
|
11044
|
+
});
|
|
11045
|
+
if (!current.ok) {
|
|
11046
|
+
return {
|
|
11047
|
+
schemaVersion: 1,
|
|
11048
|
+
command: "diff",
|
|
11049
|
+
success: false,
|
|
11050
|
+
errors: [...errors, ...current.errors],
|
|
11051
|
+
warnings,
|
|
11052
|
+
baselinePath: displayPath,
|
|
11053
|
+
regression: null
|
|
11054
|
+
};
|
|
11055
|
+
}
|
|
11056
|
+
try {
|
|
11057
|
+
const binding = loadBinding();
|
|
11058
|
+
const compareOptions = {
|
|
11059
|
+
scoreTolerance: options.scoreTolerance ?? loaded.config.regression.scoreTolerance,
|
|
11060
|
+
sizeAbsoluteToleranceBytes: loaded.config.regression.sizeAbsoluteToleranceBytes,
|
|
11061
|
+
sizeIncreasePercent: loaded.config.regression.sizeIncreasePercent
|
|
11062
|
+
};
|
|
11063
|
+
const gates = buildGateConfig(options, loaded.config);
|
|
11064
|
+
const regressionJson = binding.compareBaselines(
|
|
11065
|
+
stored.json,
|
|
11066
|
+
current.json,
|
|
11067
|
+
compareOptions,
|
|
11068
|
+
gates
|
|
11069
|
+
);
|
|
11070
|
+
const regression = JSON.parse(regressionJson);
|
|
11071
|
+
warnings.push(...regression.warnings ?? []);
|
|
11072
|
+
const gateFailed = regression.qualityGates ? regression.qualityGates.passed === false : false;
|
|
11073
|
+
const setupOk = errors.length === 0;
|
|
11074
|
+
return {
|
|
11075
|
+
schemaVersion: 1,
|
|
11076
|
+
command: "diff",
|
|
11077
|
+
success: setupOk && !gateFailed,
|
|
11078
|
+
errors: [
|
|
11079
|
+
...errors,
|
|
11080
|
+
...gateFailed && regression.qualityGates?.failureSummary ? [`Quality gates failed: ${regression.qualityGates.failureSummary}`] : []
|
|
11081
|
+
],
|
|
11082
|
+
warnings,
|
|
11083
|
+
baselinePath: displayPath,
|
|
11084
|
+
regression
|
|
11085
|
+
};
|
|
11086
|
+
} catch (error) {
|
|
11087
|
+
return {
|
|
11088
|
+
schemaVersion: 1,
|
|
11089
|
+
command: "diff",
|
|
11090
|
+
success: false,
|
|
11091
|
+
errors: [
|
|
11092
|
+
...errors,
|
|
11093
|
+
error instanceof Error ? error.message : String(error)
|
|
11094
|
+
],
|
|
11095
|
+
warnings,
|
|
11096
|
+
baselinePath: displayPath,
|
|
11097
|
+
regression: null
|
|
11098
|
+
};
|
|
11099
|
+
}
|
|
11100
|
+
}
|
|
11101
|
+
function buildGateConfig(options, config) {
|
|
11102
|
+
const maxSizeIncrease = options.maxSizeIncrease !== void 0 ? typeof options.maxSizeIncrease === "number" ? options.maxSizeIncrease : parsePercent(String(options.maxSizeIncrease)) : config.qualityGates.maxSizeIncreasePercent ?? config.regression.sizeIncreasePercent;
|
|
11103
|
+
const minScore = options.minScore ?? config.qualityGates.minScore ?? void 0;
|
|
11104
|
+
const maxNewErrors = options.maxNewErrors ?? config.qualityGates.maxNewErrors ?? config.regression.maxNewErrors ?? void 0;
|
|
11105
|
+
const maxNewWarnings = options.maxNewWarnings ?? config.qualityGates.maxNewWarnings ?? config.regression.maxNewWarnings ?? void 0;
|
|
11106
|
+
const failOnRegression = options.failOnRegression === true || config.qualityGates.failOnRegression === true;
|
|
11107
|
+
const failOnNewErrors = options.failOnNewErrors === true || config.qualityGates.failOnNewErrors === true;
|
|
11108
|
+
const failOnNewDiagnostics = options.failOnNewDiagnostics === true || config.qualityGates.failOnNewDiagnostics === true;
|
|
11109
|
+
const gates = {};
|
|
11110
|
+
if (minScore !== null && minScore !== void 0) {
|
|
11111
|
+
gates.minScore = minScore;
|
|
11112
|
+
}
|
|
11113
|
+
if (options.maxSizeIncrease !== void 0 || config.qualityGates.maxSizeIncreasePercent !== null || failOnRegression) {
|
|
11114
|
+
if (options.maxSizeIncrease !== void 0 || config.qualityGates.maxSizeIncreasePercent !== null) {
|
|
11115
|
+
gates.maxSizeIncreasePercent = maxSizeIncrease ?? void 0;
|
|
11116
|
+
}
|
|
11117
|
+
}
|
|
11118
|
+
if (maxNewErrors !== null && maxNewErrors !== void 0) {
|
|
11119
|
+
gates.maxNewErrors = maxNewErrors;
|
|
11120
|
+
}
|
|
11121
|
+
if (maxNewWarnings !== null && maxNewWarnings !== void 0) {
|
|
11122
|
+
gates.maxNewWarnings = maxNewWarnings;
|
|
11123
|
+
}
|
|
11124
|
+
if (failOnRegression) {
|
|
11125
|
+
gates.failOnRegression = true;
|
|
11126
|
+
}
|
|
11127
|
+
if (failOnNewErrors) {
|
|
11128
|
+
gates.failOnNewErrors = true;
|
|
11129
|
+
}
|
|
11130
|
+
if (failOnNewDiagnostics) {
|
|
11131
|
+
gates.failOnNewDiagnostics = true;
|
|
11132
|
+
}
|
|
11133
|
+
return gates;
|
|
11134
|
+
}
|
|
11135
|
+
function parseNonNegativeInteger2(value) {
|
|
11136
|
+
const parsed = Number(value);
|
|
11137
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
11138
|
+
throw new InvalidArgumentError("must be a non-negative integer");
|
|
11139
|
+
}
|
|
11140
|
+
return parsed;
|
|
11141
|
+
}
|
|
11142
|
+
function parseScore2(value) {
|
|
11143
|
+
const parsed = Number(value);
|
|
11144
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) {
|
|
11145
|
+
throw new InvalidArgumentError("must be an integer between 0 and 100");
|
|
11146
|
+
}
|
|
11147
|
+
return parsed;
|
|
11148
|
+
}
|
|
11149
|
+
function parsePercent(value) {
|
|
11150
|
+
const trimmed = value.trim().endsWith("%") ? value.trim().slice(0, -1) : value.trim();
|
|
11151
|
+
const parsed = Number(trimmed);
|
|
11152
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
11153
|
+
throw new InvalidArgumentError("must be a non-negative percent (e.g. 10%)");
|
|
11154
|
+
}
|
|
11155
|
+
return parsed;
|
|
11156
|
+
}
|
|
11157
|
+
|
|
11158
|
+
// src/commands/fix.ts
|
|
11159
|
+
init_esm_shims();
|
|
11160
|
+
import { stat as stat2 } from "fs/promises";
|
|
11161
|
+
import { resolve as resolve6 } from "path";
|
|
11162
|
+
|
|
11163
|
+
// src/formatters/optimization-terminal.ts
|
|
11164
|
+
init_esm_shims();
|
|
11165
|
+
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
11166
|
+
function formatOptimizationTerminalReport(report, options = {}) {
|
|
11167
|
+
const colors = import_picocolors4.default.createColors(
|
|
11168
|
+
options.color ?? import_picocolors4.default.isColorSupported
|
|
11169
|
+
);
|
|
11170
|
+
const columns = Math.max(
|
|
11171
|
+
48,
|
|
11172
|
+
Math.min(options.columns ?? detectColumns3(), 88)
|
|
11173
|
+
);
|
|
11174
|
+
const verbose = options.verbose === true;
|
|
11175
|
+
const lines = [];
|
|
11176
|
+
const rule = colors.dim("\u2500".repeat(Math.min(columns, 64)));
|
|
11177
|
+
lines.push("");
|
|
11178
|
+
lines.push(
|
|
11179
|
+
` ${colors.bold(colors.cyan("CSS DOCTOR"))}${pad3(
|
|
11180
|
+
columns - 30,
|
|
11181
|
+
""
|
|
11182
|
+
)}${colors.dim(
|
|
11183
|
+
options.dryRun === true ? "OPTIMIZATION PREVIEW" : "OPTIMIZATION REPORT"
|
|
11184
|
+
)}`
|
|
11185
|
+
);
|
|
11186
|
+
lines.push("");
|
|
11187
|
+
lines.push(
|
|
11188
|
+
` ${colors.bold(
|
|
11189
|
+
options.dryRun === true ? "Optimization preview" : "Optimization completed"
|
|
11190
|
+
)}`
|
|
11191
|
+
);
|
|
11192
|
+
lines.push("");
|
|
11193
|
+
lines.push(
|
|
11194
|
+
` ${label2("Original size").padEnd(20)}${formatBytes4(report.input.bytes)}`
|
|
11195
|
+
);
|
|
11196
|
+
lines.push(
|
|
11197
|
+
` ${label2("Emitted total").padEnd(20)}${formatBytes4(report.output.bytes)}`
|
|
11198
|
+
);
|
|
11199
|
+
const eliminated = report.impact.bytesEliminated;
|
|
11200
|
+
const elimPct = report.impact.eliminationPercent;
|
|
11201
|
+
lines.push(
|
|
11202
|
+
` ${label2("Eliminated").padEnd(20)}${formatSignedBytes(eliminated, colors)}${elimPct === null || elimPct === void 0 ? "" : colors.dim(
|
|
11203
|
+
` (${elimPct > 0 ? "+" : ""}${elimPct.toFixed(1)}% vs reference)`
|
|
11204
|
+
)}`
|
|
11205
|
+
);
|
|
11206
|
+
if (report.impact.redistributedBytes !== 0) {
|
|
11207
|
+
lines.push(
|
|
11208
|
+
` ${colors.dim(
|
|
11209
|
+
`Redistributed (split duplication): ${formatSignedBytes(report.impact.redistributedBytes, colors)}`
|
|
11210
|
+
)}`
|
|
11211
|
+
);
|
|
11212
|
+
}
|
|
11213
|
+
lines.push("");
|
|
11214
|
+
lines.push(` ${label2("Files analyzed").padEnd(20)}${report.input.files}`);
|
|
11215
|
+
lines.push(
|
|
11216
|
+
` ${label2("Output chunks").padEnd(20)}${report.impact.chunksCreated}`
|
|
11217
|
+
);
|
|
11218
|
+
lines.push(
|
|
11219
|
+
` ${label2("Selectors removed").padEnd(20)}${report.impact.selectorsRemoved}`
|
|
11220
|
+
);
|
|
11221
|
+
lines.push(
|
|
11222
|
+
` ${label2("Variables chunks").padEnd(20)}${report.impact.variablesChunks}`
|
|
11223
|
+
);
|
|
11224
|
+
lines.push("");
|
|
11225
|
+
lines.push(
|
|
11226
|
+
` ${colors.dim(`Processing time ${formatDuration3(report.analysisMs)}`)}`
|
|
11227
|
+
);
|
|
11228
|
+
lines.push("");
|
|
11229
|
+
lines.push(rule);
|
|
11230
|
+
lines.push("");
|
|
11231
|
+
lines.push(` ${colors.bold("OPTIMIZATION BREAKDOWN")}`);
|
|
11232
|
+
lines.push("");
|
|
11233
|
+
lines.push(
|
|
11234
|
+
` ${statusLine("Minification", report.operations.minified, colors)}`
|
|
11235
|
+
);
|
|
11236
|
+
lines.push(
|
|
11237
|
+
` ${statusLine("Page coverage splitting", report.operations.pageCoverageSplit, colors)}`
|
|
11238
|
+
);
|
|
11239
|
+
lines.push(
|
|
11240
|
+
` ${statusLine("Size budget splitting", report.operations.sizeBudgetSplit, colors)}`
|
|
11241
|
+
);
|
|
11242
|
+
lines.push(
|
|
11243
|
+
` ${statusLine("Variable isolation", report.operations.variablesIsolated, colors)}`
|
|
11244
|
+
);
|
|
11245
|
+
lines.push(
|
|
11246
|
+
` ${statusLine("Dead CSS elimination", report.operations.deadCodeEliminated, colors)}`
|
|
11247
|
+
);
|
|
11248
|
+
if (report.chunks.length > 0) {
|
|
11249
|
+
lines.push("");
|
|
11250
|
+
lines.push(` ${colors.bold("OUTPUT DISTRIBUTION")}`);
|
|
11251
|
+
lines.push("");
|
|
11252
|
+
const shown = report.chunks.slice(0, verbose ? 20 : 8);
|
|
11253
|
+
for (const chunk of shown) {
|
|
11254
|
+
const mark = chunk.oversized ? colors.yellow("\u25B2") : colors.dim("\xB7");
|
|
11255
|
+
lines.push(
|
|
11256
|
+
` ${mark} ${chunk.name.padEnd(24)} ${formatBytes4(chunk.sizeBytes)}`
|
|
11257
|
+
);
|
|
11258
|
+
}
|
|
11259
|
+
if (report.chunks.length > shown.length) {
|
|
11260
|
+
lines.push(
|
|
11261
|
+
` ${colors.dim(`\u2026 and ${report.chunks.length - shown.length} more chunks`)}`
|
|
11262
|
+
);
|
|
11263
|
+
}
|
|
11264
|
+
}
|
|
11265
|
+
const decisions = report.decisions.slice(0, verbose ? 30 : 8);
|
|
11266
|
+
if (decisions.length > 0) {
|
|
11267
|
+
lines.push("");
|
|
11268
|
+
lines.push(rule);
|
|
11269
|
+
lines.push("");
|
|
11270
|
+
lines.push(` ${colors.bold("OPTIMIZATION DECISIONS")}`);
|
|
11271
|
+
lines.push("");
|
|
11272
|
+
for (const decision of decisions) {
|
|
11273
|
+
lines.push(...formatDecision(decision, colors));
|
|
11274
|
+
}
|
|
11275
|
+
if (report.decisions.length > decisions.length) {
|
|
11276
|
+
lines.push(
|
|
11277
|
+
` ${colors.dim(
|
|
11278
|
+
`\u2026 and ${report.decisions.length - decisions.length} more (use --verbose or --json)`
|
|
11279
|
+
)}`
|
|
11280
|
+
);
|
|
11281
|
+
}
|
|
11282
|
+
}
|
|
11283
|
+
if (report.warnings.length > 0) {
|
|
11284
|
+
lines.push("");
|
|
11285
|
+
for (const warning of report.warnings) {
|
|
11286
|
+
lines.push(` ${colors.yellow("!")} ${warning}`);
|
|
9111
11287
|
}
|
|
9112
11288
|
}
|
|
9113
|
-
if (
|
|
11289
|
+
if (options.dryRun === true) {
|
|
9114
11290
|
lines.push("");
|
|
9115
|
-
lines.push(
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
return lines;
|
|
9119
|
-
}
|
|
9120
|
-
function formatTopSelectors(result, colors) {
|
|
9121
|
-
const top = topSelectors(result.selectors, TOP_SELECTOR_LIMIT);
|
|
9122
|
-
const total = result.selectors.length;
|
|
9123
|
-
const heading = total > top.length ? `Top selectors (${top.length} of ${total})` : `Top selectors (${total})`;
|
|
9124
|
-
const lines = [` ${colors.bold(heading)}`];
|
|
9125
|
-
top.forEach((selector, index) => {
|
|
9126
|
-
const frequency = selector.count > 1 ? ` ${colors.yellow(`\xD7${selector.count}`)}` : "";
|
|
11291
|
+
lines.push(rule);
|
|
11292
|
+
lines.push("");
|
|
11293
|
+
lines.push(` ${colors.bold("No files were modified.")}`);
|
|
9127
11294
|
lines.push(
|
|
9128
|
-
`
|
|
11295
|
+
` ${colors.dim("Run without --dry-run and with --out-dir to emit chunks.")}`
|
|
9129
11296
|
);
|
|
9130
|
-
}
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
function topSelectors(selectors, limit) {
|
|
9134
|
-
const entries = /* @__PURE__ */ new Map();
|
|
9135
|
-
selectors.forEach((selector, index) => {
|
|
9136
|
-
const existing = entries.get(selector.raw);
|
|
9137
|
-
if (existing) {
|
|
9138
|
-
existing.count += 1;
|
|
9139
|
-
return;
|
|
9140
|
-
}
|
|
9141
|
-
entries.set(selector.raw, {
|
|
9142
|
-
raw: selector.raw,
|
|
9143
|
-
count: 1,
|
|
9144
|
-
firstIndex: index,
|
|
9145
|
-
specificity: `${selector.specificity.ids},${selector.specificity.classes},${selector.specificity.types}`
|
|
9146
|
-
});
|
|
9147
|
-
});
|
|
9148
|
-
return [...entries.values()].sort(
|
|
9149
|
-
(left, right) => right.count - left.count || left.firstIndex - right.firstIndex
|
|
9150
|
-
).slice(0, limit);
|
|
9151
|
-
}
|
|
9152
|
-
function formatIssue(issue, colors) {
|
|
9153
|
-
const location = issue.location ? `${issue.location.line}:${issue.location.column}` : "\u2014";
|
|
9154
|
-
const tag = issue.kind === "syntax" ? colors.yellow("syntax") : issue.kind === "threshold" ? colors.yellow("threshold") : issue.kind === "empty-rule" ? colors.magenta("dead code") : colors.magenta("duplicate");
|
|
9155
|
-
return `${colors.dim(location.padEnd(7))} ${tag} ${issue.message}`;
|
|
11297
|
+
}
|
|
11298
|
+
lines.push("");
|
|
11299
|
+
return lines.join("\n");
|
|
9156
11300
|
}
|
|
9157
|
-
function
|
|
9158
|
-
const
|
|
9159
|
-
|
|
11301
|
+
function formatFixTerminalReport(results, options = {}) {
|
|
11302
|
+
const colors = import_picocolors4.default.createColors(
|
|
11303
|
+
options.color ?? import_picocolors4.default.isColorSupported
|
|
11304
|
+
);
|
|
11305
|
+
const columns = Math.max(
|
|
11306
|
+
48,
|
|
11307
|
+
Math.min(options.columns ?? detectColumns3(), 88)
|
|
11308
|
+
);
|
|
9160
11309
|
const lines = [];
|
|
11310
|
+
const rule = colors.dim("\u2500".repeat(Math.min(columns, 64)));
|
|
11311
|
+
const safe = results.reduce((sum2, result) => sum2 + result.plan.safeCount, 0);
|
|
11312
|
+
const review = results.reduce(
|
|
11313
|
+
(sum2, result) => sum2 + result.plan.reviewCount,
|
|
11314
|
+
0
|
|
11315
|
+
);
|
|
11316
|
+
const unsafe = results.reduce(
|
|
11317
|
+
(sum2, result) => sum2 + result.plan.unsafeCount,
|
|
11318
|
+
0
|
|
11319
|
+
);
|
|
11320
|
+
const applied = results.reduce(
|
|
11321
|
+
(sum2, result) => sum2 + result.plan.items.filter((item) => item.applied).length,
|
|
11322
|
+
0
|
|
11323
|
+
);
|
|
11324
|
+
const changedFiles = results.filter((result) => result.changed).length;
|
|
11325
|
+
const issues = results.reduce(
|
|
11326
|
+
(sum2, result) => sum2 + result.plan.items.length,
|
|
11327
|
+
0
|
|
11328
|
+
);
|
|
11329
|
+
lines.push("");
|
|
9161
11330
|
lines.push(
|
|
9162
|
-
|
|
11331
|
+
` ${colors.bold(colors.cyan("CSS DOCTOR"))}${pad3(
|
|
11332
|
+
columns - 22,
|
|
11333
|
+
""
|
|
11334
|
+
)}${colors.dim("FIX REPORT")}`
|
|
9163
11335
|
);
|
|
11336
|
+
lines.push("");
|
|
11337
|
+
lines.push(` ${colors.bold("Fix analysis completed")}`);
|
|
11338
|
+
lines.push("");
|
|
11339
|
+
lines.push(` ${issues} issues detected`);
|
|
11340
|
+
lines.push(` ${safe} safe fixes available`);
|
|
11341
|
+
lines.push(` ${review} review-required fixes`);
|
|
11342
|
+
lines.push(` ${unsafe} unsafe fixes skipped by default`);
|
|
11343
|
+
lines.push("");
|
|
11344
|
+
lines.push(` Applied:`);
|
|
9164
11345
|
lines.push(
|
|
9165
|
-
`
|
|
11346
|
+
` ${colors.green("\u2713")} ${applied} fix occurrence(s)${options.dryRun === true ? colors.dim(" (dry-run)") : ""}`
|
|
9166
11347
|
);
|
|
11348
|
+
lines.push("");
|
|
11349
|
+
lines.push(` Skipped:`);
|
|
11350
|
+
lines.push(` ${colors.yellow("\u25B2")} ${review} fixes require review`);
|
|
11351
|
+
lines.push(` ${colors.red("\u2715")} ${unsafe} fixes may change CSS behavior`);
|
|
11352
|
+
lines.push("");
|
|
9167
11353
|
lines.push(
|
|
9168
|
-
`
|
|
11354
|
+
` Files ${options.dryRun === true ? "that would change" : "modified"}: ${changedFiles}`
|
|
9169
11355
|
);
|
|
9170
|
-
if (
|
|
9171
|
-
lines.push(
|
|
11356
|
+
if (options.verbose === true) {
|
|
11357
|
+
lines.push("");
|
|
11358
|
+
lines.push(rule);
|
|
11359
|
+
lines.push("");
|
|
11360
|
+
for (const result of results) {
|
|
11361
|
+
if (!result.filePath) {
|
|
11362
|
+
continue;
|
|
11363
|
+
}
|
|
11364
|
+
lines.push(` ${result.filePath}`);
|
|
11365
|
+
for (const item of result.plan.items.slice(0, 10)) {
|
|
11366
|
+
const marker = item.applied ? colors.green("\u2713") : colors.dim("\xB7");
|
|
11367
|
+
lines.push(` ${marker} ${item.rule} \u2014 ${item.message}`);
|
|
11368
|
+
}
|
|
11369
|
+
}
|
|
9172
11370
|
}
|
|
9173
|
-
|
|
11371
|
+
if (options.dryRun === true) {
|
|
11372
|
+
lines.push("");
|
|
11373
|
+
lines.push(` ${colors.bold("No files were modified.")}`);
|
|
11374
|
+
lines.push(
|
|
11375
|
+
` ${colors.dim("Re-run without --dry-run to apply Safe fixes.")}`
|
|
11376
|
+
);
|
|
11377
|
+
}
|
|
11378
|
+
lines.push("");
|
|
11379
|
+
return lines.join("\n");
|
|
9174
11380
|
}
|
|
9175
|
-
function
|
|
9176
|
-
if (
|
|
9177
|
-
return "
|
|
11381
|
+
function formatUnifiedDiff(filePath, before, after, options = {}) {
|
|
11382
|
+
if (before === after) {
|
|
11383
|
+
return "";
|
|
9178
11384
|
}
|
|
9179
|
-
|
|
11385
|
+
const maxLines = options.maxLines ?? 80;
|
|
11386
|
+
const beforeLines = before.split("\n");
|
|
11387
|
+
const afterLines = after.split("\n");
|
|
11388
|
+
const lines = [`--- a/${filePath}`, `+++ b/${filePath}`, `@@ preview @@`];
|
|
11389
|
+
for (const line of beforeLines.slice(0, Math.floor(maxLines / 2))) {
|
|
11390
|
+
lines.push(`-${line}`);
|
|
11391
|
+
}
|
|
11392
|
+
for (const line of afterLines.slice(0, Math.floor(maxLines / 2))) {
|
|
11393
|
+
lines.push(`+${line}`);
|
|
11394
|
+
}
|
|
11395
|
+
if (beforeLines.length + afterLines.length > maxLines) {
|
|
11396
|
+
lines.push(`\\ No newline / truncated for preview`);
|
|
11397
|
+
}
|
|
11398
|
+
return `${lines.join("\n")}
|
|
11399
|
+
`;
|
|
9180
11400
|
}
|
|
9181
|
-
function
|
|
9182
|
-
|
|
9183
|
-
|
|
11401
|
+
function formatDecision(decision, colors) {
|
|
11402
|
+
const marker = decision.type === "oversized-chunk" || decision.type === "retain-uncertain" ? colors.yellow("\u25B2") : decision.changed ? colors.green("\u2713") : colors.cyan("\u25CF");
|
|
11403
|
+
const lines = [` ${marker} ${colors.bold(decision.title)}`];
|
|
11404
|
+
if (decision.source.selector) {
|
|
11405
|
+
lines.push(` ${colors.magenta(decision.source.selector)}`);
|
|
9184
11406
|
}
|
|
9185
|
-
|
|
11407
|
+
lines.push(` ${colors.dim(decision.explanation)}`);
|
|
11408
|
+
return [...lines, ""];
|
|
9186
11409
|
}
|
|
9187
|
-
function
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
return colors.bgRed(colors.white(" ERROR "));
|
|
11410
|
+
function statusLine(labelText, enabled, colors) {
|
|
11411
|
+
return `${labelText.padEnd(24)} ${enabled ? colors.green("Completed") : colors.dim("Skipped")}`;
|
|
11412
|
+
}
|
|
11413
|
+
function formatSignedBytes(value, colors) {
|
|
11414
|
+
const abs = formatBytes4(Math.abs(value));
|
|
11415
|
+
if (value > 0) {
|
|
11416
|
+
return colors.green(`+${abs}`);
|
|
9195
11417
|
}
|
|
11418
|
+
if (value < 0) {
|
|
11419
|
+
return colors.red(`-${abs}`);
|
|
11420
|
+
}
|
|
11421
|
+
return colors.dim(abs);
|
|
9196
11422
|
}
|
|
9197
|
-
function
|
|
11423
|
+
function label2(text) {
|
|
11424
|
+
return text;
|
|
11425
|
+
}
|
|
11426
|
+
function detectColumns3() {
|
|
11427
|
+
const columns = process.stdout.columns;
|
|
11428
|
+
return typeof columns === "number" && columns > 0 ? columns : 80;
|
|
11429
|
+
}
|
|
11430
|
+
function pad3(width, text) {
|
|
11431
|
+
return " ".repeat(Math.max(1, width - text.length));
|
|
11432
|
+
}
|
|
11433
|
+
function formatBytes4(bytes) {
|
|
9198
11434
|
if (bytes < 1024) {
|
|
9199
11435
|
return `${bytes} B`;
|
|
9200
11436
|
}
|
|
9201
11437
|
if (bytes < 1024 * 1024) {
|
|
9202
11438
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
9203
11439
|
}
|
|
9204
|
-
return `${(bytes / (1024 * 1024)).toFixed(
|
|
11440
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
9205
11441
|
}
|
|
9206
|
-
function
|
|
9207
|
-
if (
|
|
9208
|
-
return
|
|
11442
|
+
function formatDuration3(ms) {
|
|
11443
|
+
if (ms < 1) {
|
|
11444
|
+
return `${ms.toFixed(2)} ms`;
|
|
9209
11445
|
}
|
|
9210
|
-
|
|
11446
|
+
if (ms < 1e3) {
|
|
11447
|
+
return `${ms.toFixed(1)} ms`;
|
|
11448
|
+
}
|
|
11449
|
+
return `${(ms / 1e3).toFixed(2)} s`;
|
|
9211
11450
|
}
|
|
9212
11451
|
|
|
9213
|
-
// src/utils/
|
|
11452
|
+
// src/utils/atomic-write.ts
|
|
9214
11453
|
init_esm_shims();
|
|
9215
|
-
|
|
9216
|
-
import {
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
11454
|
+
import { randomBytes } from "crypto";
|
|
11455
|
+
import {
|
|
11456
|
+
chmod,
|
|
11457
|
+
copyFile,
|
|
11458
|
+
mkdir as mkdir2,
|
|
11459
|
+
rename,
|
|
11460
|
+
unlink as unlink2,
|
|
11461
|
+
writeFile as writeFile2
|
|
11462
|
+
} from "fs/promises";
|
|
11463
|
+
import { dirname as dirname2, join } from "path";
|
|
11464
|
+
async function atomicWriteFile(absolutePath, content, options = {}) {
|
|
11465
|
+
const directory = dirname2(absolutePath);
|
|
11466
|
+
const tempPath = join(
|
|
11467
|
+
directory,
|
|
11468
|
+
`.css-doctor-${randomBytes(8).toString("hex")}.tmp`
|
|
11469
|
+
);
|
|
9220
11470
|
try {
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
11471
|
+
await mkdir2(directory, { recursive: true });
|
|
11472
|
+
if (options.backup === true) {
|
|
11473
|
+
try {
|
|
11474
|
+
await copyFile(absolutePath, `${absolutePath}.bak`);
|
|
11475
|
+
} catch {
|
|
11476
|
+
}
|
|
11477
|
+
}
|
|
11478
|
+
await writeFile2(tempPath, content, "utf8");
|
|
11479
|
+
if (options.mode !== void 0) {
|
|
11480
|
+
await chmod(tempPath, options.mode);
|
|
11481
|
+
}
|
|
11482
|
+
await rename(tempPath, absolutePath);
|
|
11483
|
+
return { ok: true };
|
|
11484
|
+
} catch (error) {
|
|
11485
|
+
try {
|
|
11486
|
+
await unlink2(tempPath);
|
|
11487
|
+
} catch {
|
|
11488
|
+
}
|
|
11489
|
+
return {
|
|
11490
|
+
ok: false,
|
|
11491
|
+
error: `Failed to write ${absolutePath}: ${describeError(error)}`
|
|
11492
|
+
};
|
|
9224
11493
|
}
|
|
9225
11494
|
}
|
|
9226
|
-
|
|
11495
|
+
|
|
11496
|
+
// src/commands/fix.ts
|
|
11497
|
+
function registerFixCommand(program3) {
|
|
11498
|
+
program3.command("fix").description("Plan or apply Safe CSS autofixes").argument(
|
|
11499
|
+
"[patterns...]",
|
|
11500
|
+
"CSS files or glob patterns (required unless --list)"
|
|
11501
|
+
).option("--list", "List available autofix rules and exit").option("--dry-run", "Preview fixes without writing files").option("--write", "Apply Safe fixes to source files").option(
|
|
11502
|
+
"--rule <id>",
|
|
11503
|
+
"Only consider this fix rule id (repeatable)",
|
|
11504
|
+
collectRule,
|
|
11505
|
+
[]
|
|
11506
|
+
).option("--backup", "Write `.bak` copies before modifying files").option("--diff", "Show unified diffs for changed files").option("-v, --verbose", "List per-file fix items").option("-j, --json", "Emit JSON").action(
|
|
11507
|
+
async (patterns, options) => {
|
|
11508
|
+
if (options.list === true) {
|
|
11509
|
+
const report2 = await runFixList();
|
|
11510
|
+
if (options.json === true) {
|
|
11511
|
+
process.stdout.write(`${formatJsonReport(report2)}
|
|
11512
|
+
`);
|
|
11513
|
+
} else {
|
|
11514
|
+
process.stdout.write(`${formatFixCatalog(report2.catalog ?? [])}
|
|
11515
|
+
`);
|
|
11516
|
+
}
|
|
11517
|
+
process.exitCode = report2.success ? 0 : 1;
|
|
11518
|
+
return;
|
|
11519
|
+
}
|
|
11520
|
+
if (options.write === true && options.dryRun === true) {
|
|
11521
|
+
process.stderr.write(
|
|
11522
|
+
"css-doctor: --write and --dry-run are mutually exclusive.\n"
|
|
11523
|
+
);
|
|
11524
|
+
process.exitCode = 1;
|
|
11525
|
+
return;
|
|
11526
|
+
}
|
|
11527
|
+
const dryRun = options.write !== true;
|
|
11528
|
+
const report = await runFix(patterns ?? [], {
|
|
11529
|
+
...options,
|
|
11530
|
+
dryRun,
|
|
11531
|
+
rules: options.rule ?? options.rules
|
|
11532
|
+
});
|
|
11533
|
+
if (options.json === true) {
|
|
11534
|
+
process.stdout.write(`${formatJsonReport(report)}
|
|
11535
|
+
`);
|
|
11536
|
+
} else if (report.files.length > 0) {
|
|
11537
|
+
process.stdout.write(
|
|
11538
|
+
`${formatFixTerminalReport(
|
|
11539
|
+
report.files.map((file) => file.result),
|
|
11540
|
+
{
|
|
11541
|
+
dryRun: report.dryRun,
|
|
11542
|
+
verbose: options.verbose === true
|
|
11543
|
+
}
|
|
11544
|
+
)}
|
|
11545
|
+
`
|
|
11546
|
+
);
|
|
11547
|
+
if (options.diff === true) {
|
|
11548
|
+
for (const file of report.files) {
|
|
11549
|
+
if (file.diff) {
|
|
11550
|
+
process.stdout.write(`${file.diff}
|
|
11551
|
+
`);
|
|
11552
|
+
}
|
|
11553
|
+
}
|
|
11554
|
+
}
|
|
11555
|
+
if (report.errors.length > 0) {
|
|
11556
|
+
process.stderr.write(
|
|
11557
|
+
`css-doctor: ${report.errors.join("\ncss-doctor: ")}
|
|
11558
|
+
`
|
|
11559
|
+
);
|
|
11560
|
+
}
|
|
11561
|
+
} else {
|
|
11562
|
+
process.stdout.write(
|
|
11563
|
+
`css-doctor fix failed:
|
|
11564
|
+
${report.errors.map((error) => ` - ${error}`).join("\n")}
|
|
11565
|
+
`
|
|
11566
|
+
);
|
|
11567
|
+
}
|
|
11568
|
+
process.exitCode = report.success ? 0 : 1;
|
|
11569
|
+
}
|
|
11570
|
+
);
|
|
11571
|
+
}
|
|
11572
|
+
async function runFixList() {
|
|
9227
11573
|
try {
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
return
|
|
11574
|
+
const binding = loadBinding();
|
|
11575
|
+
const catalog = JSON.parse(binding.listCssFixes());
|
|
11576
|
+
return {
|
|
11577
|
+
schemaVersion: 1,
|
|
11578
|
+
command: "fix",
|
|
11579
|
+
success: true,
|
|
11580
|
+
dryRun: true,
|
|
11581
|
+
errors: [],
|
|
11582
|
+
catalog,
|
|
11583
|
+
files: []
|
|
11584
|
+
};
|
|
11585
|
+
} catch (error) {
|
|
11586
|
+
return {
|
|
11587
|
+
schemaVersion: 1,
|
|
11588
|
+
command: "fix",
|
|
11589
|
+
success: false,
|
|
11590
|
+
dryRun: true,
|
|
11591
|
+
errors: [describeError(error)],
|
|
11592
|
+
files: []
|
|
11593
|
+
};
|
|
9231
11594
|
}
|
|
9232
11595
|
}
|
|
9233
|
-
async function
|
|
9234
|
-
const
|
|
9235
|
-
const
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
unique: true,
|
|
9250
|
-
dot: false,
|
|
9251
|
-
followSymbolicLinks: true,
|
|
9252
|
-
ignore: DEFAULT_IGNORES
|
|
9253
|
-
});
|
|
9254
|
-
for (const match of matches) {
|
|
9255
|
-
files.add(resolve(match));
|
|
9256
|
-
}
|
|
11596
|
+
async function runFix(patterns, options = {}) {
|
|
11597
|
+
const cwd = resolve6(options.cwd ?? process.cwd());
|
|
11598
|
+
const dryRun = options.dryRun !== false;
|
|
11599
|
+
const errors = [];
|
|
11600
|
+
const files = [];
|
|
11601
|
+
if (patterns.length === 0) {
|
|
11602
|
+
return {
|
|
11603
|
+
schemaVersion: 1,
|
|
11604
|
+
command: "fix",
|
|
11605
|
+
success: false,
|
|
11606
|
+
dryRun,
|
|
11607
|
+
errors: [
|
|
11608
|
+
'No CSS files or glob patterns were provided. Example: css-doctor fix "src/**/*.css" --dry-run'
|
|
11609
|
+
],
|
|
11610
|
+
files: []
|
|
11611
|
+
};
|
|
9257
11612
|
}
|
|
9258
|
-
|
|
9259
|
-
}
|
|
9260
|
-
async function readCssFile(filePath) {
|
|
9261
|
-
try {
|
|
9262
|
-
const buffer = await readFile(filePath);
|
|
11613
|
+
if (!await isDirectory(cwd)) {
|
|
9263
11614
|
return {
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
11615
|
+
schemaVersion: 1,
|
|
11616
|
+
command: "fix",
|
|
11617
|
+
success: false,
|
|
11618
|
+
dryRun,
|
|
11619
|
+
errors: [
|
|
11620
|
+
`Working directory does not exist or is not a directory: ${cwd}`
|
|
11621
|
+
],
|
|
11622
|
+
files: []
|
|
9267
11623
|
};
|
|
11624
|
+
}
|
|
11625
|
+
let cssFiles = [];
|
|
11626
|
+
try {
|
|
11627
|
+
cssFiles = await resolveCssFiles(patterns, cwd);
|
|
11628
|
+
if (cssFiles.length === 0) {
|
|
11629
|
+
errors.push(`No CSS files matched: ${patterns.join(", ")}`);
|
|
11630
|
+
}
|
|
9268
11631
|
} catch (error) {
|
|
11632
|
+
errors.push(`Failed to resolve CSS files: ${describeError(error)}`);
|
|
11633
|
+
}
|
|
11634
|
+
if (errors.length > 0) {
|
|
9269
11635
|
return {
|
|
9270
|
-
|
|
9271
|
-
|
|
11636
|
+
schemaVersion: 1,
|
|
11637
|
+
command: "fix",
|
|
11638
|
+
success: false,
|
|
11639
|
+
dryRun,
|
|
11640
|
+
errors,
|
|
11641
|
+
files: []
|
|
9272
11642
|
};
|
|
9273
11643
|
}
|
|
11644
|
+
try {
|
|
11645
|
+
const binding = loadBinding();
|
|
11646
|
+
for (const absolute of cssFiles) {
|
|
11647
|
+
const displayPath = toDisplayPath(absolute, cwd);
|
|
11648
|
+
const read = await readCssFile(absolute);
|
|
11649
|
+
if (!read.ok) {
|
|
11650
|
+
errors.push(`${displayPath}: ${read.error}`);
|
|
11651
|
+
continue;
|
|
11652
|
+
}
|
|
11653
|
+
let mode;
|
|
11654
|
+
try {
|
|
11655
|
+
mode = (await stat2(absolute)).mode;
|
|
11656
|
+
} catch {
|
|
11657
|
+
mode = void 0;
|
|
11658
|
+
}
|
|
11659
|
+
const result = JSON.parse(
|
|
11660
|
+
binding.applyCssFixes({
|
|
11661
|
+
cssContent: read.content,
|
|
11662
|
+
filePath: displayPath,
|
|
11663
|
+
dryRun,
|
|
11664
|
+
rules: options.rules && options.rules.length > 0 ? options.rules : void 0,
|
|
11665
|
+
includeReview: false
|
|
11666
|
+
})
|
|
11667
|
+
);
|
|
11668
|
+
let diff;
|
|
11669
|
+
if (options.diff === true && result.changed) {
|
|
11670
|
+
diff = formatUnifiedDiff(
|
|
11671
|
+
displayPath,
|
|
11672
|
+
result.originalCss,
|
|
11673
|
+
result.fixedCss
|
|
11674
|
+
);
|
|
11675
|
+
}
|
|
11676
|
+
if (!dryRun && result.changed) {
|
|
11677
|
+
const written = await atomicWriteFile(absolute, result.fixedCss, {
|
|
11678
|
+
mode,
|
|
11679
|
+
backup: options.backup === true
|
|
11680
|
+
});
|
|
11681
|
+
if (!written.ok) {
|
|
11682
|
+
errors.push(written.error);
|
|
11683
|
+
continue;
|
|
11684
|
+
}
|
|
11685
|
+
}
|
|
11686
|
+
files.push({ path: displayPath, result, diff });
|
|
11687
|
+
}
|
|
11688
|
+
} catch (error) {
|
|
11689
|
+
errors.push(describeError(error));
|
|
11690
|
+
}
|
|
11691
|
+
return {
|
|
11692
|
+
schemaVersion: 1,
|
|
11693
|
+
command: "fix",
|
|
11694
|
+
success: errors.length === 0,
|
|
11695
|
+
dryRun,
|
|
11696
|
+
errors,
|
|
11697
|
+
files
|
|
11698
|
+
};
|
|
9274
11699
|
}
|
|
9275
|
-
function
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
return displayPath.split(sep).join("/");
|
|
11700
|
+
function collectRule(value, previous) {
|
|
11701
|
+
previous.push(value);
|
|
11702
|
+
return previous;
|
|
9279
11703
|
}
|
|
9280
|
-
function
|
|
9281
|
-
|
|
9282
|
-
|
|
11704
|
+
function formatFixCatalog(catalog) {
|
|
11705
|
+
const lines = ["", " Available autofix rules", ""];
|
|
11706
|
+
for (const entry of catalog) {
|
|
11707
|
+
lines.push(` [${entry.safety}] ${entry.id}`);
|
|
11708
|
+
lines.push(` ${entry.description}`);
|
|
11709
|
+
lines.push(` ${entry.reason}`);
|
|
11710
|
+
lines.push("");
|
|
9283
11711
|
}
|
|
9284
|
-
|
|
11712
|
+
lines.push(" Only [safe] rules are applied by default.");
|
|
11713
|
+
lines.push("");
|
|
11714
|
+
return lines.join("\n");
|
|
9285
11715
|
}
|
|
9286
11716
|
|
|
9287
|
-
// src/commands/
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
11717
|
+
// src/commands/health.ts
|
|
11718
|
+
init_esm_shims();
|
|
11719
|
+
import { createRequire as createRequire3 } from "module";
|
|
11720
|
+
import { resolve as resolve7 } from "path";
|
|
11721
|
+
var require4 = createRequire3(import.meta.url);
|
|
11722
|
+
var cachedBinding3;
|
|
11723
|
+
function loadBinding3() {
|
|
11724
|
+
if (cachedBinding3) {
|
|
11725
|
+
return cachedBinding3;
|
|
9293
11726
|
}
|
|
9294
11727
|
try {
|
|
9295
|
-
|
|
9296
|
-
return
|
|
11728
|
+
cachedBinding3 = require4("@zealakit/css-doctor-binding");
|
|
11729
|
+
return cachedBinding3;
|
|
9297
11730
|
} catch (error) {
|
|
9298
11731
|
throw new Error(
|
|
9299
11732
|
"Unable to load @zealakit/css-doctor-binding. Build it first with `pnpm --filter @zealakit/css-doctor-binding build`.\n" + describeError(error)
|
|
9300
11733
|
);
|
|
9301
11734
|
}
|
|
9302
11735
|
}
|
|
9303
|
-
function
|
|
9304
|
-
program3.command("
|
|
11736
|
+
function registerHealthCommand(program3) {
|
|
11737
|
+
program3.command("health").description("Evaluate CSS Health Score with dimension breakdown").argument(
|
|
9305
11738
|
"[patterns...]",
|
|
9306
11739
|
"CSS files or glob patterns (quote globs to let css-doctor expand them)"
|
|
9307
11740
|
).option("-j, --json", "Output raw JSON for piping and CI/CD").option(
|
|
9308
11741
|
"--threshold <number>",
|
|
9309
|
-
"Warn when a file
|
|
9310
|
-
|
|
11742
|
+
"Warn in size/complexity checks when a file has more than <number> selectors",
|
|
11743
|
+
parseNonNegativeInteger3
|
|
11744
|
+
).option(
|
|
11745
|
+
"--fail-under <number>",
|
|
11746
|
+
"Exit with code 1 when overall health score is below <number> (0-100)",
|
|
11747
|
+
parseScore3
|
|
9311
11748
|
).action(async (patterns, options) => {
|
|
9312
|
-
const report = await
|
|
9313
|
-
threshold: options.threshold
|
|
11749
|
+
const report = await runHealth(patterns ?? [], {
|
|
11750
|
+
threshold: options.threshold,
|
|
11751
|
+
failUnder: options.failUnder
|
|
9314
11752
|
});
|
|
9315
11753
|
if (options.json === true) {
|
|
9316
11754
|
process.stdout.write(`${formatJsonReport(report)}
|
|
9317
11755
|
`);
|
|
9318
|
-
} else {
|
|
9319
|
-
process.stdout.write(`${
|
|
11756
|
+
} else if (report.health) {
|
|
11757
|
+
process.stdout.write(`${formatHealthTerminalReport(report.health)}
|
|
9320
11758
|
`);
|
|
11759
|
+
if (report.errors.length > 0) {
|
|
11760
|
+
process.stderr.write(
|
|
11761
|
+
`css-doctor: ${report.errors.join("\ncss-doctor: ")}
|
|
11762
|
+
`
|
|
11763
|
+
);
|
|
11764
|
+
}
|
|
11765
|
+
} else {
|
|
11766
|
+
process.stdout.write(
|
|
11767
|
+
`css-doctor health failed:
|
|
11768
|
+
${report.errors.map((error) => ` - ${error}`).join("\n")}
|
|
11769
|
+
`
|
|
11770
|
+
);
|
|
9321
11771
|
}
|
|
9322
|
-
process.exitCode =
|
|
11772
|
+
process.exitCode = healthExitCode(report);
|
|
9323
11773
|
});
|
|
9324
11774
|
}
|
|
9325
|
-
function
|
|
9326
|
-
const
|
|
9327
|
-
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
9328
|
-
throw new InvalidArgumentError("threshold must be a non-negative integer");
|
|
9329
|
-
}
|
|
9330
|
-
return parsed;
|
|
9331
|
-
}
|
|
9332
|
-
async function runCheck(patterns, options = {}) {
|
|
9333
|
-
const startedAt = performance.now();
|
|
9334
|
-
const cwd = resolve2(options.cwd ?? process.cwd());
|
|
11775
|
+
async function runHealth(patterns, options = {}) {
|
|
11776
|
+
const cwd = resolve7(options.cwd ?? process.cwd());
|
|
9335
11777
|
const threshold = options.threshold ?? null;
|
|
11778
|
+
const failUnder = options.failUnder ?? null;
|
|
9336
11779
|
const errors = [];
|
|
9337
|
-
|
|
9338
|
-
|
|
11780
|
+
if (failUnder !== null && (failUnder < 0 || failUnder > 100)) {
|
|
11781
|
+
errors.push("--fail-under must be an integer between 0 and 100.");
|
|
11782
|
+
}
|
|
9339
11783
|
if (threshold !== null && (!Number.isInteger(threshold) || threshold < 0)) {
|
|
9340
11784
|
errors.push("Threshold must be a non-negative integer.");
|
|
9341
11785
|
}
|
|
11786
|
+
let files = [];
|
|
9342
11787
|
if (patterns.length === 0) {
|
|
9343
11788
|
errors.push(
|
|
9344
|
-
'No CSS files or glob patterns were provided. Example: css-doctor
|
|
11789
|
+
'No CSS files or glob patterns were provided. Example: css-doctor health "src/**/*.css"'
|
|
9345
11790
|
);
|
|
9346
11791
|
} else if (!await isDirectory(cwd)) {
|
|
9347
11792
|
errors.push(
|
|
@@ -9357,182 +11802,383 @@ async function runCheck(patterns, options = {}) {
|
|
|
9357
11802
|
errors.push(`Failed to resolve CSS files: ${describeError(error)}`);
|
|
9358
11803
|
}
|
|
9359
11804
|
}
|
|
9360
|
-
if (errors.length
|
|
9361
|
-
try {
|
|
9362
|
-
const analyze = loadBinding().analyzeCss;
|
|
9363
|
-
for (const file of files) {
|
|
9364
|
-
results.push(await checkFile(file, cwd, threshold, analyze));
|
|
9365
|
-
}
|
|
9366
|
-
} catch (error) {
|
|
9367
|
-
errors.push(describeError(error));
|
|
9368
|
-
}
|
|
9369
|
-
}
|
|
9370
|
-
const issues = results.flatMap((file) => file.issues);
|
|
9371
|
-
const warnings = issues.filter((issue) => issue.kind === "syntax").length;
|
|
9372
|
-
const analyzed = results.filter((file) => file.ok).length;
|
|
9373
|
-
const failed = results.length - analyzed;
|
|
9374
|
-
const summary = {
|
|
9375
|
-
files: results.length,
|
|
9376
|
-
analyzed,
|
|
9377
|
-
failed,
|
|
9378
|
-
filesWithIssues: results.filter((file) => file.issues.length > 0).length,
|
|
9379
|
-
totalSizeBytes: results.reduce((total, file) => total + file.sizeBytes, 0),
|
|
9380
|
-
rules: sum(results, (file) => file.result?.stats.ruleCount ?? 0),
|
|
9381
|
-
selectors: sum(results, (file) => file.result?.stats.selectorCount ?? 0),
|
|
9382
|
-
declarations: sum(
|
|
9383
|
-
results,
|
|
9384
|
-
(file) => (file.result?.stats.declarationCount ?? 0) + (file.result?.stats.importantDeclarationCount ?? 0)
|
|
9385
|
-
),
|
|
9386
|
-
warnings,
|
|
9387
|
-
hints: issues.length - warnings,
|
|
9388
|
-
issues: issues.length,
|
|
9389
|
-
analysisMs: roundMs(
|
|
9390
|
-
results.reduce((total, file) => total + file.analysisMs, 0)
|
|
9391
|
-
),
|
|
9392
|
-
totalMs: elapsedMs(startedAt)
|
|
9393
|
-
};
|
|
9394
|
-
return {
|
|
9395
|
-
schemaVersion: 1,
|
|
9396
|
-
command: "check",
|
|
9397
|
-
version: package_default.version,
|
|
9398
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9399
|
-
cwd,
|
|
9400
|
-
patterns,
|
|
9401
|
-
threshold,
|
|
9402
|
-
success: errors.length === 0 && failed === 0,
|
|
9403
|
-
errors,
|
|
9404
|
-
files: results,
|
|
9405
|
-
summary
|
|
9406
|
-
};
|
|
9407
|
-
}
|
|
9408
|
-
async function checkFile(filePath, cwd, threshold, analyze) {
|
|
9409
|
-
const path2 = toDisplayPath(filePath, cwd);
|
|
9410
|
-
const readStartedAt = performance.now();
|
|
9411
|
-
const read = await readCssFile(filePath);
|
|
9412
|
-
const readMs = elapsedMs(readStartedAt);
|
|
9413
|
-
if (!read.ok) {
|
|
11805
|
+
if (errors.length > 0) {
|
|
9414
11806
|
return {
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
error: read.error
|
|
11807
|
+
schemaVersion: 1,
|
|
11808
|
+
command: "health",
|
|
11809
|
+
success: false,
|
|
11810
|
+
errors,
|
|
11811
|
+
patterns,
|
|
11812
|
+
threshold,
|
|
11813
|
+
failUnder,
|
|
11814
|
+
health: null
|
|
9424
11815
|
};
|
|
9425
11816
|
}
|
|
9426
|
-
const analysisStartedAt = performance.now();
|
|
9427
11817
|
try {
|
|
9428
|
-
const
|
|
9429
|
-
const
|
|
11818
|
+
const binding = loadBinding3();
|
|
11819
|
+
const batchFiles = [];
|
|
11820
|
+
for (const file of files) {
|
|
11821
|
+
const read = await readCssFile(file);
|
|
11822
|
+
if (!read.ok) {
|
|
11823
|
+
errors.push(`${toDisplayPath(file, cwd)}: ${read.error}`);
|
|
11824
|
+
continue;
|
|
11825
|
+
}
|
|
11826
|
+
batchFiles.push({
|
|
11827
|
+
cssContent: read.content,
|
|
11828
|
+
filePath: toDisplayPath(file, cwd),
|
|
11829
|
+
sizeBytes: read.sizeBytes
|
|
11830
|
+
});
|
|
11831
|
+
}
|
|
11832
|
+
if (batchFiles.length === 0) {
|
|
11833
|
+
return {
|
|
11834
|
+
schemaVersion: 1,
|
|
11835
|
+
command: "health",
|
|
11836
|
+
success: false,
|
|
11837
|
+
errors: errors.length > 0 ? errors : ["No CSS files could be read for health scoring."],
|
|
11838
|
+
patterns,
|
|
11839
|
+
threshold,
|
|
11840
|
+
failUnder,
|
|
11841
|
+
health: null
|
|
11842
|
+
};
|
|
11843
|
+
}
|
|
11844
|
+
const health = binding.evaluateCssHealthBatch({
|
|
11845
|
+
files: batchFiles,
|
|
11846
|
+
thresholds: threshold === null ? void 0 : {
|
|
11847
|
+
manySelectors: threshold
|
|
11848
|
+
}
|
|
11849
|
+
});
|
|
11850
|
+
const belowFloor = failUnder !== null && health.overall < failUnder ? [
|
|
11851
|
+
`Overall health score ${health.overall} is below --fail-under ${failUnder}.`
|
|
11852
|
+
] : [];
|
|
9430
11853
|
return {
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
result
|
|
11854
|
+
schemaVersion: 1,
|
|
11855
|
+
command: "health",
|
|
11856
|
+
success: errors.length === 0 && belowFloor.length === 0,
|
|
11857
|
+
errors: [...errors, ...belowFloor],
|
|
11858
|
+
patterns,
|
|
11859
|
+
threshold,
|
|
11860
|
+
failUnder,
|
|
11861
|
+
health
|
|
9440
11862
|
};
|
|
9441
11863
|
} catch (error) {
|
|
9442
|
-
const analysisMs = elapsedMs(analysisStartedAt);
|
|
9443
11864
|
return {
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
error: `Failed to analyze file: ${describeError(error)}`
|
|
11865
|
+
schemaVersion: 1,
|
|
11866
|
+
command: "health",
|
|
11867
|
+
success: false,
|
|
11868
|
+
errors: [describeError(error)],
|
|
11869
|
+
patterns,
|
|
11870
|
+
threshold,
|
|
11871
|
+
failUnder,
|
|
11872
|
+
health: null
|
|
9453
11873
|
};
|
|
9454
11874
|
}
|
|
9455
11875
|
}
|
|
9456
|
-
function
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
...warning.location ? { location: warning.location } : {}
|
|
9464
|
-
});
|
|
11876
|
+
function healthExitCode(report) {
|
|
11877
|
+
return report.success ? 0 : 1;
|
|
11878
|
+
}
|
|
11879
|
+
function parseNonNegativeInteger3(value) {
|
|
11880
|
+
const parsed = Number(value);
|
|
11881
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
11882
|
+
throw new InvalidArgumentError("must be a non-negative integer");
|
|
9465
11883
|
}
|
|
9466
|
-
|
|
9467
|
-
|
|
11884
|
+
return parsed;
|
|
11885
|
+
}
|
|
11886
|
+
function parseScore3(value) {
|
|
11887
|
+
const parsed = Number(value);
|
|
11888
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) {
|
|
11889
|
+
throw new InvalidArgumentError("must be an integer between 0 and 100");
|
|
11890
|
+
}
|
|
11891
|
+
return parsed;
|
|
11892
|
+
}
|
|
11893
|
+
|
|
11894
|
+
// src/commands/optimize.ts
|
|
11895
|
+
init_esm_shims();
|
|
11896
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
11897
|
+
import { dirname as dirname3, join as join2, resolve as resolve8 } from "path";
|
|
11898
|
+
function registerOptimizeCommand(program3) {
|
|
11899
|
+
registerOptimizeLikeCommand(
|
|
11900
|
+
program3,
|
|
11901
|
+
"optimize",
|
|
11902
|
+
"Preview or emit CSS optimization results with explanations"
|
|
9468
11903
|
);
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
11904
|
+
}
|
|
11905
|
+
function registerSplitCommand(program3) {
|
|
11906
|
+
registerOptimizeLikeCommand(
|
|
11907
|
+
program3,
|
|
11908
|
+
"split",
|
|
11909
|
+
"Preview page-coverage / size-budget CSS splitting"
|
|
11910
|
+
);
|
|
11911
|
+
}
|
|
11912
|
+
function registerOptimizeLikeCommand(program3, name, description) {
|
|
11913
|
+
program3.command(name).description(description).argument(
|
|
11914
|
+
"[patterns...]",
|
|
11915
|
+
"CSS files or glob patterns (quote globs to let css-doctor expand them)"
|
|
11916
|
+
).option("--dry-run", "Analyze and report without writing files").option("--write", "Write emitted chunks to --out-dir").option("--explain", "Include optimization decision explanations").option("--diff", "Include concise unified diffs for emitted CSS").option("-v, --verbose", "Show more decisions and chunks").option("-j, --json", "Emit JSON").option("--report <path>", "Write the JSON report to a file").option("--out-dir <path>", "Directory for emitted chunks when writing").option("--minify", "Minify emitted CSS").option("--no-minify", "Disable minification").option(
|
|
11917
|
+
"--page-usage <json>",
|
|
11918
|
+
"JSON map of page name \u2192 class name arrays for coverage splitting"
|
|
11919
|
+
).option(
|
|
11920
|
+
"--min-page-ratio <number>",
|
|
11921
|
+
"Minimum coverage ratio for common CSS (0-1)",
|
|
11922
|
+
parseRatio
|
|
11923
|
+
).option(
|
|
11924
|
+
"--max-chunk-size <bytes>",
|
|
11925
|
+
"Size budget per body chunk in bytes",
|
|
11926
|
+
parsePositiveInteger
|
|
11927
|
+
).option("--no-page-coverage", "Disable page-coverage splitting").option("--no-size-budget", "Disable size-budget splitting").option("--no-isolate-variables", "Disable CSS variable isolation").action(
|
|
11928
|
+
async (patterns, options) => {
|
|
11929
|
+
if (options.write === true && options.dryRun === true) {
|
|
11930
|
+
process.stderr.write(
|
|
11931
|
+
"css-doctor: --write and --dry-run are mutually exclusive.\n"
|
|
11932
|
+
);
|
|
11933
|
+
process.exitCode = 1;
|
|
11934
|
+
return;
|
|
11935
|
+
}
|
|
11936
|
+
const dryRun = options.write !== true;
|
|
11937
|
+
const report = await runOptimize(patterns ?? [], {
|
|
11938
|
+
...options,
|
|
11939
|
+
dryRun,
|
|
11940
|
+
commandName: name,
|
|
11941
|
+
minify: options.minify !== false
|
|
9480
11942
|
});
|
|
11943
|
+
if (options.report) {
|
|
11944
|
+
const reportPath = resolve8(
|
|
11945
|
+
options.cwd ?? process.cwd(),
|
|
11946
|
+
options.report
|
|
11947
|
+
);
|
|
11948
|
+
try {
|
|
11949
|
+
await mkdir3(dirname3(reportPath), { recursive: true });
|
|
11950
|
+
await writeFile3(
|
|
11951
|
+
reportPath,
|
|
11952
|
+
`${formatJsonReport(report)}
|
|
11953
|
+
`,
|
|
11954
|
+
"utf8"
|
|
11955
|
+
);
|
|
11956
|
+
} catch (error) {
|
|
11957
|
+
process.stderr.write(
|
|
11958
|
+
`css-doctor: failed to write --report ${reportPath}: ${describeError(error)}
|
|
11959
|
+
`
|
|
11960
|
+
);
|
|
11961
|
+
process.exitCode = 1;
|
|
11962
|
+
return;
|
|
11963
|
+
}
|
|
11964
|
+
}
|
|
11965
|
+
if (options.json === true) {
|
|
11966
|
+
process.stdout.write(`${formatJsonReport(report)}
|
|
11967
|
+
`);
|
|
11968
|
+
} else if (report.files.length > 0) {
|
|
11969
|
+
for (const file of report.files) {
|
|
11970
|
+
process.stdout.write(
|
|
11971
|
+
`${formatOptimizationTerminalReport(file.report, {
|
|
11972
|
+
dryRun: report.dryRun,
|
|
11973
|
+
verbose: options.verbose === true || options.explain === true
|
|
11974
|
+
})}
|
|
11975
|
+
`
|
|
11976
|
+
);
|
|
11977
|
+
if (options.diff === true && file.diff) {
|
|
11978
|
+
process.stdout.write(`${file.diff}
|
|
11979
|
+
`);
|
|
11980
|
+
}
|
|
11981
|
+
}
|
|
11982
|
+
if (report.errors.length > 0) {
|
|
11983
|
+
process.stderr.write(
|
|
11984
|
+
`css-doctor: ${report.errors.join("\ncss-doctor: ")}
|
|
11985
|
+
`
|
|
11986
|
+
);
|
|
11987
|
+
}
|
|
11988
|
+
} else {
|
|
11989
|
+
process.stdout.write(
|
|
11990
|
+
`css-doctor ${name} failed:
|
|
11991
|
+
${report.errors.map((error) => ` - ${error}`).join("\n")}
|
|
11992
|
+
`
|
|
11993
|
+
);
|
|
11994
|
+
}
|
|
11995
|
+
process.exitCode = report.success ? 0 : 1;
|
|
9481
11996
|
}
|
|
11997
|
+
);
|
|
11998
|
+
}
|
|
11999
|
+
async function runOptimize(patterns, options = {}) {
|
|
12000
|
+
const cwd = resolve8(options.cwd ?? process.cwd());
|
|
12001
|
+
const command = options.commandName ?? "optimize";
|
|
12002
|
+
const dryRun = options.dryRun !== false;
|
|
12003
|
+
const errors = [];
|
|
12004
|
+
const warnings = [];
|
|
12005
|
+
const files = [];
|
|
12006
|
+
if (patterns.length === 0) {
|
|
12007
|
+
errors.push(
|
|
12008
|
+
`No CSS files or glob patterns were provided. Example: css-doctor ${command} "src/**/*.css" --dry-run`
|
|
12009
|
+
);
|
|
12010
|
+
} else if (!await isDirectory(cwd)) {
|
|
12011
|
+
errors.push(
|
|
12012
|
+
`Working directory does not exist or is not a directory: ${cwd}`
|
|
12013
|
+
);
|
|
9482
12014
|
}
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
const
|
|
9486
|
-
if (!
|
|
9487
|
-
|
|
9488
|
-
|
|
12015
|
+
let pageUsage = {};
|
|
12016
|
+
if (options.pageUsage) {
|
|
12017
|
+
const parsedUsage = parsePageUsage(options.pageUsage);
|
|
12018
|
+
if (!parsedUsage.ok) {
|
|
12019
|
+
errors.push(parsedUsage.error);
|
|
12020
|
+
} else {
|
|
12021
|
+
pageUsage = parsedUsage.value;
|
|
9489
12022
|
}
|
|
9490
|
-
const firstRule = result.rules[first.ruleIndex];
|
|
9491
|
-
const currentRule = result.rules[selector.ruleIndex];
|
|
9492
|
-
issues.push({
|
|
9493
|
-
kind: "duplicate-selector",
|
|
9494
|
-
severity: "warning",
|
|
9495
|
-
message: `Selector "${selector.raw}" is defined in multiple rules` + (firstRule ? ` (first at line ${firstRule.location.line})` : "") + "; verify whether this is intentional.",
|
|
9496
|
-
...currentRule ? { location: currentRule.location } : {},
|
|
9497
|
-
selector: selector.raw
|
|
9498
|
-
});
|
|
9499
12023
|
}
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
12024
|
+
let cssFiles = [];
|
|
12025
|
+
if (errors.length === 0) {
|
|
12026
|
+
try {
|
|
12027
|
+
cssFiles = await resolveCssFiles(patterns, cwd);
|
|
12028
|
+
if (cssFiles.length === 0) {
|
|
12029
|
+
errors.push(`No CSS files matched: ${patterns.join(", ")}`);
|
|
12030
|
+
}
|
|
12031
|
+
} catch (error) {
|
|
12032
|
+
errors.push(`Failed to resolve CSS files: ${describeError(error)}`);
|
|
12033
|
+
}
|
|
9506
12034
|
}
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
const lineDifference = leftLine - rightLine;
|
|
9513
|
-
if (lineDifference !== 0) {
|
|
9514
|
-
return lineDifference;
|
|
12035
|
+
const outDir = options.outDir ? resolve8(cwd, options.outDir) : null;
|
|
12036
|
+
if (!dryRun && !outDir) {
|
|
12037
|
+
errors.push(
|
|
12038
|
+
"Writing chunks requires --out-dir (or omit --write for dry-run)."
|
|
12039
|
+
);
|
|
9515
12040
|
}
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
12041
|
+
if (errors.length > 0) {
|
|
12042
|
+
return {
|
|
12043
|
+
schemaVersion: 1,
|
|
12044
|
+
command,
|
|
12045
|
+
success: false,
|
|
12046
|
+
dryRun,
|
|
12047
|
+
errors,
|
|
12048
|
+
warnings,
|
|
12049
|
+
patterns,
|
|
12050
|
+
files: [],
|
|
12051
|
+
outDir
|
|
12052
|
+
};
|
|
9519
12053
|
}
|
|
9520
|
-
|
|
12054
|
+
try {
|
|
12055
|
+
const binding = loadBinding();
|
|
12056
|
+
const pipelineOptions = {
|
|
12057
|
+
pageUsage,
|
|
12058
|
+
pageCoverage: {
|
|
12059
|
+
enabled: options.noPageCoverage !== true,
|
|
12060
|
+
minPageRatio: options.minPageRatio
|
|
12061
|
+
},
|
|
12062
|
+
sizeBudget: {
|
|
12063
|
+
enabled: options.noSizeBudget !== true,
|
|
12064
|
+
maxChunkSize: options.maxChunkSize,
|
|
12065
|
+
isolateVariables: options.noIsolateVariables !== true
|
|
12066
|
+
},
|
|
12067
|
+
minify: options.minify !== false
|
|
12068
|
+
};
|
|
12069
|
+
for (const absolute of cssFiles) {
|
|
12070
|
+
const read = await readCssFile(absolute);
|
|
12071
|
+
const displayPath = toDisplayPath(absolute, cwd);
|
|
12072
|
+
if (!read.ok) {
|
|
12073
|
+
errors.push(`${displayPath}: ${read.error}`);
|
|
12074
|
+
continue;
|
|
12075
|
+
}
|
|
12076
|
+
const explained = binding.optimizeCssExplained(
|
|
12077
|
+
{
|
|
12078
|
+
cssContent: read.content,
|
|
12079
|
+
...pipelineOptions
|
|
12080
|
+
},
|
|
12081
|
+
options.verbose === true ? 200 : 40
|
|
12082
|
+
);
|
|
12083
|
+
const report = JSON.parse(explained.reportJson);
|
|
12084
|
+
warnings.push(...report.warnings);
|
|
12085
|
+
const chunks = explained.pipeline.chunks.map((chunk) => ({
|
|
12086
|
+
name: chunk.name,
|
|
12087
|
+
css: chunk.css,
|
|
12088
|
+
sizeBytes: chunk.sizeBytes
|
|
12089
|
+
}));
|
|
12090
|
+
let diff;
|
|
12091
|
+
if (options.diff === true) {
|
|
12092
|
+
const combined = chunks.map((chunk) => `/* ${chunk.name} */
|
|
12093
|
+
${chunk.css}`).join("\n\n");
|
|
12094
|
+
diff = formatUnifiedDiff(displayPath, read.content, combined, {
|
|
12095
|
+
maxLines: options.verbose === true ? 200 : 80
|
|
12096
|
+
});
|
|
12097
|
+
}
|
|
12098
|
+
if (!dryRun && outDir) {
|
|
12099
|
+
await mkdir3(outDir, { recursive: true });
|
|
12100
|
+
const stem = displayPath.replace(/[\\/]/g, "__").replace(/\.css$/i, "");
|
|
12101
|
+
for (const chunk of chunks) {
|
|
12102
|
+
await writeFile3(
|
|
12103
|
+
join2(outDir, `${stem}.${chunk.name}.css`),
|
|
12104
|
+
chunk.css,
|
|
12105
|
+
"utf8"
|
|
12106
|
+
);
|
|
12107
|
+
}
|
|
12108
|
+
}
|
|
12109
|
+
files.push({ path: displayPath, report, chunks, diff });
|
|
12110
|
+
}
|
|
12111
|
+
} catch (error) {
|
|
12112
|
+
errors.push(describeError(error));
|
|
12113
|
+
}
|
|
12114
|
+
return {
|
|
12115
|
+
schemaVersion: 1,
|
|
12116
|
+
command,
|
|
12117
|
+
success: errors.length === 0,
|
|
12118
|
+
dryRun,
|
|
12119
|
+
errors,
|
|
12120
|
+
warnings,
|
|
12121
|
+
patterns,
|
|
12122
|
+
files,
|
|
12123
|
+
outDir
|
|
12124
|
+
};
|
|
9521
12125
|
}
|
|
9522
|
-
function
|
|
9523
|
-
|
|
12126
|
+
function parsePageUsage(raw) {
|
|
12127
|
+
try {
|
|
12128
|
+
const parsed = JSON.parse(raw);
|
|
12129
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
12130
|
+
return {
|
|
12131
|
+
ok: false,
|
|
12132
|
+
error: "--page-usage must be a JSON object of page \u2192 string[]."
|
|
12133
|
+
};
|
|
12134
|
+
}
|
|
12135
|
+
const value = {};
|
|
12136
|
+
for (const [page, classes] of Object.entries(
|
|
12137
|
+
parsed
|
|
12138
|
+
)) {
|
|
12139
|
+
if (!Array.isArray(classes) || classes.some((item) => typeof item !== "string")) {
|
|
12140
|
+
return {
|
|
12141
|
+
ok: false,
|
|
12142
|
+
error: `--page-usage page "${page}" must map to an array of class name strings.`
|
|
12143
|
+
};
|
|
12144
|
+
}
|
|
12145
|
+
value[page] = classes;
|
|
12146
|
+
}
|
|
12147
|
+
return { ok: true, value };
|
|
12148
|
+
} catch (error) {
|
|
12149
|
+
return {
|
|
12150
|
+
ok: false,
|
|
12151
|
+
error: `Invalid --page-usage JSON: ${describeError(error)}`
|
|
12152
|
+
};
|
|
12153
|
+
}
|
|
9524
12154
|
}
|
|
9525
|
-
function
|
|
9526
|
-
|
|
12155
|
+
function parseRatio(value) {
|
|
12156
|
+
const parsed = Number(value);
|
|
12157
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
12158
|
+
throw new InvalidArgumentError("must be a number between 0 and 1");
|
|
12159
|
+
}
|
|
12160
|
+
return parsed;
|
|
9527
12161
|
}
|
|
9528
|
-
function
|
|
9529
|
-
|
|
12162
|
+
function parsePositiveInteger(value) {
|
|
12163
|
+
const parsed = Number(value);
|
|
12164
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
12165
|
+
throw new InvalidArgumentError("must be a positive integer");
|
|
12166
|
+
}
|
|
12167
|
+
return parsed;
|
|
9530
12168
|
}
|
|
9531
12169
|
|
|
9532
12170
|
// src/index.ts
|
|
9533
12171
|
var program2 = new Command();
|
|
9534
|
-
program2.name("css-doctor").description(
|
|
12172
|
+
program2.name("css-doctor").description(
|
|
12173
|
+
"Diagnose, score, regress, explain, and safely optimize CSS with a native LightningCSS engine"
|
|
12174
|
+
).version(package_default.version, "-v, --version", "Output the version number").showHelpAfterError("(run with --help for usage information)").showSuggestionAfterError().exitOverride();
|
|
9535
12175
|
registerCheckCommand(program2);
|
|
12176
|
+
registerHealthCommand(program2);
|
|
12177
|
+
registerBaselineCommand(program2);
|
|
12178
|
+
registerDiffCommand(program2);
|
|
12179
|
+
registerOptimizeCommand(program2);
|
|
12180
|
+
registerSplitCommand(program2);
|
|
12181
|
+
registerFixCommand(program2);
|
|
9536
12182
|
try {
|
|
9537
12183
|
await program2.parseAsync(process.argv);
|
|
9538
12184
|
} catch (error) {
|