@code-pushup/core 0.48.0 → 0.50.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
@@ -670,6 +670,8 @@ var auditResultSchema = scorableWithPluginMetaSchema.merge(
670
670
  );
671
671
  var reportsDiffSchema = z15.object({
672
672
  commits: makeComparisonSchema(commitSchema).nullable().describe("Commits identifying compared reports"),
673
+ portalUrl: urlSchema.optional().describe("Link to comparison page in Code PushUp portal"),
674
+ label: z15.string().optional().describe("Label (e.g. project name)"),
673
675
  categories: makeArraysComparisonSchema(
674
676
  categoryDiffSchema,
675
677
  categoryResultSchema,
@@ -739,15 +741,286 @@ function comparePairs(pairs, equalsFn) {
739
741
  );
740
742
  }
741
743
 
744
+ // packages/utils/src/lib/errors.ts
745
+ function stringifyError(error) {
746
+ if (error instanceof Error) {
747
+ if (error.name === "Error" || error.message.startsWith(error.name)) {
748
+ return error.message;
749
+ }
750
+ return `${error.name}: ${error.message}`;
751
+ }
752
+ if (typeof error === "string") {
753
+ return error;
754
+ }
755
+ return JSON.stringify(error);
756
+ }
757
+
742
758
  // packages/utils/src/lib/execute-process.ts
743
- import { spawn } from "node:child_process";
759
+ import {
760
+ spawn
761
+ } from "node:child_process";
762
+
763
+ // packages/utils/src/lib/reports/utils.ts
764
+ import ansis from "ansis";
765
+ import { md } from "build-md";
766
+
767
+ // packages/utils/src/lib/reports/constants.ts
768
+ var TERMINAL_WIDTH = 80;
769
+ var SCORE_COLOR_RANGE = {
770
+ GREEN_MIN: 0.9,
771
+ YELLOW_MIN: 0.5
772
+ };
773
+ var FOOTER_PREFIX = "Made with \u2764 by";
774
+ var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
775
+ var README_LINK = "https://github.com/code-pushup/cli#readme";
776
+ var REPORT_HEADLINE_TEXT = "Code PushUp Report";
777
+ var REPORT_RAW_OVERVIEW_TABLE_HEADERS = [
778
+ "Category",
779
+ "Score",
780
+ "Audits"
781
+ ];
744
782
 
745
783
  // packages/utils/src/lib/reports/utils.ts
746
- import { join } from "node:path";
784
+ function formatReportScore(score) {
785
+ const scaledScore = score * 100;
786
+ const roundedScore = Math.round(scaledScore);
787
+ return roundedScore === 100 && score !== 1 ? Math.floor(scaledScore).toString() : roundedScore.toString();
788
+ }
789
+ function formatScoreWithColor(score, options) {
790
+ const styledNumber = options?.skipBold ? formatReportScore(score) : md.bold(formatReportScore(score));
791
+ return md`${scoreMarker(score)} ${styledNumber}`;
792
+ }
793
+ var MARKERS = {
794
+ circle: {
795
+ red: "\u{1F534}",
796
+ yellow: "\u{1F7E1}",
797
+ green: "\u{1F7E2}"
798
+ },
799
+ square: {
800
+ red: "\u{1F7E5}",
801
+ yellow: "\u{1F7E8}",
802
+ green: "\u{1F7E9}"
803
+ }
804
+ };
805
+ function scoreMarker(score, markerType = "circle") {
806
+ if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
807
+ return MARKERS[markerType].green;
808
+ }
809
+ if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
810
+ return MARKERS[markerType].yellow;
811
+ }
812
+ return MARKERS[markerType].red;
813
+ }
814
+ function getDiffMarker(diff) {
815
+ if (diff > 0) {
816
+ return "\u2191";
817
+ }
818
+ if (diff < 0) {
819
+ return "\u2193";
820
+ }
821
+ return "";
822
+ }
823
+ function colorByScoreDiff(text, diff) {
824
+ const color = diff > 0 ? "green" : diff < 0 ? "red" : "gray";
825
+ return shieldsBadge(text, color);
826
+ }
827
+ function shieldsBadge(text, color) {
828
+ return md.image(
829
+ `https://img.shields.io/badge/${encodeURIComponent(text)}-${color}`,
830
+ text
831
+ );
832
+ }
833
+ function formatDiffNumber(diff) {
834
+ const number = Math.abs(diff) === Number.POSITIVE_INFINITY ? "\u221E" : `${Math.abs(diff)}`;
835
+ const sign = diff < 0 ? "\u2212" : "+";
836
+ return `${sign}${number}`;
837
+ }
838
+ function severityMarker(severity) {
839
+ if (severity === "error") {
840
+ return "\u{1F6A8}";
841
+ }
842
+ if (severity === "warning") {
843
+ return "\u26A0\uFE0F";
844
+ }
845
+ return "\u2139\uFE0F";
846
+ }
847
+ function formatScoreChange(diff) {
848
+ const marker = getDiffMarker(diff);
849
+ const text = formatDiffNumber(Math.round(diff * 1e3) / 10);
850
+ return colorByScoreDiff(`${marker} ${text}`, diff);
851
+ }
852
+ function formatValueChange({
853
+ values,
854
+ scores
855
+ }) {
856
+ const marker = getDiffMarker(values.diff);
857
+ const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : Math.round(100 * values.diff / values.before);
858
+ const text = `${formatDiffNumber(percentage)}\u2009%`;
859
+ return colorByScoreDiff(`${marker} ${text}`, scores.diff);
860
+ }
861
+ function calcDuration(start, stop) {
862
+ return Math.round((stop ?? performance.now()) - start);
863
+ }
864
+ function countCategoryAudits(refs, plugins) {
865
+ const groupLookup = plugins.reduce(
866
+ (lookup, plugin) => {
867
+ if (plugin.groups == null || plugin.groups.length === 0) {
868
+ return lookup;
869
+ }
870
+ return {
871
+ ...lookup,
872
+ [plugin.slug]: Object.fromEntries(
873
+ plugin.groups.map((group) => [group.slug, group])
874
+ )
875
+ };
876
+ },
877
+ {}
878
+ );
879
+ return refs.reduce((acc, ref) => {
880
+ if (ref.type === "group") {
881
+ const groupRefs = groupLookup[ref.plugin]?.[ref.slug]?.refs;
882
+ return acc + (groupRefs?.length ?? 0);
883
+ }
884
+ return acc + 1;
885
+ }, 0);
886
+ }
887
+ function compareCategoryAuditsAndGroups(a, b) {
888
+ if (a.score !== b.score) {
889
+ return a.score - b.score;
890
+ }
891
+ if (a.weight !== b.weight) {
892
+ return b.weight - a.weight;
893
+ }
894
+ if ("value" in a && "value" in b && a.value !== b.value) {
895
+ return b.value - a.value;
896
+ }
897
+ return a.title.localeCompare(b.title);
898
+ }
899
+ function compareAudits(a, b) {
900
+ if (a.score !== b.score) {
901
+ return a.score - b.score;
902
+ }
903
+ if (a.value !== b.value) {
904
+ return b.value - a.value;
905
+ }
906
+ return a.title.localeCompare(b.title);
907
+ }
908
+ function compareIssueSeverity(severity1, severity2) {
909
+ const levels = {
910
+ info: 0,
911
+ warning: 1,
912
+ error: 2
913
+ };
914
+ return levels[severity1] - levels[severity2];
915
+ }
916
+ function throwIsNotPresentError(itemName, presentPlace) {
917
+ throw new Error(`${itemName} is not present in ${presentPlace}`);
918
+ }
919
+ function getPluginNameFromSlug(slug, plugins) {
920
+ return plugins.find(({ slug: pluginSlug }) => pluginSlug === slug)?.title || slug;
921
+ }
922
+ function compareIssues(a, b) {
923
+ if (a.severity !== b.severity) {
924
+ return -compareIssueSeverity(a.severity, b.severity);
925
+ }
926
+ if (!a.source && b.source) {
927
+ return -1;
928
+ }
929
+ if (a.source && !b.source) {
930
+ return 1;
931
+ }
932
+ if (a.source?.file !== b.source?.file) {
933
+ return a.source?.file.localeCompare(b.source?.file || "") ?? 0;
934
+ }
935
+ if (!a.source?.position && b.source?.position) {
936
+ return -1;
937
+ }
938
+ if (a.source?.position && !b.source?.position) {
939
+ return 1;
940
+ }
941
+ if (a.source?.position?.startLine !== b.source?.position?.startLine) {
942
+ return (a.source?.position?.startLine ?? 0) - (b.source?.position?.startLine ?? 0);
943
+ }
944
+ return 0;
945
+ }
946
+ function applyScoreColor({ score, text }, style = ansis) {
947
+ const formattedScore = text ?? formatReportScore(score);
948
+ if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
949
+ return text ? style.green(formattedScore) : style.bold(style.green(formattedScore));
950
+ }
951
+ if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
952
+ return text ? style.yellow(formattedScore) : style.bold(style.yellow(formattedScore));
953
+ }
954
+ return text ? style.red(formattedScore) : style.bold(style.red(formattedScore));
955
+ }
956
+ function targetScoreIcon(score, targetScore, options = {}) {
957
+ if (targetScore != null) {
958
+ const {
959
+ passIcon = "\u2705",
960
+ failIcon = "\u274C",
961
+ prefix = "",
962
+ postfix = ""
963
+ } = options;
964
+ if (score >= targetScore) {
965
+ return `${prefix}${passIcon}${postfix}`;
966
+ }
967
+ return `${prefix}${failIcon}${postfix}`;
968
+ }
969
+ return "";
970
+ }
971
+
972
+ // packages/utils/src/lib/execute-process.ts
973
+ var ProcessError = class extends Error {
974
+ code;
975
+ stderr;
976
+ stdout;
977
+ constructor(result) {
978
+ super(result.stderr);
979
+ this.code = result.code;
980
+ this.stderr = result.stderr;
981
+ this.stdout = result.stdout;
982
+ }
983
+ };
984
+ function executeProcess(cfg) {
985
+ const { command, args, observer, ignoreExitCode = false, ...options } = cfg;
986
+ const { onStdout, onStderr, onError, onComplete } = observer ?? {};
987
+ const date = (/* @__PURE__ */ new Date()).toISOString();
988
+ const start = performance.now();
989
+ return new Promise((resolve, reject) => {
990
+ const spawnedProcess = spawn(command, args ?? [], {
991
+ shell: true,
992
+ ...options
993
+ });
994
+ let stdout = "";
995
+ let stderr = "";
996
+ spawnedProcess.stdout.on("data", (data) => {
997
+ stdout += String(data);
998
+ onStdout?.(String(data), spawnedProcess);
999
+ });
1000
+ spawnedProcess.stderr.on("data", (data) => {
1001
+ stderr += String(data);
1002
+ onStderr?.(String(data), spawnedProcess);
1003
+ });
1004
+ spawnedProcess.on("error", (err) => {
1005
+ stderr += err.toString();
1006
+ });
1007
+ spawnedProcess.on("close", (code2) => {
1008
+ const timings = { date, duration: calcDuration(start) };
1009
+ if (code2 === 0 || ignoreExitCode) {
1010
+ onComplete?.();
1011
+ resolve({ code: code2, stdout, stderr, ...timings });
1012
+ } else {
1013
+ const errorMsg = new ProcessError({ code: code2, stdout, stderr, ...timings });
1014
+ onError?.(errorMsg);
1015
+ reject(errorMsg);
1016
+ }
1017
+ });
1018
+ });
1019
+ }
747
1020
 
748
1021
  // packages/utils/src/lib/file-system.ts
1022
+ import { bold, gray } from "ansis";
749
1023
  import { bundleRequire } from "bundle-require";
750
- import chalk2 from "chalk";
751
1024
  import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
752
1025
 
753
1026
  // packages/utils/src/lib/formatting.ts
@@ -810,55 +1083,7 @@ function isPromiseRejectedResult(result) {
810
1083
  // packages/utils/src/lib/logging.ts
811
1084
  import isaacs_cliui from "@isaacs/cliui";
812
1085
  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
1086
+ import { underline } from "ansis";
862
1087
  var singletonUiInstance;
863
1088
  function ui() {
864
1089
  if (singletonUiInstance === void 0) {
@@ -954,10 +1179,10 @@ async function ensureDirectoryExists(baseDir) {
954
1179
  function logMultipleFileResults(fileResults, messagePrefix) {
955
1180
  const succeededTransform = (result) => {
956
1181
  const [fileName, size] = result.value;
957
- const formattedSize = size ? ` (${chalk2.gray(formatBytes(size))})` : "";
958
- return `- ${chalk2.bold(fileName)}${formattedSize}`;
1182
+ const formattedSize = size ? ` (${gray(formatBytes(size))})` : "";
1183
+ return `- ${bold(fileName)}${formattedSize}`;
959
1184
  };
960
- const failedTransform = (result) => `- ${chalk2.bold(result.reason)}`;
1185
+ const failedTransform = (result) => `- ${bold(result.reason)}`;
961
1186
  logMultipleResults(
962
1187
  fileResults,
963
1188
  messagePrefix,
@@ -973,570 +1198,35 @@ async function importModule(options) {
973
1198
  return mod;
974
1199
  }
975
1200
 
976
- // packages/utils/src/lib/text-formats/constants.ts
977
- var NEW_LINE = "\n";
978
- var TAB = " ";
979
- var SPACE = " ";
980
-
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}`;
988
- }
1201
+ // packages/utils/src/lib/git/git.ts
1202
+ import { isAbsolute, join, relative } from "node:path";
1203
+ import { simpleGit } from "simple-git";
989
1204
 
990
- // packages/utils/src/lib/text-formats/html/font-style.ts
991
- var boldElement = "b";
992
- function bold(text) {
993
- return `<${boldElement}>${text}</${boldElement}>`;
1205
+ // packages/utils/src/lib/transform.ts
1206
+ function toArray(val) {
1207
+ return Array.isArray(val) ? val : [val];
994
1208
  }
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
1209
  function objectToEntries(obj) {
1011
1210
  return Object.entries(obj);
1012
1211
  }
1013
1212
  function deepClone(obj) {
1014
1213
  return obj == null || typeof obj !== "object" ? obj : structuredClone(obj);
1015
1214
  }
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;
1485
- }
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
- });
1530
- }
1215
+ function toUnixPath(path) {
1216
+ return path.replace(/\\/g, "/");
1217
+ }
1218
+ function capitalize(text) {
1219
+ return `${text.charAt(0).toLocaleUpperCase()}${text.slice(
1220
+ 1
1221
+ )}`;
1222
+ }
1531
1223
 
1532
1224
  // packages/utils/src/lib/git/git.ts
1533
- import { isAbsolute, join as join2, relative } from "node:path";
1534
- import { simpleGit } from "simple-git";
1535
1225
  function getGitRoot(git = simpleGit()) {
1536
1226
  return git.revparse("--show-toplevel");
1537
1227
  }
1538
1228
  function formatGitPath(path, gitRoot) {
1539
- const absolutePath = isAbsolute(path) ? path : join2(process.cwd(), path);
1229
+ const absolutePath = isAbsolute(path) ? path : join(process.cwd(), path);
1540
1230
  const relativePath = relative(gitRoot, absolutePath);
1541
1231
  return toUnixPath(relativePath);
1542
1232
  }
@@ -1618,17 +1308,17 @@ function groupByStatus(results) {
1618
1308
  }
1619
1309
 
1620
1310
  // packages/utils/src/lib/progress.ts
1621
- import chalk3 from "chalk";
1311
+ import { black, bold as bold2, gray as gray2, green } from "ansis";
1622
1312
  import { MultiProgressBars } from "multi-progress-bars";
1623
1313
  var barStyles = {
1624
- active: (s) => chalk3.green(s),
1625
- done: (s) => chalk3.gray(s),
1626
- idle: (s) => chalk3.gray(s)
1314
+ active: (s) => green(s),
1315
+ done: (s) => gray2(s),
1316
+ idle: (s) => gray2(s)
1627
1317
  };
1628
1318
  var messageStyles = {
1629
- active: (s) => chalk3.black(s),
1630
- done: (s) => chalk3.green(chalk3.bold(s)),
1631
- idle: (s) => chalk3.gray(s)
1319
+ active: (s) => black(s),
1320
+ done: (s) => bold2.green(s),
1321
+ idle: (s) => gray2(s)
1632
1322
  };
1633
1323
  var mpb;
1634
1324
  function getSingletonProgressBars(options) {
@@ -1684,467 +1374,483 @@ function listAuditsFromAllPlugins(report) {
1684
1374
  );
1685
1375
  }
1686
1376
 
1377
+ // packages/utils/src/lib/reports/generate-md-report.ts
1378
+ import { MarkdownDocument as MarkdownDocument3, md as md4 } from "build-md";
1379
+
1380
+ // packages/utils/src/lib/text-formats/constants.ts
1381
+ var HIERARCHY = {
1382
+ level_1: 1,
1383
+ level_2: 2,
1384
+ level_3: 3,
1385
+ level_4: 4,
1386
+ level_5: 5,
1387
+ level_6: 6
1388
+ };
1389
+
1390
+ // packages/utils/src/lib/text-formats/table.ts
1391
+ function rowToStringArray({ rows, columns = [] }) {
1392
+ if (Array.isArray(rows.at(0)) && typeof columns.at(0) === "object") {
1393
+ throw new TypeError(
1394
+ "Column can`t be object when rows are primitive values"
1395
+ );
1396
+ }
1397
+ return rows.map((row) => {
1398
+ if (Array.isArray(row)) {
1399
+ return row.map(String);
1400
+ }
1401
+ const objectRow = row;
1402
+ if (columns.length === 0 || typeof columns.at(0) === "string") {
1403
+ return Object.values(objectRow).map(
1404
+ (value) => value == null ? "" : String(value)
1405
+ );
1406
+ }
1407
+ return columns.map(
1408
+ ({ key }) => objectRow[key] == null ? "" : String(objectRow[key])
1409
+ );
1410
+ });
1411
+ }
1412
+ function columnsToStringArray({
1413
+ rows,
1414
+ columns = []
1415
+ }) {
1416
+ const firstRow = rows.at(0);
1417
+ const primitiveRows = Array.isArray(firstRow);
1418
+ if (typeof columns.at(0) === "string" && !primitiveRows) {
1419
+ throw new Error("invalid union type. Caught by model parsing.");
1420
+ }
1421
+ if (columns.length === 0) {
1422
+ if (Array.isArray(firstRow)) {
1423
+ return firstRow.map((_, idx) => String(idx));
1424
+ }
1425
+ return Object.keys(firstRow);
1426
+ }
1427
+ if (typeof columns.at(0) === "string") {
1428
+ return columns.map(String);
1429
+ }
1430
+ const cols = columns;
1431
+ return cols.map(({ label, key }) => label ?? capitalize(key));
1432
+ }
1433
+ function getColumnAlignmentForKeyAndIndex(targetKey, targetIdx, columns = []) {
1434
+ const column = columns.at(targetIdx) ?? columns.find((col) => col.key === targetKey);
1435
+ if (typeof column === "string") {
1436
+ return column;
1437
+ } else if (typeof column === "object") {
1438
+ return column.align ?? "center";
1439
+ } else {
1440
+ return "center";
1441
+ }
1442
+ }
1443
+ function getColumnAlignmentForIndex(targetIdx, columns = []) {
1444
+ const column = columns.at(targetIdx);
1445
+ if (column == null) {
1446
+ return "center";
1447
+ } else if (typeof column === "string") {
1448
+ return column;
1449
+ } else if (typeof column === "object") {
1450
+ return column.align ?? "center";
1451
+ } else {
1452
+ return "center";
1453
+ }
1454
+ }
1455
+ function getColumnAlignments(tableData) {
1456
+ const { rows, columns = [] } = tableData;
1457
+ if (rows.at(0) == null) {
1458
+ throw new Error("first row can`t be undefined.");
1459
+ }
1460
+ if (Array.isArray(rows.at(0))) {
1461
+ const firstPrimitiveRow = rows.at(0);
1462
+ return Array.from({ length: firstPrimitiveRow.length }).map(
1463
+ (_, idx) => getColumnAlignmentForIndex(idx, columns)
1464
+ );
1465
+ }
1466
+ const biggestRow = [...rows].sort((a, b) => Object.keys(a).length - Object.keys(b).length).at(-1);
1467
+ if (columns.length > 0) {
1468
+ return columns.map(
1469
+ (column, idx) => typeof column === "string" ? column : getColumnAlignmentForKeyAndIndex(
1470
+ column.key,
1471
+ idx,
1472
+ columns
1473
+ )
1474
+ );
1475
+ }
1476
+ return Object.keys(biggestRow ?? {}).map((_) => "center");
1477
+ }
1478
+
1687
1479
  // packages/utils/src/lib/reports/formatting.ts
1688
- var { headline: headline2, lines: lines2, link: link3, section: section2, table: table3 } = md;
1480
+ import {
1481
+ MarkdownDocument,
1482
+ md as md2
1483
+ } from "build-md";
1689
1484
  function tableSection(tableData, options) {
1690
1485
  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)
1486
+ return null;
1487
+ }
1488
+ const { level = HIERARCHY.level_4 } = options ?? {};
1489
+ const columns = columnsToStringArray(tableData);
1490
+ const alignments = getColumnAlignments(tableData);
1491
+ const rows = rowToStringArray(tableData);
1492
+ return new MarkdownDocument().heading(level, tableData.title).table(
1493
+ columns.map((heading, i) => {
1494
+ const alignment = alignments[i];
1495
+ if (alignment) {
1496
+ return { heading, alignment };
1497
+ }
1498
+ return heading;
1499
+ }),
1500
+ rows
1698
1501
  );
1699
1502
  }
1700
- function metaDescription({
1701
- docsUrl,
1702
- description
1703
- }) {
1503
+ function metaDescription(audit) {
1504
+ const docsUrl = audit.docsUrl;
1505
+ const description = audit.description?.trim();
1704
1506
  if (docsUrl) {
1705
- const docsLink = link3(docsUrl, "\u{1F4D6} Docs");
1507
+ const docsLink = md2.link(docsUrl, "\u{1F4D6} Docs");
1706
1508
  if (!description) {
1707
- return section2(docsLink);
1509
+ return docsLink;
1708
1510
  }
1709
- const parsedDescription = description.toString().endsWith("```") ? `${description}${NEW_LINE + NEW_LINE}` : `${description}${SPACE}`;
1710
- return section2(`${parsedDescription}${docsLink}`);
1511
+ const parsedDescription = description.endsWith("```") ? `${description}
1512
+
1513
+ ` : `${description} `;
1514
+ return md2`${parsedDescription}${docsLink}`;
1711
1515
  }
1712
1516
  if (description && description.trim().length > 0) {
1713
- return section2(description);
1517
+ return description;
1714
1518
  }
1715
1519
  return "";
1716
1520
  }
1717
1521
 
1718
1522
  // 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
- function categoriesOverviewSection(report) {
1721
- 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);
1523
+ import { MarkdownDocument as MarkdownDocument2, md as md3 } from "build-md";
1524
+
1525
+ // packages/utils/src/lib/reports/sorting.ts
1526
+ function getSortableAuditByRef({ slug, weight, plugin }, plugins) {
1527
+ const auditPlugin = plugins.find((p) => p.slug === plugin);
1528
+ if (!auditPlugin) {
1529
+ throwIsNotPresentError(`Plugin ${plugin}`, "report");
1735
1530
  }
1736
- return "";
1531
+ const audit = auditPlugin.audits.find(
1532
+ ({ slug: auditSlug }) => auditSlug === slug
1533
+ );
1534
+ if (!audit) {
1535
+ throwIsNotPresentError(`Audit ${slug}`, auditPlugin.slug);
1536
+ }
1537
+ return {
1538
+ ...audit,
1539
+ weight,
1540
+ plugin
1541
+ };
1737
1542
  }
1738
- function categoriesDetailsSection(report) {
1543
+ function getSortedGroupAudits(group, plugin, plugins) {
1544
+ return group.refs.map(
1545
+ (ref) => getSortableAuditByRef(
1546
+ {
1547
+ plugin,
1548
+ slug: ref.slug,
1549
+ weight: ref.weight,
1550
+ type: "audit"
1551
+ },
1552
+ plugins
1553
+ )
1554
+ ).sort(compareCategoryAuditsAndGroups);
1555
+ }
1556
+ function getSortableGroupByRef({ plugin, slug, weight }, plugins) {
1557
+ const groupPlugin = plugins.find((p) => p.slug === plugin);
1558
+ if (!groupPlugin) {
1559
+ throwIsNotPresentError(`Plugin ${plugin}`, "report");
1560
+ }
1561
+ const group = groupPlugin.groups?.find(
1562
+ ({ slug: groupSlug }) => groupSlug === slug
1563
+ );
1564
+ if (!group) {
1565
+ throwIsNotPresentError(`Group ${slug}`, groupPlugin.slug);
1566
+ }
1567
+ const sortedAudits = getSortedGroupAudits(group, groupPlugin.slug, plugins);
1568
+ const sortedAuditRefs = [...group.refs].sort((a, b) => {
1569
+ const aIndex = sortedAudits.findIndex((ref) => ref.slug === a.slug);
1570
+ const bIndex = sortedAudits.findIndex((ref) => ref.slug === b.slug);
1571
+ return aIndex - bIndex;
1572
+ });
1573
+ return {
1574
+ ...group,
1575
+ refs: sortedAuditRefs,
1576
+ plugin,
1577
+ weight
1578
+ };
1579
+ }
1580
+ function sortReport(report) {
1739
1581
  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
1582
+ const sortedCategories = categories.map((category) => {
1583
+ const { audits, groups } = category.refs.reduce(
1584
+ (acc, ref) => ({
1585
+ ...acc,
1586
+ ...ref.type === "group" ? {
1587
+ groups: [...acc.groups, getSortableGroupByRef(ref, plugins)]
1588
+ } : {
1589
+ audits: [...acc.audits, getSortableAuditByRef(ref, plugins)]
1590
+ }
1591
+ }),
1592
+ { groups: [], audits: [] }
1593
+ );
1594
+ const sortedAuditsAndGroups = [...audits, ...groups].sort(
1595
+ compareCategoryAuditsAndGroups
1767
1596
  );
1597
+ const sortedRefs = [...category.refs].sort((a, b) => {
1598
+ const aIndex = sortedAuditsAndGroups.findIndex(
1599
+ (ref) => ref.slug === a.slug && ref.plugin === a.plugin
1600
+ );
1601
+ const bIndex = sortedAuditsAndGroups.findIndex(
1602
+ (ref) => ref.slug === b.slug && ref.plugin === b.plugin
1603
+ );
1604
+ return aIndex - bIndex;
1605
+ });
1606
+ return { ...category, refs: sortedRefs };
1768
1607
  });
1769
- return lines3(h22(CATEGORIES_TITLE), ...categoryDetails);
1608
+ return {
1609
+ ...report,
1610
+ categories: sortedCategories,
1611
+ plugins: sortPlugins(plugins)
1612
+ };
1613
+ }
1614
+ function sortPlugins(plugins) {
1615
+ return plugins.map((plugin) => ({
1616
+ ...plugin,
1617
+ audits: [...plugin.audits].sort(compareAudits).map(
1618
+ (audit) => audit.details?.issues ? {
1619
+ ...audit,
1620
+ details: {
1621
+ ...audit.details,
1622
+ issues: [...audit.details.issues].sort(compareIssues)
1623
+ }
1624
+ } : audit
1625
+ )
1626
+ }));
1627
+ }
1628
+
1629
+ // packages/utils/src/lib/reports/generate-md-report-categoy-section.ts
1630
+ function categoriesOverviewSection(report) {
1631
+ const { categories, plugins } = report;
1632
+ return new MarkdownDocument2().table(
1633
+ [
1634
+ { heading: "\u{1F3F7} Category", alignment: "left" },
1635
+ { heading: "\u2B50 Score", alignment: "center" },
1636
+ { heading: "\u{1F6E1} Audits", alignment: "center" }
1637
+ ],
1638
+ categories.map(({ title, refs, score, isBinary }) => [
1639
+ // @TODO refactor `isBinary: boolean` to `targetScore: number` #713
1640
+ // The heading "ID" is inferred from the heading text in Markdown.
1641
+ md3.link(`#${slugify(title)}`, title),
1642
+ md3`${scoreMarker(score)} ${md3.bold(
1643
+ formatReportScore(score)
1644
+ )}${binaryIconSuffix(score, isBinary)}`,
1645
+ countCategoryAudits(refs, plugins).toString()
1646
+ ])
1647
+ );
1648
+ }
1649
+ function categoriesDetailsSection(report) {
1650
+ const { categories, plugins } = report;
1651
+ return new MarkdownDocument2().heading(HIERARCHY.level_2, "\u{1F3F7} Categories").$foreach(
1652
+ categories,
1653
+ (doc, category) => doc.heading(HIERARCHY.level_3, category.title).paragraph(metaDescription(category)).paragraph(
1654
+ md3`${scoreMarker(category.score)} Score: ${md3.bold(
1655
+ formatReportScore(category.score)
1656
+ )}${binaryIconSuffix(category.score, category.isBinary)}`
1657
+ ).list(
1658
+ category.refs.map((ref) => {
1659
+ if (ref.type === "group") {
1660
+ const group = getSortableGroupByRef(ref, plugins);
1661
+ const groupAudits = group.refs.map(
1662
+ (groupRef) => getSortableAuditByRef(
1663
+ { ...groupRef, plugin: group.plugin, type: "audit" },
1664
+ plugins
1665
+ )
1666
+ );
1667
+ const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
1668
+ return categoryGroupItem(group, groupAudits, pluginTitle);
1669
+ } else {
1670
+ const audit = getSortableAuditByRef(ref, plugins);
1671
+ const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
1672
+ return categoryRef(audit, pluginTitle);
1673
+ }
1674
+ })
1675
+ )
1676
+ );
1770
1677
  }
1771
1678
  function categoryRef({ title, score, value, displayValue }, pluginTitle) {
1772
- const auditTitleAsLink = link4(
1679
+ const auditTitleAsLink = md3.link(
1773
1680
  `#${slugify(title)}-${slugify(pluginTitle)}`,
1774
1681
  title
1775
1682
  );
1776
1683
  const marker = scoreMarker(score, "square");
1777
- return li2(
1778
- `${marker}${SPACE}${auditTitleAsLink}${SPACE}(_${pluginTitle}_) - ${boldMd2(
1779
- (displayValue || value).toString()
1780
- )}`
1781
- );
1684
+ return md3`${marker} ${auditTitleAsLink} (${md3.italic(
1685
+ pluginTitle
1686
+ )}) - ${md3.bold((displayValue || value).toString())}`;
1782
1687
  }
1783
1688
  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
- }
1689
+ const groupTitle = md3`${scoreMarker(score)} ${title} (${md3.italic(
1690
+ pluginTitle
1691
+ )})`;
1692
+ const auditsList = md3.list(
1693
+ groupAudits.map(
1694
+ ({ title: auditTitle, score: auditScore, value, displayValue }) => {
1695
+ const auditTitleLink = md3.link(
1696
+ `#${slugify(auditTitle)}-${slugify(pluginTitle)}`,
1697
+ auditTitle
1698
+ );
1699
+ const marker = scoreMarker(auditScore, "square");
1700
+ return md3`${marker} ${auditTitleLink} - ${md3.bold(
1701
+ String(displayValue ?? value)
1702
+ )}`;
1703
+ }
1704
+ )
1802
1705
  );
1803
- return lines3(groupTitle, ...auditTitles);
1706
+ return md3`${groupTitle}${auditsList}`;
1707
+ }
1708
+ function binaryIconSuffix(score, isBinary) {
1709
+ return targetScoreIcon(score, isBinary ? 1 : void 0, { prefix: " " });
1804
1710
  }
1805
1711
 
1806
1712
  // 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
1713
  function auditDetailsAuditValue({
1810
1714
  score,
1811
1715
  value,
1812
1716
  displayValue
1813
1717
  }) {
1814
- return `${scoreMarker(score, "square")} ${boldHtml(
1718
+ return md4`${scoreMarker(score, "square")} ${md4.bold(
1815
1719
  String(displayValue ?? value)
1816
1720
  )} (score: ${formatReportScore(score)})`;
1817
1721
  }
1818
1722
  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
- );
1723
+ return new MarkdownDocument3().heading(HIERARCHY.level_1, REPORT_HEADLINE_TEXT).$if(
1724
+ report.categories.length > 0,
1725
+ (doc) => doc.$concat(
1726
+ categoriesOverviewSection(report),
1727
+ categoriesDetailsSection(report)
1728
+ )
1729
+ ).$concat(auditsSection(report), aboutSection(report)).rule().paragraph(md4`${FOOTER_PREFIX} ${md4.link(README_LINK, "Code PushUp")}`).toString();
1828
1730
  }
1829
1731
  function auditDetailsIssues(issues = []) {
1830
1732
  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 };
1733
+ return null;
1734
+ }
1735
+ return new MarkdownDocument3().heading(HIERARCHY.level_4, "Issues").table(
1736
+ [
1737
+ { heading: "Severity", alignment: "center" },
1738
+ { heading: "Message", alignment: "left" },
1739
+ { heading: "Source file", alignment: "left" },
1740
+ { heading: "Line(s)", alignment: "center" }
1741
+ ],
1742
+ issues.map(({ severity: level, message, source }) => {
1743
+ const severity = md4`${severityMarker(level)} ${md4.italic(level)}`;
1744
+ if (!source) {
1745
+ return [severity, message];
1849
1746
  }
1850
- )
1851
- };
1852
- return tableSection(detailsTableData);
1747
+ const file = md4.code(source.file);
1748
+ if (!source.position) {
1749
+ return [severity, message, file];
1750
+ }
1751
+ const { startLine, endLine } = source.position;
1752
+ const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
1753
+ return [severity, message, file, line];
1754
+ })
1755
+ );
1853
1756
  }
1854
1757
  function auditDetails(audit) {
1855
- const { table: table5, issues = [] } = audit.details ?? {};
1758
+ const { table: table2, issues = [] } = audit.details ?? {};
1856
1759
  const detailsValue = auditDetailsAuditValue(audit);
1857
- if (issues.length === 0 && table5 == null) {
1858
- return section4(detailsValue);
1760
+ if (issues.length === 0 && !table2?.rows.length) {
1761
+ return new MarkdownDocument3().paragraph(detailsValue);
1859
1762
  }
1860
- const tableSectionContent = table5 == null ? "" : tableSection(table5);
1861
- const issuesSectionContent = issues.length > 0 ? auditDetailsIssues(issues) : "";
1862
- return details2(
1763
+ const tableSectionContent = table2 && tableSection(table2);
1764
+ const issuesSectionContent = issues.length > 0 && auditDetailsIssues(issues);
1765
+ return new MarkdownDocument3().details(
1863
1766
  detailsValue,
1864
- lines4(tableSectionContent, issuesSectionContent)
1767
+ new MarkdownDocument3().$concat(tableSectionContent, issuesSectionContent)
1865
1768
  );
1866
1769
  }
1867
1770
  function auditsSection({
1868
1771
  plugins
1869
1772
  }) {
1870
- const content = plugins.flatMap(
1871
- ({ slug, audits }) => audits.flatMap((audit) => {
1872
- const auditTitle = `${audit.title}${SPACE}(${getPluginNameFromSlug(
1873
- slug,
1874
- plugins
1875
- )})`;
1773
+ return new MarkdownDocument3().heading(HIERARCHY.level_2, "\u{1F6E1}\uFE0F Audits").$foreach(
1774
+ plugins.flatMap(
1775
+ (plugin) => plugin.audits.map((audit) => ({ ...audit, plugin }))
1776
+ ),
1777
+ (doc, { plugin, ...audit }) => {
1778
+ const auditTitle = `${audit.title} (${plugin.title})`;
1876
1779
  const detailsContent = auditDetails(audit);
1877
1780
  const descriptionContent = metaDescription(audit);
1878
- return [h33(auditTitle), detailsContent, descriptionContent];
1879
- })
1781
+ return doc.heading(HIERARCHY.level_3, auditTitle).$concat(detailsContent).paragraph(descriptionContent);
1782
+ }
1880
1783
  );
1881
- return section4(h23("\u{1F6E1}\uFE0F Audits"), ...content);
1882
1784
  }
1883
1785
  function aboutSection(report) {
1884
1786
  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
- }
1787
+ return new MarkdownDocument3().heading(HIERARCHY.level_2, "About").paragraph(
1788
+ md4`Report was created by ${md4.link(
1789
+ README_LINK,
1790
+ "Code PushUp"
1791
+ )} on ${formatDate(new Date(date))}.`
1792
+ ).table(...pluginMetaTable({ plugins })).table(...reportMetaTable(report));
1793
+ }
1794
+ function pluginMetaTable({
1795
+ plugins
1796
+ }) {
1797
+ return [
1798
+ [
1799
+ { heading: "Plugin", alignment: "left" },
1800
+ { heading: "Audits", alignment: "center" },
1801
+ { heading: "Version", alignment: "center" },
1802
+ { heading: "Duration", alignment: "right" }
1914
1803
  ],
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
- };
1804
+ plugins.map(({ title, audits, version: version2 = "", duration }) => [
1805
+ title,
1806
+ audits.length.toString(),
1807
+ version2 && md4.code(version2),
1808
+ formatDuration(duration)
1809
+ ])
1810
+ ];
1929
1811
  }
1930
- function reportMetaData({
1812
+ function reportMetaTable({
1931
1813
  commit,
1932
1814
  version: version2,
1933
1815
  duration,
1934
1816
  plugins,
1935
1817
  categories
1936
1818
  }) {
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
- }
1819
+ return [
1820
+ [
1821
+ { heading: "Commit", alignment: "left" },
1822
+ { heading: "Version", alignment: "center" },
1823
+ { heading: "Duration", alignment: "right" },
1824
+ { heading: "Plugins", alignment: "center" },
1825
+ { heading: "Categories", alignment: "center" },
1826
+ { heading: "Audits", alignment: "center" }
1959
1827
  ],
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
- }
1828
+ [
1829
+ [
1830
+ commit ? `${commit.message} (${commit.hash})` : "N/A",
1831
+ md4.code(version2),
1832
+ formatDuration(duration),
1833
+ plugins.length.toString(),
1834
+ categories.length.toString(),
1835
+ plugins.reduce((acc, { audits }) => acc + audits.length, 0).toString()
1836
+ ]
1969
1837
  ]
1970
- };
1838
+ ];
1971
1839
  }
1972
1840
 
1973
1841
  // 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;
1842
+ import {
1843
+ MarkdownDocument as MarkdownDocument5,
1844
+ md as md6
1845
+ } from "build-md";
1846
+
1847
+ // packages/utils/src/lib/reports/generate-md-reports-diff-utils.ts
1848
+ import { MarkdownDocument as MarkdownDocument4, md as md5 } from "build-md";
1985
1849
  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) {
1995
- 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(
1999
- "improvements and regressions"
2000
- )}`,
2001
- unchanged: `\u{1F610} Code PushUp report is ${boldMd3("unchanged")}`
2002
- };
2003
- const outcome = mergeDiffOutcomes(
2004
- changesToDiffOutcomes([
2005
- ...diff.categories.changed,
2006
- ...diff.groups.changed,
2007
- ...diff.audits.changed
2008
- ])
2009
- );
2010
- 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]}.`
2014
- );
2015
- }
2016
- function formatDiffCategoriesSection(diff) {
2017
- const { changed, unchanged, added } = diff.categories;
2018
- const categoriesCount = changed.length + unchanged.length + added.length;
2019
- const hasChanges = unchanged.length < categoriesCount;
2020
- if (categoriesCount === 0) {
2021
- return "";
2022
- }
2023
- 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" }
2028
- ];
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
- );
2060
- }
2061
- function formatDiffGroupsSection(diff) {
2062
- 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" }
2074
- ],
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
- })
2083
- );
2084
- }
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" }
2095
- ],
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(
2100
- audit.displayValues.after || audit.values.after.toString()
2101
- )}`,
2102
- before: `${scoreMarker(audit.scores.before, "square")} ${audit.displayValues.before || audit.values.before.toString()}`,
2103
- change: formatValueChange(audit)
2104
- }))
2105
- })
2106
- );
2107
- }
2108
- function formatGroupsOrAuditsDetails(token, { changed, unchanged }, tableData) {
2109
- return changed.length === 0 ? summarizeUnchanged(token, { changed, unchanged }) : details3(
2110
- 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(
2118
- `Only the ${MAX_ROWS} most affected ${pluralize(
2119
- token
2120
- )} are listed above for brevity.`
2121
- ),
2122
- unchanged.length > 0 && summarizeUnchanged(token, { changed, unchanged })
2123
- )
2124
- );
2125
- }
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
1850
  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
- );
1851
+ const pluralizedCount = changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`;
1852
+ const pluralizedVerb = unchanged.length === 1 ? "is" : "are";
1853
+ return `${pluralizedCount} ${pluralizedVerb} unchanged.`;
2148
1854
  }
2149
1855
  function summarizeDiffOutcomes(outcomes, token) {
2150
1856
  return objectToEntries(countDiffOutcomes(outcomes)).filter(
@@ -2164,20 +1870,46 @@ function summarizeDiffOutcomes(outcomes, token) {
2164
1870
  }
2165
1871
  }).join(", ");
2166
1872
  }
1873
+ function createGroupsOrAuditsDetails(token, { changed, unchanged }, ...[columns, rows]) {
1874
+ if (changed.length === 0) {
1875
+ return new MarkdownDocument4().paragraph(
1876
+ summarizeUnchanged(token, { changed, unchanged })
1877
+ );
1878
+ }
1879
+ return new MarkdownDocument4().table(columns, rows.slice(0, MAX_ROWS)).paragraph(
1880
+ changed.length > MAX_ROWS && md5.italic(
1881
+ `Only the ${MAX_ROWS} most affected ${pluralize(
1882
+ token
1883
+ )} are listed above for brevity.`
1884
+ )
1885
+ ).paragraph(
1886
+ unchanged.length > 0 && summarizeUnchanged(token, { changed, unchanged })
1887
+ );
1888
+ }
2167
1889
  function formatTitle({
2168
1890
  title,
2169
1891
  docsUrl
2170
1892
  }) {
2171
1893
  if (docsUrl) {
2172
- return link6(docsUrl, title);
1894
+ return md5.link(docsUrl, title);
2173
1895
  }
2174
1896
  return title;
2175
1897
  }
1898
+ function formatPortalLink(portalUrl) {
1899
+ return portalUrl && md5.link(portalUrl, "\u{1F575}\uFE0F See full comparison in Code PushUp portal \u{1F50D}");
1900
+ }
2176
1901
  function sortChanges(changes) {
2177
1902
  return [...changes].sort(
2178
1903
  (a, b) => Math.abs(b.scores.diff) - Math.abs(a.scores.diff) || Math.abs(b.values?.diff ?? 0) - Math.abs(a.values?.diff ?? 0)
2179
1904
  );
2180
1905
  }
1906
+ function getDiffChanges(diff) {
1907
+ return [
1908
+ ...diff.categories.changed,
1909
+ ...diff.groups.changed,
1910
+ ...diff.audits.changed
1911
+ ];
1912
+ }
2181
1913
  function changesToDiffOutcomes(changes) {
2182
1914
  return changes.map((change) => {
2183
1915
  if (change.scores.diff > 0) {
@@ -2212,9 +1944,218 @@ function countDiffOutcomes(outcomes) {
2212
1944
  unchanged: outcomes.filter((outcome) => outcome === "unchanged").length
2213
1945
  };
2214
1946
  }
1947
+ function formatReportOutcome(outcome, commits) {
1948
+ const outcomeTexts = {
1949
+ positive: md5`🥳 Code PushUp report has ${md5.bold("improved")}`,
1950
+ negative: md5`😟 Code PushUp report has ${md5.bold("regressed")}`,
1951
+ mixed: md5`🤨 Code PushUp report has both ${md5.bold(
1952
+ "improvements and regressions"
1953
+ )}`,
1954
+ unchanged: md5`😐 Code PushUp report is ${md5.bold("unchanged")}`
1955
+ };
1956
+ if (commits) {
1957
+ const commitsText = `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
1958
+ return md5`${outcomeTexts[outcome]} – ${commitsText}.`;
1959
+ }
1960
+ return md5`${outcomeTexts[outcome]}.`;
1961
+ }
1962
+ function compareDiffsBy(type, a, b) {
1963
+ return sumScoreChanges(b[type].changed) - sumScoreChanges(a[type].changed) || sumConfigChanges(b[type]) - sumConfigChanges(a[type]);
1964
+ }
1965
+ function sumScoreChanges(changes) {
1966
+ return changes.reduce(
1967
+ (acc, { scores }) => acc + Math.abs(scores.diff),
1968
+ 0
1969
+ );
1970
+ }
1971
+ function sumConfigChanges({
1972
+ added,
1973
+ removed
1974
+ }) {
1975
+ return added.length + removed.length;
1976
+ }
1977
+
1978
+ // packages/utils/src/lib/reports/generate-md-reports-diff.ts
1979
+ function generateMdReportsDiff(diff) {
1980
+ return new MarkdownDocument5().$concat(
1981
+ createDiffHeaderSection(diff),
1982
+ createDiffCategoriesSection(diff),
1983
+ createDiffDetailsSection(diff)
1984
+ ).toString();
1985
+ }
1986
+ function generateMdReportsDiffForMonorepo(diffs) {
1987
+ const diffsWithOutcomes = diffs.map((diff) => ({
1988
+ ...diff,
1989
+ outcome: mergeDiffOutcomes(changesToDiffOutcomes(getDiffChanges(diff)))
1990
+ })).sort(
1991
+ (a, b) => compareDiffsBy("categories", a, b) || compareDiffsBy("groups", a, b) || compareDiffsBy("audits", a, b) || a.label.localeCompare(b.label)
1992
+ );
1993
+ const unchanged = diffsWithOutcomes.filter(
1994
+ ({ outcome }) => outcome === "unchanged"
1995
+ );
1996
+ const changed = diffsWithOutcomes.filter((diff) => !unchanged.includes(diff));
1997
+ return new MarkdownDocument5().$concat(
1998
+ createDiffHeaderSection(diffs),
1999
+ ...changed.map(createDiffProjectSection)
2000
+ ).$if(
2001
+ unchanged.length > 0,
2002
+ (doc) => doc.rule().paragraph(summarizeUnchanged("project", { unchanged, changed }))
2003
+ ).toString();
2004
+ }
2005
+ function createDiffHeaderSection(diff) {
2006
+ const outcome = mergeDiffOutcomes(
2007
+ changesToDiffOutcomes(toArray(diff).flatMap(getDiffChanges))
2008
+ );
2009
+ const commits = Array.isArray(diff) ? diff[0]?.commits : diff.commits;
2010
+ const portalUrl = Array.isArray(diff) ? void 0 : diff.portalUrl;
2011
+ return new MarkdownDocument5().heading(HIERARCHY.level_1, "Code PushUp").paragraph(formatReportOutcome(outcome, commits)).paragraph(formatPortalLink(portalUrl));
2012
+ }
2013
+ function createDiffProjectSection(diff) {
2014
+ return new MarkdownDocument5().heading(HIERARCHY.level_2, md6`💼 Project ${md6.code(diff.label)}`).paragraph(formatReportOutcome(diff.outcome)).paragraph(formatPortalLink(diff.portalUrl)).$concat(
2015
+ createDiffCategoriesSection(diff, {
2016
+ skipHeading: true,
2017
+ skipUnchanged: true
2018
+ }),
2019
+ createDiffDetailsSection(diff, HIERARCHY.level_3)
2020
+ );
2021
+ }
2022
+ function createDiffCategoriesSection(diff, options) {
2023
+ const { changed, unchanged, added } = diff.categories;
2024
+ const { skipHeading, skipUnchanged } = options ?? {};
2025
+ const categoriesCount = changed.length + unchanged.length + added.length;
2026
+ const hasChanges = unchanged.length < categoriesCount;
2027
+ if (categoriesCount === 0) {
2028
+ return null;
2029
+ }
2030
+ const [columns, rows] = createCategoriesTable(diff, {
2031
+ hasChanges,
2032
+ skipUnchanged
2033
+ });
2034
+ return new MarkdownDocument5().heading(HIERARCHY.level_2, !skipHeading && "\u{1F3F7}\uFE0F Categories").table(columns, rows).paragraph(added.length > 0 && md6.italic("(\\*) New category.")).paragraph(
2035
+ skipUnchanged && unchanged.length > 0 && summarizeUnchanged("category", { changed, unchanged })
2036
+ );
2037
+ }
2038
+ function createCategoriesTable(diff, options) {
2039
+ const { changed, unchanged, added } = diff.categories;
2040
+ const { hasChanges, skipUnchanged } = options;
2041
+ const columns = [
2042
+ { heading: "\u{1F3F7}\uFE0F Category", alignment: "left" },
2043
+ {
2044
+ heading: hasChanges ? "\u2B50 Previous score" : "\u2B50 Score",
2045
+ alignment: "center"
2046
+ },
2047
+ { heading: "\u2B50 Current score", alignment: "center" },
2048
+ { heading: "\u{1F504} Score change", alignment: "center" }
2049
+ ];
2050
+ const rows = [
2051
+ ...sortChanges(changed).map((category) => [
2052
+ formatTitle(category),
2053
+ formatScoreWithColor(category.scores.before, {
2054
+ skipBold: true
2055
+ }),
2056
+ formatScoreWithColor(category.scores.after),
2057
+ formatScoreChange(category.scores.diff)
2058
+ ]),
2059
+ ...added.map((category) => [
2060
+ formatTitle(category),
2061
+ md6.italic("n/a (\\*)"),
2062
+ formatScoreWithColor(category.score),
2063
+ md6.italic("n/a (\\*)")
2064
+ ]),
2065
+ ...skipUnchanged ? [] : unchanged.map((category) => [
2066
+ formatTitle(category),
2067
+ formatScoreWithColor(category.score, { skipBold: true }),
2068
+ formatScoreWithColor(category.score),
2069
+ "\u2013"
2070
+ ])
2071
+ ];
2072
+ return [
2073
+ hasChanges ? columns : columns.slice(0, 2),
2074
+ rows.map((row) => hasChanges ? row : row.slice(0, 2))
2075
+ ];
2076
+ }
2077
+ function createDiffDetailsSection(diff, level = HIERARCHY.level_2) {
2078
+ if (diff.groups.changed.length + diff.audits.changed.length === 0) {
2079
+ return null;
2080
+ }
2081
+ const summary = ["group", "audit"].map(
2082
+ (token) => summarizeDiffOutcomes(
2083
+ changesToDiffOutcomes(diff[`${token}s`].changed),
2084
+ token
2085
+ )
2086
+ ).filter(Boolean).join(", ");
2087
+ const details2 = new MarkdownDocument5().$concat(
2088
+ createDiffGroupsSection(diff, level),
2089
+ createDiffAuditsSection(diff, level)
2090
+ );
2091
+ return new MarkdownDocument5().details(summary, details2);
2092
+ }
2093
+ function createDiffGroupsSection(diff, level) {
2094
+ if (diff.groups.changed.length + diff.groups.unchanged.length === 0) {
2095
+ return null;
2096
+ }
2097
+ return new MarkdownDocument5().heading(level, "\u{1F5C3}\uFE0F Groups").$concat(
2098
+ createGroupsOrAuditsDetails(
2099
+ "group",
2100
+ diff.groups,
2101
+ [
2102
+ { heading: "\u{1F50C} Plugin", alignment: "left" },
2103
+ { heading: "\u{1F5C3}\uFE0F Group", alignment: "left" },
2104
+ { heading: "\u2B50 Previous score", alignment: "center" },
2105
+ { heading: "\u2B50 Current score", alignment: "center" },
2106
+ { heading: "\u{1F504} Score change", alignment: "center" }
2107
+ ],
2108
+ sortChanges(diff.groups.changed).map((group) => [
2109
+ formatTitle(group.plugin),
2110
+ formatTitle(group),
2111
+ formatScoreWithColor(group.scores.before, { skipBold: true }),
2112
+ formatScoreWithColor(group.scores.after),
2113
+ formatScoreChange(group.scores.diff)
2114
+ ])
2115
+ )
2116
+ );
2117
+ }
2118
+ function createDiffAuditsSection(diff, level) {
2119
+ return new MarkdownDocument5().heading(level, "\u{1F6E1}\uFE0F Audits").$concat(
2120
+ createGroupsOrAuditsDetails(
2121
+ "audit",
2122
+ diff.audits,
2123
+ [
2124
+ { heading: "\u{1F50C} Plugin", alignment: "left" },
2125
+ { heading: "\u{1F6E1}\uFE0F Audit", alignment: "left" },
2126
+ { heading: "\u{1F4CF} Previous value", alignment: "center" },
2127
+ { heading: "\u{1F4CF} Current value", alignment: "center" },
2128
+ { heading: "\u{1F504} Value change", alignment: "center" }
2129
+ ],
2130
+ sortChanges(diff.audits.changed).map((audit) => [
2131
+ formatTitle(audit.plugin),
2132
+ formatTitle(audit),
2133
+ `${scoreMarker(audit.scores.before, "square")} ${audit.displayValues.before || audit.values.before.toString()}`,
2134
+ md6`${scoreMarker(audit.scores.after, "square")} ${md6.bold(
2135
+ audit.displayValues.after || audit.values.after.toString()
2136
+ )}`,
2137
+ formatValueChange(audit)
2138
+ ])
2139
+ )
2140
+ );
2141
+ }
2142
+
2143
+ // packages/utils/src/lib/reports/load-report.ts
2144
+ import { join as join2 } from "node:path";
2145
+ async function loadReport(options) {
2146
+ const { outputDir, filename, format } = options;
2147
+ await ensureDirectoryExists(outputDir);
2148
+ const filePath = join2(outputDir, `${filename}.${format}`);
2149
+ if (format === "json") {
2150
+ const content = await readJsonFile(filePath);
2151
+ return reportSchema.parse(content);
2152
+ }
2153
+ const text = await readTextFile(filePath);
2154
+ return text;
2155
+ }
2215
2156
 
2216
2157
  // packages/utils/src/lib/reports/log-stdout-summary.ts
2217
- import chalk4 from "chalk";
2158
+ import { bold as bold4, cyan, cyanBright, green as green2, red } from "ansis";
2218
2159
  function log(msg = "") {
2219
2160
  ui().logger.log(msg);
2220
2161
  }
@@ -2231,14 +2172,14 @@ function logStdoutSummary(report) {
2231
2172
  }
2232
2173
  function reportToHeaderSection(report) {
2233
2174
  const { packageName, version: version2 } = report;
2234
- return `${chalk4.bold(reportHeadlineText)} - ${packageName}@${version2}`;
2175
+ return `${bold4(REPORT_HEADLINE_TEXT)} - ${packageName}@${version2}`;
2235
2176
  }
2236
2177
  function logPlugins(report) {
2237
2178
  const { plugins } = report;
2238
2179
  plugins.forEach((plugin) => {
2239
2180
  const { title, audits } = plugin;
2240
2181
  log();
2241
- log(chalk4.magentaBright.bold(`${title} audits`));
2182
+ log(bold4.magentaBright(`${title} audits`));
2242
2183
  log();
2243
2184
  audits.forEach((audit) => {
2244
2185
  ui().row([
@@ -2253,8 +2194,9 @@ function logPlugins(report) {
2253
2194
  padding: [0, 3, 0, 0]
2254
2195
  },
2255
2196
  {
2256
- text: chalk4.cyanBright(audit.displayValue || `${audit.value}`),
2257
- width: 10,
2197
+ text: cyanBright(audit.displayValue || `${audit.value}`),
2198
+ // eslint-disable-next-line no-magic-numbers
2199
+ width: 20,
2258
2200
  padding: [0, 0, 0, 0]
2259
2201
  }
2260
2202
  ]);
@@ -2264,42 +2206,38 @@ function logPlugins(report) {
2264
2206
  }
2265
2207
  function logCategories({ categories, plugins }) {
2266
2208
  const hAlign = (idx) => idx === 0 ? "left" : "right";
2267
- const rows = categories.map(({ title, score, refs }) => [
2209
+ const rows = categories.map(({ title, score, refs, isBinary }) => [
2268
2210
  title,
2269
- applyScoreColor({ score }),
2211
+ `${binaryIconPrefix(score, isBinary)}${applyScoreColor({ score })}`,
2270
2212
  countCategoryAudits(refs, plugins)
2271
2213
  ]);
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),
2214
+ const table2 = ui().table();
2215
+ table2.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
2216
+ table2.head(
2217
+ REPORT_RAW_OVERVIEW_TABLE_HEADERS.map((heading, idx) => ({
2218
+ content: cyan(heading),
2277
2219
  hAlign: hAlign(idx)
2278
2220
  }))
2279
2221
  );
2280
2222
  rows.forEach(
2281
- (row) => table5.row(
2223
+ (row) => table2.row(
2282
2224
  row.map((content, idx) => ({
2283
2225
  content: content.toString(),
2284
2226
  hAlign: hAlign(idx)
2285
2227
  }))
2286
2228
  )
2287
2229
  );
2288
- log(chalk4.magentaBright.bold("Categories"));
2230
+ log(bold4.magentaBright("Categories"));
2289
2231
  log();
2290
- table5.render();
2232
+ table2.render();
2291
2233
  log();
2292
2234
  }
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);
2235
+ function binaryIconPrefix(score, isBinary) {
2236
+ return targetScoreIcon(score, isBinary ? 1 : void 0, {
2237
+ passIcon: bold4(green2("\u2713")),
2238
+ failIcon: bold4(red("\u2717")),
2239
+ postfix: " "
2240
+ });
2303
2241
  }
2304
2242
 
2305
2243
  // packages/utils/src/lib/reports/scoring.ts
@@ -2389,56 +2327,6 @@ function parseScoringParameters(refs, scoreFn) {
2389
2327
  return scoredRefs;
2390
2328
  }
2391
2329
 
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
2330
  // packages/utils/src/lib/verbose-utils.ts
2443
2331
  function getLogVerbose(verbose = false) {
2444
2332
  return (msg) => {
@@ -2461,10 +2349,10 @@ var verboseUtils = (verbose = false) => ({
2461
2349
 
2462
2350
  // packages/core/package.json
2463
2351
  var name = "@code-pushup/core";
2464
- var version = "0.48.0";
2352
+ var version = "0.50.0";
2465
2353
 
2466
2354
  // packages/core/src/lib/implementation/execute-plugin.ts
2467
- import chalk5 from "chalk";
2355
+ import { bold as bold5 } from "ansis";
2468
2356
 
2469
2357
  // packages/core/src/lib/normalize.ts
2470
2358
  function normalizeIssue(issue, gitRoot) {
@@ -2527,7 +2415,7 @@ async function executeRunnerFunction(runner, onProgress) {
2527
2415
  var PluginOutputMissingAuditError = class extends Error {
2528
2416
  constructor(auditSlug) {
2529
2417
  super(
2530
- `Audit metadata not present in plugin config. Missing slug: ${chalk5.bold(
2418
+ `Audit metadata not present in plugin config. Missing slug: ${bold5(
2531
2419
  auditSlug
2532
2420
  )}`
2533
2421
  );
@@ -2569,7 +2457,7 @@ async function executePlugin(pluginConfig, onProgress) {
2569
2457
  };
2570
2458
  }
2571
2459
  var wrapProgress = async (pluginCfg, steps, progressBar) => {
2572
- progressBar?.updateTitle(`Executing ${chalk5.bold(pluginCfg.title)}`);
2460
+ progressBar?.updateTitle(`Executing ${bold5(pluginCfg.title)}`);
2573
2461
  try {
2574
2462
  const pluginReport = await executePlugin(pluginCfg);
2575
2463
  progressBar?.incrementInSteps(steps);
@@ -2577,7 +2465,7 @@ var wrapProgress = async (pluginCfg, steps, progressBar) => {
2577
2465
  } catch (error) {
2578
2466
  progressBar?.incrementInSteps(steps);
2579
2467
  throw new Error(
2580
- error instanceof Error ? `- Plugin ${chalk5.bold(pluginCfg.title)} (${chalk5.bold(
2468
+ error instanceof Error ? `- Plugin ${bold5(pluginCfg.title)} (${bold5(
2581
2469
  pluginCfg.slug
2582
2470
  )}) produced the following error:
2583
2471
  - ${error.message}` : String(error)
@@ -2717,6 +2605,10 @@ async function collectAndPersistReports(options) {
2717
2605
  // packages/core/src/lib/compare.ts
2718
2606
  import { writeFile as writeFile2 } from "node:fs/promises";
2719
2607
  import { join as join5 } from "node:path";
2608
+ import {
2609
+ PortalOperationError,
2610
+ getPortalComparisonLink
2611
+ } from "@code-pushup/portal-client";
2720
2612
 
2721
2613
  // packages/core/src/lib/implementation/compare-scorables.ts
2722
2614
  function compareCategories(reports) {
@@ -2853,7 +2745,7 @@ function selectMeta(meta) {
2853
2745
  }
2854
2746
 
2855
2747
  // packages/core/src/lib/compare.ts
2856
- async function compareReportFiles(inputPaths, persistConfig) {
2748
+ async function compareReportFiles(inputPaths, persistConfig, uploadConfig, label) {
2857
2749
  const { outputDir, filename, format } = persistConfig;
2858
2750
  const [reportBefore, reportAfter] = await Promise.all([
2859
2751
  readJsonFile(inputPaths.before),
@@ -2863,11 +2755,20 @@ async function compareReportFiles(inputPaths, persistConfig) {
2863
2755
  before: reportSchema.parse(reportBefore),
2864
2756
  after: reportSchema.parse(reportAfter)
2865
2757
  };
2866
- const reportsDiff = compareReports(reports);
2758
+ const diff = compareReports(reports);
2759
+ if (label) {
2760
+ diff.label = label;
2761
+ }
2762
+ if (uploadConfig && diff.commits) {
2763
+ diff.portalUrl = await fetchPortalComparisonLink(
2764
+ uploadConfig,
2765
+ diff.commits
2766
+ );
2767
+ }
2867
2768
  return Promise.all(
2868
2769
  format.map(async (fmt) => {
2869
2770
  const outputPath = join5(outputDir, `${filename}-diff.${fmt}`);
2870
- const content = reportsDiffToFileContent(reportsDiff, fmt);
2771
+ const content = reportsDiffToFileContent(diff, fmt);
2871
2772
  await ensureDirectoryExists(outputDir);
2872
2773
  await writeFile2(outputPath, content);
2873
2774
  return outputPath;
@@ -2905,6 +2806,29 @@ function reportsDiffToFileContent(reportsDiff, format) {
2905
2806
  return generateMdReportsDiff(reportsDiff);
2906
2807
  }
2907
2808
  }
2809
+ async function fetchPortalComparisonLink(uploadConfig, commits) {
2810
+ const { server, apiKey, organization, project } = uploadConfig;
2811
+ try {
2812
+ return await getPortalComparisonLink({
2813
+ server,
2814
+ apiKey,
2815
+ parameters: {
2816
+ organization,
2817
+ project,
2818
+ before: commits.before.hash,
2819
+ after: commits.after.hash
2820
+ }
2821
+ });
2822
+ } catch (error) {
2823
+ if (error instanceof PortalOperationError) {
2824
+ ui().logger.warning(
2825
+ `Failed to fetch portal comparison link - ${error.message}`
2826
+ );
2827
+ return void 0;
2828
+ }
2829
+ throw error;
2830
+ }
2831
+ }
2908
2832
 
2909
2833
  // packages/core/src/lib/upload.ts
2910
2834
  import {
@@ -2960,9 +2884,9 @@ function auditToGQL(audit) {
2960
2884
  score,
2961
2885
  value,
2962
2886
  displayValue: formattedValue,
2963
- details: details4
2887
+ details: details2
2964
2888
  } = audit;
2965
- const { issues, table: table5 } = details4 ?? {};
2889
+ const { issues, table: table2 } = details2 ?? {};
2966
2890
  return {
2967
2891
  slug,
2968
2892
  title,
@@ -2971,10 +2895,10 @@ function auditToGQL(audit) {
2971
2895
  score,
2972
2896
  value,
2973
2897
  formattedValue,
2974
- ...details4 && {
2898
+ ...details2 && {
2975
2899
  details: {
2976
2900
  ...issues && { issues: issues.map(issueToGQL) },
2977
- ...table5 && { tables: [tableToGQL(table5)] }
2901
+ ...table2 && { tables: [tableToGQL(table2)] }
2978
2902
  }
2979
2903
  }
2980
2904
  };
@@ -2993,11 +2917,11 @@ function issueToGQL(issue) {
2993
2917
  }
2994
2918
  };
2995
2919
  }
2996
- function tableToGQL(table5) {
2920
+ function tableToGQL(table2) {
2997
2921
  return {
2998
- ...table5.title && { title: table5.title },
2999
- ...table5.columns?.length && {
3000
- columns: table5.columns.map(
2922
+ ...table2.title && { title: table2.title },
2923
+ ...table2.columns?.length && {
2924
+ columns: table2.columns.map(
3001
2925
  (column) => typeof column === "string" ? { alignment: tableAlignmentToGQL(column) } : {
3002
2926
  key: column.key,
3003
2927
  label: column.label,
@@ -3005,7 +2929,7 @@ function tableToGQL(table5) {
3005
2929
  }
3006
2930
  )
3007
2931
  },
3008
- rows: table5.rows.map(
2932
+ rows: table2.rows.map(
3009
2933
  (row) => Array.isArray(row) ? row.map((content) => ({ content: content?.toString() ?? "" })) : Object.entries(row).map(([key, content]) => ({
3010
2934
  key,
3011
2935
  content: content?.toString() ?? ""
@@ -3151,6 +3075,45 @@ async function autoloadRc(tsconfig) {
3151
3075
  tsconfig
3152
3076
  );
3153
3077
  }
3078
+
3079
+ // packages/core/src/lib/merge-diffs.ts
3080
+ import { writeFile as writeFile3 } from "node:fs/promises";
3081
+ import { basename, dirname, join as join7 } from "node:path";
3082
+ async function mergeDiffs(files, persistConfig) {
3083
+ const results = await Promise.allSettled(
3084
+ files.map(async (file) => {
3085
+ const json = await readJsonFile(file).catch((error) => {
3086
+ throw new Error(
3087
+ `Failed to read JSON file ${file} - ${stringifyError(error)}`
3088
+ );
3089
+ });
3090
+ const result = await reportsDiffSchema.safeParseAsync(json);
3091
+ if (!result.success) {
3092
+ throw new Error(
3093
+ `Invalid reports diff in ${file} - ${result.error.message}`
3094
+ );
3095
+ }
3096
+ return { ...result.data, file };
3097
+ })
3098
+ );
3099
+ results.filter(isPromiseRejectedResult).forEach(({ reason }) => {
3100
+ ui().logger.warning(
3101
+ `Skipped invalid report diff - ${stringifyError(reason)}`
3102
+ );
3103
+ });
3104
+ const diffs = results.filter(isPromiseFulfilledResult).map(({ value }) => value);
3105
+ const labeledDiffs = diffs.map((diff) => ({
3106
+ ...diff,
3107
+ label: diff.label || basename(dirname(diff.file))
3108
+ // fallback is parent folder name
3109
+ }));
3110
+ const markdown = generateMdReportsDiffForMonorepo(labeledDiffs);
3111
+ const { outputDir, filename } = persistConfig;
3112
+ const outputPath = join7(outputDir, `${filename}-diff.md`);
3113
+ await ensureDirectoryExists(outputDir);
3114
+ await writeFile3(outputPath, markdown);
3115
+ return outputPath;
3116
+ }
3154
3117
  export {
3155
3118
  ConfigPathError,
3156
3119
  PersistDirError,
@@ -3164,6 +3127,7 @@ export {
3164
3127
  executePlugin,
3165
3128
  executePlugins,
3166
3129
  history,
3130
+ mergeDiffs,
3167
3131
  persistReport,
3168
3132
  readRcByPath,
3169
3133
  upload