@code-pushup/core 0.48.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
@@ -743,11 +743,262 @@ function comparePairs(pairs, equalsFn) {
743
743
  import { spawn } from "node:child_process";
744
744
 
745
745
  // packages/utils/src/lib/reports/utils.ts
746
- import { join } from "node:path";
746
+ import ansis from "ansis";
747
+ import { md } from "build-md";
748
+
749
+ // packages/utils/src/lib/reports/constants.ts
750
+ var TERMINAL_WIDTH = 80;
751
+ var SCORE_COLOR_RANGE = {
752
+ GREEN_MIN: 0.9,
753
+ YELLOW_MIN: 0.5
754
+ };
755
+ var FOOTER_PREFIX = "Made with \u2764 by";
756
+ var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
757
+ var README_LINK = "https://github.com/code-pushup/cli#readme";
758
+ var REPORT_HEADLINE_TEXT = "Code PushUp Report";
759
+ var REPORT_RAW_OVERVIEW_TABLE_HEADERS = [
760
+ "Category",
761
+ "Score",
762
+ "Audits"
763
+ ];
764
+
765
+ // packages/utils/src/lib/reports/utils.ts
766
+ function formatReportScore(score) {
767
+ const scaledScore = score * 100;
768
+ const roundedScore = Math.round(scaledScore);
769
+ return roundedScore === 100 && score !== 1 ? Math.floor(scaledScore).toString() : roundedScore.toString();
770
+ }
771
+ function formatScoreWithColor(score, options) {
772
+ const styledNumber = options?.skipBold ? formatReportScore(score) : md.bold(formatReportScore(score));
773
+ return md`${scoreMarker(score)} ${styledNumber}`;
774
+ }
775
+ var MARKERS = {
776
+ circle: {
777
+ red: "\u{1F534}",
778
+ yellow: "\u{1F7E1}",
779
+ green: "\u{1F7E2}"
780
+ },
781
+ square: {
782
+ red: "\u{1F7E5}",
783
+ yellow: "\u{1F7E8}",
784
+ green: "\u{1F7E9}"
785
+ }
786
+ };
787
+ function scoreMarker(score, markerType = "circle") {
788
+ if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
789
+ return MARKERS[markerType].green;
790
+ }
791
+ if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
792
+ return MARKERS[markerType].yellow;
793
+ }
794
+ return MARKERS[markerType].red;
795
+ }
796
+ function getDiffMarker(diff) {
797
+ if (diff > 0) {
798
+ return "\u2191";
799
+ }
800
+ if (diff < 0) {
801
+ return "\u2193";
802
+ }
803
+ return "";
804
+ }
805
+ function colorByScoreDiff(text, diff) {
806
+ const color = diff > 0 ? "green" : diff < 0 ? "red" : "gray";
807
+ return shieldsBadge(text, color);
808
+ }
809
+ function shieldsBadge(text, color) {
810
+ return md.image(
811
+ `https://img.shields.io/badge/${encodeURIComponent(text)}-${color}`,
812
+ text
813
+ );
814
+ }
815
+ function formatDiffNumber(diff) {
816
+ const number = Math.abs(diff) === Number.POSITIVE_INFINITY ? "\u221E" : `${Math.abs(diff)}`;
817
+ const sign = diff < 0 ? "\u2212" : "+";
818
+ return `${sign}${number}`;
819
+ }
820
+ function severityMarker(severity) {
821
+ if (severity === "error") {
822
+ return "\u{1F6A8}";
823
+ }
824
+ if (severity === "warning") {
825
+ return "\u26A0\uFE0F";
826
+ }
827
+ return "\u2139\uFE0F";
828
+ }
829
+ function formatScoreChange(diff) {
830
+ const marker = getDiffMarker(diff);
831
+ const text = formatDiffNumber(Math.round(diff * 1e3) / 10);
832
+ return colorByScoreDiff(`${marker} ${text}`, diff);
833
+ }
834
+ function formatValueChange({
835
+ values,
836
+ scores
837
+ }) {
838
+ const marker = getDiffMarker(values.diff);
839
+ const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : Math.round(100 * values.diff / values.before);
840
+ const text = `${formatDiffNumber(percentage)}\u2009%`;
841
+ return colorByScoreDiff(`${marker} ${text}`, scores.diff);
842
+ }
843
+ function calcDuration(start, stop) {
844
+ return Math.round((stop ?? performance.now()) - start);
845
+ }
846
+ function countCategoryAudits(refs, plugins) {
847
+ const groupLookup = plugins.reduce(
848
+ (lookup, plugin) => {
849
+ if (plugin.groups == null || plugin.groups.length === 0) {
850
+ return lookup;
851
+ }
852
+ return {
853
+ ...lookup,
854
+ [plugin.slug]: Object.fromEntries(
855
+ plugin.groups.map((group) => [group.slug, group])
856
+ )
857
+ };
858
+ },
859
+ {}
860
+ );
861
+ return refs.reduce((acc, ref) => {
862
+ if (ref.type === "group") {
863
+ const groupRefs = groupLookup[ref.plugin]?.[ref.slug]?.refs;
864
+ return acc + (groupRefs?.length ?? 0);
865
+ }
866
+ return acc + 1;
867
+ }, 0);
868
+ }
869
+ function compareCategoryAuditsAndGroups(a, b) {
870
+ if (a.weight !== b.weight) {
871
+ return b.weight - a.weight;
872
+ }
873
+ if (a.score !== b.score) {
874
+ return a.score - b.score;
875
+ }
876
+ if ("value" in a && "value" in b && a.value !== b.value) {
877
+ return b.value - a.value;
878
+ }
879
+ return a.title.localeCompare(b.title);
880
+ }
881
+ function compareAudits(a, b) {
882
+ if (a.score !== b.score) {
883
+ return a.score - b.score;
884
+ }
885
+ if (a.value !== b.value) {
886
+ return b.value - a.value;
887
+ }
888
+ return a.title.localeCompare(b.title);
889
+ }
890
+ function compareIssueSeverity(severity1, severity2) {
891
+ const levels = {
892
+ info: 0,
893
+ warning: 1,
894
+ error: 2
895
+ };
896
+ return levels[severity1] - levels[severity2];
897
+ }
898
+ function throwIsNotPresentError(itemName, presentPlace) {
899
+ throw new Error(`${itemName} is not present in ${presentPlace}`);
900
+ }
901
+ function getPluginNameFromSlug(slug, plugins) {
902
+ return plugins.find(({ slug: pluginSlug }) => pluginSlug === slug)?.title || slug;
903
+ }
904
+ function compareIssues(a, b) {
905
+ if (a.severity !== b.severity) {
906
+ return -compareIssueSeverity(a.severity, b.severity);
907
+ }
908
+ if (!a.source && b.source) {
909
+ return -1;
910
+ }
911
+ if (a.source && !b.source) {
912
+ return 1;
913
+ }
914
+ if (a.source?.file !== b.source?.file) {
915
+ return a.source?.file.localeCompare(b.source?.file || "") ?? 0;
916
+ }
917
+ if (!a.source?.position && b.source?.position) {
918
+ return -1;
919
+ }
920
+ if (a.source?.position && !b.source?.position) {
921
+ return 1;
922
+ }
923
+ if (a.source?.position?.startLine !== b.source?.position?.startLine) {
924
+ return (a.source?.position?.startLine ?? 0) - (b.source?.position?.startLine ?? 0);
925
+ }
926
+ return 0;
927
+ }
928
+ function applyScoreColor({ score, text }, style = ansis) {
929
+ const formattedScore = text ?? formatReportScore(score);
930
+ if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
931
+ return text ? style.green(formattedScore) : style.bold(style.green(formattedScore));
932
+ }
933
+ if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
934
+ return text ? style.yellow(formattedScore) : style.bold(style.yellow(formattedScore));
935
+ }
936
+ return text ? style.red(formattedScore) : style.bold(style.red(formattedScore));
937
+ }
938
+ function targetScoreIcon(score, targetScore, options = {}) {
939
+ if (targetScore != null) {
940
+ const {
941
+ passIcon = "\u2705",
942
+ failIcon = "\u274C",
943
+ prefix = "",
944
+ postfix = ""
945
+ } = options;
946
+ if (score >= targetScore) {
947
+ return `${prefix}${passIcon}${postfix}`;
948
+ }
949
+ return `${prefix}${failIcon}${postfix}`;
950
+ }
951
+ return "";
952
+ }
953
+
954
+ // packages/utils/src/lib/execute-process.ts
955
+ var ProcessError = class extends Error {
956
+ code;
957
+ stderr;
958
+ stdout;
959
+ constructor(result) {
960
+ super(result.stderr);
961
+ this.code = result.code;
962
+ this.stderr = result.stderr;
963
+ this.stdout = result.stdout;
964
+ }
965
+ };
966
+ function executeProcess(cfg) {
967
+ const { observer, cwd, command, args, ignoreExitCode = false } = cfg;
968
+ const { onStdout, onError, onComplete } = observer ?? {};
969
+ const date = (/* @__PURE__ */ new Date()).toISOString();
970
+ const start = performance.now();
971
+ return new Promise((resolve, reject) => {
972
+ const process2 = spawn(command, args, { cwd, shell: true });
973
+ let stdout = "";
974
+ let stderr = "";
975
+ process2.stdout.on("data", (data) => {
976
+ stdout += String(data);
977
+ onStdout?.(String(data));
978
+ });
979
+ process2.stderr.on("data", (data) => {
980
+ stderr += String(data);
981
+ });
982
+ process2.on("error", (err) => {
983
+ stderr += err.toString();
984
+ });
985
+ process2.on("close", (code2) => {
986
+ const timings = { date, duration: calcDuration(start) };
987
+ if (code2 === 0 || ignoreExitCode) {
988
+ onComplete?.();
989
+ resolve({ code: code2, stdout, stderr, ...timings });
990
+ } else {
991
+ const errorMsg = new ProcessError({ code: code2, stdout, stderr, ...timings });
992
+ onError?.(errorMsg);
993
+ reject(errorMsg);
994
+ }
995
+ });
996
+ });
997
+ }
747
998
 
748
999
  // packages/utils/src/lib/file-system.ts
1000
+ import { bold, gray } from "ansis";
749
1001
  import { bundleRequire } from "bundle-require";
750
- import chalk2 from "chalk";
751
1002
  import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
752
1003
 
753
1004
  // packages/utils/src/lib/formatting.ts
@@ -810,55 +1061,7 @@ function isPromiseRejectedResult(result) {
810
1061
  // packages/utils/src/lib/logging.ts
811
1062
  import isaacs_cliui from "@isaacs/cliui";
812
1063
  import { cliui } from "@poppinss/cliui";
813
- import chalk from "chalk";
814
-
815
- // packages/utils/src/lib/reports/constants.ts
816
- var TERMINAL_WIDTH = 80;
817
- var SCORE_COLOR_RANGE = {
818
- GREEN_MIN: 0.9,
819
- YELLOW_MIN: 0.5
820
- };
821
- var CATEGORIES_TITLE = "\u{1F3F7} Categories";
822
- var FOOTER_PREFIX = "Made with \u2764 by";
823
- var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
824
- var README_LINK = "https://github.com/code-pushup/cli#readme";
825
- var reportHeadlineText = "Code PushUp Report";
826
- var reportOverviewTableHeaders = [
827
- {
828
- key: "category",
829
- label: "\u{1F3F7} Category",
830
- align: "left"
831
- },
832
- {
833
- key: "score",
834
- label: "\u2B50 Score"
835
- },
836
- {
837
- key: "audits",
838
- label: "\u{1F6E1} Audits"
839
- }
840
- ];
841
- var reportRawOverviewTableHeaders = ["Category", "Score", "Audits"];
842
- var issuesTableHeadings = [
843
- {
844
- key: "severity",
845
- label: "Severity"
846
- },
847
- {
848
- key: "message",
849
- label: "Message"
850
- },
851
- {
852
- key: "file",
853
- label: "Source file"
854
- },
855
- {
856
- key: "line",
857
- label: "Line(s)"
858
- }
859
- ];
860
-
861
- // packages/utils/src/lib/logging.ts
1064
+ import { underline } from "ansis";
862
1065
  var singletonUiInstance;
863
1066
  function ui() {
864
1067
  if (singletonUiInstance === void 0) {
@@ -954,10 +1157,10 @@ async function ensureDirectoryExists(baseDir) {
954
1157
  function logMultipleFileResults(fileResults, messagePrefix) {
955
1158
  const succeededTransform = (result) => {
956
1159
  const [fileName, size] = result.value;
957
- const formattedSize = size ? ` (${chalk2.gray(formatBytes(size))})` : "";
958
- return `- ${chalk2.bold(fileName)}${formattedSize}`;
1160
+ const formattedSize = size ? ` (${gray(formatBytes(size))})` : "";
1161
+ return `- ${bold(fileName)}${formattedSize}`;
959
1162
  };
960
- const failedTransform = (result) => `- ${chalk2.bold(result.reason)}`;
1163
+ const failedTransform = (result) => `- ${bold(result.reason)}`;
961
1164
  logMultipleResults(
962
1165
  fileResults,
963
1166
  messagePrefix,
@@ -973,570 +1176,32 @@ async function importModule(options) {
973
1176
  return mod;
974
1177
  }
975
1178
 
976
- // packages/utils/src/lib/text-formats/constants.ts
977
- var NEW_LINE = "\n";
978
- var TAB = " ";
979
- var SPACE = " ";
1179
+ // packages/utils/src/lib/git/git.ts
1180
+ import { isAbsolute, join, relative } from "node:path";
1181
+ import { simpleGit } from "simple-git";
980
1182
 
981
- // packages/utils/src/lib/text-formats/html/details.ts
982
- function details(title, content, cfg = { open: false }) {
983
- 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.
984
- NEW_LINE}${content}${NEW_LINE}${// @TODO in the future we could consider adding it only if the content ends with a code block
985
- // ⚠️ The blank line ensure Markdown in content is rendered correctly.
986
- NEW_LINE}</details>${// ⚠️ The blank line is needed to ensure Markdown after details is rendered correctly.
987
- NEW_LINE}`;
1183
+ // packages/utils/src/lib/transform.ts
1184
+ function objectToEntries(obj) {
1185
+ return Object.entries(obj);
988
1186
  }
989
-
990
- // packages/utils/src/lib/text-formats/html/font-style.ts
991
- var boldElement = "b";
992
- function bold(text) {
993
- return `<${boldElement}>${text}</${boldElement}>`;
994
- }
995
- var italicElement = "i";
996
- function italic(text) {
997
- return `<${italicElement}>${text}</${italicElement}>`;
998
- }
999
- var codeElement = "code";
1000
- function code(text) {
1001
- return `<${codeElement}>${text}</${codeElement}>`;
1002
- }
1003
-
1004
- // packages/utils/src/lib/text-formats/html/link.ts
1005
- function link(href, text) {
1006
- return `<a href="${href}">${text || href}</a>`;
1007
- }
1008
-
1009
- // packages/utils/src/lib/transform.ts
1010
- function objectToEntries(obj) {
1011
- return Object.entries(obj);
1012
- }
1013
- function deepClone(obj) {
1014
- return obj == null || typeof obj !== "object" ? obj : structuredClone(obj);
1015
- }
1016
- function toUnixPath(path) {
1017
- return path.replace(/\\/g, "/");
1018
- }
1019
- function capitalize(text) {
1020
- return `${text.charAt(0).toLocaleUpperCase()}${text.slice(
1021
- 1
1022
- )}`;
1023
- }
1024
-
1025
- // packages/utils/src/lib/text-formats/table.ts
1026
- function rowToStringArray({ rows, columns = [] }) {
1027
- if (Array.isArray(rows.at(0)) && typeof columns.at(0) === "object") {
1028
- throw new TypeError(
1029
- "Column can`t be object when rows are primitive values"
1030
- );
1031
- }
1032
- return rows.map((row) => {
1033
- if (Array.isArray(row)) {
1034
- return row.map(String);
1035
- }
1036
- const objectRow = row;
1037
- if (columns.length === 0 || typeof columns.at(0) === "string") {
1038
- return Object.values(objectRow).map(
1039
- (value) => value == null ? "" : String(value)
1040
- );
1041
- }
1042
- return columns.map(
1043
- ({ key }) => objectRow[key] == null ? "" : String(objectRow[key])
1044
- );
1045
- });
1046
- }
1047
- function columnsToStringArray({
1048
- rows,
1049
- columns = []
1050
- }) {
1051
- const firstRow = rows.at(0);
1052
- const primitiveRows = Array.isArray(firstRow);
1053
- if (typeof columns.at(0) === "string" && !primitiveRows) {
1054
- throw new Error("invalid union type. Caught by model parsing.");
1055
- }
1056
- if (columns.length === 0) {
1057
- if (Array.isArray(firstRow)) {
1058
- return firstRow.map((_, idx) => String(idx));
1059
- }
1060
- return Object.keys(firstRow);
1061
- }
1062
- if (typeof columns.at(0) === "string") {
1063
- return columns.map(String);
1064
- }
1065
- const cols = columns;
1066
- return cols.map(({ label, key }) => label ?? capitalize(key));
1067
- }
1068
- function getColumnAlignmentForKeyAndIndex(targetKey, targetIdx, columns = []) {
1069
- const column = columns.at(targetIdx) ?? columns.find((col) => col.key === targetKey);
1070
- if (typeof column === "string") {
1071
- return column;
1072
- } else if (typeof column === "object") {
1073
- return column.align ?? "center";
1074
- } else {
1075
- return "center";
1076
- }
1077
- }
1078
- function getColumnAlignmentForIndex(targetIdx, columns = []) {
1079
- const column = columns.at(targetIdx);
1080
- if (column == null) {
1081
- return "center";
1082
- } else if (typeof column === "string") {
1083
- return column;
1084
- } else if (typeof column === "object") {
1085
- return column.align ?? "center";
1086
- } else {
1087
- return "center";
1088
- }
1089
- }
1090
- function getColumnAlignments(tableData) {
1091
- const { rows, columns = [] } = tableData;
1092
- if (rows.at(0) == null) {
1093
- throw new Error("first row can`t be undefined.");
1094
- }
1095
- if (Array.isArray(rows.at(0))) {
1096
- const firstPrimitiveRow = rows.at(0);
1097
- return Array.from({ length: firstPrimitiveRow.length }).map(
1098
- (_, idx) => getColumnAlignmentForIndex(idx, columns)
1099
- );
1100
- }
1101
- const biggestRow = [...rows].sort((a, b) => Object.keys(a).length - Object.keys(b).length).at(-1);
1102
- if (columns.length > 0) {
1103
- return columns.map(
1104
- (column, idx) => typeof column === "string" ? column : getColumnAlignmentForKeyAndIndex(
1105
- column.key,
1106
- idx,
1107
- columns
1108
- )
1109
- );
1110
- }
1111
- return Object.keys(biggestRow ?? {}).map((_) => "center");
1112
- }
1113
-
1114
- // packages/utils/src/lib/text-formats/html/table.ts
1115
- function wrap(elem, content) {
1116
- return `<${elem}>${content}</${elem}>${NEW_LINE}`;
1117
- }
1118
- function wrapRow(content) {
1119
- const elem = "tr";
1120
- return `<${elem}>${NEW_LINE}${content}</${elem}>${NEW_LINE}`;
1121
- }
1122
- function table(tableData) {
1123
- if (tableData.rows.length === 0) {
1124
- throw new Error("Data can't be empty");
1125
- }
1126
- const tableHeaderCols = columnsToStringArray(tableData).map((s) => wrap("th", s)).join("");
1127
- const tableHeaderRow = wrapRow(tableHeaderCols);
1128
- const tableBody = rowToStringArray(tableData).map((arr) => {
1129
- const columns = arr.map((s) => wrap("td", s)).join("");
1130
- return wrapRow(columns);
1131
- }).join("");
1132
- return wrap("table", `${NEW_LINE}${tableHeaderRow}${tableBody}`);
1133
- }
1134
-
1135
- // packages/utils/src/lib/text-formats/md/font-style.ts
1136
- var boldWrap = "**";
1137
- function bold2(text) {
1138
- return `${boldWrap}${text}${boldWrap}`;
1139
- }
1140
- var italicWrap = "_";
1141
- function italic2(text) {
1142
- return `${italicWrap}${text}${italicWrap}`;
1143
- }
1144
- var strikeThroughWrap = "~";
1145
- function strikeThrough(text) {
1146
- return `${strikeThroughWrap}${text}${strikeThroughWrap}`;
1147
- }
1148
- var codeWrap = "`";
1149
- function code2(text) {
1150
- return `${codeWrap}${text}${codeWrap}`;
1151
- }
1152
-
1153
- // packages/utils/src/lib/text-formats/md/headline.ts
1154
- function headline(text, hierarchy = 1) {
1155
- return `${"#".repeat(hierarchy)} ${text}${NEW_LINE}`;
1156
- }
1157
- function h(text, hierarchy = 1) {
1158
- return headline(text, hierarchy);
1159
- }
1160
- function h1(text) {
1161
- return headline(text, 1);
1162
- }
1163
- function h2(text) {
1164
- return headline(text, 2);
1165
- }
1166
- function h3(text) {
1167
- return headline(text, 3);
1168
- }
1169
- function h4(text) {
1170
- return headline(text, 4);
1171
- }
1172
- function h5(text) {
1173
- return headline(text, 5);
1174
- }
1175
- function h6(text) {
1176
- return headline(text, 6);
1177
- }
1178
-
1179
- // packages/utils/src/lib/text-formats/md/image.ts
1180
- function image(src, alt) {
1181
- return `![${alt}](${src})`;
1182
- }
1183
-
1184
- // packages/utils/src/lib/text-formats/md/link.ts
1185
- function link2(href, text) {
1186
- return `[${text || href}](${href})`;
1187
- }
1188
-
1189
- // packages/utils/src/lib/text-formats/md/list.ts
1190
- function li(text, order = "unordered") {
1191
- const style = order === "unordered" ? "-" : "- [ ]";
1192
- return `${style} ${text}`;
1193
- }
1194
- function indentation(text, level = 1) {
1195
- return `${TAB.repeat(level)}${text}`;
1196
- }
1197
-
1198
- // packages/utils/src/lib/text-formats/md/paragraphs.ts
1199
- function paragraphs(...sections) {
1200
- return sections.filter(Boolean).join(`${NEW_LINE}${NEW_LINE}`);
1201
- }
1202
-
1203
- // packages/utils/src/lib/text-formats/md/section.ts
1204
- function section(...contents) {
1205
- return `${lines(...contents)}${NEW_LINE}`;
1206
- }
1207
- function lines(...contents) {
1208
- const filteredContent = contents.filter(
1209
- (value) => value != null && value !== "" && value !== false
1210
- );
1211
- return `${filteredContent.join(NEW_LINE)}`;
1212
- }
1213
-
1214
- // packages/utils/src/lib/text-formats/md/table.ts
1215
- var alignString = /* @__PURE__ */ new Map([
1216
- ["left", ":--"],
1217
- ["center", ":--:"],
1218
- ["right", "--:"]
1219
- ]);
1220
- function tableRow(rows) {
1221
- return `|${rows.join("|")}|`;
1222
- }
1223
- function table2(data) {
1224
- if (data.rows.length === 0) {
1225
- throw new Error("Data can't be empty");
1226
- }
1227
- const alignmentRow = getColumnAlignments(data).map(
1228
- (s) => alignString.get(s) ?? String(alignString.get("center"))
1229
- );
1230
- return section(
1231
- `${lines(
1232
- tableRow(columnsToStringArray(data)),
1233
- tableRow(alignmentRow),
1234
- ...rowToStringArray(data).map(tableRow)
1235
- )}`
1236
- );
1237
- }
1238
-
1239
- // packages/utils/src/lib/text-formats/index.ts
1240
- var md = {
1241
- bold: bold2,
1242
- italic: italic2,
1243
- strikeThrough,
1244
- code: code2,
1245
- link: link2,
1246
- image,
1247
- headline,
1248
- h,
1249
- h1,
1250
- h2,
1251
- h3,
1252
- h4,
1253
- h5,
1254
- h6,
1255
- indentation,
1256
- lines,
1257
- li,
1258
- section,
1259
- paragraphs,
1260
- table: table2
1261
- };
1262
- var html = {
1263
- bold,
1264
- italic,
1265
- code,
1266
- link,
1267
- details,
1268
- table
1269
- };
1270
-
1271
- // packages/utils/src/lib/reports/utils.ts
1272
- var { image: image2, bold: boldMd } = md;
1273
- function formatReportScore(score) {
1274
- const scaledScore = score * 100;
1275
- const roundedScore = Math.round(scaledScore);
1276
- return roundedScore === 100 && score !== 1 ? Math.floor(scaledScore).toString() : roundedScore.toString();
1277
- }
1278
- function formatScoreWithColor(score, options) {
1279
- const styledNumber = options?.skipBold ? formatReportScore(score) : boldMd(formatReportScore(score));
1280
- return `${scoreMarker(score)} ${styledNumber}`;
1281
- }
1282
- var MARKERS = {
1283
- circle: {
1284
- red: "\u{1F534}",
1285
- yellow: "\u{1F7E1}",
1286
- green: "\u{1F7E2}"
1287
- },
1288
- square: {
1289
- red: "\u{1F7E5}",
1290
- yellow: "\u{1F7E8}",
1291
- green: "\u{1F7E9}"
1292
- }
1293
- };
1294
- function scoreMarker(score, markerType = "circle") {
1295
- if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
1296
- return MARKERS[markerType].green;
1297
- }
1298
- if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
1299
- return MARKERS[markerType].yellow;
1300
- }
1301
- return MARKERS[markerType].red;
1302
- }
1303
- function getDiffMarker(diff) {
1304
- if (diff > 0) {
1305
- return "\u2191";
1306
- }
1307
- if (diff < 0) {
1308
- return "\u2193";
1309
- }
1310
- return "";
1311
- }
1312
- function colorByScoreDiff(text, diff) {
1313
- const color = diff > 0 ? "green" : diff < 0 ? "red" : "gray";
1314
- return shieldsBadge(text, color);
1315
- }
1316
- function shieldsBadge(text, color) {
1317
- return image2(
1318
- `https://img.shields.io/badge/${encodeURIComponent(text)}-${color}`,
1319
- text
1320
- );
1321
- }
1322
- function formatDiffNumber(diff) {
1323
- const number = Math.abs(diff) === Number.POSITIVE_INFINITY ? "\u221E" : `${Math.abs(diff)}`;
1324
- const sign = diff < 0 ? "\u2212" : "+";
1325
- return `${sign}${number}`;
1326
- }
1327
- function severityMarker(severity) {
1328
- if (severity === "error") {
1329
- return "\u{1F6A8}";
1330
- }
1331
- if (severity === "warning") {
1332
- return "\u26A0\uFE0F";
1333
- }
1334
- return "\u2139\uFE0F";
1335
- }
1336
- function calcDuration(start, stop) {
1337
- return Math.round((stop ?? performance.now()) - start);
1338
- }
1339
- function countCategoryAudits(refs, plugins) {
1340
- const groupLookup = plugins.reduce(
1341
- (lookup, plugin) => {
1342
- if (plugin.groups == null || plugin.groups.length === 0) {
1343
- return lookup;
1344
- }
1345
- return {
1346
- ...lookup,
1347
- [plugin.slug]: Object.fromEntries(
1348
- plugin.groups.map((group) => [group.slug, group])
1349
- )
1350
- };
1351
- },
1352
- {}
1353
- );
1354
- return refs.reduce((acc, ref) => {
1355
- if (ref.type === "group") {
1356
- const groupRefs = groupLookup[ref.plugin]?.[ref.slug]?.refs;
1357
- return acc + (groupRefs?.length ?? 0);
1358
- }
1359
- return acc + 1;
1360
- }, 0);
1361
- }
1362
- function getSortableAuditByRef({ slug, weight, plugin }, plugins) {
1363
- const auditPlugin = plugins.find((p) => p.slug === plugin);
1364
- if (!auditPlugin) {
1365
- throwIsNotPresentError(`Plugin ${plugin}`, "report");
1366
- }
1367
- const audit = auditPlugin.audits.find(
1368
- ({ slug: auditSlug }) => auditSlug === slug
1369
- );
1370
- if (!audit) {
1371
- throwIsNotPresentError(`Audit ${slug}`, auditPlugin.slug);
1372
- }
1373
- return {
1374
- ...audit,
1375
- weight,
1376
- plugin
1377
- };
1378
- }
1379
- function getSortableGroupByRef({ plugin, slug, weight }, plugins) {
1380
- const groupPlugin = plugins.find((p) => p.slug === plugin);
1381
- if (!groupPlugin) {
1382
- throwIsNotPresentError(`Plugin ${plugin}`, "report");
1383
- }
1384
- const group = groupPlugin.groups?.find(
1385
- ({ slug: groupSlug }) => groupSlug === slug
1386
- );
1387
- if (!group) {
1388
- throwIsNotPresentError(`Group ${slug}`, groupPlugin.slug);
1389
- }
1390
- const sortedAudits = getSortedGroupAudits(group, groupPlugin.slug, plugins);
1391
- const sortedAuditRefs = [...group.refs].sort((a, b) => {
1392
- const aIndex = sortedAudits.findIndex((ref) => ref.slug === a.slug);
1393
- const bIndex = sortedAudits.findIndex((ref) => ref.slug === b.slug);
1394
- return aIndex - bIndex;
1395
- });
1396
- return {
1397
- ...group,
1398
- refs: sortedAuditRefs,
1399
- plugin,
1400
- weight
1401
- };
1402
- }
1403
- function getSortedGroupAudits(group, plugin, plugins) {
1404
- return group.refs.map(
1405
- (ref) => getSortableAuditByRef(
1406
- {
1407
- plugin,
1408
- slug: ref.slug,
1409
- weight: ref.weight,
1410
- type: "audit"
1411
- },
1412
- plugins
1413
- )
1414
- ).sort(compareCategoryAuditsAndGroups);
1415
- }
1416
- function compareCategoryAuditsAndGroups(a, b) {
1417
- if (a.weight !== b.weight) {
1418
- return b.weight - a.weight;
1419
- }
1420
- if (a.score !== b.score) {
1421
- return a.score - b.score;
1422
- }
1423
- if ("value" in a && "value" in b && a.value !== b.value) {
1424
- return b.value - a.value;
1425
- }
1426
- return a.title.localeCompare(b.title);
1427
- }
1428
- function compareAudits(a, b) {
1429
- if (a.score !== b.score) {
1430
- return a.score - b.score;
1431
- }
1432
- if (a.value !== b.value) {
1433
- return b.value - a.value;
1434
- }
1435
- return a.title.localeCompare(b.title);
1436
- }
1437
- function compareIssueSeverity(severity1, severity2) {
1438
- const levels = {
1439
- info: 0,
1440
- warning: 1,
1441
- error: 2
1442
- };
1443
- return levels[severity1] - levels[severity2];
1444
- }
1445
- async function loadReport(options) {
1446
- const { outputDir, filename, format } = options;
1447
- await ensureDirectoryExists(outputDir);
1448
- const filePath = join(outputDir, `${filename}.${format}`);
1449
- if (format === "json") {
1450
- const content = await readJsonFile(filePath);
1451
- return reportSchema.parse(content);
1452
- }
1453
- const text = await readTextFile(filePath);
1454
- return text;
1455
- }
1456
- function throwIsNotPresentError(itemName, presentPlace) {
1457
- throw new Error(`${itemName} is not present in ${presentPlace}`);
1458
- }
1459
- function getPluginNameFromSlug(slug, plugins) {
1460
- return plugins.find(({ slug: pluginSlug }) => pluginSlug === slug)?.title || slug;
1461
- }
1462
- function compareIssues(a, b) {
1463
- if (a.severity !== b.severity) {
1464
- return -compareIssueSeverity(a.severity, b.severity);
1465
- }
1466
- if (!a.source && b.source) {
1467
- return -1;
1468
- }
1469
- if (a.source && !b.source) {
1470
- return 1;
1471
- }
1472
- if (a.source?.file !== b.source?.file) {
1473
- return a.source?.file.localeCompare(b.source?.file || "") ?? 0;
1474
- }
1475
- if (!a.source?.position && b.source?.position) {
1476
- return -1;
1477
- }
1478
- if (a.source?.position && !b.source?.position) {
1479
- return 1;
1480
- }
1481
- if (a.source?.position?.startLine !== b.source?.position?.startLine) {
1482
- return (a.source?.position?.startLine ?? 0) - (b.source?.position?.startLine ?? 0);
1483
- }
1484
- return 0;
1187
+ function deepClone(obj) {
1188
+ return obj == null || typeof obj !== "object" ? obj : structuredClone(obj);
1485
1189
  }
1486
-
1487
- // packages/utils/src/lib/execute-process.ts
1488
- var ProcessError = class extends Error {
1489
- code;
1490
- stderr;
1491
- stdout;
1492
- constructor(result) {
1493
- super(result.stderr);
1494
- this.code = result.code;
1495
- this.stderr = result.stderr;
1496
- this.stdout = result.stdout;
1497
- }
1498
- };
1499
- function executeProcess(cfg) {
1500
- const { observer, cwd, command, args, ignoreExitCode = false } = cfg;
1501
- const { onStdout, onError, onComplete } = observer ?? {};
1502
- const date = (/* @__PURE__ */ new Date()).toISOString();
1503
- const start = performance.now();
1504
- return new Promise((resolve, reject) => {
1505
- const process2 = spawn(command, args, { cwd, shell: true });
1506
- let stdout = "";
1507
- let stderr = "";
1508
- process2.stdout.on("data", (data) => {
1509
- stdout += String(data);
1510
- onStdout?.(String(data));
1511
- });
1512
- process2.stderr.on("data", (data) => {
1513
- stderr += String(data);
1514
- });
1515
- process2.on("error", (err) => {
1516
- stderr += err.toString();
1517
- });
1518
- process2.on("close", (code3) => {
1519
- const timings = { date, duration: calcDuration(start) };
1520
- if (code3 === 0 || ignoreExitCode) {
1521
- onComplete?.();
1522
- resolve({ code: code3, stdout, stderr, ...timings });
1523
- } else {
1524
- const errorMsg = new ProcessError({ code: code3, stdout, stderr, ...timings });
1525
- onError?.(errorMsg);
1526
- reject(errorMsg);
1527
- }
1528
- });
1529
- });
1190
+ function toUnixPath(path) {
1191
+ return path.replace(/\\/g, "/");
1192
+ }
1193
+ function capitalize(text) {
1194
+ return `${text.charAt(0).toLocaleUpperCase()}${text.slice(
1195
+ 1
1196
+ )}`;
1530
1197
  }
1531
1198
 
1532
1199
  // packages/utils/src/lib/git/git.ts
1533
- import { isAbsolute, join as join2, relative } from "node:path";
1534
- import { simpleGit } from "simple-git";
1535
1200
  function getGitRoot(git = simpleGit()) {
1536
1201
  return git.revparse("--show-toplevel");
1537
1202
  }
1538
1203
  function formatGitPath(path, gitRoot) {
1539
- const absolutePath = isAbsolute(path) ? path : join2(process.cwd(), path);
1204
+ const absolutePath = isAbsolute(path) ? path : join(process.cwd(), path);
1540
1205
  const relativePath = relative(gitRoot, absolutePath);
1541
1206
  return toUnixPath(relativePath);
1542
1207
  }
@@ -1618,17 +1283,17 @@ function groupByStatus(results) {
1618
1283
  }
1619
1284
 
1620
1285
  // packages/utils/src/lib/progress.ts
1621
- import chalk3 from "chalk";
1286
+ import { black, bold as bold2, gray as gray2, green } from "ansis";
1622
1287
  import { MultiProgressBars } from "multi-progress-bars";
1623
1288
  var barStyles = {
1624
- active: (s) => chalk3.green(s),
1625
- done: (s) => chalk3.gray(s),
1626
- idle: (s) => chalk3.gray(s)
1289
+ active: (s) => green(s),
1290
+ done: (s) => gray2(s),
1291
+ idle: (s) => gray2(s)
1627
1292
  };
1628
1293
  var messageStyles = {
1629
- active: (s) => chalk3.black(s),
1630
- done: (s) => chalk3.green(chalk3.bold(s)),
1631
- idle: (s) => chalk3.gray(s)
1294
+ active: (s) => black(s),
1295
+ done: (s) => bold2.green(s),
1296
+ idle: (s) => gray2(s)
1632
1297
  };
1633
1298
  var mpb;
1634
1299
  function getSingletonProgressBars(options) {
@@ -1671,334 +1336,502 @@ function getProgressBar(taskName) {
1671
1336
  }
1672
1337
  };
1673
1338
  }
1674
-
1675
- // packages/utils/src/lib/reports/flatten-plugins.ts
1676
- function listGroupsFromAllPlugins(report) {
1677
- return report.plugins.flatMap(
1678
- (plugin) => plugin.groups?.map((group) => ({ plugin, group })) ?? []
1679
- );
1680
- }
1681
- function listAuditsFromAllPlugins(report) {
1682
- return report.plugins.flatMap(
1683
- (plugin) => plugin.audits.map((audit) => ({ plugin, audit }))
1684
- );
1685
- }
1686
-
1687
- // packages/utils/src/lib/reports/formatting.ts
1688
- var { headline: headline2, lines: lines2, link: link3, section: section2, table: table3 } = md;
1689
- function tableSection(tableData, options) {
1690
- if (tableData.rows.length === 0) {
1691
- return "";
1692
- }
1693
- const { level = 4 } = options ?? {};
1694
- const render = (h7, l) => l === 0 ? h7 : headline2(h7, l);
1695
- return lines2(
1696
- tableData.title && render(tableData.title, level),
1697
- table3(tableData)
1698
- );
1699
- }
1700
- function metaDescription({
1701
- docsUrl,
1702
- description
1703
- }) {
1704
- if (docsUrl) {
1705
- const docsLink = link3(docsUrl, "\u{1F4D6} Docs");
1706
- if (!description) {
1707
- return section2(docsLink);
1708
- }
1709
- const parsedDescription = description.toString().endsWith("```") ? `${description}${NEW_LINE + NEW_LINE}` : `${description}${SPACE}`;
1710
- return section2(`${parsedDescription}${docsLink}`);
1711
- }
1712
- if (description && description.trim().length > 0) {
1713
- return section2(description);
1714
- }
1715
- return "";
1339
+
1340
+ // packages/utils/src/lib/reports/flatten-plugins.ts
1341
+ function listGroupsFromAllPlugins(report) {
1342
+ return report.plugins.flatMap(
1343
+ (plugin) => plugin.groups?.map((group) => ({ plugin, group })) ?? []
1344
+ );
1345
+ }
1346
+ function listAuditsFromAllPlugins(report) {
1347
+ return report.plugins.flatMap(
1348
+ (plugin) => plugin.audits.map((audit) => ({ plugin, audit }))
1349
+ );
1350
+ }
1351
+
1352
+ // packages/utils/src/lib/reports/generate-md-report.ts
1353
+ import { MarkdownDocument as MarkdownDocument3, md as md4 } from "build-md";
1354
+
1355
+ // packages/utils/src/lib/text-formats/constants.ts
1356
+ var HIERARCHY = {
1357
+ level_1: 1,
1358
+ level_2: 2,
1359
+ level_3: 3,
1360
+ level_4: 4,
1361
+ level_5: 5,
1362
+ level_6: 6
1363
+ };
1364
+
1365
+ // packages/utils/src/lib/text-formats/table.ts
1366
+ function rowToStringArray({ rows, columns = [] }) {
1367
+ if (Array.isArray(rows.at(0)) && typeof columns.at(0) === "object") {
1368
+ throw new TypeError(
1369
+ "Column can`t be object when rows are primitive values"
1370
+ );
1371
+ }
1372
+ return rows.map((row) => {
1373
+ if (Array.isArray(row)) {
1374
+ return row.map(String);
1375
+ }
1376
+ const objectRow = row;
1377
+ if (columns.length === 0 || typeof columns.at(0) === "string") {
1378
+ return Object.values(objectRow).map(
1379
+ (value) => value == null ? "" : String(value)
1380
+ );
1381
+ }
1382
+ return columns.map(
1383
+ ({ key }) => objectRow[key] == null ? "" : String(objectRow[key])
1384
+ );
1385
+ });
1386
+ }
1387
+ function columnsToStringArray({
1388
+ rows,
1389
+ columns = []
1390
+ }) {
1391
+ const firstRow = rows.at(0);
1392
+ const primitiveRows = Array.isArray(firstRow);
1393
+ if (typeof columns.at(0) === "string" && !primitiveRows) {
1394
+ throw new Error("invalid union type. Caught by model parsing.");
1395
+ }
1396
+ if (columns.length === 0) {
1397
+ if (Array.isArray(firstRow)) {
1398
+ return firstRow.map((_, idx) => String(idx));
1399
+ }
1400
+ return Object.keys(firstRow);
1401
+ }
1402
+ if (typeof columns.at(0) === "string") {
1403
+ return columns.map(String);
1404
+ }
1405
+ const cols = columns;
1406
+ return cols.map(({ label, key }) => label ?? capitalize(key));
1407
+ }
1408
+ function getColumnAlignmentForKeyAndIndex(targetKey, targetIdx, columns = []) {
1409
+ const column = columns.at(targetIdx) ?? columns.find((col) => col.key === targetKey);
1410
+ if (typeof column === "string") {
1411
+ return column;
1412
+ } else if (typeof column === "object") {
1413
+ return column.align ?? "center";
1414
+ } else {
1415
+ return "center";
1416
+ }
1417
+ }
1418
+ function getColumnAlignmentForIndex(targetIdx, columns = []) {
1419
+ const column = columns.at(targetIdx);
1420
+ if (column == null) {
1421
+ return "center";
1422
+ } else if (typeof column === "string") {
1423
+ return column;
1424
+ } else if (typeof column === "object") {
1425
+ return column.align ?? "center";
1426
+ } else {
1427
+ return "center";
1428
+ }
1429
+ }
1430
+ function getColumnAlignments(tableData) {
1431
+ const { rows, columns = [] } = tableData;
1432
+ if (rows.at(0) == null) {
1433
+ throw new Error("first row can`t be undefined.");
1434
+ }
1435
+ if (Array.isArray(rows.at(0))) {
1436
+ const firstPrimitiveRow = rows.at(0);
1437
+ return Array.from({ length: firstPrimitiveRow.length }).map(
1438
+ (_, idx) => getColumnAlignmentForIndex(idx, columns)
1439
+ );
1440
+ }
1441
+ const biggestRow = [...rows].sort((a, b) => Object.keys(a).length - Object.keys(b).length).at(-1);
1442
+ if (columns.length > 0) {
1443
+ return columns.map(
1444
+ (column, idx) => typeof column === "string" ? column : getColumnAlignmentForKeyAndIndex(
1445
+ column.key,
1446
+ idx,
1447
+ columns
1448
+ )
1449
+ );
1450
+ }
1451
+ return Object.keys(biggestRow ?? {}).map((_) => "center");
1452
+ }
1453
+
1454
+ // packages/utils/src/lib/reports/formatting.ts
1455
+ import { MarkdownDocument, md as md2 } from "build-md";
1456
+ function tableSection(tableData, options) {
1457
+ if (tableData.rows.length === 0) {
1458
+ return null;
1459
+ }
1460
+ const { level = HIERARCHY.level_4 } = options ?? {};
1461
+ const columns = columnsToStringArray(tableData);
1462
+ const alignments = getColumnAlignments(tableData);
1463
+ const rows = rowToStringArray(tableData);
1464
+ return new MarkdownDocument().heading(level, tableData.title).table(
1465
+ columns.map((heading, i) => {
1466
+ const alignment = alignments[i];
1467
+ if (alignment) {
1468
+ return { heading, alignment };
1469
+ }
1470
+ return heading;
1471
+ }),
1472
+ rows
1473
+ );
1474
+ }
1475
+ function metaDescription(audit) {
1476
+ const docsUrl = audit.docsUrl;
1477
+ const description = audit.description?.trim();
1478
+ if (docsUrl) {
1479
+ const docsLink = md2.link(docsUrl, "\u{1F4D6} Docs");
1480
+ if (!description) {
1481
+ return docsLink;
1482
+ }
1483
+ const parsedDescription = description.endsWith("```") ? `${description}
1484
+
1485
+ ` : `${description} `;
1486
+ return md2`${parsedDescription}${docsLink}`;
1487
+ }
1488
+ if (description && description.trim().length > 0) {
1489
+ return description;
1490
+ }
1491
+ return "";
1492
+ }
1493
+
1494
+ // packages/utils/src/lib/reports/generate-md-report-categoy-section.ts
1495
+ import { MarkdownDocument as MarkdownDocument2, md as md3 } from "build-md";
1496
+
1497
+ // packages/utils/src/lib/reports/sorting.ts
1498
+ function getSortableAuditByRef({ slug, weight, plugin }, plugins) {
1499
+ const auditPlugin = plugins.find((p) => p.slug === plugin);
1500
+ if (!auditPlugin) {
1501
+ throwIsNotPresentError(`Plugin ${plugin}`, "report");
1502
+ }
1503
+ const audit = auditPlugin.audits.find(
1504
+ ({ slug: auditSlug }) => auditSlug === slug
1505
+ );
1506
+ if (!audit) {
1507
+ throwIsNotPresentError(`Audit ${slug}`, auditPlugin.slug);
1508
+ }
1509
+ return {
1510
+ ...audit,
1511
+ weight,
1512
+ plugin
1513
+ };
1514
+ }
1515
+ function getSortedGroupAudits(group, plugin, plugins) {
1516
+ return group.refs.map(
1517
+ (ref) => getSortableAuditByRef(
1518
+ {
1519
+ plugin,
1520
+ slug: ref.slug,
1521
+ weight: ref.weight,
1522
+ type: "audit"
1523
+ },
1524
+ plugins
1525
+ )
1526
+ ).sort(compareCategoryAuditsAndGroups);
1527
+ }
1528
+ function getSortableGroupByRef({ plugin, slug, weight }, plugins) {
1529
+ const groupPlugin = plugins.find((p) => p.slug === plugin);
1530
+ if (!groupPlugin) {
1531
+ throwIsNotPresentError(`Plugin ${plugin}`, "report");
1532
+ }
1533
+ const group = groupPlugin.groups?.find(
1534
+ ({ slug: groupSlug }) => groupSlug === slug
1535
+ );
1536
+ if (!group) {
1537
+ throwIsNotPresentError(`Group ${slug}`, groupPlugin.slug);
1538
+ }
1539
+ const sortedAudits = getSortedGroupAudits(group, groupPlugin.slug, plugins);
1540
+ const sortedAuditRefs = [...group.refs].sort((a, b) => {
1541
+ const aIndex = sortedAudits.findIndex((ref) => ref.slug === a.slug);
1542
+ const bIndex = sortedAudits.findIndex((ref) => ref.slug === b.slug);
1543
+ return aIndex - bIndex;
1544
+ });
1545
+ return {
1546
+ ...group,
1547
+ refs: sortedAuditRefs,
1548
+ plugin,
1549
+ weight
1550
+ };
1551
+ }
1552
+ function sortReport(report) {
1553
+ const { categories, plugins } = report;
1554
+ const sortedCategories = categories.map((category) => {
1555
+ const { audits, groups } = category.refs.reduce(
1556
+ (acc, ref) => ({
1557
+ ...acc,
1558
+ ...ref.type === "group" ? {
1559
+ groups: [...acc.groups, getSortableGroupByRef(ref, plugins)]
1560
+ } : {
1561
+ audits: [...acc.audits, getSortableAuditByRef(ref, plugins)]
1562
+ }
1563
+ }),
1564
+ { groups: [], audits: [] }
1565
+ );
1566
+ const sortedAuditsAndGroups = [...audits, ...groups].sort(
1567
+ compareCategoryAuditsAndGroups
1568
+ );
1569
+ const sortedRefs = [...category.refs].sort((a, b) => {
1570
+ const aIndex = sortedAuditsAndGroups.findIndex(
1571
+ (ref) => ref.slug === a.slug && ref.plugin === a.plugin
1572
+ );
1573
+ const bIndex = sortedAuditsAndGroups.findIndex(
1574
+ (ref) => ref.slug === b.slug && ref.plugin === b.plugin
1575
+ );
1576
+ return aIndex - bIndex;
1577
+ });
1578
+ return { ...category, refs: sortedRefs };
1579
+ });
1580
+ return {
1581
+ ...report,
1582
+ categories: sortedCategories,
1583
+ plugins: sortPlugins(plugins)
1584
+ };
1585
+ }
1586
+ function sortPlugins(plugins) {
1587
+ return plugins.map((plugin) => ({
1588
+ ...plugin,
1589
+ audits: [...plugin.audits].sort(compareAudits).map(
1590
+ (audit) => audit.details?.issues ? {
1591
+ ...audit,
1592
+ details: {
1593
+ ...audit.details,
1594
+ issues: [...audit.details.issues].sort(compareIssues)
1595
+ }
1596
+ } : audit
1597
+ )
1598
+ }));
1716
1599
  }
1717
1600
 
1718
1601
  // packages/utils/src/lib/reports/generate-md-report-categoy-section.ts
1719
- var { link: link4, section: section3, h2: h22, lines: lines3, li: li2, bold: boldMd2, h3: h32, indentation: indentation2 } = md;
1720
1602
  function categoriesOverviewSection(report) {
1721
1603
  const { categories, plugins } = report;
1722
- if (categories.length > 0 && plugins.length > 0) {
1723
- const tableContent = {
1724
- columns: reportOverviewTableHeaders,
1725
- rows: categories.map(({ title, refs, score }) => ({
1726
- // The heading "ID" is inferred from the heading text in Markdown.
1727
- category: link4(`#${slugify(title)}`, title),
1728
- score: `${scoreMarker(score)}${SPACE}${boldMd2(
1729
- formatReportScore(score)
1730
- )}`,
1731
- audits: countCategoryAudits(refs, plugins).toString()
1732
- }))
1733
- };
1734
- return tableSection(tableContent);
1735
- }
1736
- return "";
1604
+ return new MarkdownDocument2().table(
1605
+ [
1606
+ { heading: "\u{1F3F7} Category", alignment: "left" },
1607
+ { heading: "\u2B50 Score", alignment: "center" },
1608
+ { heading: "\u{1F6E1} Audits", alignment: "center" }
1609
+ ],
1610
+ categories.map(({ title, refs, score, isBinary }) => [
1611
+ // @TODO refactor `isBinary: boolean` to `targetScore: number` #713
1612
+ // The heading "ID" is inferred from the heading text in Markdown.
1613
+ md3.link(`#${slugify(title)}`, title),
1614
+ md3`${scoreMarker(score)} ${md3.bold(
1615
+ formatReportScore(score)
1616
+ )}${binaryIconSuffix(score, isBinary)}`,
1617
+ countCategoryAudits(refs, plugins).toString()
1618
+ ])
1619
+ );
1737
1620
  }
1738
1621
  function categoriesDetailsSection(report) {
1739
1622
  const { categories, plugins } = report;
1740
- const categoryDetails = categories.flatMap((category) => {
1741
- const categoryTitle = h32(category.title);
1742
- const categoryScore = `${scoreMarker(
1743
- category.score
1744
- )}${SPACE}Score: ${boldMd2(formatReportScore(category.score))}`;
1745
- const categoryMDItems = category.refs.map((ref) => {
1746
- if (ref.type === "group") {
1747
- const group = getSortableGroupByRef(ref, plugins);
1748
- const groupAudits = group.refs.map(
1749
- (groupRef) => getSortableAuditByRef(
1750
- { ...groupRef, plugin: group.plugin, type: "audit" },
1751
- plugins
1752
- )
1753
- );
1754
- const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
1755
- return categoryGroupItem(group, groupAudits, pluginTitle);
1756
- } else {
1757
- const audit = getSortableAuditByRef(ref, plugins);
1758
- const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
1759
- return categoryRef(audit, pluginTitle);
1760
- }
1761
- });
1762
- return section3(
1763
- categoryTitle,
1764
- metaDescription(category),
1765
- categoryScore,
1766
- ...categoryMDItems
1767
- );
1768
- });
1769
- return lines3(h22(CATEGORIES_TITLE), ...categoryDetails);
1623
+ return new MarkdownDocument2().heading(HIERARCHY.level_2, "\u{1F3F7} Categories").$foreach(
1624
+ categories,
1625
+ (doc, category) => doc.heading(HIERARCHY.level_3, category.title).paragraph(metaDescription(category)).paragraph(
1626
+ md3`${scoreMarker(category.score)} Score: ${md3.bold(
1627
+ formatReportScore(category.score)
1628
+ )}${binaryIconSuffix(category.score, category.isBinary)}`
1629
+ ).list(
1630
+ category.refs.map((ref) => {
1631
+ if (ref.type === "group") {
1632
+ const group = getSortableGroupByRef(ref, plugins);
1633
+ const groupAudits = group.refs.map(
1634
+ (groupRef) => getSortableAuditByRef(
1635
+ { ...groupRef, plugin: group.plugin, type: "audit" },
1636
+ plugins
1637
+ )
1638
+ );
1639
+ const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
1640
+ return categoryGroupItem(group, groupAudits, pluginTitle);
1641
+ } else {
1642
+ const audit = getSortableAuditByRef(ref, plugins);
1643
+ const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
1644
+ return categoryRef(audit, pluginTitle);
1645
+ }
1646
+ })
1647
+ )
1648
+ );
1770
1649
  }
1771
1650
  function categoryRef({ title, score, value, displayValue }, pluginTitle) {
1772
- const auditTitleAsLink = link4(
1651
+ const auditTitleAsLink = md3.link(
1773
1652
  `#${slugify(title)}-${slugify(pluginTitle)}`,
1774
1653
  title
1775
1654
  );
1776
1655
  const marker = scoreMarker(score, "square");
1777
- return li2(
1778
- `${marker}${SPACE}${auditTitleAsLink}${SPACE}(_${pluginTitle}_) - ${boldMd2(
1779
- (displayValue || value).toString()
1780
- )}`
1781
- );
1656
+ return md3`${marker} ${auditTitleAsLink} (${md3.italic(
1657
+ pluginTitle
1658
+ )}) - ${md3.bold((displayValue || value).toString())}`;
1782
1659
  }
1783
1660
  function categoryGroupItem({ score = 0, title }, groupAudits, pluginTitle) {
1784
- const groupTitle = li2(
1785
- `${scoreMarker(score)}${SPACE}${title}${SPACE}(_${pluginTitle}_)`
1786
- );
1787
- const auditTitles = groupAudits.map(
1788
- ({ title: auditTitle, score: auditScore, value, displayValue }) => {
1789
- const auditTitleLink = link4(
1790
- `#${slugify(auditTitle)}-${slugify(pluginTitle)}`,
1791
- auditTitle
1792
- );
1793
- const marker = scoreMarker(auditScore, "square");
1794
- return indentation2(
1795
- li2(
1796
- `${marker}${SPACE}${auditTitleLink} - ${boldMd2(
1797
- String(displayValue ?? value)
1798
- )}`
1799
- )
1800
- );
1801
- }
1661
+ const groupTitle = md3`${scoreMarker(score)} ${title} (${md3.italic(
1662
+ pluginTitle
1663
+ )})`;
1664
+ const auditsList = md3.list(
1665
+ groupAudits.map(
1666
+ ({ title: auditTitle, score: auditScore, value, displayValue }) => {
1667
+ const auditTitleLink = md3.link(
1668
+ `#${slugify(auditTitle)}-${slugify(pluginTitle)}`,
1669
+ auditTitle
1670
+ );
1671
+ const marker = scoreMarker(auditScore, "square");
1672
+ return md3`${marker} ${auditTitleLink} - ${md3.bold(
1673
+ String(displayValue ?? value)
1674
+ )}`;
1675
+ }
1676
+ )
1802
1677
  );
1803
- return lines3(groupTitle, ...auditTitles);
1678
+ return md3`${groupTitle}${auditsList}`;
1679
+ }
1680
+ function binaryIconSuffix(score, isBinary) {
1681
+ return targetScoreIcon(score, isBinary ? 1 : void 0, { prefix: " " });
1804
1682
  }
1805
1683
 
1806
1684
  // packages/utils/src/lib/reports/generate-md-report.ts
1807
- var { h1: h12, h2: h23, h3: h33, lines: lines4, link: link5, section: section4, code: codeMd } = md;
1808
- var { bold: boldHtml, details: details2 } = html;
1809
1685
  function auditDetailsAuditValue({
1810
1686
  score,
1811
1687
  value,
1812
1688
  displayValue
1813
1689
  }) {
1814
- return `${scoreMarker(score, "square")} ${boldHtml(
1690
+ return md4`${scoreMarker(score, "square")} ${md4.bold(
1815
1691
  String(displayValue ?? value)
1816
1692
  )} (score: ${formatReportScore(score)})`;
1817
1693
  }
1818
1694
  function generateMdReport(report) {
1819
- const printCategories = report.categories.length > 0;
1820
- return lines4(
1821
- h12(reportHeadlineText),
1822
- printCategories ? categoriesOverviewSection(report) : "",
1823
- printCategories ? categoriesDetailsSection(report) : "",
1824
- auditsSection(report),
1825
- aboutSection(report),
1826
- `${FOOTER_PREFIX}${SPACE}${link5(README_LINK, "Code PushUp")}`
1827
- );
1695
+ return new MarkdownDocument3().heading(HIERARCHY.level_1, REPORT_HEADLINE_TEXT).$if(
1696
+ report.categories.length > 0,
1697
+ (doc) => doc.$concat(
1698
+ categoriesOverviewSection(report),
1699
+ categoriesDetailsSection(report)
1700
+ )
1701
+ ).$concat(auditsSection(report), aboutSection(report)).rule().paragraph(md4`${FOOTER_PREFIX} ${md4.link(README_LINK, "Code PushUp")}`).toString();
1828
1702
  }
1829
1703
  function auditDetailsIssues(issues = []) {
1830
1704
  if (issues.length === 0) {
1831
- return "";
1832
- }
1833
- const detailsTableData = {
1834
- title: "Issues",
1835
- columns: issuesTableHeadings,
1836
- rows: issues.map(
1837
- ({ severity: severityVal, message, source: sourceVal }) => {
1838
- const severity = `${severityMarker(severityVal)} <i>${severityVal}</i>`;
1839
- if (!sourceVal) {
1840
- return { severity, message, file: "", line: "" };
1841
- }
1842
- const file = `<code>${sourceVal.file}</code>`;
1843
- if (!sourceVal.position) {
1844
- return { severity, message, file, line: "" };
1845
- }
1846
- const { startLine, endLine } = sourceVal.position;
1847
- const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
1848
- return { severity, message, file, line };
1705
+ return null;
1706
+ }
1707
+ return new MarkdownDocument3().heading(HIERARCHY.level_4, "Issues").table(
1708
+ [
1709
+ { heading: "Severity", alignment: "center" },
1710
+ { heading: "Message", alignment: "left" },
1711
+ { heading: "Source file", alignment: "left" },
1712
+ { heading: "Line(s)", alignment: "center" }
1713
+ ],
1714
+ issues.map(({ severity: level, message, source }) => {
1715
+ const severity = md4`${severityMarker(level)} ${md4.italic(level)}`;
1716
+ if (!source) {
1717
+ return [severity, message];
1849
1718
  }
1850
- )
1851
- };
1852
- return tableSection(detailsTableData);
1719
+ const file = md4.code(source.file);
1720
+ if (!source.position) {
1721
+ return [severity, message, file];
1722
+ }
1723
+ const { startLine, endLine } = source.position;
1724
+ const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
1725
+ return [severity, message, file, line];
1726
+ })
1727
+ );
1853
1728
  }
1854
1729
  function auditDetails(audit) {
1855
- const { table: table5, issues = [] } = audit.details ?? {};
1730
+ const { table: table2, issues = [] } = audit.details ?? {};
1856
1731
  const detailsValue = auditDetailsAuditValue(audit);
1857
- if (issues.length === 0 && table5 == null) {
1858
- return section4(detailsValue);
1732
+ if (issues.length === 0 && !table2?.rows.length) {
1733
+ return new MarkdownDocument3().paragraph(detailsValue);
1859
1734
  }
1860
- const tableSectionContent = table5 == null ? "" : tableSection(table5);
1861
- const issuesSectionContent = issues.length > 0 ? auditDetailsIssues(issues) : "";
1862
- return details2(
1735
+ const tableSectionContent = table2 && tableSection(table2);
1736
+ const issuesSectionContent = issues.length > 0 && auditDetailsIssues(issues);
1737
+ return new MarkdownDocument3().details(
1863
1738
  detailsValue,
1864
- lines4(tableSectionContent, issuesSectionContent)
1739
+ new MarkdownDocument3().$concat(tableSectionContent, issuesSectionContent)
1865
1740
  );
1866
1741
  }
1867
1742
  function auditsSection({
1868
1743
  plugins
1869
1744
  }) {
1870
- const content = plugins.flatMap(
1871
- ({ slug, audits }) => audits.flatMap((audit) => {
1872
- const auditTitle = `${audit.title}${SPACE}(${getPluginNameFromSlug(
1873
- slug,
1874
- plugins
1875
- )})`;
1745
+ return new MarkdownDocument3().heading(HIERARCHY.level_2, "\u{1F6E1}\uFE0F Audits").$foreach(
1746
+ plugins.flatMap(
1747
+ (plugin) => plugin.audits.map((audit) => ({ ...audit, plugin }))
1748
+ ),
1749
+ (doc, { plugin, ...audit }) => {
1750
+ const auditTitle = `${audit.title} (${plugin.title})`;
1876
1751
  const detailsContent = auditDetails(audit);
1877
1752
  const descriptionContent = metaDescription(audit);
1878
- return [h33(auditTitle), detailsContent, descriptionContent];
1879
- })
1753
+ return doc.heading(HIERARCHY.level_3, auditTitle).$concat(detailsContent).paragraph(descriptionContent);
1754
+ }
1880
1755
  );
1881
- return section4(h23("\u{1F6E1}\uFE0F Audits"), ...content);
1882
1756
  }
1883
1757
  function aboutSection(report) {
1884
1758
  const { date, plugins } = report;
1885
- const reportMetaTable = reportMetaData(report);
1886
- const pluginMetaTable = reportPluginMeta({ plugins });
1887
- return lines4(
1888
- h23("About"),
1889
- section4(
1890
- `Report was created by [Code PushUp](${README_LINK}) on ${formatDate(
1891
- new Date(date)
1892
- )}.`
1893
- ),
1894
- tableSection(pluginMetaTable),
1895
- tableSection(reportMetaTable)
1896
- );
1897
- }
1898
- function reportPluginMeta({ plugins }) {
1899
- return {
1900
- columns: [
1901
- {
1902
- key: "plugin",
1903
- align: "left"
1904
- },
1905
- {
1906
- key: "audits"
1907
- },
1908
- {
1909
- key: "version"
1910
- },
1911
- {
1912
- key: "duration"
1913
- }
1759
+ return new MarkdownDocument3().heading(HIERARCHY.level_2, "About").paragraph(
1760
+ md4`Report was created by ${md4.link(
1761
+ README_LINK,
1762
+ "Code PushUp"
1763
+ )} on ${formatDate(new Date(date))}.`
1764
+ ).table(...pluginMetaTable({ plugins })).table(...reportMetaTable(report));
1765
+ }
1766
+ function pluginMetaTable({
1767
+ plugins
1768
+ }) {
1769
+ return [
1770
+ [
1771
+ { heading: "Plugin", alignment: "left" },
1772
+ { heading: "Audits", alignment: "center" },
1773
+ { heading: "Version", alignment: "center" },
1774
+ { heading: "Duration", alignment: "right" }
1914
1775
  ],
1915
- rows: plugins.map(
1916
- ({
1917
- title: pluginTitle,
1918
- audits,
1919
- version: pluginVersion,
1920
- duration: pluginDuration
1921
- }) => ({
1922
- plugin: pluginTitle,
1923
- audits: audits.length.toString(),
1924
- version: codeMd(pluginVersion || ""),
1925
- duration: formatDuration(pluginDuration)
1926
- })
1927
- )
1928
- };
1776
+ plugins.map(({ title, audits, version: version2 = "", duration }) => [
1777
+ title,
1778
+ audits.length.toString(),
1779
+ version2 && md4.code(version2),
1780
+ formatDuration(duration)
1781
+ ])
1782
+ ];
1929
1783
  }
1930
- function reportMetaData({
1784
+ function reportMetaTable({
1931
1785
  commit,
1932
1786
  version: version2,
1933
1787
  duration,
1934
1788
  plugins,
1935
1789
  categories
1936
1790
  }) {
1937
- const commitInfo = commit ? `${commit.message}${SPACE}(${commit.hash})` : "N/A";
1938
- return {
1939
- columns: [
1940
- {
1941
- key: "commit",
1942
- align: "left"
1943
- },
1944
- {
1945
- key: "version"
1946
- },
1947
- {
1948
- key: "duration"
1949
- },
1950
- {
1951
- key: "plugins"
1952
- },
1953
- {
1954
- key: "categories"
1955
- },
1956
- {
1957
- key: "audits"
1958
- }
1791
+ return [
1792
+ [
1793
+ { heading: "Commit", alignment: "left" },
1794
+ { heading: "Version", alignment: "center" },
1795
+ { heading: "Duration", alignment: "right" },
1796
+ { heading: "Plugins", alignment: "center" },
1797
+ { heading: "Categories", alignment: "center" },
1798
+ { heading: "Audits", alignment: "center" }
1959
1799
  ],
1960
- rows: [
1961
- {
1962
- commit: commitInfo,
1963
- version: codeMd(version2 || ""),
1964
- duration: formatDuration(duration),
1965
- plugins: plugins.length,
1966
- categories: categories.length,
1967
- audits: plugins.reduce((acc, { audits }) => acc + audits.length, 0).toString()
1968
- }
1800
+ [
1801
+ [
1802
+ commit ? `${commit.message} (${commit.hash})` : "N/A",
1803
+ md4.code(version2),
1804
+ formatDuration(duration),
1805
+ plugins.length.toString(),
1806
+ categories.length.toString(),
1807
+ plugins.reduce((acc, { audits }) => acc + audits.length, 0).toString()
1808
+ ]
1969
1809
  ]
1970
- };
1810
+ ];
1971
1811
  }
1972
1812
 
1973
1813
  // packages/utils/src/lib/reports/generate-md-reports-diff.ts
1974
- var {
1975
- h1: h13,
1976
- h2: h24,
1977
- lines: lines5,
1978
- link: link6,
1979
- bold: boldMd3,
1980
- italic: italicMd,
1981
- table: table4,
1982
- section: section5
1983
- } = md;
1984
- var { details: details3 } = html;
1814
+ import {
1815
+ MarkdownDocument as MarkdownDocument4,
1816
+ md as md5
1817
+ } from "build-md";
1985
1818
  var MAX_ROWS = 100;
1986
- function generateMdReportsDiff(diff) {
1987
- return lines5(
1988
- section5(formatDiffHeaderSection(diff)),
1989
- formatDiffCategoriesSection(diff),
1990
- formatDiffGroupsSection(diff),
1991
- formatDiffAuditsSection(diff)
1992
- );
1993
- }
1994
- function formatDiffHeaderSection(diff) {
1819
+ function generateMdReportsDiff(diff, portalUrl) {
1820
+ return new MarkdownDocument4().$concat(
1821
+ createDiffHeaderSection(diff, portalUrl),
1822
+ createDiffCategoriesSection(diff),
1823
+ createDiffGroupsSection(diff),
1824
+ createDiffAuditsSection(diff)
1825
+ ).toString();
1826
+ }
1827
+ function createDiffHeaderSection(diff, portalUrl) {
1995
1828
  const outcomeTexts = {
1996
- positive: `\u{1F973} Code PushUp report has ${boldMd3("improved")}`,
1997
- negative: `\u{1F61F} Code PushUp report has ${boldMd3("regressed")}`,
1998
- mixed: `\u{1F928} Code PushUp report has both ${boldMd3(
1829
+ positive: md5`🥳 Code PushUp report has ${md5.bold("improved")}`,
1830
+ negative: md5`😟 Code PushUp report has ${md5.bold("regressed")}`,
1831
+ mixed: md5`🤨 Code PushUp report has both ${md5.bold(
1999
1832
  "improvements and regressions"
2000
1833
  )}`,
2001
- unchanged: `\u{1F610} Code PushUp report is ${boldMd3("unchanged")}`
1834
+ unchanged: md5`😐 Code PushUp report is ${md5.bold("unchanged")}`
2002
1835
  };
2003
1836
  const outcome = mergeDiffOutcomes(
2004
1837
  changesToDiffOutcomes([
@@ -2008,143 +1841,127 @@ function formatDiffHeaderSection(diff) {
2008
1841
  ])
2009
1842
  );
2010
1843
  const styleCommits = (commits) => `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
2011
- return lines5(
2012
- h13("Code PushUp"),
2013
- diff.commits ? `${outcomeTexts[outcome]} \u2013 ${styleCommits(diff.commits)}.` : `${outcomeTexts[outcome]}.`
1844
+ return new MarkdownDocument4().heading(HIERARCHY.level_1, "Code PushUp").paragraph(
1845
+ diff.commits ? md5`${outcomeTexts[outcome]} – ${styleCommits(diff.commits)}.` : outcomeTexts[outcome]
1846
+ ).paragraph(
1847
+ portalUrl && md5.link(portalUrl, "\u{1F575}\uFE0F See full comparison in Code PushUp portal \u{1F50D}")
2014
1848
  );
2015
1849
  }
2016
- function formatDiffCategoriesSection(diff) {
1850
+ function createDiffCategoriesSection(diff) {
2017
1851
  const { changed, unchanged, added } = diff.categories;
2018
1852
  const categoriesCount = changed.length + unchanged.length + added.length;
2019
1853
  const hasChanges = unchanged.length < categoriesCount;
2020
1854
  if (categoriesCount === 0) {
2021
- return "";
1855
+ return null;
2022
1856
  }
2023
1857
  const columns = [
2024
- { key: "category", label: "\u{1F3F7}\uFE0F Category", align: "left" },
2025
- { key: "before", label: hasChanges ? "\u2B50 Previous score" : "\u2B50 Score" },
2026
- { key: "after", label: "\u2B50 Current score" },
2027
- { key: "change", label: "\u{1F504} Score change" }
1858
+ { heading: "\u{1F3F7}\uFE0F Category", alignment: "left" },
1859
+ {
1860
+ heading: hasChanges ? "\u2B50 Previous score" : "\u2B50 Score",
1861
+ alignment: "center"
1862
+ },
1863
+ { heading: "\u2B50 Current score", alignment: "center" },
1864
+ { heading: "\u{1F504} Score change", alignment: "center" }
2028
1865
  ];
2029
- return lines5(
2030
- h24("\u{1F3F7}\uFE0F Categories"),
2031
- categoriesCount > 0 && table4({
2032
- columns: hasChanges ? columns : columns.slice(0, 2),
2033
- rows: [
2034
- ...sortChanges(changed).map((category) => ({
2035
- category: formatTitle(category),
2036
- after: formatScoreWithColor(category.scores.after),
2037
- before: formatScoreWithColor(category.scores.before, {
2038
- skipBold: true
2039
- }),
2040
- change: formatScoreChange(category.scores.diff)
2041
- })),
2042
- ...added.map((category) => ({
2043
- category: formatTitle(category),
2044
- after: formatScoreWithColor(category.score),
2045
- before: italicMd("n/a (\\*)"),
2046
- change: italicMd("n/a (\\*)")
2047
- })),
2048
- ...unchanged.map((category) => ({
2049
- category: formatTitle(category),
2050
- after: formatScoreWithColor(category.score),
2051
- before: formatScoreWithColor(category.score, { skipBold: true }),
2052
- change: "\u2013"
2053
- }))
2054
- ].map(
2055
- (row) => hasChanges ? row : { category: row.category, before: row.before }
2056
- )
2057
- }),
2058
- added.length > 0 && section5(italicMd("(\\*) New category."))
2059
- );
1866
+ const rows = [
1867
+ ...sortChanges(changed).map((category) => [
1868
+ formatTitle(category),
1869
+ formatScoreWithColor(category.scores.before, {
1870
+ skipBold: true
1871
+ }),
1872
+ formatScoreWithColor(category.scores.after),
1873
+ formatScoreChange(category.scores.diff)
1874
+ ]),
1875
+ ...added.map((category) => [
1876
+ formatTitle(category),
1877
+ md5.italic("n/a (\\*)"),
1878
+ formatScoreWithColor(category.score),
1879
+ md5.italic("n/a (\\*)")
1880
+ ]),
1881
+ ...unchanged.map((category) => [
1882
+ formatTitle(category),
1883
+ formatScoreWithColor(category.score, { skipBold: true }),
1884
+ formatScoreWithColor(category.score),
1885
+ "\u2013"
1886
+ ])
1887
+ ];
1888
+ return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F3F7}\uFE0F Categories").table(
1889
+ hasChanges ? columns : columns.slice(0, 2),
1890
+ rows.map((row) => hasChanges ? row : row.slice(0, 2))
1891
+ ).paragraph(added.length > 0 && md5.italic("(\\*) New category."));
2060
1892
  }
2061
- function formatDiffGroupsSection(diff) {
1893
+ function createDiffGroupsSection(diff) {
2062
1894
  if (diff.groups.changed.length + diff.groups.unchanged.length === 0) {
2063
- return "";
2064
- }
2065
- return lines5(
2066
- h24("\u{1F5C3}\uFE0F Groups"),
2067
- formatGroupsOrAuditsDetails("group", diff.groups, {
2068
- columns: [
2069
- { key: "plugin", label: "\u{1F50C} Plugin", align: "left" },
2070
- { key: "group", label: "\u{1F5C3}\uFE0F Group", align: "left" },
2071
- { key: "before", label: "\u2B50 Previous score" },
2072
- { key: "after", label: "\u2B50 Current score" },
2073
- { key: "change", label: "\u{1F504} Score change" }
1895
+ return null;
1896
+ }
1897
+ return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F5C3}\uFE0F Groups").$concat(
1898
+ createGroupsOrAuditsDetails(
1899
+ "group",
1900
+ diff.groups,
1901
+ [
1902
+ { heading: "\u{1F50C} Plugin", alignment: "left" },
1903
+ { heading: "\u{1F5C3}\uFE0F Group", alignment: "left" },
1904
+ { heading: "\u2B50 Previous score", alignment: "center" },
1905
+ { heading: "\u2B50 Current score", alignment: "center" },
1906
+ { heading: "\u{1F504} Score change", alignment: "center" }
2074
1907
  ],
2075
- rows: sortChanges(diff.groups.changed).map((group) => ({
2076
- plugin: formatTitle(group.plugin),
2077
- group: formatTitle(group),
2078
- after: formatScoreWithColor(group.scores.after),
2079
- before: formatScoreWithColor(group.scores.before, { skipBold: true }),
2080
- change: formatScoreChange(group.scores.diff)
2081
- }))
2082
- })
1908
+ sortChanges(diff.groups.changed).map((group) => [
1909
+ formatTitle(group.plugin),
1910
+ formatTitle(group),
1911
+ formatScoreWithColor(group.scores.before, { skipBold: true }),
1912
+ formatScoreWithColor(group.scores.after),
1913
+ formatScoreChange(group.scores.diff)
1914
+ ])
1915
+ )
2083
1916
  );
2084
1917
  }
2085
- function formatDiffAuditsSection(diff) {
2086
- return lines5(
2087
- h24("\u{1F6E1}\uFE0F Audits"),
2088
- formatGroupsOrAuditsDetails("audit", diff.audits, {
2089
- columns: [
2090
- { key: "plugin", label: "\u{1F50C} Plugin", align: "left" },
2091
- { key: "audit", label: "\u{1F6E1}\uFE0F Audit", align: "left" },
2092
- { key: "before", label: "\u{1F4CF} Previous value" },
2093
- { key: "after", label: "\u{1F4CF} Current value" },
2094
- { key: "change", label: "\u{1F504} Value change" }
1918
+ function createDiffAuditsSection(diff) {
1919
+ return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F6E1}\uFE0F Audits").$concat(
1920
+ createGroupsOrAuditsDetails(
1921
+ "audit",
1922
+ diff.audits,
1923
+ [
1924
+ { heading: "\u{1F50C} Plugin", alignment: "left" },
1925
+ { heading: "\u{1F6E1}\uFE0F Audit", alignment: "left" },
1926
+ { heading: "\u{1F4CF} Previous value", alignment: "center" },
1927
+ { heading: "\u{1F4CF} Current value", alignment: "center" },
1928
+ { heading: "\u{1F504} Value change", alignment: "center" }
2095
1929
  ],
2096
- rows: sortChanges(diff.audits.changed).map((audit) => ({
2097
- plugin: formatTitle(audit.plugin),
2098
- audit: formatTitle(audit),
2099
- after: `${scoreMarker(audit.scores.after, "square")} ${boldMd3(
1930
+ sortChanges(diff.audits.changed).map((audit) => [
1931
+ formatTitle(audit.plugin),
1932
+ formatTitle(audit),
1933
+ `${scoreMarker(audit.scores.before, "square")} ${audit.displayValues.before || audit.values.before.toString()}`,
1934
+ md5`${scoreMarker(audit.scores.after, "square")} ${md5.bold(
2100
1935
  audit.displayValues.after || audit.values.after.toString()
2101
1936
  )}`,
2102
- before: `${scoreMarker(audit.scores.before, "square")} ${audit.displayValues.before || audit.values.before.toString()}`,
2103
- change: formatValueChange(audit)
2104
- }))
2105
- })
1937
+ formatValueChange(audit)
1938
+ ])
1939
+ )
2106
1940
  );
2107
1941
  }
2108
- function formatGroupsOrAuditsDetails(token, { changed, unchanged }, tableData) {
2109
- return changed.length === 0 ? summarizeUnchanged(token, { changed, unchanged }) : details3(
1942
+ function createGroupsOrAuditsDetails(token, { changed, unchanged }, ...[columns, rows]) {
1943
+ if (changed.length === 0) {
1944
+ return new MarkdownDocument4().paragraph(
1945
+ summarizeUnchanged(token, { changed, unchanged })
1946
+ );
1947
+ }
1948
+ return new MarkdownDocument4().details(
2110
1949
  summarizeDiffOutcomes(changesToDiffOutcomes(changed), token),
2111
- lines5(
2112
- table4({
2113
- ...tableData,
2114
- rows: tableData.rows.slice(0, MAX_ROWS)
2115
- // use never to avoid typing problem
2116
- }),
2117
- changed.length > MAX_ROWS && italicMd(
1950
+ md5`${md5.table(columns, rows.slice(0, MAX_ROWS))}${changed.length > MAX_ROWS ? md5.paragraph(
1951
+ md5.italic(
2118
1952
  `Only the ${MAX_ROWS} most affected ${pluralize(
2119
1953
  token
2120
1954
  )} are listed above for brevity.`
2121
- ),
2122
- unchanged.length > 0 && summarizeUnchanged(token, { changed, unchanged })
2123
- )
1955
+ )
1956
+ ) : ""}${unchanged.length > 0 ? md5.paragraph(summarizeUnchanged(token, { changed, unchanged })) : ""}`
2124
1957
  );
2125
1958
  }
2126
- function formatScoreChange(diff) {
2127
- const marker = getDiffMarker(diff);
2128
- const text = formatDiffNumber(Math.round(diff * 1e3) / 10);
2129
- return colorByScoreDiff(`${marker} ${text}`, diff);
2130
- }
2131
- function formatValueChange({
2132
- values,
2133
- scores
2134
- }) {
2135
- const marker = getDiffMarker(values.diff);
2136
- const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : Math.round(100 * values.diff / values.before);
2137
- const text = `${formatDiffNumber(percentage)}\u2009%`;
2138
- return colorByScoreDiff(`${marker} ${text}`, scores.diff);
2139
- }
2140
1959
  function summarizeUnchanged(token, { changed, unchanged }) {
2141
- return section5(
2142
- [
2143
- changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`,
2144
- unchanged.length === 1 ? "is" : "are",
2145
- "unchanged."
2146
- ].join(" ")
2147
- );
1960
+ return [
1961
+ changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`,
1962
+ unchanged.length === 1 ? "is" : "are",
1963
+ "unchanged."
1964
+ ].join(" ");
2148
1965
  }
2149
1966
  function summarizeDiffOutcomes(outcomes, token) {
2150
1967
  return objectToEntries(countDiffOutcomes(outcomes)).filter(
@@ -2169,7 +1986,7 @@ function formatTitle({
2169
1986
  docsUrl
2170
1987
  }) {
2171
1988
  if (docsUrl) {
2172
- return link6(docsUrl, title);
1989
+ return md5.link(docsUrl, title);
2173
1990
  }
2174
1991
  return title;
2175
1992
  }
@@ -2213,8 +2030,22 @@ function countDiffOutcomes(outcomes) {
2213
2030
  };
2214
2031
  }
2215
2032
 
2033
+ // packages/utils/src/lib/reports/load-report.ts
2034
+ import { join as join2 } from "node:path";
2035
+ async function loadReport(options) {
2036
+ const { outputDir, filename, format } = options;
2037
+ await ensureDirectoryExists(outputDir);
2038
+ const filePath = join2(outputDir, `${filename}.${format}`);
2039
+ if (format === "json") {
2040
+ const content = await readJsonFile(filePath);
2041
+ return reportSchema.parse(content);
2042
+ }
2043
+ const text = await readTextFile(filePath);
2044
+ return text;
2045
+ }
2046
+
2216
2047
  // packages/utils/src/lib/reports/log-stdout-summary.ts
2217
- import chalk4 from "chalk";
2048
+ import { bold as bold4, cyan, cyanBright, green as green2, red } from "ansis";
2218
2049
  function log(msg = "") {
2219
2050
  ui().logger.log(msg);
2220
2051
  }
@@ -2231,14 +2062,14 @@ function logStdoutSummary(report) {
2231
2062
  }
2232
2063
  function reportToHeaderSection(report) {
2233
2064
  const { packageName, version: version2 } = report;
2234
- return `${chalk4.bold(reportHeadlineText)} - ${packageName}@${version2}`;
2065
+ return `${bold4(REPORT_HEADLINE_TEXT)} - ${packageName}@${version2}`;
2235
2066
  }
2236
2067
  function logPlugins(report) {
2237
2068
  const { plugins } = report;
2238
2069
  plugins.forEach((plugin) => {
2239
2070
  const { title, audits } = plugin;
2240
2071
  log();
2241
- log(chalk4.magentaBright.bold(`${title} audits`));
2072
+ log(bold4.magentaBright(`${title} audits`));
2242
2073
  log();
2243
2074
  audits.forEach((audit) => {
2244
2075
  ui().row([
@@ -2253,7 +2084,7 @@ function logPlugins(report) {
2253
2084
  padding: [0, 3, 0, 0]
2254
2085
  },
2255
2086
  {
2256
- text: chalk4.cyanBright(audit.displayValue || `${audit.value}`),
2087
+ text: cyanBright(audit.displayValue || `${audit.value}`),
2257
2088
  width: 10,
2258
2089
  padding: [0, 0, 0, 0]
2259
2090
  }
@@ -2264,42 +2095,38 @@ function logPlugins(report) {
2264
2095
  }
2265
2096
  function logCategories({ categories, plugins }) {
2266
2097
  const hAlign = (idx) => idx === 0 ? "left" : "right";
2267
- const rows = categories.map(({ title, score, refs }) => [
2098
+ const rows = categories.map(({ title, score, refs, isBinary }) => [
2268
2099
  title,
2269
- applyScoreColor({ score }),
2100
+ `${binaryIconPrefix(score, isBinary)}${applyScoreColor({ score })}`,
2270
2101
  countCategoryAudits(refs, plugins)
2271
2102
  ]);
2272
- const table5 = ui().table();
2273
- table5.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
2274
- table5.head(
2275
- reportRawOverviewTableHeaders.map((heading, idx) => ({
2276
- content: chalk4.cyan(heading),
2103
+ const table2 = ui().table();
2104
+ table2.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
2105
+ table2.head(
2106
+ REPORT_RAW_OVERVIEW_TABLE_HEADERS.map((heading, idx) => ({
2107
+ content: cyan(heading),
2277
2108
  hAlign: hAlign(idx)
2278
2109
  }))
2279
2110
  );
2280
2111
  rows.forEach(
2281
- (row) => table5.row(
2112
+ (row) => table2.row(
2282
2113
  row.map((content, idx) => ({
2283
2114
  content: content.toString(),
2284
2115
  hAlign: hAlign(idx)
2285
2116
  }))
2286
2117
  )
2287
2118
  );
2288
- log(chalk4.magentaBright.bold("Categories"));
2119
+ log(bold4.magentaBright("Categories"));
2289
2120
  log();
2290
- table5.render();
2121
+ table2.render();
2291
2122
  log();
2292
2123
  }
2293
- function applyScoreColor({ score, text }) {
2294
- const formattedScore = text ?? formatReportScore(score);
2295
- const style = text ? chalk4 : chalk4.bold;
2296
- if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
2297
- return style.green(formattedScore);
2298
- }
2299
- if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
2300
- return style.yellow(formattedScore);
2301
- }
2302
- return style.red(formattedScore);
2124
+ function binaryIconPrefix(score, isBinary) {
2125
+ return targetScoreIcon(score, isBinary ? 1 : void 0, {
2126
+ passIcon: bold4(green2("\u2713")),
2127
+ failIcon: bold4(red("\u2717")),
2128
+ postfix: " "
2129
+ });
2303
2130
  }
2304
2131
 
2305
2132
  // packages/utils/src/lib/reports/scoring.ts
@@ -2389,56 +2216,6 @@ function parseScoringParameters(refs, scoreFn) {
2389
2216
  return scoredRefs;
2390
2217
  }
2391
2218
 
2392
- // packages/utils/src/lib/reports/sorting.ts
2393
- function sortReport(report) {
2394
- const { categories, plugins } = report;
2395
- const sortedCategories = categories.map((category) => {
2396
- const { audits, groups } = category.refs.reduce(
2397
- (acc, ref) => ({
2398
- ...acc,
2399
- ...ref.type === "group" ? {
2400
- groups: [...acc.groups, getSortableGroupByRef(ref, plugins)]
2401
- } : {
2402
- audits: [...acc.audits, getSortableAuditByRef(ref, plugins)]
2403
- }
2404
- }),
2405
- { groups: [], audits: [] }
2406
- );
2407
- const sortedAuditsAndGroups = [...audits, ...groups].sort(
2408
- compareCategoryAuditsAndGroups
2409
- );
2410
- const sortedRefs = [...category.refs].sort((a, b) => {
2411
- const aIndex = sortedAuditsAndGroups.findIndex(
2412
- (ref) => ref.slug === a.slug && ref.plugin === a.plugin
2413
- );
2414
- const bIndex = sortedAuditsAndGroups.findIndex(
2415
- (ref) => ref.slug === b.slug && ref.plugin === b.plugin
2416
- );
2417
- return aIndex - bIndex;
2418
- });
2419
- return { ...category, refs: sortedRefs };
2420
- });
2421
- return {
2422
- ...report,
2423
- categories: sortedCategories,
2424
- plugins: sortPlugins(plugins)
2425
- };
2426
- }
2427
- function sortPlugins(plugins) {
2428
- return plugins.map((plugin) => ({
2429
- ...plugin,
2430
- audits: [...plugin.audits].sort(compareAudits).map(
2431
- (audit) => audit.details?.issues ? {
2432
- ...audit,
2433
- details: {
2434
- ...audit.details,
2435
- issues: [...audit.details.issues].sort(compareIssues)
2436
- }
2437
- } : audit
2438
- )
2439
- }));
2440
- }
2441
-
2442
2219
  // packages/utils/src/lib/verbose-utils.ts
2443
2220
  function getLogVerbose(verbose = false) {
2444
2221
  return (msg) => {
@@ -2461,10 +2238,10 @@ var verboseUtils = (verbose = false) => ({
2461
2238
 
2462
2239
  // packages/core/package.json
2463
2240
  var name = "@code-pushup/core";
2464
- var version = "0.48.0";
2241
+ var version = "0.49.0";
2465
2242
 
2466
2243
  // packages/core/src/lib/implementation/execute-plugin.ts
2467
- import chalk5 from "chalk";
2244
+ import { bold as bold5 } from "ansis";
2468
2245
 
2469
2246
  // packages/core/src/lib/normalize.ts
2470
2247
  function normalizeIssue(issue, gitRoot) {
@@ -2527,7 +2304,7 @@ async function executeRunnerFunction(runner, onProgress) {
2527
2304
  var PluginOutputMissingAuditError = class extends Error {
2528
2305
  constructor(auditSlug) {
2529
2306
  super(
2530
- `Audit metadata not present in plugin config. Missing slug: ${chalk5.bold(
2307
+ `Audit metadata not present in plugin config. Missing slug: ${bold5(
2531
2308
  auditSlug
2532
2309
  )}`
2533
2310
  );
@@ -2569,7 +2346,7 @@ async function executePlugin(pluginConfig, onProgress) {
2569
2346
  };
2570
2347
  }
2571
2348
  var wrapProgress = async (pluginCfg, steps, progressBar) => {
2572
- progressBar?.updateTitle(`Executing ${chalk5.bold(pluginCfg.title)}`);
2349
+ progressBar?.updateTitle(`Executing ${bold5(pluginCfg.title)}`);
2573
2350
  try {
2574
2351
  const pluginReport = await executePlugin(pluginCfg);
2575
2352
  progressBar?.incrementInSteps(steps);
@@ -2577,7 +2354,7 @@ var wrapProgress = async (pluginCfg, steps, progressBar) => {
2577
2354
  } catch (error) {
2578
2355
  progressBar?.incrementInSteps(steps);
2579
2356
  throw new Error(
2580
- error instanceof Error ? `- Plugin ${chalk5.bold(pluginCfg.title)} (${chalk5.bold(
2357
+ error instanceof Error ? `- Plugin ${bold5(pluginCfg.title)} (${bold5(
2581
2358
  pluginCfg.slug
2582
2359
  )}) produced the following error:
2583
2360
  - ${error.message}` : String(error)
@@ -2717,6 +2494,10 @@ async function collectAndPersistReports(options) {
2717
2494
  // packages/core/src/lib/compare.ts
2718
2495
  import { writeFile as writeFile2 } from "node:fs/promises";
2719
2496
  import { join as join5 } from "node:path";
2497
+ import {
2498
+ PortalOperationError,
2499
+ getPortalComparisonLink
2500
+ } from "@code-pushup/portal-client";
2720
2501
 
2721
2502
  // packages/core/src/lib/implementation/compare-scorables.ts
2722
2503
  function compareCategories(reports) {
@@ -2853,7 +2634,7 @@ function selectMeta(meta) {
2853
2634
  }
2854
2635
 
2855
2636
  // packages/core/src/lib/compare.ts
2856
- async function compareReportFiles(inputPaths, persistConfig) {
2637
+ async function compareReportFiles(inputPaths, persistConfig, uploadConfig) {
2857
2638
  const { outputDir, filename, format } = persistConfig;
2858
2639
  const [reportBefore, reportAfter] = await Promise.all([
2859
2640
  readJsonFile(inputPaths.before),
@@ -2864,10 +2645,11 @@ async function compareReportFiles(inputPaths, persistConfig) {
2864
2645
  after: reportSchema.parse(reportAfter)
2865
2646
  };
2866
2647
  const reportsDiff = compareReports(reports);
2648
+ const portalUrl = uploadConfig && reportsDiff.commits && format.includes("md") ? await fetchPortalComparisonLink(uploadConfig, reportsDiff.commits) : void 0;
2867
2649
  return Promise.all(
2868
2650
  format.map(async (fmt) => {
2869
2651
  const outputPath = join5(outputDir, `${filename}-diff.${fmt}`);
2870
- const content = reportsDiffToFileContent(reportsDiff, fmt);
2652
+ const content = reportsDiffToFileContent(reportsDiff, fmt, portalUrl);
2871
2653
  await ensureDirectoryExists(outputDir);
2872
2654
  await writeFile2(outputPath, content);
2873
2655
  return outputPath;
@@ -2897,12 +2679,35 @@ function compareReports(reports) {
2897
2679
  duration
2898
2680
  };
2899
2681
  }
2900
- function reportsDiffToFileContent(reportsDiff, format) {
2682
+ function reportsDiffToFileContent(reportsDiff, format, portalUrl) {
2901
2683
  switch (format) {
2902
2684
  case "json":
2903
2685
  return JSON.stringify(reportsDiff, null, 2);
2904
2686
  case "md":
2905
- return generateMdReportsDiff(reportsDiff);
2687
+ return generateMdReportsDiff(reportsDiff, portalUrl ?? void 0);
2688
+ }
2689
+ }
2690
+ async function fetchPortalComparisonLink(uploadConfig, commits) {
2691
+ const { server, apiKey, organization, project } = uploadConfig;
2692
+ try {
2693
+ return await getPortalComparisonLink({
2694
+ server,
2695
+ apiKey,
2696
+ parameters: {
2697
+ organization,
2698
+ project,
2699
+ before: commits.before.hash,
2700
+ after: commits.after.hash
2701
+ }
2702
+ });
2703
+ } catch (error) {
2704
+ if (error instanceof PortalOperationError) {
2705
+ ui().logger.warning(
2706
+ `Failed to fetch portal comparison link - ${error.message}`
2707
+ );
2708
+ return void 0;
2709
+ }
2710
+ throw error;
2906
2711
  }
2907
2712
  }
2908
2713
 
@@ -2960,9 +2765,9 @@ function auditToGQL(audit) {
2960
2765
  score,
2961
2766
  value,
2962
2767
  displayValue: formattedValue,
2963
- details: details4
2768
+ details: details2
2964
2769
  } = audit;
2965
- const { issues, table: table5 } = details4 ?? {};
2770
+ const { issues, table: table2 } = details2 ?? {};
2966
2771
  return {
2967
2772
  slug,
2968
2773
  title,
@@ -2971,10 +2776,10 @@ function auditToGQL(audit) {
2971
2776
  score,
2972
2777
  value,
2973
2778
  formattedValue,
2974
- ...details4 && {
2779
+ ...details2 && {
2975
2780
  details: {
2976
2781
  ...issues && { issues: issues.map(issueToGQL) },
2977
- ...table5 && { tables: [tableToGQL(table5)] }
2782
+ ...table2 && { tables: [tableToGQL(table2)] }
2978
2783
  }
2979
2784
  }
2980
2785
  };
@@ -2993,11 +2798,11 @@ function issueToGQL(issue) {
2993
2798
  }
2994
2799
  };
2995
2800
  }
2996
- function tableToGQL(table5) {
2801
+ function tableToGQL(table2) {
2997
2802
  return {
2998
- ...table5.title && { title: table5.title },
2999
- ...table5.columns?.length && {
3000
- columns: table5.columns.map(
2803
+ ...table2.title && { title: table2.title },
2804
+ ...table2.columns?.length && {
2805
+ columns: table2.columns.map(
3001
2806
  (column) => typeof column === "string" ? { alignment: tableAlignmentToGQL(column) } : {
3002
2807
  key: column.key,
3003
2808
  label: column.label,
@@ -3005,7 +2810,7 @@ function tableToGQL(table5) {
3005
2810
  }
3006
2811
  )
3007
2812
  },
3008
- rows: table5.rows.map(
2813
+ rows: table2.rows.map(
3009
2814
  (row) => Array.isArray(row) ? row.map((content) => ({ content: content?.toString() ?? "" })) : Object.entries(row).map(([key, content]) => ({
3010
2815
  key,
3011
2816
  content: content?.toString() ?? ""