@code-pushup/utils 0.47.0 → 0.49.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/index.js CHANGED
@@ -739,11 +739,262 @@ function comparePairs(pairs, equalsFn) {
739
739
  import { spawn } from "node:child_process";
740
740
 
741
741
  // packages/utils/src/lib/reports/utils.ts
742
- import { join as join2 } from "node:path";
742
+ import ansis from "ansis";
743
+ import { md } from "build-md";
744
+
745
+ // packages/utils/src/lib/reports/constants.ts
746
+ var TERMINAL_WIDTH = 80;
747
+ var SCORE_COLOR_RANGE = {
748
+ GREEN_MIN: 0.9,
749
+ YELLOW_MIN: 0.5
750
+ };
751
+ var FOOTER_PREFIX = "Made with \u2764 by";
752
+ var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
753
+ var README_LINK = "https://github.com/code-pushup/cli#readme";
754
+ var REPORT_HEADLINE_TEXT = "Code PushUp Report";
755
+ var REPORT_RAW_OVERVIEW_TABLE_HEADERS = [
756
+ "Category",
757
+ "Score",
758
+ "Audits"
759
+ ];
760
+
761
+ // packages/utils/src/lib/reports/utils.ts
762
+ function formatReportScore(score) {
763
+ const scaledScore = score * 100;
764
+ const roundedScore = Math.round(scaledScore);
765
+ return roundedScore === 100 && score !== 1 ? Math.floor(scaledScore).toString() : roundedScore.toString();
766
+ }
767
+ function formatScoreWithColor(score, options) {
768
+ const styledNumber = options?.skipBold ? formatReportScore(score) : md.bold(formatReportScore(score));
769
+ return md`${scoreMarker(score)} ${styledNumber}`;
770
+ }
771
+ var MARKERS = {
772
+ circle: {
773
+ red: "\u{1F534}",
774
+ yellow: "\u{1F7E1}",
775
+ green: "\u{1F7E2}"
776
+ },
777
+ square: {
778
+ red: "\u{1F7E5}",
779
+ yellow: "\u{1F7E8}",
780
+ green: "\u{1F7E9}"
781
+ }
782
+ };
783
+ function scoreMarker(score, markerType = "circle") {
784
+ if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
785
+ return MARKERS[markerType].green;
786
+ }
787
+ if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
788
+ return MARKERS[markerType].yellow;
789
+ }
790
+ return MARKERS[markerType].red;
791
+ }
792
+ function getDiffMarker(diff) {
793
+ if (diff > 0) {
794
+ return "\u2191";
795
+ }
796
+ if (diff < 0) {
797
+ return "\u2193";
798
+ }
799
+ return "";
800
+ }
801
+ function colorByScoreDiff(text, diff) {
802
+ const color = diff > 0 ? "green" : diff < 0 ? "red" : "gray";
803
+ return shieldsBadge(text, color);
804
+ }
805
+ function shieldsBadge(text, color) {
806
+ return md.image(
807
+ `https://img.shields.io/badge/${encodeURIComponent(text)}-${color}`,
808
+ text
809
+ );
810
+ }
811
+ function formatDiffNumber(diff) {
812
+ const number = Math.abs(diff) === Number.POSITIVE_INFINITY ? "\u221E" : `${Math.abs(diff)}`;
813
+ const sign = diff < 0 ? "\u2212" : "+";
814
+ return `${sign}${number}`;
815
+ }
816
+ function severityMarker(severity) {
817
+ if (severity === "error") {
818
+ return "\u{1F6A8}";
819
+ }
820
+ if (severity === "warning") {
821
+ return "\u26A0\uFE0F";
822
+ }
823
+ return "\u2139\uFE0F";
824
+ }
825
+ function formatScoreChange(diff) {
826
+ const marker = getDiffMarker(diff);
827
+ const text = formatDiffNumber(Math.round(diff * 1e3) / 10);
828
+ return colorByScoreDiff(`${marker} ${text}`, diff);
829
+ }
830
+ function formatValueChange({
831
+ values,
832
+ scores
833
+ }) {
834
+ const marker = getDiffMarker(values.diff);
835
+ const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : Math.round(100 * values.diff / values.before);
836
+ const text = `${formatDiffNumber(percentage)}\u2009%`;
837
+ return colorByScoreDiff(`${marker} ${text}`, scores.diff);
838
+ }
839
+ function calcDuration(start, stop) {
840
+ return Math.round((stop ?? performance.now()) - start);
841
+ }
842
+ function countCategoryAudits(refs, plugins) {
843
+ const groupLookup = plugins.reduce(
844
+ (lookup, plugin) => {
845
+ if (plugin.groups == null || plugin.groups.length === 0) {
846
+ return lookup;
847
+ }
848
+ return {
849
+ ...lookup,
850
+ [plugin.slug]: Object.fromEntries(
851
+ plugin.groups.map((group) => [group.slug, group])
852
+ )
853
+ };
854
+ },
855
+ {}
856
+ );
857
+ return refs.reduce((acc, ref) => {
858
+ if (ref.type === "group") {
859
+ const groupRefs = groupLookup[ref.plugin]?.[ref.slug]?.refs;
860
+ return acc + (groupRefs?.length ?? 0);
861
+ }
862
+ return acc + 1;
863
+ }, 0);
864
+ }
865
+ function compareCategoryAuditsAndGroups(a, b) {
866
+ if (a.weight !== b.weight) {
867
+ return b.weight - a.weight;
868
+ }
869
+ if (a.score !== b.score) {
870
+ return a.score - b.score;
871
+ }
872
+ if ("value" in a && "value" in b && a.value !== b.value) {
873
+ return b.value - a.value;
874
+ }
875
+ return a.title.localeCompare(b.title);
876
+ }
877
+ function compareAudits(a, b) {
878
+ if (a.score !== b.score) {
879
+ return a.score - b.score;
880
+ }
881
+ if (a.value !== b.value) {
882
+ return b.value - a.value;
883
+ }
884
+ return a.title.localeCompare(b.title);
885
+ }
886
+ function compareIssueSeverity(severity1, severity2) {
887
+ const levels = {
888
+ info: 0,
889
+ warning: 1,
890
+ error: 2
891
+ };
892
+ return levels[severity1] - levels[severity2];
893
+ }
894
+ function throwIsNotPresentError(itemName, presentPlace) {
895
+ throw new Error(`${itemName} is not present in ${presentPlace}`);
896
+ }
897
+ function getPluginNameFromSlug(slug, plugins) {
898
+ return plugins.find(({ slug: pluginSlug }) => pluginSlug === slug)?.title || slug;
899
+ }
900
+ function compareIssues(a, b) {
901
+ if (a.severity !== b.severity) {
902
+ return -compareIssueSeverity(a.severity, b.severity);
903
+ }
904
+ if (!a.source && b.source) {
905
+ return -1;
906
+ }
907
+ if (a.source && !b.source) {
908
+ return 1;
909
+ }
910
+ if (a.source?.file !== b.source?.file) {
911
+ return a.source?.file.localeCompare(b.source?.file || "") ?? 0;
912
+ }
913
+ if (!a.source?.position && b.source?.position) {
914
+ return -1;
915
+ }
916
+ if (a.source?.position && !b.source?.position) {
917
+ return 1;
918
+ }
919
+ if (a.source?.position?.startLine !== b.source?.position?.startLine) {
920
+ return (a.source?.position?.startLine ?? 0) - (b.source?.position?.startLine ?? 0);
921
+ }
922
+ return 0;
923
+ }
924
+ function applyScoreColor({ score, text }, style = ansis) {
925
+ const formattedScore = text ?? formatReportScore(score);
926
+ if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
927
+ return text ? style.green(formattedScore) : style.bold(style.green(formattedScore));
928
+ }
929
+ if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
930
+ return text ? style.yellow(formattedScore) : style.bold(style.yellow(formattedScore));
931
+ }
932
+ return text ? style.red(formattedScore) : style.bold(style.red(formattedScore));
933
+ }
934
+ function targetScoreIcon(score, targetScore, options = {}) {
935
+ if (targetScore != null) {
936
+ const {
937
+ passIcon = "\u2705",
938
+ failIcon = "\u274C",
939
+ prefix = "",
940
+ postfix = ""
941
+ } = options;
942
+ if (score >= targetScore) {
943
+ return `${prefix}${passIcon}${postfix}`;
944
+ }
945
+ return `${prefix}${failIcon}${postfix}`;
946
+ }
947
+ return "";
948
+ }
949
+
950
+ // packages/utils/src/lib/execute-process.ts
951
+ var ProcessError = class extends Error {
952
+ code;
953
+ stderr;
954
+ stdout;
955
+ constructor(result) {
956
+ super(result.stderr);
957
+ this.code = result.code;
958
+ this.stderr = result.stderr;
959
+ this.stdout = result.stdout;
960
+ }
961
+ };
962
+ function executeProcess(cfg) {
963
+ const { observer, cwd, command, args, ignoreExitCode = false } = cfg;
964
+ const { onStdout, onError, onComplete } = observer ?? {};
965
+ const date = (/* @__PURE__ */ new Date()).toISOString();
966
+ const start = performance.now();
967
+ return new Promise((resolve, reject) => {
968
+ const process2 = spawn(command, args, { cwd, shell: true });
969
+ let stdout = "";
970
+ let stderr = "";
971
+ process2.stdout.on("data", (data) => {
972
+ stdout += String(data);
973
+ onStdout?.(String(data));
974
+ });
975
+ process2.stderr.on("data", (data) => {
976
+ stderr += String(data);
977
+ });
978
+ process2.on("error", (err) => {
979
+ stderr += err.toString();
980
+ });
981
+ process2.on("close", (code2) => {
982
+ const timings = { date, duration: calcDuration(start) };
983
+ if (code2 === 0 || ignoreExitCode) {
984
+ onComplete?.();
985
+ resolve({ code: code2, stdout, stderr, ...timings });
986
+ } else {
987
+ const errorMsg = new ProcessError({ code: code2, stdout, stderr, ...timings });
988
+ onError?.(errorMsg);
989
+ reject(errorMsg);
990
+ }
991
+ });
992
+ });
993
+ }
743
994
 
744
995
  // packages/utils/src/lib/file-system.ts
996
+ import { bold, gray } from "ansis";
745
997
  import { bundleRequire } from "bundle-require";
746
- import chalk2 from "chalk";
747
998
  import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
748
999
  import { join } from "node:path";
749
1000
 
@@ -836,55 +1087,7 @@ function isPromiseRejectedResult(result) {
836
1087
  // packages/utils/src/lib/logging.ts
837
1088
  import isaacs_cliui from "@isaacs/cliui";
838
1089
  import { cliui } from "@poppinss/cliui";
839
- import chalk from "chalk";
840
-
841
- // packages/utils/src/lib/reports/constants.ts
842
- var TERMINAL_WIDTH = 80;
843
- var SCORE_COLOR_RANGE = {
844
- GREEN_MIN: 0.9,
845
- YELLOW_MIN: 0.5
846
- };
847
- var CATEGORIES_TITLE = "\u{1F3F7} Categories";
848
- var FOOTER_PREFIX = "Made with \u2764 by";
849
- var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
850
- var README_LINK = "https://github.com/code-pushup/cli#readme";
851
- var reportHeadlineText = "Code PushUp Report";
852
- var reportOverviewTableHeaders = [
853
- {
854
- key: "category",
855
- label: "\u{1F3F7} Category",
856
- align: "left"
857
- },
858
- {
859
- key: "score",
860
- label: "\u2B50 Score"
861
- },
862
- {
863
- key: "audits",
864
- label: "\u{1F6E1} Audits"
865
- }
866
- ];
867
- var reportRawOverviewTableHeaders = ["Category", "Score", "Audits"];
868
- var issuesTableHeadings = [
869
- {
870
- key: "severity",
871
- label: "Severity"
872
- },
873
- {
874
- key: "message",
875
- label: "Message"
876
- },
877
- {
878
- key: "file",
879
- label: "Source file"
880
- },
881
- {
882
- key: "line",
883
- label: "Line(s)"
884
- }
885
- ];
886
-
887
- // packages/utils/src/lib/logging.ts
1090
+ import { underline } from "ansis";
888
1091
  var singletonUiInstance;
889
1092
  function ui() {
890
1093
  if (singletonUiInstance === void 0) {
@@ -908,7 +1111,7 @@ function logListItem(args) {
908
1111
  singletonUiInstance?.logger.log(content);
909
1112
  }
910
1113
  function link(text) {
911
- return chalk.underline(chalk.blueBright(text));
1114
+ return underline.blueBright(text);
912
1115
  }
913
1116
 
914
1117
  // packages/utils/src/lib/log-results.ts
@@ -988,10 +1191,10 @@ async function removeDirectoryIfExists(dir) {
988
1191
  function logMultipleFileResults(fileResults, messagePrefix) {
989
1192
  const succeededTransform = (result) => {
990
1193
  const [fileName, size] = result.value;
991
- const formattedSize = size ? ` (${chalk2.gray(formatBytes(size))})` : "";
992
- return `- ${chalk2.bold(fileName)}${formattedSize}`;
1194
+ const formattedSize = size ? ` (${gray(formatBytes(size))})` : "";
1195
+ return `- ${bold(fileName)}${formattedSize}`;
993
1196
  };
994
- const failedTransform = (result) => `- ${chalk2.bold(result.reason)}`;
1197
+ const failedTransform = (result) => `- ${bold(result.reason)}`;
995
1198
  logMultipleResults(
996
1199
  fileResults,
997
1200
  messagePrefix,
@@ -1031,46 +1234,25 @@ async function crawlFileSystem(options) {
1031
1234
  return resultsNestedArray.flat();
1032
1235
  }
1033
1236
  function findLineNumberInText(content, pattern) {
1034
- const lines6 = content.split(/\r?\n/);
1035
- const lineNumber = lines6.findIndex((line) => line.includes(pattern)) + 1;
1237
+ const lines = content.split(/\r?\n/);
1238
+ const lineNumber = lines.findIndex((line) => line.includes(pattern)) + 1;
1036
1239
  return lineNumber === 0 ? null : lineNumber;
1037
1240
  }
1038
1241
  function filePathToCliArg(path) {
1039
1242
  return `"${path}"`;
1040
1243
  }
1041
1244
 
1042
- // packages/utils/src/lib/text-formats/constants.ts
1043
- var NEW_LINE = "\n";
1044
- var TAB = " ";
1045
- var SPACE = " ";
1046
-
1047
- // packages/utils/src/lib/text-formats/html/details.ts
1048
- function details(title, content, cfg = { open: false }) {
1049
- return `<details${cfg.open ? " open" : ""}>${NEW_LINE}<summary>${title}</summary>${NEW_LINE}${// ⚠️ The blank line is needed to ensure Markdown in content is rendered correctly.
1050
- NEW_LINE}${content}${NEW_LINE}${// @TODO in the future we could consider adding it only if the content ends with a code block
1051
- // ⚠️ The blank line ensure Markdown in content is rendered correctly.
1052
- NEW_LINE}</details>${// ⚠️ The blank line is needed to ensure Markdown after details is rendered correctly.
1053
- NEW_LINE}`;
1054
- }
1055
-
1056
- // packages/utils/src/lib/text-formats/html/font-style.ts
1057
- var boldElement = "b";
1058
- function bold(text) {
1059
- return `<${boldElement}>${text}</${boldElement}>`;
1060
- }
1061
- var italicElement = "i";
1062
- function italic(text) {
1063
- return `<${italicElement}>${text}</${italicElement}>`;
1064
- }
1065
- var codeElement = "code";
1066
- function code(text) {
1067
- return `<${codeElement}>${text}</${codeElement}>`;
1245
+ // packages/utils/src/lib/filter.ts
1246
+ function filterItemRefsBy(items, refFilterFn) {
1247
+ return items.map((item) => ({
1248
+ ...item,
1249
+ refs: item.refs.filter(refFilterFn)
1250
+ })).filter((item) => item.refs.length);
1068
1251
  }
1069
1252
 
1070
- // packages/utils/src/lib/text-formats/html/link.ts
1071
- function link2(href, text) {
1072
- return `<a href="${href}">${text || href}</a>`;
1073
- }
1253
+ // packages/utils/src/lib/git/git.ts
1254
+ import { isAbsolute, join as join2, relative } from "node:path";
1255
+ import { simpleGit } from "simple-git";
1074
1256
 
1075
1257
  // packages/utils/src/lib/transform.ts
1076
1258
  import { platform } from "node:os";
@@ -1121,6 +1303,12 @@ function objectToCliArgs(params) {
1121
1303
  if (Array.isArray(value)) {
1122
1304
  return value.map((v) => `${prefix}${key}="${v}"`);
1123
1305
  }
1306
+ if (typeof value === "object") {
1307
+ return Object.entries(value).flatMap(
1308
+ // transform nested objects to the dot notation `key.subkey`
1309
+ ([k, v]) => objectToCliArgs({ [`${key}.${k}`]: v })
1310
+ );
1311
+ }
1124
1312
  if (typeof value === "string") {
1125
1313
  return [`${prefix}${key}="${value}"`];
1126
1314
  }
@@ -1151,11 +1339,6 @@ function capitalize(text) {
1151
1339
  1
1152
1340
  )}`;
1153
1341
  }
1154
- function apostrophize(text, upperCase) {
1155
- const lastCharMatch = text.match(/(\w)\W*$/);
1156
- const lastChar = lastCharMatch?.[1] ?? "";
1157
- return `${text}'${lastChar.toLocaleLowerCase() === "s" ? "" : upperCase ? "S" : "s"}`;
1158
- }
1159
1342
  function toNumberPrecision(value, decimalPlaces) {
1160
1343
  return Number(
1161
1344
  `${Math.round(
@@ -1176,1088 +1359,908 @@ function toOrdinal(value) {
1176
1359
  return `${value}th`;
1177
1360
  }
1178
1361
 
1179
- // packages/utils/src/lib/text-formats/table.ts
1180
- function rowToStringArray({ rows, columns = [] }) {
1181
- if (Array.isArray(rows.at(0)) && typeof columns.at(0) === "object") {
1182
- throw new TypeError(
1183
- "Column can`t be object when rows are primitive values"
1184
- );
1185
- }
1186
- return rows.map((row) => {
1187
- if (Array.isArray(row)) {
1188
- return row.map(String);
1189
- }
1190
- const objectRow = row;
1191
- if (columns.length === 0 || typeof columns.at(0) === "string") {
1192
- return Object.values(objectRow).map(
1193
- (value) => value == null ? "" : String(value)
1194
- );
1195
- }
1196
- return columns.map(
1197
- ({ key }) => objectRow[key] == null ? "" : String(objectRow[key])
1198
- );
1199
- });
1200
- }
1201
- function columnsToStringArray({
1202
- rows,
1203
- columns = []
1204
- }) {
1205
- const firstRow = rows.at(0);
1206
- const primitiveRows = Array.isArray(firstRow);
1207
- if (typeof columns.at(0) === "string" && !primitiveRows) {
1208
- throw new Error("invalid union type. Caught by model parsing.");
1209
- }
1210
- if (columns.length === 0) {
1211
- if (Array.isArray(firstRow)) {
1212
- return firstRow.map((_, idx) => String(idx));
1213
- }
1214
- return Object.keys(firstRow);
1215
- }
1216
- if (typeof columns.at(0) === "string") {
1217
- return columns.map(String);
1218
- }
1219
- const cols = columns;
1220
- return cols.map(({ label, key }) => label ?? capitalize(key));
1362
+ // packages/utils/src/lib/git/git.ts
1363
+ function getGitRoot(git = simpleGit()) {
1364
+ return git.revparse("--show-toplevel");
1221
1365
  }
1222
- function getColumnAlignmentForKeyAndIndex(targetKey, targetIdx, columns = []) {
1223
- const column = columns.at(targetIdx) ?? columns.find((col) => col.key === targetKey);
1224
- if (typeof column === "string") {
1225
- return column;
1226
- } else if (typeof column === "object") {
1227
- return column.align ?? "center";
1228
- } else {
1229
- return "center";
1230
- }
1366
+ function formatGitPath(path, gitRoot) {
1367
+ const absolutePath = isAbsolute(path) ? path : join2(process.cwd(), path);
1368
+ const relativePath = relative(gitRoot, absolutePath);
1369
+ return toUnixPath(relativePath);
1231
1370
  }
1232
- function getColumnAlignmentForIndex(targetIdx, columns = []) {
1233
- const column = columns.at(targetIdx);
1234
- if (column == null) {
1235
- return "center";
1236
- } else if (typeof column === "string") {
1237
- return column;
1238
- } else if (typeof column === "object") {
1239
- return column.align ?? "center";
1240
- } else {
1241
- return "center";
1242
- }
1371
+ async function toGitPath(path, git = simpleGit()) {
1372
+ const gitRoot = await getGitRoot(git);
1373
+ return formatGitPath(path, gitRoot);
1243
1374
  }
1244
- function getColumnAlignments(tableData) {
1245
- const { rows, columns = [] } = tableData;
1246
- if (rows.at(0) == null) {
1247
- throw new Error("first row can`t be undefined.");
1248
- }
1249
- if (Array.isArray(rows.at(0))) {
1250
- const firstPrimitiveRow = rows.at(0);
1251
- return Array.from({ length: firstPrimitiveRow.length }).map(
1252
- (_, idx) => getColumnAlignmentForIndex(idx, columns)
1375
+ var GitStatusError = class _GitStatusError extends Error {
1376
+ static ignoredProps = /* @__PURE__ */ new Set(["current", "tracking"]);
1377
+ static getReducedStatus(status) {
1378
+ return Object.fromEntries(
1379
+ Object.entries(status).filter(([key]) => !this.ignoredProps.has(key)).filter(
1380
+ (entry) => {
1381
+ const value = entry[1];
1382
+ if (value == null) {
1383
+ return false;
1384
+ }
1385
+ if (Array.isArray(value) && value.length === 0) {
1386
+ return false;
1387
+ }
1388
+ if (typeof value === "number" && value === 0) {
1389
+ return false;
1390
+ }
1391
+ return !(typeof value === "boolean" && !value);
1392
+ }
1393
+ )
1253
1394
  );
1254
1395
  }
1255
- const biggestRow = [...rows].sort((a, b) => Object.keys(a).length - Object.keys(b).length).at(-1);
1256
- if (columns.length > 0) {
1257
- return columns.map(
1258
- (column, idx) => typeof column === "string" ? column : getColumnAlignmentForKeyAndIndex(
1259
- column.key,
1260
- idx,
1261
- columns
1262
- )
1396
+ constructor(status) {
1397
+ super(
1398
+ `Working directory needs to be clean before we you can proceed. Commit your local changes or stash them:
1399
+ ${JSON.stringify(
1400
+ _GitStatusError.getReducedStatus(status),
1401
+ null,
1402
+ 2
1403
+ )}`
1263
1404
  );
1264
1405
  }
1265
- return Object.keys(biggestRow ?? {}).map((_) => "center");
1266
- }
1267
-
1268
- // packages/utils/src/lib/text-formats/html/table.ts
1269
- function wrap(elem, content) {
1270
- return `<${elem}>${content}</${elem}>${NEW_LINE}`;
1271
- }
1272
- function wrapRow(content) {
1273
- const elem = "tr";
1274
- return `<${elem}>${NEW_LINE}${content}</${elem}>${NEW_LINE}`;
1275
- }
1276
- function table(tableData) {
1277
- if (tableData.rows.length === 0) {
1278
- throw new Error("Data can't be empty");
1406
+ };
1407
+ async function guardAgainstLocalChanges(git = simpleGit()) {
1408
+ const status = await git.status(["-s"]);
1409
+ if (status.files.length > 0) {
1410
+ throw new GitStatusError(status);
1279
1411
  }
1280
- const tableHeaderCols = columnsToStringArray(tableData).map((s) => wrap("th", s)).join("");
1281
- const tableHeaderRow = wrapRow(tableHeaderCols);
1282
- const tableBody = rowToStringArray(tableData).map((arr) => {
1283
- const columns = arr.map((s) => wrap("td", s)).join("");
1284
- return wrapRow(columns);
1285
- }).join("");
1286
- return wrap("table", `${NEW_LINE}${tableHeaderRow}${tableBody}`);
1287
- }
1288
-
1289
- // packages/utils/src/lib/text-formats/md/font-style.ts
1290
- var boldWrap = "**";
1291
- function bold2(text) {
1292
- return `${boldWrap}${text}${boldWrap}`;
1293
1412
  }
1294
- var italicWrap = "_";
1295
- function italic2(text) {
1296
- return `${italicWrap}${text}${italicWrap}`;
1297
- }
1298
- var strikeThroughWrap = "~";
1299
- function strikeThrough(text) {
1300
- return `${strikeThroughWrap}${text}${strikeThroughWrap}`;
1301
- }
1302
- var codeWrap = "`";
1303
- function code2(text) {
1304
- return `${codeWrap}${text}${codeWrap}`;
1305
- }
1306
-
1307
- // packages/utils/src/lib/text-formats/md/headline.ts
1308
- function headline(text, hierarchy = 1) {
1309
- return `${"#".repeat(hierarchy)} ${text}${NEW_LINE}`;
1310
- }
1311
- function h(text, hierarchy = 1) {
1312
- return headline(text, hierarchy);
1313
- }
1314
- function h1(text) {
1315
- return headline(text, 1);
1316
- }
1317
- function h2(text) {
1318
- return headline(text, 2);
1319
- }
1320
- function h3(text) {
1321
- return headline(text, 3);
1322
- }
1323
- function h4(text) {
1324
- return headline(text, 4);
1325
- }
1326
- function h5(text) {
1327
- return headline(text, 5);
1328
- }
1329
- function h6(text) {
1330
- return headline(text, 6);
1413
+ async function safeCheckout(branchOrHash, forceCleanStatus = false, git = simpleGit()) {
1414
+ if (forceCleanStatus) {
1415
+ await git.raw(["reset", "--hard"]);
1416
+ await git.clean(["f", "d"]);
1417
+ ui().logger.info(`git status cleaned`);
1418
+ }
1419
+ await guardAgainstLocalChanges(git);
1420
+ await git.checkout(branchOrHash);
1331
1421
  }
1332
1422
 
1333
- // packages/utils/src/lib/text-formats/md/image.ts
1334
- function image(src, alt) {
1335
- return `![${alt}](${src})`;
1336
- }
1423
+ // packages/utils/src/lib/git/git.commits-and-tags.ts
1424
+ import { simpleGit as simpleGit2 } from "simple-git";
1337
1425
 
1338
- // packages/utils/src/lib/text-formats/md/link.ts
1339
- function link3(href, text) {
1340
- return `[${text || href}](${href})`;
1426
+ // packages/utils/src/lib/semver.ts
1427
+ import { rcompare, valid } from "semver";
1428
+ function normalizeSemver(semverString) {
1429
+ if (semverString.startsWith("v") || semverString.startsWith("V")) {
1430
+ return semverString.slice(1);
1431
+ }
1432
+ if (semverString.includes("@")) {
1433
+ return semverString.split("@").at(-1) ?? "";
1434
+ }
1435
+ return semverString;
1341
1436
  }
1342
-
1343
- // packages/utils/src/lib/text-formats/md/list.ts
1344
- function li(text, order = "unordered") {
1345
- const style = order === "unordered" ? "-" : "- [ ]";
1346
- return `${style} ${text}`;
1437
+ function isSemver(semverString = "") {
1438
+ return valid(normalizeSemver(semverString)) != null;
1347
1439
  }
1348
- function indentation(text, level = 1) {
1349
- return `${TAB.repeat(level)}${text}`;
1440
+ function sortSemvers(semverStrings) {
1441
+ return semverStrings.map(normalizeSemver).filter(isSemver).sort(rcompare);
1350
1442
  }
1351
1443
 
1352
- // packages/utils/src/lib/text-formats/md/paragraphs.ts
1353
- function paragraphs(...sections) {
1354
- return sections.filter(Boolean).join(`${NEW_LINE}${NEW_LINE}`);
1444
+ // packages/utils/src/lib/git/git.commits-and-tags.ts
1445
+ async function getLatestCommit(git = simpleGit2()) {
1446
+ const log2 = await git.log({
1447
+ maxCount: 1,
1448
+ // git log -1 --pretty=format:"%H %s %an %aI" - See: https://git-scm.com/docs/pretty-formats
1449
+ format: { hash: "%H", message: "%s", author: "%an", date: "%aI" }
1450
+ });
1451
+ return commitSchema.parse(log2.latest);
1355
1452
  }
1356
-
1357
- // packages/utils/src/lib/text-formats/md/section.ts
1358
- function section(...contents) {
1359
- return `${lines(...contents)}${NEW_LINE}`;
1453
+ async function getCurrentBranchOrTag(git = simpleGit2()) {
1454
+ return await git.branch().then((r) => r.current) || // If no current branch, try to get the tag
1455
+ // @TODO use simple git
1456
+ await git.raw(["describe", "--tags", "--exact-match"]).then((out) => out.trim());
1360
1457
  }
1361
- function lines(...contents) {
1362
- const filteredContent = contents.filter(
1363
- (value) => value != null && value !== "" && value !== false
1364
- );
1365
- return `${filteredContent.join(NEW_LINE)}`;
1458
+ function validateFilter({ from, to }) {
1459
+ if (to && !from) {
1460
+ throw new Error(
1461
+ `filter needs the "from" option defined to accept the "to" option.
1462
+ `
1463
+ );
1464
+ }
1366
1465
  }
1367
-
1368
- // packages/utils/src/lib/text-formats/md/table.ts
1369
- var alignString = /* @__PURE__ */ new Map([
1370
- ["left", ":--"],
1371
- ["center", ":--:"],
1372
- ["right", "--:"]
1373
- ]);
1374
- function tableRow(rows) {
1375
- return `|${rows.join("|")}|`;
1376
- }
1377
- function table2(data) {
1378
- if (data.rows.length === 0) {
1379
- throw new Error("Data can't be empty");
1466
+ function filterLogs(allTags, opt) {
1467
+ if (!opt) {
1468
+ return allTags;
1380
1469
  }
1381
- const alignmentRow = getColumnAlignments(data).map(
1382
- (s) => alignString.get(s) ?? String(alignString.get("center"))
1383
- );
1384
- return section(
1385
- `${lines(
1386
- tableRow(columnsToStringArray(data)),
1387
- tableRow(alignmentRow),
1388
- ...rowToStringArray(data).map(tableRow)
1389
- )}`
1390
- );
1470
+ validateFilter(opt);
1471
+ const { from, to, maxCount } = opt;
1472
+ const finIndex = (tagName, fallback) => {
1473
+ const idx = allTags.indexOf(tagName ?? "");
1474
+ if (idx > -1) {
1475
+ return idx;
1476
+ }
1477
+ return fallback;
1478
+ };
1479
+ const fromIndex = finIndex(from, 0);
1480
+ const toIndex = finIndex(to, void 0);
1481
+ return allTags.slice(fromIndex, toIndex ? toIndex + 1 : toIndex).slice(0, maxCount ?? void 0);
1391
1482
  }
1392
-
1393
- // packages/utils/src/lib/text-formats/index.ts
1394
- var md = {
1395
- bold: bold2,
1396
- italic: italic2,
1397
- strikeThrough,
1398
- code: code2,
1399
- link: link3,
1400
- image,
1401
- headline,
1402
- h,
1403
- h1,
1404
- h2,
1405
- h3,
1406
- h4,
1407
- h5,
1408
- h6,
1409
- indentation,
1410
- lines,
1411
- li,
1412
- section,
1413
- paragraphs,
1414
- table: table2
1415
- };
1416
- var html = {
1417
- bold,
1418
- italic,
1419
- code,
1420
- link: link2,
1421
- details,
1422
- table
1423
- };
1424
-
1425
- // packages/utils/src/lib/reports/utils.ts
1426
- var { image: image2, bold: boldMd } = md;
1427
- function formatReportScore(score) {
1428
- const scaledScore = score * 100;
1429
- const roundedScore = Math.round(scaledScore);
1430
- return roundedScore === 100 && score !== 1 ? Math.floor(scaledScore).toString() : roundedScore.toString();
1431
- }
1432
- function formatScoreWithColor(score, options) {
1433
- const styledNumber = options?.skipBold ? formatReportScore(score) : boldMd(formatReportScore(score));
1434
- return `${scoreMarker(score)} ${styledNumber}`;
1483
+ async function getHashFromTag(tag, git = simpleGit2()) {
1484
+ const tagDetails = await git.show(["--no-patch", "--format=%H", tag]);
1485
+ const hash = tagDetails.trim();
1486
+ return {
1487
+ hash: hash.split("\n").at(-1) ?? "",
1488
+ message: tag
1489
+ };
1435
1490
  }
1436
- var MARKERS = {
1437
- circle: {
1438
- red: "\u{1F534}",
1439
- yellow: "\u{1F7E1}",
1440
- green: "\u{1F7E2}"
1441
- },
1442
- square: {
1443
- red: "\u{1F7E5}",
1444
- yellow: "\u{1F7E8}",
1445
- green: "\u{1F7E9}"
1446
- }
1447
- };
1448
- function scoreMarker(score, markerType = "circle") {
1449
- if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
1450
- return MARKERS[markerType].green;
1491
+ async function getSemverTags(opt = {}, git = simpleGit2()) {
1492
+ validateFilter(opt);
1493
+ const { targetBranch, ...options } = opt;
1494
+ let currentBranch;
1495
+ if (targetBranch) {
1496
+ currentBranch = await getCurrentBranchOrTag(git);
1497
+ await git.checkout(targetBranch);
1451
1498
  }
1452
- if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
1453
- return MARKERS[markerType].yellow;
1499
+ const tagsRaw = await git.tag([
1500
+ "--merged",
1501
+ targetBranch ?? await getCurrentBranchOrTag(git)
1502
+ ]);
1503
+ const allTags = tagsRaw.split(/\n/).map((tag) => tag.trim()).filter(Boolean).filter(isSemver);
1504
+ const relevantTags = filterLogs(allTags, options);
1505
+ const tagsWithHashes = await Promise.all(
1506
+ relevantTags.map((tag) => getHashFromTag(tag, git))
1507
+ );
1508
+ if (currentBranch) {
1509
+ await git.checkout(currentBranch);
1454
1510
  }
1455
- return MARKERS[markerType].red;
1511
+ return tagsWithHashes;
1456
1512
  }
1457
- function getDiffMarker(diff) {
1458
- if (diff > 0) {
1459
- return "\u2191";
1513
+ async function getHashes(options = {}, git = simpleGit2()) {
1514
+ const { targetBranch, from, to, maxCount, ...opt } = options;
1515
+ validateFilter({ from, to });
1516
+ let currentBranch;
1517
+ if (targetBranch) {
1518
+ currentBranch = await getCurrentBranchOrTag(git);
1519
+ await git.checkout(targetBranch);
1460
1520
  }
1461
- if (diff < 0) {
1462
- return "\u2193";
1521
+ const logs = await git.log({
1522
+ ...opt,
1523
+ format: {
1524
+ hash: "%H",
1525
+ message: "%s"
1526
+ },
1527
+ from,
1528
+ to,
1529
+ maxCount
1530
+ });
1531
+ if (targetBranch) {
1532
+ await git.checkout(currentBranch);
1463
1533
  }
1464
- return "";
1465
- }
1466
- function colorByScoreDiff(text, diff) {
1467
- const color = diff > 0 ? "green" : diff < 0 ? "red" : "gray";
1468
- return shieldsBadge(text, color);
1534
+ return [...logs.all];
1469
1535
  }
1470
- function shieldsBadge(text, color) {
1471
- return image2(
1472
- `https://img.shields.io/badge/${encodeURIComponent(text)}-${color}`,
1473
- text
1536
+
1537
+ // packages/utils/src/lib/group-by-status.ts
1538
+ function groupByStatus(results) {
1539
+ return results.reduce(
1540
+ (acc, result) => result.status === "fulfilled" ? { ...acc, fulfilled: [...acc.fulfilled, result] } : { ...acc, rejected: [...acc.rejected, result] },
1541
+ { fulfilled: [], rejected: [] }
1474
1542
  );
1475
1543
  }
1476
- function formatDiffNumber(diff) {
1477
- const number = Math.abs(diff) === Number.POSITIVE_INFINITY ? "\u221E" : `${Math.abs(diff)}`;
1478
- const sign = diff < 0 ? "\u2212" : "+";
1479
- return `${sign}${number}`;
1544
+
1545
+ // packages/utils/src/lib/merge-configs.ts
1546
+ function mergeConfigs(config, ...configs) {
1547
+ return configs.reduce(
1548
+ (acc, obj) => ({
1549
+ ...acc,
1550
+ ...mergeCategories(acc.categories, obj.categories),
1551
+ ...mergePlugins(acc.plugins, obj.plugins),
1552
+ ...mergePersist(acc.persist, obj.persist),
1553
+ ...mergeUpload(acc.upload, obj.upload)
1554
+ }),
1555
+ config
1556
+ );
1480
1557
  }
1481
- function severityMarker(severity) {
1482
- if (severity === "error") {
1483
- return "\u{1F6A8}";
1484
- }
1485
- if (severity === "warning") {
1486
- return "\u26A0\uFE0F";
1558
+ function mergeCategories(a, b) {
1559
+ if (!a && !b) {
1560
+ return {};
1487
1561
  }
1488
- return "\u2139\uFE0F";
1489
- }
1490
- function calcDuration(start, stop) {
1491
- return Math.round((stop ?? performance.now()) - start);
1492
- }
1493
- function countCategoryAudits(refs, plugins) {
1494
- const groupLookup = plugins.reduce(
1495
- (lookup, plugin) => {
1496
- if (plugin.groups == null || plugin.groups.length === 0) {
1497
- return lookup;
1562
+ const mergedMap = /* @__PURE__ */ new Map();
1563
+ const addToMap = (categories) => {
1564
+ categories.forEach((newObject) => {
1565
+ if (mergedMap.has(newObject.slug)) {
1566
+ const existingObject = mergedMap.get(
1567
+ newObject.slug
1568
+ );
1569
+ mergedMap.set(newObject.slug, {
1570
+ ...existingObject,
1571
+ ...newObject,
1572
+ refs: mergeByUniqueCategoryRefCombination(
1573
+ existingObject?.refs,
1574
+ newObject.refs
1575
+ )
1576
+ });
1577
+ } else {
1578
+ mergedMap.set(newObject.slug, newObject);
1498
1579
  }
1499
- return {
1500
- ...lookup,
1501
- [plugin.slug]: Object.fromEntries(
1502
- plugin.groups.map((group) => [group.slug, group])
1503
- )
1504
- };
1505
- },
1506
- {}
1507
- );
1508
- return refs.reduce((acc, ref) => {
1509
- if (ref.type === "group") {
1510
- const groupRefs = groupLookup[ref.plugin]?.[ref.slug]?.refs;
1511
- return acc + (groupRefs?.length ?? 0);
1512
- }
1513
- return acc + 1;
1514
- }, 0);
1515
- }
1516
- function getSortableAuditByRef({ slug, weight, plugin }, plugins) {
1517
- const auditPlugin = plugins.find((p) => p.slug === plugin);
1518
- if (!auditPlugin) {
1519
- throwIsNotPresentError(`Plugin ${plugin}`, "report");
1580
+ });
1581
+ };
1582
+ if (a) {
1583
+ addToMap(a);
1520
1584
  }
1521
- const audit = auditPlugin.audits.find(
1522
- ({ slug: auditSlug }) => auditSlug === slug
1523
- );
1524
- if (!audit) {
1525
- throwIsNotPresentError(`Audit ${slug}`, auditPlugin.slug);
1585
+ if (b) {
1586
+ addToMap(b);
1526
1587
  }
1527
- return {
1528
- ...audit,
1529
- weight,
1530
- plugin
1531
- };
1588
+ return { categories: [...mergedMap.values()] };
1532
1589
  }
1533
- function getSortableGroupByRef({ plugin, slug, weight }, plugins) {
1534
- const groupPlugin = plugins.find((p) => p.slug === plugin);
1535
- if (!groupPlugin) {
1536
- throwIsNotPresentError(`Plugin ${plugin}`, "report");
1537
- }
1538
- const group = groupPlugin.groups?.find(
1539
- ({ slug: groupSlug }) => groupSlug === slug
1540
- );
1541
- if (!group) {
1542
- throwIsNotPresentError(`Group ${slug}`, groupPlugin.slug);
1590
+ function mergePlugins(a, b) {
1591
+ if (!a && !b) {
1592
+ return { plugins: [] };
1543
1593
  }
1544
- const sortedAudits = getSortedGroupAudits(group, groupPlugin.slug, plugins);
1545
- const sortedAuditRefs = [...group.refs].sort((a, b) => {
1546
- const aIndex = sortedAudits.findIndex((ref) => ref.slug === a.slug);
1547
- const bIndex = sortedAudits.findIndex((ref) => ref.slug === b.slug);
1548
- return aIndex - bIndex;
1549
- });
1550
- return {
1551
- ...group,
1552
- refs: sortedAuditRefs,
1553
- plugin,
1554
- weight
1594
+ const mergedMap = /* @__PURE__ */ new Map();
1595
+ const addToMap = (plugins) => {
1596
+ plugins.forEach((newObject) => {
1597
+ mergedMap.set(newObject.slug, newObject);
1598
+ });
1555
1599
  };
1600
+ if (a) {
1601
+ addToMap(a);
1602
+ }
1603
+ if (b) {
1604
+ addToMap(b);
1605
+ }
1606
+ return { plugins: [...mergedMap.values()] };
1556
1607
  }
1557
- function getSortedGroupAudits(group, plugin, plugins) {
1558
- return group.refs.map(
1559
- (ref) => getSortableAuditByRef(
1560
- {
1561
- plugin,
1562
- slug: ref.slug,
1563
- weight: ref.weight,
1564
- type: "audit"
1565
- },
1566
- plugins
1567
- )
1568
- ).sort(compareCategoryAuditsAndGroups);
1569
- }
1570
- function compareCategoryAuditsAndGroups(a, b) {
1571
- if (a.weight !== b.weight) {
1572
- return b.weight - a.weight;
1608
+ function mergePersist(a, b) {
1609
+ if (!a && !b) {
1610
+ return {};
1573
1611
  }
1574
- if (a.score !== b.score) {
1575
- return a.score - b.score;
1612
+ if (a) {
1613
+ return b ? { persist: { ...a, ...b } } : {};
1614
+ } else {
1615
+ return { persist: b };
1616
+ }
1617
+ }
1618
+ function mergeByUniqueCategoryRefCombination(a, b) {
1619
+ const map = /* @__PURE__ */ new Map();
1620
+ const addToMap = (refs) => {
1621
+ refs.forEach((ref) => {
1622
+ const uniqueIdentification = `${ref.type}:${ref.plugin}:${ref.slug}`;
1623
+ if (map.has(uniqueIdentification)) {
1624
+ map.set(uniqueIdentification, {
1625
+ ...map.get(uniqueIdentification),
1626
+ ...ref
1627
+ });
1628
+ } else {
1629
+ map.set(uniqueIdentification, ref);
1630
+ }
1631
+ });
1632
+ };
1633
+ if (a) {
1634
+ addToMap(a);
1576
1635
  }
1577
- if ("value" in a && "value" in b && a.value !== b.value) {
1578
- return b.value - a.value;
1636
+ if (b) {
1637
+ addToMap(b);
1579
1638
  }
1580
- return a.title.localeCompare(b.title);
1639
+ return [...map.values()];
1581
1640
  }
1582
- function compareAudits(a, b) {
1583
- if (a.score !== b.score) {
1584
- return a.score - b.score;
1641
+ function mergeUpload(a, b) {
1642
+ if (!a && !b) {
1643
+ return {};
1585
1644
  }
1586
- if (a.value !== b.value) {
1587
- return b.value - a.value;
1645
+ if (a) {
1646
+ return b ? { upload: { ...a, ...b } } : {};
1647
+ } else {
1648
+ return { upload: b };
1588
1649
  }
1589
- return a.title.localeCompare(b.title);
1590
- }
1591
- function compareIssueSeverity(severity1, severity2) {
1592
- const levels = {
1593
- info: 0,
1594
- warning: 1,
1595
- error: 2
1596
- };
1597
- return levels[severity1] - levels[severity2];
1598
1650
  }
1599
- async function loadReport(options) {
1600
- const { outputDir, filename, format } = options;
1601
- await ensureDirectoryExists(outputDir);
1602
- const filePath = join2(outputDir, `${filename}.${format}`);
1603
- if (format === "json") {
1604
- const content = await readJsonFile(filePath);
1605
- return reportSchema.parse(content);
1651
+
1652
+ // packages/utils/src/lib/progress.ts
1653
+ import { black, bold as bold2, gray as gray2, green } from "ansis";
1654
+ import { MultiProgressBars } from "multi-progress-bars";
1655
+ var barStyles = {
1656
+ active: (s) => green(s),
1657
+ done: (s) => gray2(s),
1658
+ idle: (s) => gray2(s)
1659
+ };
1660
+ var messageStyles = {
1661
+ active: (s) => black(s),
1662
+ done: (s) => bold2.green(s),
1663
+ idle: (s) => gray2(s)
1664
+ };
1665
+ var mpb;
1666
+ function getSingletonProgressBars(options) {
1667
+ if (!mpb) {
1668
+ mpb = new MultiProgressBars({
1669
+ progressWidth: TERMINAL_WIDTH,
1670
+ initMessage: "",
1671
+ border: true,
1672
+ ...options
1673
+ });
1606
1674
  }
1607
- const text = await readTextFile(filePath);
1608
- return text;
1609
- }
1610
- function throwIsNotPresentError(itemName, presentPlace) {
1611
- throw new Error(`${itemName} is not present in ${presentPlace}`);
1612
- }
1613
- function getPluginNameFromSlug(slug, plugins) {
1614
- return plugins.find(({ slug: pluginSlug }) => pluginSlug === slug)?.title || slug;
1675
+ return mpb;
1615
1676
  }
1616
- function compareIssues(a, b) {
1617
- if (a.severity !== b.severity) {
1618
- return -compareIssueSeverity(a.severity, b.severity);
1619
- }
1620
- if (!a.source && b.source) {
1621
- return -1;
1622
- }
1623
- if (a.source && !b.source) {
1624
- return 1;
1625
- }
1626
- if (a.source?.file !== b.source?.file) {
1627
- return a.source?.file.localeCompare(b.source?.file || "") ?? 0;
1628
- }
1629
- if (!a.source?.position && b.source?.position) {
1630
- return -1;
1631
- }
1632
- if (a.source?.position && !b.source?.position) {
1633
- return 1;
1634
- }
1635
- if (a.source?.position?.startLine !== b.source?.position?.startLine) {
1636
- return (a.source?.position?.startLine ?? 0) - (b.source?.position?.startLine ?? 0);
1637
- }
1638
- return 0;
1677
+ function getProgressBar(taskName) {
1678
+ const tasks = getSingletonProgressBars();
1679
+ tasks.addTask(taskName, {
1680
+ type: "percentage",
1681
+ percentage: 0,
1682
+ message: "",
1683
+ barTransformFn: barStyles.idle
1684
+ });
1685
+ return {
1686
+ incrementInSteps: (numPlugins) => {
1687
+ tasks.incrementTask(taskName, {
1688
+ percentage: 1 / numPlugins,
1689
+ barTransformFn: barStyles.active
1690
+ });
1691
+ },
1692
+ updateTitle: (title) => {
1693
+ tasks.updateTask(taskName, {
1694
+ message: title,
1695
+ barTransformFn: barStyles.active
1696
+ });
1697
+ },
1698
+ endProgress: (message = "") => {
1699
+ tasks.done(taskName, {
1700
+ message: messageStyles.done(message),
1701
+ barTransformFn: barStyles.done
1702
+ });
1703
+ }
1704
+ };
1639
1705
  }
1640
1706
 
1641
- // packages/utils/src/lib/execute-process.ts
1642
- var ProcessError = class extends Error {
1643
- code;
1644
- stderr;
1645
- stdout;
1646
- constructor(result) {
1647
- super(result.stderr);
1648
- this.code = result.code;
1649
- this.stderr = result.stderr;
1650
- this.stdout = result.stdout;
1651
- }
1652
- };
1653
- function executeProcess(cfg) {
1654
- const { observer, cwd, command, args, ignoreExitCode = false } = cfg;
1655
- const { onStdout, onError, onComplete } = observer ?? {};
1656
- const date = (/* @__PURE__ */ new Date()).toISOString();
1657
- const start = performance.now();
1658
- return new Promise((resolve, reject) => {
1659
- const process2 = spawn(command, args, { cwd, shell: true });
1660
- let stdout = "";
1661
- let stderr = "";
1662
- process2.stdout.on("data", (data) => {
1663
- stdout += String(data);
1664
- onStdout?.(String(data));
1665
- });
1666
- process2.stderr.on("data", (data) => {
1667
- stderr += String(data);
1668
- });
1669
- process2.on("error", (err) => {
1670
- stderr += err.toString();
1671
- });
1672
- process2.on("close", (code3) => {
1673
- const timings = { date, duration: calcDuration(start) };
1674
- if (code3 === 0 || ignoreExitCode) {
1675
- onComplete?.();
1676
- resolve({ code: code3, stdout, stderr, ...timings });
1677
- } else {
1678
- const errorMsg = new ProcessError({ code: code3, stdout, stderr, ...timings });
1679
- onError?.(errorMsg);
1680
- reject(errorMsg);
1681
- }
1682
- });
1683
- });
1707
+ // packages/utils/src/lib/reports/flatten-plugins.ts
1708
+ function listGroupsFromAllPlugins(report) {
1709
+ return report.plugins.flatMap(
1710
+ (plugin) => plugin.groups?.map((group) => ({ plugin, group })) ?? []
1711
+ );
1712
+ }
1713
+ function listAuditsFromAllPlugins(report) {
1714
+ return report.plugins.flatMap(
1715
+ (plugin) => plugin.audits.map((audit) => ({ plugin, audit }))
1716
+ );
1684
1717
  }
1685
1718
 
1686
- // packages/utils/src/lib/filter.ts
1687
- function filterItemRefsBy(items, refFilterFn) {
1688
- return items.map((item) => ({
1689
- ...item,
1690
- refs: item.refs.filter(refFilterFn)
1691
- })).filter((item) => item.refs.length);
1719
+ // packages/utils/src/lib/reports/generate-md-report.ts
1720
+ import { MarkdownDocument as MarkdownDocument3, md as md4 } from "build-md";
1721
+
1722
+ // packages/utils/src/lib/text-formats/constants.ts
1723
+ var NEW_LINE = "\n";
1724
+ var TAB = " ";
1725
+ var SPACE = " ";
1726
+ var HIERARCHY = {
1727
+ level_1: 1,
1728
+ level_2: 2,
1729
+ level_3: 3,
1730
+ level_4: 4,
1731
+ level_5: 5,
1732
+ level_6: 6
1733
+ };
1734
+
1735
+ // packages/utils/src/lib/text-formats/html/details.ts
1736
+ function details(title, content, cfg = { open: false }) {
1737
+ return `<details${cfg.open ? " open" : ""}>${NEW_LINE}<summary>${title}</summary>${NEW_LINE}${// ⚠️ The blank line is needed to ensure Markdown in content is rendered correctly.
1738
+ NEW_LINE}${content}${NEW_LINE}${// @TODO in the future we could consider adding it only if the content ends with a code block
1739
+ // ⚠️ The blank line ensure Markdown in content is rendered correctly.
1740
+ NEW_LINE}</details>${// ⚠️ The blank line is needed to ensure Markdown after details is rendered correctly.
1741
+ NEW_LINE}`;
1692
1742
  }
1693
1743
 
1694
- // packages/utils/src/lib/git/git.ts
1695
- import { isAbsolute, join as join3, relative } from "node:path";
1696
- import { simpleGit } from "simple-git";
1697
- function getGitRoot(git = simpleGit()) {
1698
- return git.revparse("--show-toplevel");
1744
+ // packages/utils/src/lib/text-formats/html/font-style.ts
1745
+ var boldElement = "b";
1746
+ function bold3(text) {
1747
+ return `<${boldElement}>${text}</${boldElement}>`;
1699
1748
  }
1700
- function formatGitPath(path, gitRoot) {
1701
- const absolutePath = isAbsolute(path) ? path : join3(process.cwd(), path);
1702
- const relativePath = relative(gitRoot, absolutePath);
1703
- return toUnixPath(relativePath);
1749
+ var italicElement = "i";
1750
+ function italic(text) {
1751
+ return `<${italicElement}>${text}</${italicElement}>`;
1704
1752
  }
1705
- async function toGitPath(path, git = simpleGit()) {
1706
- const gitRoot = await getGitRoot(git);
1707
- return formatGitPath(path, gitRoot);
1753
+ var codeElement = "code";
1754
+ function code(text) {
1755
+ return `<${codeElement}>${text}</${codeElement}>`;
1708
1756
  }
1709
- var GitStatusError = class _GitStatusError extends Error {
1710
- static ignoredProps = /* @__PURE__ */ new Set(["current", "tracking"]);
1711
- static getReducedStatus(status) {
1712
- return Object.fromEntries(
1713
- Object.entries(status).filter(([key]) => !this.ignoredProps.has(key)).filter(
1714
- (entry) => {
1715
- const value = entry[1];
1716
- if (value == null) {
1717
- return false;
1718
- }
1719
- if (Array.isArray(value) && value.length === 0) {
1720
- return false;
1721
- }
1722
- if (typeof value === "number" && value === 0) {
1723
- return false;
1724
- }
1725
- return !(typeof value === "boolean" && !value);
1726
- }
1727
- )
1757
+
1758
+ // packages/utils/src/lib/text-formats/html/link.ts
1759
+ function link2(href, text) {
1760
+ return `<a href="${href}">${text || href}</a>`;
1761
+ }
1762
+
1763
+ // packages/utils/src/lib/text-formats/table.ts
1764
+ function rowToStringArray({ rows, columns = [] }) {
1765
+ if (Array.isArray(rows.at(0)) && typeof columns.at(0) === "object") {
1766
+ throw new TypeError(
1767
+ "Column can`t be object when rows are primitive values"
1728
1768
  );
1729
1769
  }
1730
- constructor(status) {
1731
- super(
1732
- `Working directory needs to be clean before we you can proceed. Commit your local changes or stash them:
1733
- ${JSON.stringify(
1734
- _GitStatusError.getReducedStatus(status),
1735
- null,
1736
- 2
1737
- )}`
1770
+ return rows.map((row) => {
1771
+ if (Array.isArray(row)) {
1772
+ return row.map(String);
1773
+ }
1774
+ const objectRow = row;
1775
+ if (columns.length === 0 || typeof columns.at(0) === "string") {
1776
+ return Object.values(objectRow).map(
1777
+ (value) => value == null ? "" : String(value)
1778
+ );
1779
+ }
1780
+ return columns.map(
1781
+ ({ key }) => objectRow[key] == null ? "" : String(objectRow[key])
1738
1782
  );
1739
- }
1740
- };
1741
- async function guardAgainstLocalChanges(git = simpleGit()) {
1742
- const status = await git.status(["-s"]);
1743
- if (status.files.length > 0) {
1744
- throw new GitStatusError(status);
1745
- }
1783
+ });
1746
1784
  }
1747
- async function safeCheckout(branchOrHash, forceCleanStatus = false, git = simpleGit()) {
1748
- if (forceCleanStatus) {
1749
- await git.raw(["reset", "--hard"]);
1750
- await git.clean(["f", "d"]);
1751
- ui().logger.info(`git status cleaned`);
1785
+ function columnsToStringArray({
1786
+ rows,
1787
+ columns = []
1788
+ }) {
1789
+ const firstRow = rows.at(0);
1790
+ const primitiveRows = Array.isArray(firstRow);
1791
+ if (typeof columns.at(0) === "string" && !primitiveRows) {
1792
+ throw new Error("invalid union type. Caught by model parsing.");
1752
1793
  }
1753
- await guardAgainstLocalChanges(git);
1754
- await git.checkout(branchOrHash);
1755
- }
1756
-
1757
- // packages/utils/src/lib/git/git.commits-and-tags.ts
1758
- import { simpleGit as simpleGit2 } from "simple-git";
1759
-
1760
- // packages/utils/src/lib/semver.ts
1761
- import { rcompare, valid } from "semver";
1762
- function normalizeSemver(semverString) {
1763
- if (semverString.startsWith("v") || semverString.startsWith("V")) {
1764
- return semverString.slice(1);
1794
+ if (columns.length === 0) {
1795
+ if (Array.isArray(firstRow)) {
1796
+ return firstRow.map((_, idx) => String(idx));
1797
+ }
1798
+ return Object.keys(firstRow);
1765
1799
  }
1766
- if (semverString.includes("@")) {
1767
- return semverString.split("@").at(-1) ?? "";
1800
+ if (typeof columns.at(0) === "string") {
1801
+ return columns.map(String);
1768
1802
  }
1769
- return semverString;
1770
- }
1771
- function isSemver(semverString = "") {
1772
- return valid(normalizeSemver(semverString)) != null;
1773
- }
1774
- function sortSemvers(semverStrings) {
1775
- return semverStrings.map(normalizeSemver).filter(isSemver).sort(rcompare);
1803
+ const cols = columns;
1804
+ return cols.map(({ label, key }) => label ?? capitalize(key));
1776
1805
  }
1777
-
1778
- // packages/utils/src/lib/git/git.commits-and-tags.ts
1779
- async function getLatestCommit(git = simpleGit2()) {
1780
- const log2 = await git.log({
1781
- maxCount: 1,
1782
- // git log -1 --pretty=format:"%H %s %an %aI" - See: https://git-scm.com/docs/pretty-formats
1783
- format: { hash: "%H", message: "%s", author: "%an", date: "%aI" }
1784
- });
1785
- return commitSchema.parse(log2.latest);
1806
+ function getColumnAlignmentForKeyAndIndex(targetKey, targetIdx, columns = []) {
1807
+ const column = columns.at(targetIdx) ?? columns.find((col) => col.key === targetKey);
1808
+ if (typeof column === "string") {
1809
+ return column;
1810
+ } else if (typeof column === "object") {
1811
+ return column.align ?? "center";
1812
+ } else {
1813
+ return "center";
1814
+ }
1786
1815
  }
1787
- async function getCurrentBranchOrTag(git = simpleGit2()) {
1788
- return await git.branch().then((r) => r.current) || // If no current branch, try to get the tag
1789
- // @TODO use simple git
1790
- await git.raw(["describe", "--tags", "--exact-match"]).then((out) => out.trim());
1816
+ function getColumnAlignmentForIndex(targetIdx, columns = []) {
1817
+ const column = columns.at(targetIdx);
1818
+ if (column == null) {
1819
+ return "center";
1820
+ } else if (typeof column === "string") {
1821
+ return column;
1822
+ } else if (typeof column === "object") {
1823
+ return column.align ?? "center";
1824
+ } else {
1825
+ return "center";
1826
+ }
1791
1827
  }
1792
- function validateFilter({ from, to }) {
1793
- if (to && !from) {
1794
- throw new Error(
1795
- `filter needs the "from" option defined to accept the "to" option.
1796
- `
1828
+ function getColumnAlignments(tableData) {
1829
+ const { rows, columns = [] } = tableData;
1830
+ if (rows.at(0) == null) {
1831
+ throw new Error("first row can`t be undefined.");
1832
+ }
1833
+ if (Array.isArray(rows.at(0))) {
1834
+ const firstPrimitiveRow = rows.at(0);
1835
+ return Array.from({ length: firstPrimitiveRow.length }).map(
1836
+ (_, idx) => getColumnAlignmentForIndex(idx, columns)
1797
1837
  );
1798
1838
  }
1799
- }
1800
- function filterLogs(allTags, opt) {
1801
- if (!opt) {
1802
- return allTags;
1839
+ const biggestRow = [...rows].sort((a, b) => Object.keys(a).length - Object.keys(b).length).at(-1);
1840
+ if (columns.length > 0) {
1841
+ return columns.map(
1842
+ (column, idx) => typeof column === "string" ? column : getColumnAlignmentForKeyAndIndex(
1843
+ column.key,
1844
+ idx,
1845
+ columns
1846
+ )
1847
+ );
1803
1848
  }
1804
- validateFilter(opt);
1805
- const { from, to, maxCount } = opt;
1806
- const finIndex = (tagName, fallback) => {
1807
- const idx = allTags.indexOf(tagName ?? "");
1808
- if (idx > -1) {
1809
- return idx;
1810
- }
1811
- return fallback;
1812
- };
1813
- const fromIndex = finIndex(from, 0);
1814
- const toIndex = finIndex(to, void 0);
1815
- return allTags.slice(fromIndex, toIndex ? toIndex + 1 : toIndex).slice(0, maxCount ?? void 0);
1849
+ return Object.keys(biggestRow ?? {}).map((_) => "center");
1816
1850
  }
1817
- async function getHashFromTag(tag, git = simpleGit2()) {
1818
- const tagDetails = await git.show(["--no-patch", "--format=%H", tag]);
1819
- const hash = tagDetails.trim();
1820
- return {
1821
- hash: hash.split("\n").at(-1) ?? "",
1822
- message: tag
1823
- };
1851
+
1852
+ // packages/utils/src/lib/text-formats/html/table.ts
1853
+ function wrap(elem, content) {
1854
+ return `<${elem}>${content}</${elem}>${NEW_LINE}`;
1824
1855
  }
1825
- async function getSemverTags(opt = {}, git = simpleGit2()) {
1826
- validateFilter(opt);
1827
- const { targetBranch, ...options } = opt;
1828
- let currentBranch;
1829
- if (targetBranch) {
1830
- currentBranch = await getCurrentBranchOrTag(git);
1831
- await git.checkout(targetBranch);
1856
+ function wrapRow(content) {
1857
+ const elem = "tr";
1858
+ return `<${elem}>${NEW_LINE}${content}</${elem}>${NEW_LINE}`;
1859
+ }
1860
+ function table(tableData) {
1861
+ if (tableData.rows.length === 0) {
1862
+ throw new Error("Data can't be empty");
1832
1863
  }
1833
- const tagsRaw = await git.tag([
1834
- "--merged",
1835
- targetBranch ?? await getCurrentBranchOrTag(git)
1836
- ]);
1837
- const allTags = tagsRaw.split(/\n/).map((tag) => tag.trim()).filter(Boolean).filter(isSemver);
1838
- const relevantTags = filterLogs(allTags, options);
1839
- const tagsWithHashes = await Promise.all(
1840
- relevantTags.map((tag) => getHashFromTag(tag, git))
1864
+ const tableHeaderCols = columnsToStringArray(tableData).map((s) => wrap("th", s)).join("");
1865
+ const tableHeaderRow = wrapRow(tableHeaderCols);
1866
+ const tableBody = rowToStringArray(tableData).map((arr) => {
1867
+ const columns = arr.map((s) => wrap("td", s)).join("");
1868
+ return wrapRow(columns);
1869
+ }).join("");
1870
+ return wrap("table", `${NEW_LINE}${tableHeaderRow}${tableBody}`);
1871
+ }
1872
+
1873
+ // packages/utils/src/lib/text-formats/index.ts
1874
+ var html = {
1875
+ bold: bold3,
1876
+ italic,
1877
+ code,
1878
+ link: link2,
1879
+ details,
1880
+ table
1881
+ };
1882
+
1883
+ // packages/utils/src/lib/reports/formatting.ts
1884
+ import { MarkdownDocument, md as md2 } from "build-md";
1885
+ function tableSection(tableData, options) {
1886
+ if (tableData.rows.length === 0) {
1887
+ return null;
1888
+ }
1889
+ const { level = HIERARCHY.level_4 } = options ?? {};
1890
+ const columns = columnsToStringArray(tableData);
1891
+ const alignments = getColumnAlignments(tableData);
1892
+ const rows = rowToStringArray(tableData);
1893
+ return new MarkdownDocument().heading(level, tableData.title).table(
1894
+ columns.map((heading, i) => {
1895
+ const alignment = alignments[i];
1896
+ if (alignment) {
1897
+ return { heading, alignment };
1898
+ }
1899
+ return heading;
1900
+ }),
1901
+ rows
1841
1902
  );
1842
- if (currentBranch) {
1843
- await git.checkout(currentBranch);
1844
- }
1845
- return tagsWithHashes;
1846
1903
  }
1847
- async function getHashes(options = {}, git = simpleGit2()) {
1848
- const { targetBranch, from, to, maxCount, ...opt } = options;
1849
- validateFilter({ from, to });
1850
- let currentBranch;
1851
- if (targetBranch) {
1852
- currentBranch = await getCurrentBranchOrTag(git);
1853
- await git.checkout(targetBranch);
1904
+ function metaDescription(audit) {
1905
+ const docsUrl = audit.docsUrl;
1906
+ const description = audit.description?.trim();
1907
+ if (docsUrl) {
1908
+ const docsLink = md2.link(docsUrl, "\u{1F4D6} Docs");
1909
+ if (!description) {
1910
+ return docsLink;
1911
+ }
1912
+ const parsedDescription = description.endsWith("```") ? `${description}
1913
+
1914
+ ` : `${description} `;
1915
+ return md2`${parsedDescription}${docsLink}`;
1854
1916
  }
1855
- const logs = await git.log({
1856
- ...opt,
1857
- format: {
1858
- hash: "%H",
1859
- message: "%s"
1860
- },
1861
- from,
1862
- to,
1863
- maxCount
1864
- });
1865
- if (targetBranch) {
1866
- await git.checkout(currentBranch);
1917
+ if (description && description.trim().length > 0) {
1918
+ return description;
1867
1919
  }
1868
- return [...logs.all];
1920
+ return "";
1869
1921
  }
1870
1922
 
1871
- // packages/utils/src/lib/group-by-status.ts
1872
- function groupByStatus(results) {
1873
- return results.reduce(
1874
- (acc, result) => result.status === "fulfilled" ? { ...acc, fulfilled: [...acc.fulfilled, result] } : { ...acc, rejected: [...acc.rejected, result] },
1875
- { fulfilled: [], rejected: [] }
1923
+ // packages/utils/src/lib/reports/generate-md-report-categoy-section.ts
1924
+ import { MarkdownDocument as MarkdownDocument2, md as md3 } from "build-md";
1925
+
1926
+ // packages/utils/src/lib/reports/sorting.ts
1927
+ function getSortableAuditByRef({ slug, weight, plugin }, plugins) {
1928
+ const auditPlugin = plugins.find((p) => p.slug === plugin);
1929
+ if (!auditPlugin) {
1930
+ throwIsNotPresentError(`Plugin ${plugin}`, "report");
1931
+ }
1932
+ const audit = auditPlugin.audits.find(
1933
+ ({ slug: auditSlug }) => auditSlug === slug
1876
1934
  );
1935
+ if (!audit) {
1936
+ throwIsNotPresentError(`Audit ${slug}`, auditPlugin.slug);
1937
+ }
1938
+ return {
1939
+ ...audit,
1940
+ weight,
1941
+ plugin
1942
+ };
1877
1943
  }
1878
-
1879
- // packages/utils/src/lib/progress.ts
1880
- import chalk3 from "chalk";
1881
- import { MultiProgressBars } from "multi-progress-bars";
1882
- var barStyles = {
1883
- active: (s) => chalk3.green(s),
1884
- done: (s) => chalk3.gray(s),
1885
- idle: (s) => chalk3.gray(s)
1886
- };
1887
- var messageStyles = {
1888
- active: (s) => chalk3.black(s),
1889
- done: (s) => chalk3.green(chalk3.bold(s)),
1890
- idle: (s) => chalk3.gray(s)
1891
- };
1892
- var mpb;
1893
- function getSingletonProgressBars(options) {
1894
- if (!mpb) {
1895
- mpb = new MultiProgressBars({
1896
- progressWidth: TERMINAL_WIDTH,
1897
- initMessage: "",
1898
- border: true,
1899
- ...options
1900
- });
1944
+ function getSortedGroupAudits(group, plugin, plugins) {
1945
+ return group.refs.map(
1946
+ (ref) => getSortableAuditByRef(
1947
+ {
1948
+ plugin,
1949
+ slug: ref.slug,
1950
+ weight: ref.weight,
1951
+ type: "audit"
1952
+ },
1953
+ plugins
1954
+ )
1955
+ ).sort(compareCategoryAuditsAndGroups);
1956
+ }
1957
+ function getSortableGroupByRef({ plugin, slug, weight }, plugins) {
1958
+ const groupPlugin = plugins.find((p) => p.slug === plugin);
1959
+ if (!groupPlugin) {
1960
+ throwIsNotPresentError(`Plugin ${plugin}`, "report");
1901
1961
  }
1902
- return mpb;
1962
+ const group = groupPlugin.groups?.find(
1963
+ ({ slug: groupSlug }) => groupSlug === slug
1964
+ );
1965
+ if (!group) {
1966
+ throwIsNotPresentError(`Group ${slug}`, groupPlugin.slug);
1967
+ }
1968
+ const sortedAudits = getSortedGroupAudits(group, groupPlugin.slug, plugins);
1969
+ const sortedAuditRefs = [...group.refs].sort((a, b) => {
1970
+ const aIndex = sortedAudits.findIndex((ref) => ref.slug === a.slug);
1971
+ const bIndex = sortedAudits.findIndex((ref) => ref.slug === b.slug);
1972
+ return aIndex - bIndex;
1973
+ });
1974
+ return {
1975
+ ...group,
1976
+ refs: sortedAuditRefs,
1977
+ plugin,
1978
+ weight
1979
+ };
1903
1980
  }
1904
- function getProgressBar(taskName) {
1905
- const tasks = getSingletonProgressBars();
1906
- tasks.addTask(taskName, {
1907
- type: "percentage",
1908
- percentage: 0,
1909
- message: "",
1910
- barTransformFn: barStyles.idle
1981
+ function sortReport(report) {
1982
+ const { categories, plugins } = report;
1983
+ const sortedCategories = categories.map((category) => {
1984
+ const { audits, groups } = category.refs.reduce(
1985
+ (acc, ref) => ({
1986
+ ...acc,
1987
+ ...ref.type === "group" ? {
1988
+ groups: [...acc.groups, getSortableGroupByRef(ref, plugins)]
1989
+ } : {
1990
+ audits: [...acc.audits, getSortableAuditByRef(ref, plugins)]
1991
+ }
1992
+ }),
1993
+ { groups: [], audits: [] }
1994
+ );
1995
+ const sortedAuditsAndGroups = [...audits, ...groups].sort(
1996
+ compareCategoryAuditsAndGroups
1997
+ );
1998
+ const sortedRefs = [...category.refs].sort((a, b) => {
1999
+ const aIndex = sortedAuditsAndGroups.findIndex(
2000
+ (ref) => ref.slug === a.slug && ref.plugin === a.plugin
2001
+ );
2002
+ const bIndex = sortedAuditsAndGroups.findIndex(
2003
+ (ref) => ref.slug === b.slug && ref.plugin === b.plugin
2004
+ );
2005
+ return aIndex - bIndex;
2006
+ });
2007
+ return { ...category, refs: sortedRefs };
1911
2008
  });
1912
2009
  return {
1913
- incrementInSteps: (numPlugins) => {
1914
- tasks.incrementTask(taskName, {
1915
- percentage: 1 / numPlugins,
1916
- barTransformFn: barStyles.active
1917
- });
1918
- },
1919
- updateTitle: (title) => {
1920
- tasks.updateTask(taskName, {
1921
- message: title,
1922
- barTransformFn: barStyles.active
1923
- });
1924
- },
1925
- endProgress: (message = "") => {
1926
- tasks.done(taskName, {
1927
- message: messageStyles.done(message),
1928
- barTransformFn: barStyles.done
1929
- });
1930
- }
2010
+ ...report,
2011
+ categories: sortedCategories,
2012
+ plugins: sortPlugins(plugins)
1931
2013
  };
1932
2014
  }
1933
-
1934
- // packages/utils/src/lib/reports/flatten-plugins.ts
1935
- function listGroupsFromAllPlugins(report) {
1936
- return report.plugins.flatMap(
1937
- (plugin) => plugin.groups?.map((group) => ({ plugin, group })) ?? []
1938
- );
1939
- }
1940
- function listAuditsFromAllPlugins(report) {
1941
- return report.plugins.flatMap(
1942
- (plugin) => plugin.audits.map((audit) => ({ plugin, audit }))
1943
- );
1944
- }
1945
-
1946
- // packages/utils/src/lib/reports/formatting.ts
1947
- var { headline: headline2, lines: lines2, link: link4, section: section2, table: table3 } = md;
1948
- function tableSection(tableData, options) {
1949
- if (tableData.rows.length === 0) {
1950
- return "";
1951
- }
1952
- const { level = 4 } = options ?? {};
1953
- const render = (h7, l) => l === 0 ? h7 : headline2(h7, l);
1954
- return lines2(
1955
- tableData.title && render(tableData.title, level),
1956
- table3(tableData)
1957
- );
1958
- }
1959
- function metaDescription({
1960
- docsUrl,
1961
- description
1962
- }) {
1963
- if (docsUrl) {
1964
- const docsLink = link4(docsUrl, "\u{1F4D6} Docs");
1965
- if (!description) {
1966
- return section2(docsLink);
1967
- }
1968
- const parsedDescription = description.toString().endsWith("```") ? `${description}${NEW_LINE + NEW_LINE}` : `${description}${SPACE}`;
1969
- return section2(`${parsedDescription}${docsLink}`);
1970
- }
1971
- if (description && description.trim().length > 0) {
1972
- return section2(description);
1973
- }
1974
- return "";
2015
+ function sortPlugins(plugins) {
2016
+ return plugins.map((plugin) => ({
2017
+ ...plugin,
2018
+ audits: [...plugin.audits].sort(compareAudits).map(
2019
+ (audit) => audit.details?.issues ? {
2020
+ ...audit,
2021
+ details: {
2022
+ ...audit.details,
2023
+ issues: [...audit.details.issues].sort(compareIssues)
2024
+ }
2025
+ } : audit
2026
+ )
2027
+ }));
1975
2028
  }
1976
2029
 
1977
2030
  // packages/utils/src/lib/reports/generate-md-report-categoy-section.ts
1978
- var { link: link5, section: section3, h2: h22, lines: lines3, li: li2, bold: boldMd2, h3: h32, indentation: indentation2 } = md;
1979
2031
  function categoriesOverviewSection(report) {
1980
2032
  const { categories, plugins } = report;
1981
- if (categories.length > 0 && plugins.length > 0) {
1982
- const tableContent = {
1983
- columns: reportOverviewTableHeaders,
1984
- rows: categories.map(({ title, refs, score }) => ({
1985
- // The heading "ID" is inferred from the heading text in Markdown.
1986
- category: link5(`#${slugify(title)}`, title),
1987
- score: `${scoreMarker(score)}${SPACE}${boldMd2(
1988
- formatReportScore(score)
1989
- )}`,
1990
- audits: countCategoryAudits(refs, plugins).toString()
1991
- }))
1992
- };
1993
- return tableSection(tableContent);
1994
- }
1995
- return "";
2033
+ return new MarkdownDocument2().table(
2034
+ [
2035
+ { heading: "\u{1F3F7} Category", alignment: "left" },
2036
+ { heading: "\u2B50 Score", alignment: "center" },
2037
+ { heading: "\u{1F6E1} Audits", alignment: "center" }
2038
+ ],
2039
+ categories.map(({ title, refs, score, isBinary }) => [
2040
+ // @TODO refactor `isBinary: boolean` to `targetScore: number` #713
2041
+ // The heading "ID" is inferred from the heading text in Markdown.
2042
+ md3.link(`#${slugify(title)}`, title),
2043
+ md3`${scoreMarker(score)} ${md3.bold(
2044
+ formatReportScore(score)
2045
+ )}${binaryIconSuffix(score, isBinary)}`,
2046
+ countCategoryAudits(refs, plugins).toString()
2047
+ ])
2048
+ );
1996
2049
  }
1997
2050
  function categoriesDetailsSection(report) {
1998
2051
  const { categories, plugins } = report;
1999
- const categoryDetails = categories.flatMap((category) => {
2000
- const categoryTitle = h32(category.title);
2001
- const categoryScore = `${scoreMarker(
2002
- category.score
2003
- )}${SPACE}Score: ${boldMd2(formatReportScore(category.score))}`;
2004
- const categoryMDItems = category.refs.map((ref) => {
2005
- if (ref.type === "group") {
2006
- const group = getSortableGroupByRef(ref, plugins);
2007
- const groupAudits = group.refs.map(
2008
- (groupRef) => getSortableAuditByRef(
2009
- { ...groupRef, plugin: group.plugin, type: "audit" },
2010
- plugins
2011
- )
2012
- );
2013
- const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
2014
- return categoryGroupItem(group, groupAudits, pluginTitle);
2015
- } else {
2016
- const audit = getSortableAuditByRef(ref, plugins);
2017
- const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
2018
- return categoryRef(audit, pluginTitle);
2019
- }
2020
- });
2021
- return section3(
2022
- categoryTitle,
2023
- metaDescription(category),
2024
- categoryScore,
2025
- ...categoryMDItems
2026
- );
2027
- });
2028
- return lines3(h22(CATEGORIES_TITLE), ...categoryDetails);
2052
+ return new MarkdownDocument2().heading(HIERARCHY.level_2, "\u{1F3F7} Categories").$foreach(
2053
+ categories,
2054
+ (doc, category) => doc.heading(HIERARCHY.level_3, category.title).paragraph(metaDescription(category)).paragraph(
2055
+ md3`${scoreMarker(category.score)} Score: ${md3.bold(
2056
+ formatReportScore(category.score)
2057
+ )}${binaryIconSuffix(category.score, category.isBinary)}`
2058
+ ).list(
2059
+ category.refs.map((ref) => {
2060
+ if (ref.type === "group") {
2061
+ const group = getSortableGroupByRef(ref, plugins);
2062
+ const groupAudits = group.refs.map(
2063
+ (groupRef) => getSortableAuditByRef(
2064
+ { ...groupRef, plugin: group.plugin, type: "audit" },
2065
+ plugins
2066
+ )
2067
+ );
2068
+ const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
2069
+ return categoryGroupItem(group, groupAudits, pluginTitle);
2070
+ } else {
2071
+ const audit = getSortableAuditByRef(ref, plugins);
2072
+ const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
2073
+ return categoryRef(audit, pluginTitle);
2074
+ }
2075
+ })
2076
+ )
2077
+ );
2029
2078
  }
2030
2079
  function categoryRef({ title, score, value, displayValue }, pluginTitle) {
2031
- const auditTitleAsLink = link5(
2080
+ const auditTitleAsLink = md3.link(
2032
2081
  `#${slugify(title)}-${slugify(pluginTitle)}`,
2033
2082
  title
2034
2083
  );
2035
2084
  const marker = scoreMarker(score, "square");
2036
- return li2(
2037
- `${marker}${SPACE}${auditTitleAsLink}${SPACE}(_${pluginTitle}_) - ${boldMd2(
2038
- (displayValue || value).toString()
2039
- )}`
2040
- );
2085
+ return md3`${marker} ${auditTitleAsLink} (${md3.italic(
2086
+ pluginTitle
2087
+ )}) - ${md3.bold((displayValue || value).toString())}`;
2041
2088
  }
2042
2089
  function categoryGroupItem({ score = 0, title }, groupAudits, pluginTitle) {
2043
- const groupTitle = li2(
2044
- `${scoreMarker(score)}${SPACE}${title}${SPACE}(_${pluginTitle}_)`
2045
- );
2046
- const auditTitles = groupAudits.map(
2047
- ({ title: auditTitle, score: auditScore, value, displayValue }) => {
2048
- const auditTitleLink = link5(
2049
- `#${slugify(auditTitle)}-${slugify(pluginTitle)}`,
2050
- auditTitle
2051
- );
2052
- const marker = scoreMarker(auditScore, "square");
2053
- return indentation2(
2054
- li2(
2055
- `${marker}${SPACE}${auditTitleLink} - ${boldMd2(
2056
- String(displayValue ?? value)
2057
- )}`
2058
- )
2059
- );
2060
- }
2090
+ const groupTitle = md3`${scoreMarker(score)} ${title} (${md3.italic(
2091
+ pluginTitle
2092
+ )})`;
2093
+ const auditsList = md3.list(
2094
+ groupAudits.map(
2095
+ ({ title: auditTitle, score: auditScore, value, displayValue }) => {
2096
+ const auditTitleLink = md3.link(
2097
+ `#${slugify(auditTitle)}-${slugify(pluginTitle)}`,
2098
+ auditTitle
2099
+ );
2100
+ const marker = scoreMarker(auditScore, "square");
2101
+ return md3`${marker} ${auditTitleLink} - ${md3.bold(
2102
+ String(displayValue ?? value)
2103
+ )}`;
2104
+ }
2105
+ )
2061
2106
  );
2062
- return lines3(groupTitle, ...auditTitles);
2107
+ return md3`${groupTitle}${auditsList}`;
2108
+ }
2109
+ function binaryIconSuffix(score, isBinary) {
2110
+ return targetScoreIcon(score, isBinary ? 1 : void 0, { prefix: " " });
2063
2111
  }
2064
2112
 
2065
2113
  // packages/utils/src/lib/reports/generate-md-report.ts
2066
- var { h1: h12, h2: h23, h3: h33, lines: lines4, link: link6, section: section4, code: codeMd } = md;
2067
- var { bold: boldHtml, details: details2 } = html;
2068
2114
  function auditDetailsAuditValue({
2069
2115
  score,
2070
2116
  value,
2071
2117
  displayValue
2072
2118
  }) {
2073
- return `${scoreMarker(score, "square")} ${boldHtml(
2119
+ return md4`${scoreMarker(score, "square")} ${md4.bold(
2074
2120
  String(displayValue ?? value)
2075
2121
  )} (score: ${formatReportScore(score)})`;
2076
2122
  }
2077
2123
  function generateMdReport(report) {
2078
- const printCategories = report.categories.length > 0;
2079
- return lines4(
2080
- h12(reportHeadlineText),
2081
- printCategories ? categoriesOverviewSection(report) : "",
2082
- printCategories ? categoriesDetailsSection(report) : "",
2083
- auditsSection(report),
2084
- aboutSection(report),
2085
- `${FOOTER_PREFIX}${SPACE}${link6(README_LINK, "Code PushUp")}`
2086
- );
2124
+ return new MarkdownDocument3().heading(HIERARCHY.level_1, REPORT_HEADLINE_TEXT).$if(
2125
+ report.categories.length > 0,
2126
+ (doc) => doc.$concat(
2127
+ categoriesOverviewSection(report),
2128
+ categoriesDetailsSection(report)
2129
+ )
2130
+ ).$concat(auditsSection(report), aboutSection(report)).rule().paragraph(md4`${FOOTER_PREFIX} ${md4.link(README_LINK, "Code PushUp")}`).toString();
2087
2131
  }
2088
2132
  function auditDetailsIssues(issues = []) {
2089
2133
  if (issues.length === 0) {
2090
- return "";
2091
- }
2092
- const detailsTableData = {
2093
- title: "Issues",
2094
- columns: issuesTableHeadings,
2095
- rows: issues.map(
2096
- ({ severity: severityVal, message, source: sourceVal }) => {
2097
- const severity = `${severityMarker(severityVal)} <i>${severityVal}</i>`;
2098
- if (!sourceVal) {
2099
- return { severity, message, file: "", line: "" };
2100
- }
2101
- const file = `<code>${sourceVal.file}</code>`;
2102
- if (!sourceVal.position) {
2103
- return { severity, message, file, line: "" };
2104
- }
2105
- const { startLine, endLine } = sourceVal.position;
2106
- const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
2107
- return { severity, message, file, line };
2134
+ return null;
2135
+ }
2136
+ return new MarkdownDocument3().heading(HIERARCHY.level_4, "Issues").table(
2137
+ [
2138
+ { heading: "Severity", alignment: "center" },
2139
+ { heading: "Message", alignment: "left" },
2140
+ { heading: "Source file", alignment: "left" },
2141
+ { heading: "Line(s)", alignment: "center" }
2142
+ ],
2143
+ issues.map(({ severity: level, message, source }) => {
2144
+ const severity = md4`${severityMarker(level)} ${md4.italic(level)}`;
2145
+ if (!source) {
2146
+ return [severity, message];
2108
2147
  }
2109
- )
2110
- };
2111
- return tableSection(detailsTableData);
2148
+ const file = md4.code(source.file);
2149
+ if (!source.position) {
2150
+ return [severity, message, file];
2151
+ }
2152
+ const { startLine, endLine } = source.position;
2153
+ const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
2154
+ return [severity, message, file, line];
2155
+ })
2156
+ );
2112
2157
  }
2113
2158
  function auditDetails(audit) {
2114
- const { table: table5, issues = [] } = audit.details ?? {};
2159
+ const { table: table2, issues = [] } = audit.details ?? {};
2115
2160
  const detailsValue = auditDetailsAuditValue(audit);
2116
- if (issues.length === 0 && table5 == null) {
2117
- return section4(detailsValue);
2161
+ if (issues.length === 0 && !table2?.rows.length) {
2162
+ return new MarkdownDocument3().paragraph(detailsValue);
2118
2163
  }
2119
- const tableSectionContent = table5 == null ? "" : tableSection(table5);
2120
- const issuesSectionContent = issues.length > 0 ? auditDetailsIssues(issues) : "";
2121
- return details2(
2164
+ const tableSectionContent = table2 && tableSection(table2);
2165
+ const issuesSectionContent = issues.length > 0 && auditDetailsIssues(issues);
2166
+ return new MarkdownDocument3().details(
2122
2167
  detailsValue,
2123
- lines4(tableSectionContent, issuesSectionContent)
2168
+ new MarkdownDocument3().$concat(tableSectionContent, issuesSectionContent)
2124
2169
  );
2125
2170
  }
2126
2171
  function auditsSection({
2127
2172
  plugins
2128
2173
  }) {
2129
- const content = plugins.flatMap(
2130
- ({ slug, audits }) => audits.flatMap((audit) => {
2131
- const auditTitle = `${audit.title}${SPACE}(${getPluginNameFromSlug(
2132
- slug,
2133
- plugins
2134
- )})`;
2174
+ return new MarkdownDocument3().heading(HIERARCHY.level_2, "\u{1F6E1}\uFE0F Audits").$foreach(
2175
+ plugins.flatMap(
2176
+ (plugin) => plugin.audits.map((audit) => ({ ...audit, plugin }))
2177
+ ),
2178
+ (doc, { plugin, ...audit }) => {
2179
+ const auditTitle = `${audit.title} (${plugin.title})`;
2135
2180
  const detailsContent = auditDetails(audit);
2136
2181
  const descriptionContent = metaDescription(audit);
2137
- return [h33(auditTitle), detailsContent, descriptionContent];
2138
- })
2182
+ return doc.heading(HIERARCHY.level_3, auditTitle).$concat(detailsContent).paragraph(descriptionContent);
2183
+ }
2139
2184
  );
2140
- return section4(h23("\u{1F6E1}\uFE0F Audits"), ...content);
2141
2185
  }
2142
2186
  function aboutSection(report) {
2143
2187
  const { date, plugins } = report;
2144
- const reportMetaTable = reportMetaData(report);
2145
- const pluginMetaTable = reportPluginMeta({ plugins });
2146
- return lines4(
2147
- h23("About"),
2148
- section4(
2149
- `Report was created by [Code PushUp](${README_LINK}) on ${formatDate(
2150
- new Date(date)
2151
- )}.`
2152
- ),
2153
- tableSection(pluginMetaTable),
2154
- tableSection(reportMetaTable)
2155
- );
2156
- }
2157
- function reportPluginMeta({ plugins }) {
2158
- return {
2159
- columns: [
2160
- {
2161
- key: "plugin",
2162
- align: "left"
2163
- },
2164
- {
2165
- key: "audits"
2166
- },
2167
- {
2168
- key: "version"
2169
- },
2170
- {
2171
- key: "duration"
2172
- }
2188
+ return new MarkdownDocument3().heading(HIERARCHY.level_2, "About").paragraph(
2189
+ md4`Report was created by ${md4.link(
2190
+ README_LINK,
2191
+ "Code PushUp"
2192
+ )} on ${formatDate(new Date(date))}.`
2193
+ ).table(...pluginMetaTable({ plugins })).table(...reportMetaTable(report));
2194
+ }
2195
+ function pluginMetaTable({
2196
+ plugins
2197
+ }) {
2198
+ return [
2199
+ [
2200
+ { heading: "Plugin", alignment: "left" },
2201
+ { heading: "Audits", alignment: "center" },
2202
+ { heading: "Version", alignment: "center" },
2203
+ { heading: "Duration", alignment: "right" }
2173
2204
  ],
2174
- rows: plugins.map(
2175
- ({
2176
- title: pluginTitle,
2177
- audits,
2178
- version: pluginVersion,
2179
- duration: pluginDuration
2180
- }) => ({
2181
- plugin: pluginTitle,
2182
- audits: audits.length.toString(),
2183
- version: codeMd(pluginVersion || ""),
2184
- duration: formatDuration(pluginDuration)
2185
- })
2186
- )
2187
- };
2205
+ plugins.map(({ title, audits, version = "", duration }) => [
2206
+ title,
2207
+ audits.length.toString(),
2208
+ version && md4.code(version),
2209
+ formatDuration(duration)
2210
+ ])
2211
+ ];
2188
2212
  }
2189
- function reportMetaData({
2213
+ function reportMetaTable({
2190
2214
  commit,
2191
2215
  version,
2192
2216
  duration,
2193
2217
  plugins,
2194
2218
  categories
2195
2219
  }) {
2196
- const commitInfo = commit ? `${commit.message}${SPACE}(${commit.hash})` : "N/A";
2197
- return {
2198
- columns: [
2199
- {
2200
- key: "commit",
2201
- align: "left"
2202
- },
2203
- {
2204
- key: "version"
2205
- },
2206
- {
2207
- key: "duration"
2208
- },
2209
- {
2210
- key: "plugins"
2211
- },
2212
- {
2213
- key: "categories"
2214
- },
2215
- {
2216
- key: "audits"
2217
- }
2220
+ return [
2221
+ [
2222
+ { heading: "Commit", alignment: "left" },
2223
+ { heading: "Version", alignment: "center" },
2224
+ { heading: "Duration", alignment: "right" },
2225
+ { heading: "Plugins", alignment: "center" },
2226
+ { heading: "Categories", alignment: "center" },
2227
+ { heading: "Audits", alignment: "center" }
2218
2228
  ],
2219
- rows: [
2220
- {
2221
- commit: commitInfo,
2222
- version: codeMd(version || ""),
2223
- duration: formatDuration(duration),
2224
- plugins: plugins.length,
2225
- categories: categories.length,
2226
- audits: plugins.reduce((acc, { audits }) => acc + audits.length, 0).toString()
2227
- }
2229
+ [
2230
+ [
2231
+ commit ? `${commit.message} (${commit.hash})` : "N/A",
2232
+ md4.code(version),
2233
+ formatDuration(duration),
2234
+ plugins.length.toString(),
2235
+ categories.length.toString(),
2236
+ plugins.reduce((acc, { audits }) => acc + audits.length, 0).toString()
2237
+ ]
2228
2238
  ]
2229
- };
2239
+ ];
2230
2240
  }
2231
2241
 
2232
2242
  // packages/utils/src/lib/reports/generate-md-reports-diff.ts
2233
- var {
2234
- h1: h13,
2235
- h2: h24,
2236
- lines: lines5,
2237
- link: link7,
2238
- bold: boldMd3,
2239
- italic: italicMd,
2240
- table: table4,
2241
- section: section5
2242
- } = md;
2243
- var { details: details3 } = html;
2243
+ import {
2244
+ MarkdownDocument as MarkdownDocument4,
2245
+ md as md5
2246
+ } from "build-md";
2244
2247
  var MAX_ROWS = 100;
2245
- function generateMdReportsDiff(diff) {
2246
- return lines5(
2247
- section5(formatDiffHeaderSection(diff)),
2248
- formatDiffCategoriesSection(diff),
2249
- formatDiffGroupsSection(diff),
2250
- formatDiffAuditsSection(diff)
2251
- );
2252
- }
2253
- function formatDiffHeaderSection(diff) {
2248
+ function generateMdReportsDiff(diff, portalUrl) {
2249
+ return new MarkdownDocument4().$concat(
2250
+ createDiffHeaderSection(diff, portalUrl),
2251
+ createDiffCategoriesSection(diff),
2252
+ createDiffGroupsSection(diff),
2253
+ createDiffAuditsSection(diff)
2254
+ ).toString();
2255
+ }
2256
+ function createDiffHeaderSection(diff, portalUrl) {
2254
2257
  const outcomeTexts = {
2255
- positive: `\u{1F973} Code PushUp report has ${boldMd3("improved")}`,
2256
- negative: `\u{1F61F} Code PushUp report has ${boldMd3("regressed")}`,
2257
- mixed: `\u{1F928} Code PushUp report has both ${boldMd3(
2258
+ positive: md5`🥳 Code PushUp report has ${md5.bold("improved")}`,
2259
+ negative: md5`😟 Code PushUp report has ${md5.bold("regressed")}`,
2260
+ mixed: md5`🤨 Code PushUp report has both ${md5.bold(
2258
2261
  "improvements and regressions"
2259
2262
  )}`,
2260
- unchanged: `\u{1F610} Code PushUp report is ${boldMd3("unchanged")}`
2263
+ unchanged: md5`😐 Code PushUp report is ${md5.bold("unchanged")}`
2261
2264
  };
2262
2265
  const outcome = mergeDiffOutcomes(
2263
2266
  changesToDiffOutcomes([
@@ -2267,143 +2270,127 @@ function formatDiffHeaderSection(diff) {
2267
2270
  ])
2268
2271
  );
2269
2272
  const styleCommits = (commits) => `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
2270
- return lines5(
2271
- h13("Code PushUp"),
2272
- diff.commits ? `${outcomeTexts[outcome]} \u2013 ${styleCommits(diff.commits)}.` : `${outcomeTexts[outcome]}.`
2273
+ return new MarkdownDocument4().heading(HIERARCHY.level_1, "Code PushUp").paragraph(
2274
+ diff.commits ? md5`${outcomeTexts[outcome]} – ${styleCommits(diff.commits)}.` : outcomeTexts[outcome]
2275
+ ).paragraph(
2276
+ portalUrl && md5.link(portalUrl, "\u{1F575}\uFE0F See full comparison in Code PushUp portal \u{1F50D}")
2273
2277
  );
2274
2278
  }
2275
- function formatDiffCategoriesSection(diff) {
2279
+ function createDiffCategoriesSection(diff) {
2276
2280
  const { changed, unchanged, added } = diff.categories;
2277
2281
  const categoriesCount = changed.length + unchanged.length + added.length;
2278
2282
  const hasChanges = unchanged.length < categoriesCount;
2279
2283
  if (categoriesCount === 0) {
2280
- return "";
2284
+ return null;
2281
2285
  }
2282
2286
  const columns = [
2283
- { key: "category", label: "\u{1F3F7}\uFE0F Category", align: "left" },
2284
- { key: "before", label: "\u2B50 Previous score" },
2285
- { key: "after", label: hasChanges ? "\u2B50 Current score" : "\u2B50 Score" },
2286
- { key: "change", label: "\u{1F504} Score change" }
2287
+ { heading: "\u{1F3F7}\uFE0F Category", alignment: "left" },
2288
+ {
2289
+ heading: hasChanges ? "\u2B50 Previous score" : "\u2B50 Score",
2290
+ alignment: "center"
2291
+ },
2292
+ { heading: "\u2B50 Current score", alignment: "center" },
2293
+ { heading: "\u{1F504} Score change", alignment: "center" }
2287
2294
  ];
2288
- return lines5(
2289
- h24("\u{1F3F7}\uFE0F Categories"),
2290
- categoriesCount > 0 && table4({
2291
- columns: hasChanges ? columns : columns.slice(0, 2),
2292
- rows: [
2293
- ...sortChanges(changed).map((category) => ({
2294
- category: formatTitle(category),
2295
- after: formatScoreWithColor(category.scores.after),
2296
- before: formatScoreWithColor(category.scores.before, {
2297
- skipBold: true
2298
- }),
2299
- change: formatScoreChange(category.scores.diff)
2300
- })),
2301
- ...added.map((category) => ({
2302
- category: formatTitle(category),
2303
- after: formatScoreWithColor(category.score),
2304
- before: italicMd("n/a (\\*)"),
2305
- change: italicMd("n/a (\\*)")
2306
- })),
2307
- ...unchanged.map((category) => ({
2308
- category: formatTitle(category),
2309
- after: formatScoreWithColor(category.score),
2310
- before: formatScoreWithColor(category.score, { skipBold: true }),
2311
- change: "\u2013"
2312
- }))
2313
- ].map(
2314
- (row) => hasChanges ? row : { category: row.category, after: row.after }
2315
- )
2316
- }),
2317
- added.length > 0 && section5(italicMd("(\\*) New category."))
2318
- );
2295
+ const rows = [
2296
+ ...sortChanges(changed).map((category) => [
2297
+ formatTitle(category),
2298
+ formatScoreWithColor(category.scores.before, {
2299
+ skipBold: true
2300
+ }),
2301
+ formatScoreWithColor(category.scores.after),
2302
+ formatScoreChange(category.scores.diff)
2303
+ ]),
2304
+ ...added.map((category) => [
2305
+ formatTitle(category),
2306
+ md5.italic("n/a (\\*)"),
2307
+ formatScoreWithColor(category.score),
2308
+ md5.italic("n/a (\\*)")
2309
+ ]),
2310
+ ...unchanged.map((category) => [
2311
+ formatTitle(category),
2312
+ formatScoreWithColor(category.score, { skipBold: true }),
2313
+ formatScoreWithColor(category.score),
2314
+ "\u2013"
2315
+ ])
2316
+ ];
2317
+ return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F3F7}\uFE0F Categories").table(
2318
+ hasChanges ? columns : columns.slice(0, 2),
2319
+ rows.map((row) => hasChanges ? row : row.slice(0, 2))
2320
+ ).paragraph(added.length > 0 && md5.italic("(\\*) New category."));
2319
2321
  }
2320
- function formatDiffGroupsSection(diff) {
2322
+ function createDiffGroupsSection(diff) {
2321
2323
  if (diff.groups.changed.length + diff.groups.unchanged.length === 0) {
2322
- return "";
2323
- }
2324
- return lines5(
2325
- h24("\u{1F5C3}\uFE0F Groups"),
2326
- formatGroupsOrAuditsDetails("group", diff.groups, {
2327
- columns: [
2328
- { key: "plugin", label: "\u{1F50C} Plugin", align: "left" },
2329
- { key: "group", label: "\u{1F5C3}\uFE0F Group", align: "left" },
2330
- { key: "before", label: "\u2B50 Previous score" },
2331
- { key: "after", label: "\u2B50 Current score" },
2332
- { key: "change", label: "\u{1F504} Score change" }
2324
+ return null;
2325
+ }
2326
+ return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F5C3}\uFE0F Groups").$concat(
2327
+ createGroupsOrAuditsDetails(
2328
+ "group",
2329
+ diff.groups,
2330
+ [
2331
+ { heading: "\u{1F50C} Plugin", alignment: "left" },
2332
+ { heading: "\u{1F5C3}\uFE0F Group", alignment: "left" },
2333
+ { heading: "\u2B50 Previous score", alignment: "center" },
2334
+ { heading: "\u2B50 Current score", alignment: "center" },
2335
+ { heading: "\u{1F504} Score change", alignment: "center" }
2333
2336
  ],
2334
- rows: sortChanges(diff.groups.changed).map((group) => ({
2335
- plugin: formatTitle(group.plugin),
2336
- group: formatTitle(group),
2337
- after: formatScoreWithColor(group.scores.after),
2338
- before: formatScoreWithColor(group.scores.before, { skipBold: true }),
2339
- change: formatScoreChange(group.scores.diff)
2340
- }))
2341
- })
2337
+ sortChanges(diff.groups.changed).map((group) => [
2338
+ formatTitle(group.plugin),
2339
+ formatTitle(group),
2340
+ formatScoreWithColor(group.scores.before, { skipBold: true }),
2341
+ formatScoreWithColor(group.scores.after),
2342
+ formatScoreChange(group.scores.diff)
2343
+ ])
2344
+ )
2342
2345
  );
2343
2346
  }
2344
- function formatDiffAuditsSection(diff) {
2345
- return lines5(
2346
- h24("\u{1F6E1}\uFE0F Audits"),
2347
- formatGroupsOrAuditsDetails("audit", diff.audits, {
2348
- columns: [
2349
- { key: "plugin", label: "\u{1F50C} Plugin", align: "left" },
2350
- { key: "audit", label: "\u{1F6E1}\uFE0F Audit", align: "left" },
2351
- { key: "before", label: "\u{1F4CF} Previous value" },
2352
- { key: "after", label: "\u{1F4CF} Current value" },
2353
- { key: "change", label: "\u{1F504} Value change" }
2347
+ function createDiffAuditsSection(diff) {
2348
+ return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F6E1}\uFE0F Audits").$concat(
2349
+ createGroupsOrAuditsDetails(
2350
+ "audit",
2351
+ diff.audits,
2352
+ [
2353
+ { heading: "\u{1F50C} Plugin", alignment: "left" },
2354
+ { heading: "\u{1F6E1}\uFE0F Audit", alignment: "left" },
2355
+ { heading: "\u{1F4CF} Previous value", alignment: "center" },
2356
+ { heading: "\u{1F4CF} Current value", alignment: "center" },
2357
+ { heading: "\u{1F504} Value change", alignment: "center" }
2354
2358
  ],
2355
- rows: sortChanges(diff.audits.changed).map((audit) => ({
2356
- plugin: formatTitle(audit.plugin),
2357
- audit: formatTitle(audit),
2358
- after: `${scoreMarker(audit.scores.after, "square")} ${boldMd3(
2359
+ sortChanges(diff.audits.changed).map((audit) => [
2360
+ formatTitle(audit.plugin),
2361
+ formatTitle(audit),
2362
+ `${scoreMarker(audit.scores.before, "square")} ${audit.displayValues.before || audit.values.before.toString()}`,
2363
+ md5`${scoreMarker(audit.scores.after, "square")} ${md5.bold(
2359
2364
  audit.displayValues.after || audit.values.after.toString()
2360
2365
  )}`,
2361
- before: `${scoreMarker(audit.scores.before, "square")} ${audit.displayValues.before || audit.values.before.toString()}`,
2362
- change: formatValueChange(audit)
2363
- }))
2364
- })
2366
+ formatValueChange(audit)
2367
+ ])
2368
+ )
2365
2369
  );
2366
2370
  }
2367
- function formatGroupsOrAuditsDetails(token, { changed, unchanged }, tableData) {
2368
- return changed.length === 0 ? summarizeUnchanged(token, { changed, unchanged }) : details3(
2371
+ function createGroupsOrAuditsDetails(token, { changed, unchanged }, ...[columns, rows]) {
2372
+ if (changed.length === 0) {
2373
+ return new MarkdownDocument4().paragraph(
2374
+ summarizeUnchanged(token, { changed, unchanged })
2375
+ );
2376
+ }
2377
+ return new MarkdownDocument4().details(
2369
2378
  summarizeDiffOutcomes(changesToDiffOutcomes(changed), token),
2370
- lines5(
2371
- table4({
2372
- ...tableData,
2373
- rows: tableData.rows.slice(0, MAX_ROWS)
2374
- // use never to avoid typing problem
2375
- }),
2376
- changed.length > MAX_ROWS && italicMd(
2379
+ md5`${md5.table(columns, rows.slice(0, MAX_ROWS))}${changed.length > MAX_ROWS ? md5.paragraph(
2380
+ md5.italic(
2377
2381
  `Only the ${MAX_ROWS} most affected ${pluralize(
2378
2382
  token
2379
2383
  )} are listed above for brevity.`
2380
- ),
2381
- unchanged.length > 0 && summarizeUnchanged(token, { changed, unchanged })
2382
- )
2384
+ )
2385
+ ) : ""}${unchanged.length > 0 ? md5.paragraph(summarizeUnchanged(token, { changed, unchanged })) : ""}`
2383
2386
  );
2384
2387
  }
2385
- function formatScoreChange(diff) {
2386
- const marker = getDiffMarker(diff);
2387
- const text = formatDiffNumber(Math.round(diff * 1e3) / 10);
2388
- return colorByScoreDiff(`${marker} ${text}`, diff);
2389
- }
2390
- function formatValueChange({
2391
- values,
2392
- scores
2393
- }) {
2394
- const marker = getDiffMarker(values.diff);
2395
- const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : Math.round(100 * values.diff / values.before);
2396
- const text = `${formatDiffNumber(percentage)}\u2009%`;
2397
- return colorByScoreDiff(`${marker} ${text}`, scores.diff);
2398
- }
2399
2388
  function summarizeUnchanged(token, { changed, unchanged }) {
2400
- return section5(
2401
- [
2402
- changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`,
2403
- unchanged.length === 1 ? "is" : "are",
2404
- "unchanged."
2405
- ].join(" ")
2406
- );
2389
+ return [
2390
+ changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`,
2391
+ unchanged.length === 1 ? "is" : "are",
2392
+ "unchanged."
2393
+ ].join(" ");
2407
2394
  }
2408
2395
  function summarizeDiffOutcomes(outcomes, token) {
2409
2396
  return objectToEntries(countDiffOutcomes(outcomes)).filter(
@@ -2428,7 +2415,7 @@ function formatTitle({
2428
2415
  docsUrl
2429
2416
  }) {
2430
2417
  if (docsUrl) {
2431
- return link7(docsUrl, title);
2418
+ return md5.link(docsUrl, title);
2432
2419
  }
2433
2420
  return title;
2434
2421
  }
@@ -2472,8 +2459,22 @@ function countDiffOutcomes(outcomes) {
2472
2459
  };
2473
2460
  }
2474
2461
 
2462
+ // packages/utils/src/lib/reports/load-report.ts
2463
+ import { join as join3 } from "node:path";
2464
+ async function loadReport(options) {
2465
+ const { outputDir, filename, format } = options;
2466
+ await ensureDirectoryExists(outputDir);
2467
+ const filePath = join3(outputDir, `${filename}.${format}`);
2468
+ if (format === "json") {
2469
+ const content = await readJsonFile(filePath);
2470
+ return reportSchema.parse(content);
2471
+ }
2472
+ const text = await readTextFile(filePath);
2473
+ return text;
2474
+ }
2475
+
2475
2476
  // packages/utils/src/lib/reports/log-stdout-summary.ts
2476
- import chalk4 from "chalk";
2477
+ import { bold as bold4, cyan, cyanBright, green as green2, red } from "ansis";
2477
2478
  function log(msg = "") {
2478
2479
  ui().logger.log(msg);
2479
2480
  }
@@ -2490,14 +2491,14 @@ function logStdoutSummary(report) {
2490
2491
  }
2491
2492
  function reportToHeaderSection(report) {
2492
2493
  const { packageName, version } = report;
2493
- return `${chalk4.bold(reportHeadlineText)} - ${packageName}@${version}`;
2494
+ return `${bold4(REPORT_HEADLINE_TEXT)} - ${packageName}@${version}`;
2494
2495
  }
2495
2496
  function logPlugins(report) {
2496
2497
  const { plugins } = report;
2497
2498
  plugins.forEach((plugin) => {
2498
2499
  const { title, audits } = plugin;
2499
2500
  log();
2500
- log(chalk4.magentaBright.bold(`${title} audits`));
2501
+ log(bold4.magentaBright(`${title} audits`));
2501
2502
  log();
2502
2503
  audits.forEach((audit) => {
2503
2504
  ui().row([
@@ -2512,7 +2513,7 @@ function logPlugins(report) {
2512
2513
  padding: [0, 3, 0, 0]
2513
2514
  },
2514
2515
  {
2515
- text: chalk4.cyanBright(audit.displayValue || `${audit.value}`),
2516
+ text: cyanBright(audit.displayValue || `${audit.value}`),
2516
2517
  width: 10,
2517
2518
  padding: [0, 0, 0, 0]
2518
2519
  }
@@ -2523,42 +2524,38 @@ function logPlugins(report) {
2523
2524
  }
2524
2525
  function logCategories({ categories, plugins }) {
2525
2526
  const hAlign = (idx) => idx === 0 ? "left" : "right";
2526
- const rows = categories.map(({ title, score, refs }) => [
2527
+ const rows = categories.map(({ title, score, refs, isBinary }) => [
2527
2528
  title,
2528
- applyScoreColor({ score }),
2529
+ `${binaryIconPrefix(score, isBinary)}${applyScoreColor({ score })}`,
2529
2530
  countCategoryAudits(refs, plugins)
2530
2531
  ]);
2531
- const table5 = ui().table();
2532
- table5.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
2533
- table5.head(
2534
- reportRawOverviewTableHeaders.map((heading, idx) => ({
2535
- content: chalk4.cyan(heading),
2532
+ const table2 = ui().table();
2533
+ table2.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
2534
+ table2.head(
2535
+ REPORT_RAW_OVERVIEW_TABLE_HEADERS.map((heading, idx) => ({
2536
+ content: cyan(heading),
2536
2537
  hAlign: hAlign(idx)
2537
2538
  }))
2538
2539
  );
2539
2540
  rows.forEach(
2540
- (row) => table5.row(
2541
+ (row) => table2.row(
2541
2542
  row.map((content, idx) => ({
2542
2543
  content: content.toString(),
2543
2544
  hAlign: hAlign(idx)
2544
2545
  }))
2545
2546
  )
2546
2547
  );
2547
- log(chalk4.magentaBright.bold("Categories"));
2548
+ log(bold4.magentaBright("Categories"));
2548
2549
  log();
2549
- table5.render();
2550
+ table2.render();
2550
2551
  log();
2551
2552
  }
2552
- function applyScoreColor({ score, text }) {
2553
- const formattedScore = text ?? formatReportScore(score);
2554
- const style = text ? chalk4 : chalk4.bold;
2555
- if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
2556
- return style.green(formattedScore);
2557
- }
2558
- if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
2559
- return style.yellow(formattedScore);
2560
- }
2561
- return style.red(formattedScore);
2553
+ function binaryIconPrefix(score, isBinary) {
2554
+ return targetScoreIcon(score, isBinary ? 1 : void 0, {
2555
+ passIcon: bold4(green2("\u2713")),
2556
+ failIcon: bold4(red("\u2717")),
2557
+ postfix: " "
2558
+ });
2562
2559
  }
2563
2560
 
2564
2561
  // packages/utils/src/lib/reports/scoring.ts
@@ -2648,56 +2645,6 @@ function parseScoringParameters(refs, scoreFn) {
2648
2645
  return scoredRefs;
2649
2646
  }
2650
2647
 
2651
- // packages/utils/src/lib/reports/sorting.ts
2652
- function sortReport(report) {
2653
- const { categories, plugins } = report;
2654
- const sortedCategories = categories.map((category) => {
2655
- const { audits, groups } = category.refs.reduce(
2656
- (acc, ref) => ({
2657
- ...acc,
2658
- ...ref.type === "group" ? {
2659
- groups: [...acc.groups, getSortableGroupByRef(ref, plugins)]
2660
- } : {
2661
- audits: [...acc.audits, getSortableAuditByRef(ref, plugins)]
2662
- }
2663
- }),
2664
- { groups: [], audits: [] }
2665
- );
2666
- const sortedAuditsAndGroups = [...audits, ...groups].sort(
2667
- compareCategoryAuditsAndGroups
2668
- );
2669
- const sortedRefs = [...category.refs].sort((a, b) => {
2670
- const aIndex = sortedAuditsAndGroups.findIndex(
2671
- (ref) => ref.slug === a.slug && ref.plugin === a.plugin
2672
- );
2673
- const bIndex = sortedAuditsAndGroups.findIndex(
2674
- (ref) => ref.slug === b.slug && ref.plugin === b.plugin
2675
- );
2676
- return aIndex - bIndex;
2677
- });
2678
- return { ...category, refs: sortedRefs };
2679
- });
2680
- return {
2681
- ...report,
2682
- categories: sortedCategories,
2683
- plugins: sortPlugins(plugins)
2684
- };
2685
- }
2686
- function sortPlugins(plugins) {
2687
- return plugins.map((plugin) => ({
2688
- ...plugin,
2689
- audits: [...plugin.audits].sort(compareAudits).map(
2690
- (audit) => audit.details?.issues ? {
2691
- ...audit,
2692
- details: {
2693
- ...audit.details,
2694
- issues: [...audit.details.issues].sort(compareIssues)
2695
- }
2696
- } : audit
2697
- )
2698
- }));
2699
- }
2700
-
2701
2648
  // packages/utils/src/lib/verbose-utils.ts
2702
2649
  function getLogVerbose(verbose = false) {
2703
2650
  return (msg) => {
@@ -2720,13 +2667,13 @@ var verboseUtils = (verbose = false) => ({
2720
2667
  export {
2721
2668
  CODE_PUSHUP_DOMAIN,
2722
2669
  FOOTER_PREFIX,
2670
+ HIERARCHY,
2723
2671
  NEW_LINE,
2724
2672
  ProcessError,
2725
2673
  README_LINK,
2726
2674
  SPACE,
2727
2675
  TAB,
2728
2676
  TERMINAL_WIDTH,
2729
- apostrophize,
2730
2677
  calcDuration,
2731
2678
  capitalize,
2732
2679
  compareIssueSeverity,
@@ -2771,7 +2718,7 @@ export {
2771
2718
  logMultipleResults,
2772
2719
  logStdoutSummary,
2773
2720
  matchArrayItemsByKey,
2774
- md,
2721
+ mergeConfigs,
2775
2722
  normalizeSemver,
2776
2723
  objectFromEntries,
2777
2724
  objectToCliArgs,