@code-pushup/cli 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.
Files changed (29) hide show
  1. package/README.md +20 -5
  2. package/index.js +1331 -1276
  3. package/package.json +7 -33
  4. package/src/lib/autorun/autorun-command.d.ts +1 -1
  5. package/src/lib/collect/collect-command.d.ts +1 -1
  6. package/src/lib/commands.d.ts +1 -1
  7. package/src/lib/compare/compare-command.d.ts +1 -2
  8. package/src/lib/history/history.model.d.ts +2 -2
  9. package/src/lib/history/history.options.d.ts +2 -2
  10. package/src/lib/history/utils.d.ts +2 -2
  11. package/src/lib/implementation/compare.model.d.ts +4 -2
  12. package/src/lib/implementation/compare.options.d.ts +1 -1
  13. package/src/lib/implementation/core-config.middleware.d.ts +8 -6
  14. package/src/lib/implementation/core-config.options.d.ts +2 -2
  15. package/src/lib/implementation/formatting.d.ts +11 -0
  16. package/src/lib/implementation/global.model.d.ts +2 -2
  17. package/src/lib/implementation/global.options.d.ts +2 -2
  18. package/src/lib/implementation/merge-diffs.model.d.ts +3 -0
  19. package/src/lib/implementation/merge-diffs.options.d.ts +3 -0
  20. package/src/lib/implementation/only-plugins.middleware.d.ts +1 -1
  21. package/src/lib/implementation/only-plugins.model.d.ts +2 -2
  22. package/src/lib/implementation/only-plugins.options.d.ts +3 -2
  23. package/src/lib/implementation/skip-plugins.middleware.d.ts +1 -1
  24. package/src/lib/implementation/skip-plugins.model.d.ts +2 -2
  25. package/src/lib/implementation/skip-plugins.options.d.ts +3 -2
  26. package/src/lib/merge-diffs/merge-diffs-command.d.ts +6 -0
  27. package/src/lib/middlewares.d.ts +1 -1
  28. package/src/lib/upload/upload-command.d.ts +1 -1
  29. package/src/lib/yargs-cli.d.ts +14 -1
package/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { hideBin } from "yargs/helpers";
5
5
 
6
6
  // packages/cli/src/lib/autorun/autorun-command.ts
7
- import chalk7 from "chalk";
7
+ import { bold as bold7, gray as gray4 } from "ansis";
8
8
 
9
9
  // packages/models/src/lib/implementation/schemas.ts
10
10
  import { MATERIAL_ICONS } from "vscode-material-icons";
@@ -683,6 +683,8 @@ var auditResultSchema = scorableWithPluginMetaSchema.merge(
683
683
  );
684
684
  var reportsDiffSchema = z15.object({
685
685
  commits: makeComparisonSchema(commitSchema).nullable().describe("Commits identifying compared reports"),
686
+ portalUrl: urlSchema.optional().describe("Link to comparison page in Code PushUp portal"),
687
+ label: z15.string().optional().describe("Label (e.g. project name)"),
686
688
  categories: makeArraysComparisonSchema(
687
689
  categoryDiffSchema,
688
690
  categoryResultSchema,
@@ -752,15 +754,286 @@ function comparePairs(pairs, equalsFn) {
752
754
  );
753
755
  }
754
756
 
757
+ // packages/utils/src/lib/errors.ts
758
+ function stringifyError(error) {
759
+ if (error instanceof Error) {
760
+ if (error.name === "Error" || error.message.startsWith(error.name)) {
761
+ return error.message;
762
+ }
763
+ return `${error.name}: ${error.message}`;
764
+ }
765
+ if (typeof error === "string") {
766
+ return error;
767
+ }
768
+ return JSON.stringify(error);
769
+ }
770
+
755
771
  // packages/utils/src/lib/execute-process.ts
756
- import { spawn } from "node:child_process";
772
+ import {
773
+ spawn
774
+ } from "node:child_process";
775
+
776
+ // packages/utils/src/lib/reports/utils.ts
777
+ import ansis from "ansis";
778
+ import { md } from "build-md";
779
+
780
+ // packages/utils/src/lib/reports/constants.ts
781
+ var TERMINAL_WIDTH = 80;
782
+ var SCORE_COLOR_RANGE = {
783
+ GREEN_MIN: 0.9,
784
+ YELLOW_MIN: 0.5
785
+ };
786
+ var FOOTER_PREFIX = "Made with \u2764 by";
787
+ var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
788
+ var README_LINK = "https://github.com/code-pushup/cli#readme";
789
+ var REPORT_HEADLINE_TEXT = "Code PushUp Report";
790
+ var REPORT_RAW_OVERVIEW_TABLE_HEADERS = [
791
+ "Category",
792
+ "Score",
793
+ "Audits"
794
+ ];
757
795
 
758
796
  // packages/utils/src/lib/reports/utils.ts
759
- import { join } from "node:path";
797
+ function formatReportScore(score) {
798
+ const scaledScore = score * 100;
799
+ const roundedScore = Math.round(scaledScore);
800
+ return roundedScore === 100 && score !== 1 ? Math.floor(scaledScore).toString() : roundedScore.toString();
801
+ }
802
+ function formatScoreWithColor(score, options2) {
803
+ const styledNumber = options2?.skipBold ? formatReportScore(score) : md.bold(formatReportScore(score));
804
+ return md`${scoreMarker(score)} ${styledNumber}`;
805
+ }
806
+ var MARKERS = {
807
+ circle: {
808
+ red: "\u{1F534}",
809
+ yellow: "\u{1F7E1}",
810
+ green: "\u{1F7E2}"
811
+ },
812
+ square: {
813
+ red: "\u{1F7E5}",
814
+ yellow: "\u{1F7E8}",
815
+ green: "\u{1F7E9}"
816
+ }
817
+ };
818
+ function scoreMarker(score, markerType = "circle") {
819
+ if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
820
+ return MARKERS[markerType].green;
821
+ }
822
+ if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
823
+ return MARKERS[markerType].yellow;
824
+ }
825
+ return MARKERS[markerType].red;
826
+ }
827
+ function getDiffMarker(diff) {
828
+ if (diff > 0) {
829
+ return "\u2191";
830
+ }
831
+ if (diff < 0) {
832
+ return "\u2193";
833
+ }
834
+ return "";
835
+ }
836
+ function colorByScoreDiff(text, diff) {
837
+ const color = diff > 0 ? "green" : diff < 0 ? "red" : "gray";
838
+ return shieldsBadge(text, color);
839
+ }
840
+ function shieldsBadge(text, color) {
841
+ return md.image(
842
+ `https://img.shields.io/badge/${encodeURIComponent(text)}-${color}`,
843
+ text
844
+ );
845
+ }
846
+ function formatDiffNumber(diff) {
847
+ const number = Math.abs(diff) === Number.POSITIVE_INFINITY ? "\u221E" : `${Math.abs(diff)}`;
848
+ const sign = diff < 0 ? "\u2212" : "+";
849
+ return `${sign}${number}`;
850
+ }
851
+ function severityMarker(severity) {
852
+ if (severity === "error") {
853
+ return "\u{1F6A8}";
854
+ }
855
+ if (severity === "warning") {
856
+ return "\u26A0\uFE0F";
857
+ }
858
+ return "\u2139\uFE0F";
859
+ }
860
+ function formatScoreChange(diff) {
861
+ const marker = getDiffMarker(diff);
862
+ const text = formatDiffNumber(Math.round(diff * 1e3) / 10);
863
+ return colorByScoreDiff(`${marker} ${text}`, diff);
864
+ }
865
+ function formatValueChange({
866
+ values,
867
+ scores
868
+ }) {
869
+ const marker = getDiffMarker(values.diff);
870
+ const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : Math.round(100 * values.diff / values.before);
871
+ const text = `${formatDiffNumber(percentage)}\u2009%`;
872
+ return colorByScoreDiff(`${marker} ${text}`, scores.diff);
873
+ }
874
+ function calcDuration(start, stop) {
875
+ return Math.round((stop ?? performance.now()) - start);
876
+ }
877
+ function countCategoryAudits(refs, plugins) {
878
+ const groupLookup = plugins.reduce(
879
+ (lookup, plugin) => {
880
+ if (plugin.groups == null || plugin.groups.length === 0) {
881
+ return lookup;
882
+ }
883
+ return {
884
+ ...lookup,
885
+ [plugin.slug]: Object.fromEntries(
886
+ plugin.groups.map((group) => [group.slug, group])
887
+ )
888
+ };
889
+ },
890
+ {}
891
+ );
892
+ return refs.reduce((acc, ref) => {
893
+ if (ref.type === "group") {
894
+ const groupRefs = groupLookup[ref.plugin]?.[ref.slug]?.refs;
895
+ return acc + (groupRefs?.length ?? 0);
896
+ }
897
+ return acc + 1;
898
+ }, 0);
899
+ }
900
+ function compareCategoryAuditsAndGroups(a, b) {
901
+ if (a.score !== b.score) {
902
+ return a.score - b.score;
903
+ }
904
+ if (a.weight !== b.weight) {
905
+ return b.weight - a.weight;
906
+ }
907
+ if ("value" in a && "value" in b && a.value !== b.value) {
908
+ return b.value - a.value;
909
+ }
910
+ return a.title.localeCompare(b.title);
911
+ }
912
+ function compareAudits(a, b) {
913
+ if (a.score !== b.score) {
914
+ return a.score - b.score;
915
+ }
916
+ if (a.value !== b.value) {
917
+ return b.value - a.value;
918
+ }
919
+ return a.title.localeCompare(b.title);
920
+ }
921
+ function compareIssueSeverity(severity1, severity2) {
922
+ const levels = {
923
+ info: 0,
924
+ warning: 1,
925
+ error: 2
926
+ };
927
+ return levels[severity1] - levels[severity2];
928
+ }
929
+ function throwIsNotPresentError(itemName, presentPlace) {
930
+ throw new Error(`${itemName} is not present in ${presentPlace}`);
931
+ }
932
+ function getPluginNameFromSlug(slug, plugins) {
933
+ return plugins.find(({ slug: pluginSlug }) => pluginSlug === slug)?.title || slug;
934
+ }
935
+ function compareIssues(a, b) {
936
+ if (a.severity !== b.severity) {
937
+ return -compareIssueSeverity(a.severity, b.severity);
938
+ }
939
+ if (!a.source && b.source) {
940
+ return -1;
941
+ }
942
+ if (a.source && !b.source) {
943
+ return 1;
944
+ }
945
+ if (a.source?.file !== b.source?.file) {
946
+ return a.source?.file.localeCompare(b.source?.file || "") ?? 0;
947
+ }
948
+ if (!a.source?.position && b.source?.position) {
949
+ return -1;
950
+ }
951
+ if (a.source?.position && !b.source?.position) {
952
+ return 1;
953
+ }
954
+ if (a.source?.position?.startLine !== b.source?.position?.startLine) {
955
+ return (a.source?.position?.startLine ?? 0) - (b.source?.position?.startLine ?? 0);
956
+ }
957
+ return 0;
958
+ }
959
+ function applyScoreColor({ score, text }, style = ansis) {
960
+ const formattedScore = text ?? formatReportScore(score);
961
+ if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
962
+ return text ? style.green(formattedScore) : style.bold(style.green(formattedScore));
963
+ }
964
+ if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
965
+ return text ? style.yellow(formattedScore) : style.bold(style.yellow(formattedScore));
966
+ }
967
+ return text ? style.red(formattedScore) : style.bold(style.red(formattedScore));
968
+ }
969
+ function targetScoreIcon(score, targetScore, options2 = {}) {
970
+ if (targetScore != null) {
971
+ const {
972
+ passIcon = "\u2705",
973
+ failIcon = "\u274C",
974
+ prefix = "",
975
+ postfix = ""
976
+ } = options2;
977
+ if (score >= targetScore) {
978
+ return `${prefix}${passIcon}${postfix}`;
979
+ }
980
+ return `${prefix}${failIcon}${postfix}`;
981
+ }
982
+ return "";
983
+ }
984
+
985
+ // packages/utils/src/lib/execute-process.ts
986
+ var ProcessError = class extends Error {
987
+ code;
988
+ stderr;
989
+ stdout;
990
+ constructor(result) {
991
+ super(result.stderr);
992
+ this.code = result.code;
993
+ this.stderr = result.stderr;
994
+ this.stdout = result.stdout;
995
+ }
996
+ };
997
+ function executeProcess(cfg) {
998
+ const { command: command2, args, observer, ignoreExitCode = false, ...options2 } = cfg;
999
+ const { onStdout, onStderr, onError, onComplete } = observer ?? {};
1000
+ const date = (/* @__PURE__ */ new Date()).toISOString();
1001
+ const start = performance.now();
1002
+ return new Promise((resolve, reject) => {
1003
+ const spawnedProcess = spawn(command2, args ?? [], {
1004
+ shell: true,
1005
+ ...options2
1006
+ });
1007
+ let stdout = "";
1008
+ let stderr = "";
1009
+ spawnedProcess.stdout.on("data", (data) => {
1010
+ stdout += String(data);
1011
+ onStdout?.(String(data), spawnedProcess);
1012
+ });
1013
+ spawnedProcess.stderr.on("data", (data) => {
1014
+ stderr += String(data);
1015
+ onStderr?.(String(data), spawnedProcess);
1016
+ });
1017
+ spawnedProcess.on("error", (err) => {
1018
+ stderr += err.toString();
1019
+ });
1020
+ spawnedProcess.on("close", (code2) => {
1021
+ const timings = { date, duration: calcDuration(start) };
1022
+ if (code2 === 0 || ignoreExitCode) {
1023
+ onComplete?.();
1024
+ resolve({ code: code2, stdout, stderr, ...timings });
1025
+ } else {
1026
+ const errorMsg = new ProcessError({ code: code2, stdout, stderr, ...timings });
1027
+ onError?.(errorMsg);
1028
+ reject(errorMsg);
1029
+ }
1030
+ });
1031
+ });
1032
+ }
760
1033
 
761
1034
  // packages/utils/src/lib/file-system.ts
1035
+ import { bold, gray } from "ansis";
762
1036
  import { bundleRequire } from "bundle-require";
763
- import chalk2 from "chalk";
764
1037
  import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
765
1038
 
766
1039
  // packages/utils/src/lib/formatting.ts
@@ -823,55 +1096,7 @@ function isPromiseRejectedResult(result) {
823
1096
  // packages/utils/src/lib/logging.ts
824
1097
  import isaacs_cliui from "@isaacs/cliui";
825
1098
  import { cliui } from "@poppinss/cliui";
826
- import chalk from "chalk";
827
-
828
- // packages/utils/src/lib/reports/constants.ts
829
- var TERMINAL_WIDTH = 80;
830
- var SCORE_COLOR_RANGE = {
831
- GREEN_MIN: 0.9,
832
- YELLOW_MIN: 0.5
833
- };
834
- var CATEGORIES_TITLE = "\u{1F3F7} Categories";
835
- var FOOTER_PREFIX = "Made with \u2764 by";
836
- var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
837
- var README_LINK = "https://github.com/code-pushup/cli#readme";
838
- var reportHeadlineText = "Code PushUp Report";
839
- var reportOverviewTableHeaders = [
840
- {
841
- key: "category",
842
- label: "\u{1F3F7} Category",
843
- align: "left"
844
- },
845
- {
846
- key: "score",
847
- label: "\u2B50 Score"
848
- },
849
- {
850
- key: "audits",
851
- label: "\u{1F6E1} Audits"
852
- }
853
- ];
854
- var reportRawOverviewTableHeaders = ["Category", "Score", "Audits"];
855
- var issuesTableHeadings = [
856
- {
857
- key: "severity",
858
- label: "Severity"
859
- },
860
- {
861
- key: "message",
862
- label: "Message"
863
- },
864
- {
865
- key: "file",
866
- label: "Source file"
867
- },
868
- {
869
- key: "line",
870
- label: "Line(s)"
871
- }
872
- ];
873
-
874
- // packages/utils/src/lib/logging.ts
1099
+ import { underline } from "ansis";
875
1100
  var singletonUiInstance;
876
1101
  function ui() {
877
1102
  if (singletonUiInstance === void 0) {
@@ -895,7 +1120,7 @@ function logListItem(args) {
895
1120
  singletonUiInstance?.logger.log(content);
896
1121
  }
897
1122
  function link(text) {
898
- return chalk.underline(chalk.blueBright(text));
1123
+ return underline.blueBright(text);
899
1124
  }
900
1125
 
901
1126
  // packages/utils/src/lib/log-results.ts
@@ -970,10 +1195,10 @@ async function ensureDirectoryExists(baseDir) {
970
1195
  function logMultipleFileResults(fileResults, messagePrefix) {
971
1196
  const succeededTransform = (result) => {
972
1197
  const [fileName, size] = result.value;
973
- const formattedSize = size ? ` (${chalk2.gray(formatBytes(size))})` : "";
974
- return `- ${chalk2.bold(fileName)}${formattedSize}`;
1198
+ const formattedSize = size ? ` (${gray(formatBytes(size))})` : "";
1199
+ return `- ${bold(fileName)}${formattedSize}`;
975
1200
  };
976
- const failedTransform = (result) => `- ${chalk2.bold(result.reason)}`;
1201
+ const failedTransform = (result) => `- ${bold(result.reason)}`;
977
1202
  logMultipleResults(
978
1203
  fileResults,
979
1204
  messagePrefix,
@@ -989,38 +1214,17 @@ async function importModule(options2) {
989
1214
  return mod;
990
1215
  }
991
1216
 
992
- // packages/utils/src/lib/text-formats/constants.ts
993
- var NEW_LINE = "\n";
994
- var TAB = " ";
995
- var SPACE = " ";
996
-
997
- // packages/utils/src/lib/text-formats/html/details.ts
998
- function details(title, content, cfg = { open: false }) {
999
- 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.
1000
- NEW_LINE}${content}${NEW_LINE}${// @TODO in the future we could consider adding it only if the content ends with a code block
1001
- // ⚠️ The blank line ensure Markdown in content is rendered correctly.
1002
- NEW_LINE}</details>${// ⚠️ The blank line is needed to ensure Markdown after details is rendered correctly.
1003
- NEW_LINE}`;
1004
- }
1005
-
1006
- // packages/utils/src/lib/text-formats/html/font-style.ts
1007
- var boldElement = "b";
1008
- function bold(text) {
1009
- return `<${boldElement}>${text}</${boldElement}>`;
1010
- }
1011
- var italicElement = "i";
1012
- function italic(text) {
1013
- return `<${italicElement}>${text}</${italicElement}>`;
1014
- }
1015
- var codeElement = "code";
1016
- function code(text) {
1017
- return `<${codeElement}>${text}</${codeElement}>`;
1217
+ // packages/utils/src/lib/filter.ts
1218
+ function filterItemRefsBy(items, refFilterFn) {
1219
+ return items.map((item) => ({
1220
+ ...item,
1221
+ refs: item.refs.filter(refFilterFn)
1222
+ })).filter((item) => item.refs.length);
1018
1223
  }
1019
1224
 
1020
- // packages/utils/src/lib/text-formats/html/link.ts
1021
- function link2(href, text) {
1022
- return `<a href="${href}">${text || href}</a>`;
1023
- }
1225
+ // packages/utils/src/lib/git/git.ts
1226
+ import { isAbsolute, join, relative } from "node:path";
1227
+ import { simpleGit } from "simple-git";
1024
1228
 
1025
1229
  // packages/utils/src/lib/transform.ts
1026
1230
  function toArray(val) {
@@ -1041,6 +1245,262 @@ function capitalize(text) {
1041
1245
  )}`;
1042
1246
  }
1043
1247
 
1248
+ // packages/utils/src/lib/git/git.ts
1249
+ function getGitRoot(git = simpleGit()) {
1250
+ return git.revparse("--show-toplevel");
1251
+ }
1252
+ function formatGitPath(path, gitRoot) {
1253
+ const absolutePath = isAbsolute(path) ? path : join(process.cwd(), path);
1254
+ const relativePath = relative(gitRoot, absolutePath);
1255
+ return toUnixPath(relativePath);
1256
+ }
1257
+ var GitStatusError = class _GitStatusError extends Error {
1258
+ static ignoredProps = /* @__PURE__ */ new Set(["current", "tracking"]);
1259
+ static getReducedStatus(status) {
1260
+ return Object.fromEntries(
1261
+ Object.entries(status).filter(([key]) => !this.ignoredProps.has(key)).filter(
1262
+ (entry) => {
1263
+ const value = entry[1];
1264
+ if (value == null) {
1265
+ return false;
1266
+ }
1267
+ if (Array.isArray(value) && value.length === 0) {
1268
+ return false;
1269
+ }
1270
+ if (typeof value === "number" && value === 0) {
1271
+ return false;
1272
+ }
1273
+ return !(typeof value === "boolean" && !value);
1274
+ }
1275
+ )
1276
+ );
1277
+ }
1278
+ constructor(status) {
1279
+ super(
1280
+ `Working directory needs to be clean before we you can proceed. Commit your local changes or stash them:
1281
+ ${JSON.stringify(
1282
+ _GitStatusError.getReducedStatus(status),
1283
+ null,
1284
+ 2
1285
+ )}`
1286
+ );
1287
+ }
1288
+ };
1289
+ async function guardAgainstLocalChanges(git = simpleGit()) {
1290
+ const status = await git.status(["-s"]);
1291
+ if (status.files.length > 0) {
1292
+ throw new GitStatusError(status);
1293
+ }
1294
+ }
1295
+ async function safeCheckout(branchOrHash, forceCleanStatus = false, git = simpleGit()) {
1296
+ if (forceCleanStatus) {
1297
+ await git.raw(["reset", "--hard"]);
1298
+ await git.clean(["f", "d"]);
1299
+ ui().logger.info(`git status cleaned`);
1300
+ }
1301
+ await guardAgainstLocalChanges(git);
1302
+ await git.checkout(branchOrHash);
1303
+ }
1304
+
1305
+ // packages/utils/src/lib/git/git.commits-and-tags.ts
1306
+ import { simpleGit as simpleGit2 } from "simple-git";
1307
+
1308
+ // packages/utils/src/lib/semver.ts
1309
+ import { rcompare, valid } from "semver";
1310
+ function normalizeSemver(semverString) {
1311
+ if (semverString.startsWith("v") || semverString.startsWith("V")) {
1312
+ return semverString.slice(1);
1313
+ }
1314
+ if (semverString.includes("@")) {
1315
+ return semverString.split("@").at(-1) ?? "";
1316
+ }
1317
+ return semverString;
1318
+ }
1319
+ function isSemver(semverString = "") {
1320
+ return valid(normalizeSemver(semverString)) != null;
1321
+ }
1322
+
1323
+ // packages/utils/src/lib/git/git.commits-and-tags.ts
1324
+ async function getLatestCommit(git = simpleGit2()) {
1325
+ const log2 = await git.log({
1326
+ maxCount: 1,
1327
+ // git log -1 --pretty=format:"%H %s %an %aI" - See: https://git-scm.com/docs/pretty-formats
1328
+ format: { hash: "%H", message: "%s", author: "%an", date: "%aI" }
1329
+ });
1330
+ return commitSchema.parse(log2.latest);
1331
+ }
1332
+ async function getCurrentBranchOrTag(git = simpleGit2()) {
1333
+ return await git.branch().then((r) => r.current) || // If no current branch, try to get the tag
1334
+ // @TODO use simple git
1335
+ await git.raw(["describe", "--tags", "--exact-match"]).then((out) => out.trim());
1336
+ }
1337
+ function validateFilter({ from, to }) {
1338
+ if (to && !from) {
1339
+ throw new Error(
1340
+ `filter needs the "from" option defined to accept the "to" option.
1341
+ `
1342
+ );
1343
+ }
1344
+ }
1345
+ function filterLogs(allTags, opt) {
1346
+ if (!opt) {
1347
+ return allTags;
1348
+ }
1349
+ validateFilter(opt);
1350
+ const { from, to, maxCount } = opt;
1351
+ const finIndex = (tagName, fallback) => {
1352
+ const idx = allTags.indexOf(tagName ?? "");
1353
+ if (idx > -1) {
1354
+ return idx;
1355
+ }
1356
+ return fallback;
1357
+ };
1358
+ const fromIndex = finIndex(from, 0);
1359
+ const toIndex = finIndex(to, void 0);
1360
+ return allTags.slice(fromIndex, toIndex ? toIndex + 1 : toIndex).slice(0, maxCount ?? void 0);
1361
+ }
1362
+ async function getHashFromTag(tag, git = simpleGit2()) {
1363
+ const tagDetails = await git.show(["--no-patch", "--format=%H", tag]);
1364
+ const hash = tagDetails.trim();
1365
+ return {
1366
+ hash: hash.split("\n").at(-1) ?? "",
1367
+ message: tag
1368
+ };
1369
+ }
1370
+ async function getSemverTags(opt = {}, git = simpleGit2()) {
1371
+ validateFilter(opt);
1372
+ const { targetBranch, ...options2 } = opt;
1373
+ let currentBranch;
1374
+ if (targetBranch) {
1375
+ currentBranch = await getCurrentBranchOrTag(git);
1376
+ await git.checkout(targetBranch);
1377
+ }
1378
+ const tagsRaw = await git.tag([
1379
+ "--merged",
1380
+ targetBranch ?? await getCurrentBranchOrTag(git)
1381
+ ]);
1382
+ const allTags = tagsRaw.split(/\n/).map((tag) => tag.trim()).filter(Boolean).filter(isSemver);
1383
+ const relevantTags = filterLogs(allTags, options2);
1384
+ const tagsWithHashes = await Promise.all(
1385
+ relevantTags.map((tag) => getHashFromTag(tag, git))
1386
+ );
1387
+ if (currentBranch) {
1388
+ await git.checkout(currentBranch);
1389
+ }
1390
+ return tagsWithHashes;
1391
+ }
1392
+ async function getHashes(options2 = {}, git = simpleGit2()) {
1393
+ const { targetBranch, from, to, maxCount, ...opt } = options2;
1394
+ validateFilter({ from, to });
1395
+ let currentBranch;
1396
+ if (targetBranch) {
1397
+ currentBranch = await getCurrentBranchOrTag(git);
1398
+ await git.checkout(targetBranch);
1399
+ }
1400
+ const logs = await git.log({
1401
+ ...opt,
1402
+ format: {
1403
+ hash: "%H",
1404
+ message: "%s"
1405
+ },
1406
+ from,
1407
+ to,
1408
+ maxCount
1409
+ });
1410
+ if (targetBranch) {
1411
+ await git.checkout(currentBranch);
1412
+ }
1413
+ return [...logs.all];
1414
+ }
1415
+
1416
+ // packages/utils/src/lib/group-by-status.ts
1417
+ function groupByStatus(results) {
1418
+ return results.reduce(
1419
+ (acc, result) => result.status === "fulfilled" ? { ...acc, fulfilled: [...acc.fulfilled, result] } : { ...acc, rejected: [...acc.rejected, result] },
1420
+ { fulfilled: [], rejected: [] }
1421
+ );
1422
+ }
1423
+
1424
+ // packages/utils/src/lib/progress.ts
1425
+ import { black, bold as bold2, gray as gray2, green } from "ansis";
1426
+ import { MultiProgressBars } from "multi-progress-bars";
1427
+ var barStyles = {
1428
+ active: (s) => green(s),
1429
+ done: (s) => gray2(s),
1430
+ idle: (s) => gray2(s)
1431
+ };
1432
+ var messageStyles = {
1433
+ active: (s) => black(s),
1434
+ done: (s) => bold2.green(s),
1435
+ idle: (s) => gray2(s)
1436
+ };
1437
+ var mpb;
1438
+ function getSingletonProgressBars(options2) {
1439
+ if (!mpb) {
1440
+ mpb = new MultiProgressBars({
1441
+ progressWidth: TERMINAL_WIDTH,
1442
+ initMessage: "",
1443
+ border: true,
1444
+ ...options2
1445
+ });
1446
+ }
1447
+ return mpb;
1448
+ }
1449
+ function getProgressBar(taskName) {
1450
+ const tasks = getSingletonProgressBars();
1451
+ tasks.addTask(taskName, {
1452
+ type: "percentage",
1453
+ percentage: 0,
1454
+ message: "",
1455
+ barTransformFn: barStyles.idle
1456
+ });
1457
+ return {
1458
+ incrementInSteps: (numPlugins) => {
1459
+ tasks.incrementTask(taskName, {
1460
+ percentage: 1 / numPlugins,
1461
+ barTransformFn: barStyles.active
1462
+ });
1463
+ },
1464
+ updateTitle: (title) => {
1465
+ tasks.updateTask(taskName, {
1466
+ message: title,
1467
+ barTransformFn: barStyles.active
1468
+ });
1469
+ },
1470
+ endProgress: (message = "") => {
1471
+ tasks.done(taskName, {
1472
+ message: messageStyles.done(message),
1473
+ barTransformFn: barStyles.done
1474
+ });
1475
+ }
1476
+ };
1477
+ }
1478
+
1479
+ // packages/utils/src/lib/reports/flatten-plugins.ts
1480
+ function listGroupsFromAllPlugins(report) {
1481
+ return report.plugins.flatMap(
1482
+ (plugin) => plugin.groups?.map((group) => ({ plugin, group })) ?? []
1483
+ );
1484
+ }
1485
+ function listAuditsFromAllPlugins(report) {
1486
+ return report.plugins.flatMap(
1487
+ (plugin) => plugin.audits.map((audit) => ({ plugin, audit }))
1488
+ );
1489
+ }
1490
+
1491
+ // packages/utils/src/lib/reports/generate-md-report.ts
1492
+ import { MarkdownDocument as MarkdownDocument3, md as md4 } from "build-md";
1493
+
1494
+ // packages/utils/src/lib/text-formats/constants.ts
1495
+ var HIERARCHY = {
1496
+ level_1: 1,
1497
+ level_2: 2,
1498
+ level_3: 3,
1499
+ level_4: 4,
1500
+ level_5: 5,
1501
+ level_6: 6
1502
+ };
1503
+
1044
1504
  // packages/utils/src/lib/text-formats/table.ts
1045
1505
  function rowToStringArray({ rows, columns = [] }) {
1046
1506
  if (Array.isArray(rows.at(0)) && typeof columns.at(0) === "object") {
@@ -1130,254 +1590,53 @@ function getColumnAlignments(tableData) {
1130
1590
  return Object.keys(biggestRow ?? {}).map((_) => "center");
1131
1591
  }
1132
1592
 
1133
- // packages/utils/src/lib/text-formats/html/table.ts
1134
- function wrap(elem, content) {
1135
- return `<${elem}>${content}</${elem}>${NEW_LINE}`;
1136
- }
1137
- function wrapRow(content) {
1138
- const elem = "tr";
1139
- return `<${elem}>${NEW_LINE}${content}</${elem}>${NEW_LINE}`;
1140
- }
1141
- function table(tableData) {
1593
+ // packages/utils/src/lib/reports/formatting.ts
1594
+ import {
1595
+ MarkdownDocument,
1596
+ md as md2
1597
+ } from "build-md";
1598
+ function tableSection(tableData, options2) {
1142
1599
  if (tableData.rows.length === 0) {
1143
- throw new Error("Data can't be empty");
1144
- }
1145
- const tableHeaderCols = columnsToStringArray(tableData).map((s) => wrap("th", s)).join("");
1146
- const tableHeaderRow = wrapRow(tableHeaderCols);
1147
- const tableBody = rowToStringArray(tableData).map((arr) => {
1148
- const columns = arr.map((s) => wrap("td", s)).join("");
1149
- return wrapRow(columns);
1150
- }).join("");
1151
- return wrap("table", `${NEW_LINE}${tableHeaderRow}${tableBody}`);
1152
- }
1153
-
1154
- // packages/utils/src/lib/text-formats/md/font-style.ts
1155
- var boldWrap = "**";
1156
- function bold2(text) {
1157
- return `${boldWrap}${text}${boldWrap}`;
1158
- }
1159
- var italicWrap = "_";
1160
- function italic2(text) {
1161
- return `${italicWrap}${text}${italicWrap}`;
1162
- }
1163
- var strikeThroughWrap = "~";
1164
- function strikeThrough(text) {
1165
- return `${strikeThroughWrap}${text}${strikeThroughWrap}`;
1166
- }
1167
- var codeWrap = "`";
1168
- function code2(text) {
1169
- return `${codeWrap}${text}${codeWrap}`;
1170
- }
1171
-
1172
- // packages/utils/src/lib/text-formats/md/headline.ts
1173
- function headline(text, hierarchy = 1) {
1174
- return `${"#".repeat(hierarchy)} ${text}${NEW_LINE}`;
1175
- }
1176
- function h(text, hierarchy = 1) {
1177
- return headline(text, hierarchy);
1178
- }
1179
- function h1(text) {
1180
- return headline(text, 1);
1181
- }
1182
- function h2(text) {
1183
- return headline(text, 2);
1184
- }
1185
- function h3(text) {
1186
- return headline(text, 3);
1187
- }
1188
- function h4(text) {
1189
- return headline(text, 4);
1190
- }
1191
- function h5(text) {
1192
- return headline(text, 5);
1193
- }
1194
- function h6(text) {
1195
- return headline(text, 6);
1196
- }
1197
-
1198
- // packages/utils/src/lib/text-formats/md/image.ts
1199
- function image(src, alt) {
1200
- return `![${alt}](${src})`;
1201
- }
1202
-
1203
- // packages/utils/src/lib/text-formats/md/link.ts
1204
- function link3(href, text) {
1205
- return `[${text || href}](${href})`;
1206
- }
1207
-
1208
- // packages/utils/src/lib/text-formats/md/list.ts
1209
- function li(text, order = "unordered") {
1210
- const style = order === "unordered" ? "-" : "- [ ]";
1211
- return `${style} ${text}`;
1212
- }
1213
- function indentation(text, level = 1) {
1214
- return `${TAB.repeat(level)}${text}`;
1215
- }
1216
-
1217
- // packages/utils/src/lib/text-formats/md/paragraphs.ts
1218
- function paragraphs(...sections) {
1219
- return sections.filter(Boolean).join(`${NEW_LINE}${NEW_LINE}`);
1220
- }
1221
-
1222
- // packages/utils/src/lib/text-formats/md/section.ts
1223
- function section(...contents) {
1224
- return `${lines(...contents)}${NEW_LINE}`;
1225
- }
1226
- function lines(...contents) {
1227
- const filteredContent = contents.filter(
1228
- (value) => value != null && value !== "" && value !== false
1229
- );
1230
- return `${filteredContent.join(NEW_LINE)}`;
1231
- }
1232
-
1233
- // packages/utils/src/lib/text-formats/md/table.ts
1234
- var alignString = /* @__PURE__ */ new Map([
1235
- ["left", ":--"],
1236
- ["center", ":--:"],
1237
- ["right", "--:"]
1238
- ]);
1239
- function tableRow(rows) {
1240
- return `|${rows.join("|")}|`;
1241
- }
1242
- function table2(data) {
1243
- if (data.rows.length === 0) {
1244
- throw new Error("Data can't be empty");
1245
- }
1246
- const alignmentRow = getColumnAlignments(data).map(
1247
- (s) => alignString.get(s) ?? String(alignString.get("center"))
1248
- );
1249
- return section(
1250
- `${lines(
1251
- tableRow(columnsToStringArray(data)),
1252
- tableRow(alignmentRow),
1253
- ...rowToStringArray(data).map(tableRow)
1254
- )}`
1600
+ return null;
1601
+ }
1602
+ const { level = HIERARCHY.level_4 } = options2 ?? {};
1603
+ const columns = columnsToStringArray(tableData);
1604
+ const alignments = getColumnAlignments(tableData);
1605
+ const rows = rowToStringArray(tableData);
1606
+ return new MarkdownDocument().heading(level, tableData.title).table(
1607
+ columns.map((heading, i) => {
1608
+ const alignment = alignments[i];
1609
+ if (alignment) {
1610
+ return { heading, alignment };
1611
+ }
1612
+ return heading;
1613
+ }),
1614
+ rows
1255
1615
  );
1256
1616
  }
1617
+ function metaDescription(audit) {
1618
+ const docsUrl = audit.docsUrl;
1619
+ const description = audit.description?.trim();
1620
+ if (docsUrl) {
1621
+ const docsLink = md2.link(docsUrl, "\u{1F4D6} Docs");
1622
+ if (!description) {
1623
+ return docsLink;
1624
+ }
1625
+ const parsedDescription = description.endsWith("```") ? `${description}
1257
1626
 
1258
- // packages/utils/src/lib/text-formats/index.ts
1259
- var md = {
1260
- bold: bold2,
1261
- italic: italic2,
1262
- strikeThrough,
1263
- code: code2,
1264
- link: link3,
1265
- image,
1266
- headline,
1267
- h,
1268
- h1,
1269
- h2,
1270
- h3,
1271
- h4,
1272
- h5,
1273
- h6,
1274
- indentation,
1275
- lines,
1276
- li,
1277
- section,
1278
- paragraphs,
1279
- table: table2
1280
- };
1281
- var html = {
1282
- bold,
1283
- italic,
1284
- code,
1285
- link: link2,
1286
- details,
1287
- table
1288
- };
1289
-
1290
- // packages/utils/src/lib/reports/utils.ts
1291
- var { image: image2, bold: boldMd } = md;
1292
- function formatReportScore(score) {
1293
- const scaledScore = score * 100;
1294
- const roundedScore = Math.round(scaledScore);
1295
- return roundedScore === 100 && score !== 1 ? Math.floor(scaledScore).toString() : roundedScore.toString();
1296
- }
1297
- function formatScoreWithColor(score, options2) {
1298
- const styledNumber = options2?.skipBold ? formatReportScore(score) : boldMd(formatReportScore(score));
1299
- return `${scoreMarker(score)} ${styledNumber}`;
1300
- }
1301
- var MARKERS = {
1302
- circle: {
1303
- red: "\u{1F534}",
1304
- yellow: "\u{1F7E1}",
1305
- green: "\u{1F7E2}"
1306
- },
1307
- square: {
1308
- red: "\u{1F7E5}",
1309
- yellow: "\u{1F7E8}",
1310
- green: "\u{1F7E9}"
1311
- }
1312
- };
1313
- function scoreMarker(score, markerType = "circle") {
1314
- if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
1315
- return MARKERS[markerType].green;
1316
- }
1317
- if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
1318
- return MARKERS[markerType].yellow;
1319
- }
1320
- return MARKERS[markerType].red;
1321
- }
1322
- function getDiffMarker(diff) {
1323
- if (diff > 0) {
1324
- return "\u2191";
1627
+ ` : `${description} `;
1628
+ return md2`${parsedDescription}${docsLink}`;
1325
1629
  }
1326
- if (diff < 0) {
1327
- return "\u2193";
1630
+ if (description && description.trim().length > 0) {
1631
+ return description;
1328
1632
  }
1329
1633
  return "";
1330
1634
  }
1331
- function colorByScoreDiff(text, diff) {
1332
- const color = diff > 0 ? "green" : diff < 0 ? "red" : "gray";
1333
- return shieldsBadge(text, color);
1334
- }
1335
- function shieldsBadge(text, color) {
1336
- return image2(
1337
- `https://img.shields.io/badge/${encodeURIComponent(text)}-${color}`,
1338
- text
1339
- );
1340
- }
1341
- function formatDiffNumber(diff) {
1342
- const number = Math.abs(diff) === Number.POSITIVE_INFINITY ? "\u221E" : `${Math.abs(diff)}`;
1343
- const sign = diff < 0 ? "\u2212" : "+";
1344
- return `${sign}${number}`;
1345
- }
1346
- function severityMarker(severity) {
1347
- if (severity === "error") {
1348
- return "\u{1F6A8}";
1349
- }
1350
- if (severity === "warning") {
1351
- return "\u26A0\uFE0F";
1352
- }
1353
- return "\u2139\uFE0F";
1354
- }
1355
- function calcDuration(start, stop) {
1356
- return Math.round((stop ?? performance.now()) - start);
1357
- }
1358
- function countCategoryAudits(refs, plugins) {
1359
- const groupLookup = plugins.reduce(
1360
- (lookup, plugin) => {
1361
- if (plugin.groups == null || plugin.groups.length === 0) {
1362
- return lookup;
1363
- }
1364
- return {
1365
- ...lookup,
1366
- [plugin.slug]: Object.fromEntries(
1367
- plugin.groups.map((group) => [group.slug, group])
1368
- )
1369
- };
1370
- },
1371
- {}
1372
- );
1373
- return refs.reduce((acc, ref) => {
1374
- if (ref.type === "group") {
1375
- const groupRefs = groupLookup[ref.plugin]?.[ref.slug]?.refs;
1376
- return acc + (groupRefs?.length ?? 0);
1377
- }
1378
- return acc + 1;
1379
- }, 0);
1380
- }
1635
+
1636
+ // packages/utils/src/lib/reports/generate-md-report-categoy-section.ts
1637
+ import { MarkdownDocument as MarkdownDocument2, md as md3 } from "build-md";
1638
+
1639
+ // packages/utils/src/lib/reports/sorting.ts
1381
1640
  function getSortableAuditByRef({ slug, weight, plugin }, plugins) {
1382
1641
  const auditPlugin = plugins.find((p) => p.slug === plugin);
1383
1642
  if (!auditPlugin) {
@@ -1395,6 +1654,19 @@ function getSortableAuditByRef({ slug, weight, plugin }, plugins) {
1395
1654
  plugin
1396
1655
  };
1397
1656
  }
1657
+ function getSortedGroupAudits(group, plugin, plugins) {
1658
+ return group.refs.map(
1659
+ (ref) => getSortableAuditByRef(
1660
+ {
1661
+ plugin,
1662
+ slug: ref.slug,
1663
+ weight: ref.weight,
1664
+ type: "audit"
1665
+ },
1666
+ plugins
1667
+ )
1668
+ ).sort(compareCategoryAuditsAndGroups);
1669
+ }
1398
1670
  function getSortableGroupByRef({ plugin, slug, weight }, plugins) {
1399
1671
  const groupPlugin = plugins.find((p) => p.slug === plugin);
1400
1672
  if (!groupPlugin) {
@@ -1419,849 +1691,280 @@ function getSortableGroupByRef({ plugin, slug, weight }, plugins) {
1419
1691
  weight
1420
1692
  };
1421
1693
  }
1422
- function getSortedGroupAudits(group, plugin, plugins) {
1423
- return group.refs.map(
1424
- (ref) => getSortableAuditByRef(
1425
- {
1426
- plugin,
1427
- slug: ref.slug,
1428
- weight: ref.weight,
1429
- type: "audit"
1430
- },
1431
- plugins
1432
- )
1433
- ).sort(compareCategoryAuditsAndGroups);
1434
- }
1435
- function compareCategoryAuditsAndGroups(a, b) {
1436
- if (a.weight !== b.weight) {
1437
- return b.weight - a.weight;
1438
- }
1439
- if (a.score !== b.score) {
1440
- return a.score - b.score;
1441
- }
1442
- if ("value" in a && "value" in b && a.value !== b.value) {
1443
- return b.value - a.value;
1444
- }
1445
- return a.title.localeCompare(b.title);
1446
- }
1447
- function compareAudits(a, b) {
1448
- if (a.score !== b.score) {
1449
- return a.score - b.score;
1450
- }
1451
- if (a.value !== b.value) {
1452
- return b.value - a.value;
1453
- }
1454
- return a.title.localeCompare(b.title);
1455
- }
1456
- function compareIssueSeverity(severity1, severity2) {
1457
- const levels = {
1458
- info: 0,
1459
- warning: 1,
1460
- error: 2
1694
+ function sortReport(report) {
1695
+ const { categories, plugins } = report;
1696
+ const sortedCategories = categories.map((category) => {
1697
+ const { audits, groups: groups2 } = category.refs.reduce(
1698
+ (acc, ref) => ({
1699
+ ...acc,
1700
+ ...ref.type === "group" ? {
1701
+ groups: [...acc.groups, getSortableGroupByRef(ref, plugins)]
1702
+ } : {
1703
+ audits: [...acc.audits, getSortableAuditByRef(ref, plugins)]
1704
+ }
1705
+ }),
1706
+ { groups: [], audits: [] }
1707
+ );
1708
+ const sortedAuditsAndGroups = [...audits, ...groups2].sort(
1709
+ compareCategoryAuditsAndGroups
1710
+ );
1711
+ const sortedRefs = [...category.refs].sort((a, b) => {
1712
+ const aIndex = sortedAuditsAndGroups.findIndex(
1713
+ (ref) => ref.slug === a.slug && ref.plugin === a.plugin
1714
+ );
1715
+ const bIndex = sortedAuditsAndGroups.findIndex(
1716
+ (ref) => ref.slug === b.slug && ref.plugin === b.plugin
1717
+ );
1718
+ return aIndex - bIndex;
1719
+ });
1720
+ return { ...category, refs: sortedRefs };
1721
+ });
1722
+ return {
1723
+ ...report,
1724
+ categories: sortedCategories,
1725
+ plugins: sortPlugins(plugins)
1461
1726
  };
1462
- return levels[severity1] - levels[severity2];
1463
1727
  }
1464
- async function loadReport(options2) {
1465
- const { outputDir, filename, format } = options2;
1466
- await ensureDirectoryExists(outputDir);
1467
- const filePath = join(outputDir, `${filename}.${format}`);
1468
- if (format === "json") {
1469
- const content = await readJsonFile(filePath);
1470
- return reportSchema.parse(content);
1471
- }
1472
- const text = await readTextFile(filePath);
1473
- return text;
1728
+ function sortPlugins(plugins) {
1729
+ return plugins.map((plugin) => ({
1730
+ ...plugin,
1731
+ audits: [...plugin.audits].sort(compareAudits).map(
1732
+ (audit) => audit.details?.issues ? {
1733
+ ...audit,
1734
+ details: {
1735
+ ...audit.details,
1736
+ issues: [...audit.details.issues].sort(compareIssues)
1737
+ }
1738
+ } : audit
1739
+ )
1740
+ }));
1474
1741
  }
1475
- function throwIsNotPresentError(itemName, presentPlace) {
1476
- throw new Error(`${itemName} is not present in ${presentPlace}`);
1742
+
1743
+ // packages/utils/src/lib/reports/generate-md-report-categoy-section.ts
1744
+ function categoriesOverviewSection(report) {
1745
+ const { categories, plugins } = report;
1746
+ return new MarkdownDocument2().table(
1747
+ [
1748
+ { heading: "\u{1F3F7} Category", alignment: "left" },
1749
+ { heading: "\u2B50 Score", alignment: "center" },
1750
+ { heading: "\u{1F6E1} Audits", alignment: "center" }
1751
+ ],
1752
+ categories.map(({ title, refs, score, isBinary }) => [
1753
+ // @TODO refactor `isBinary: boolean` to `targetScore: number` #713
1754
+ // The heading "ID" is inferred from the heading text in Markdown.
1755
+ md3.link(`#${slugify(title)}`, title),
1756
+ md3`${scoreMarker(score)} ${md3.bold(
1757
+ formatReportScore(score)
1758
+ )}${binaryIconSuffix(score, isBinary)}`,
1759
+ countCategoryAudits(refs, plugins).toString()
1760
+ ])
1761
+ );
1477
1762
  }
1478
- function getPluginNameFromSlug(slug, plugins) {
1479
- return plugins.find(({ slug: pluginSlug }) => pluginSlug === slug)?.title || slug;
1480
- }
1481
- function compareIssues(a, b) {
1482
- if (a.severity !== b.severity) {
1483
- return -compareIssueSeverity(a.severity, b.severity);
1484
- }
1485
- if (!a.source && b.source) {
1486
- return -1;
1487
- }
1488
- if (a.source && !b.source) {
1489
- return 1;
1490
- }
1491
- if (a.source?.file !== b.source?.file) {
1492
- return a.source?.file.localeCompare(b.source?.file || "") ?? 0;
1493
- }
1494
- if (!a.source?.position && b.source?.position) {
1495
- return -1;
1496
- }
1497
- if (a.source?.position && !b.source?.position) {
1498
- return 1;
1499
- }
1500
- if (a.source?.position?.startLine !== b.source?.position?.startLine) {
1501
- return (a.source?.position?.startLine ?? 0) - (b.source?.position?.startLine ?? 0);
1502
- }
1503
- return 0;
1504
- }
1505
-
1506
- // packages/utils/src/lib/execute-process.ts
1507
- var ProcessError = class extends Error {
1508
- code;
1509
- stderr;
1510
- stdout;
1511
- constructor(result) {
1512
- super(result.stderr);
1513
- this.code = result.code;
1514
- this.stderr = result.stderr;
1515
- this.stdout = result.stdout;
1516
- }
1517
- };
1518
- function executeProcess(cfg) {
1519
- const { observer, cwd, command: command2, args, ignoreExitCode = false } = cfg;
1520
- const { onStdout, onError, onComplete } = observer ?? {};
1521
- const date = (/* @__PURE__ */ new Date()).toISOString();
1522
- const start = performance.now();
1523
- return new Promise((resolve, reject) => {
1524
- const process2 = spawn(command2, args, { cwd, shell: true });
1525
- let stdout = "";
1526
- let stderr = "";
1527
- process2.stdout.on("data", (data) => {
1528
- stdout += String(data);
1529
- onStdout?.(String(data));
1530
- });
1531
- process2.stderr.on("data", (data) => {
1532
- stderr += String(data);
1533
- });
1534
- process2.on("error", (err) => {
1535
- stderr += err.toString();
1536
- });
1537
- process2.on("close", (code3) => {
1538
- const timings = { date, duration: calcDuration(start) };
1539
- if (code3 === 0 || ignoreExitCode) {
1540
- onComplete?.();
1541
- resolve({ code: code3, stdout, stderr, ...timings });
1542
- } else {
1543
- const errorMsg = new ProcessError({ code: code3, stdout, stderr, ...timings });
1544
- onError?.(errorMsg);
1545
- reject(errorMsg);
1546
- }
1547
- });
1548
- });
1549
- }
1550
-
1551
- // packages/utils/src/lib/filter.ts
1552
- function filterItemRefsBy(items, refFilterFn) {
1553
- return items.map((item) => ({
1554
- ...item,
1555
- refs: item.refs.filter(refFilterFn)
1556
- })).filter((item) => item.refs.length);
1557
- }
1558
-
1559
- // packages/utils/src/lib/git/git.ts
1560
- import { isAbsolute, join as join2, relative } from "node:path";
1561
- import { simpleGit } from "simple-git";
1562
- function getGitRoot(git = simpleGit()) {
1563
- return git.revparse("--show-toplevel");
1564
- }
1565
- function formatGitPath(path, gitRoot) {
1566
- const absolutePath = isAbsolute(path) ? path : join2(process.cwd(), path);
1567
- const relativePath = relative(gitRoot, absolutePath);
1568
- return toUnixPath(relativePath);
1569
- }
1570
- var GitStatusError = class _GitStatusError extends Error {
1571
- static ignoredProps = /* @__PURE__ */ new Set(["current", "tracking"]);
1572
- static getReducedStatus(status) {
1573
- return Object.fromEntries(
1574
- Object.entries(status).filter(([key]) => !this.ignoredProps.has(key)).filter(
1575
- (entry) => {
1576
- const value = entry[1];
1577
- if (value == null) {
1578
- return false;
1579
- }
1580
- if (Array.isArray(value) && value.length === 0) {
1581
- return false;
1582
- }
1583
- if (typeof value === "number" && value === 0) {
1584
- return false;
1585
- }
1586
- return !(typeof value === "boolean" && !value);
1587
- }
1588
- )
1589
- );
1590
- }
1591
- constructor(status) {
1592
- super(
1593
- `Working directory needs to be clean before we you can proceed. Commit your local changes or stash them:
1594
- ${JSON.stringify(
1595
- _GitStatusError.getReducedStatus(status),
1596
- null,
1597
- 2
1598
- )}`
1599
- );
1600
- }
1601
- };
1602
- async function guardAgainstLocalChanges(git = simpleGit()) {
1603
- const status = await git.status(["-s"]);
1604
- if (status.files.length > 0) {
1605
- throw new GitStatusError(status);
1606
- }
1607
- }
1608
- async function safeCheckout(branchOrHash, forceCleanStatus = false, git = simpleGit()) {
1609
- if (forceCleanStatus) {
1610
- await git.raw(["reset", "--hard"]);
1611
- await git.clean(["f", "d"]);
1612
- ui().logger.info(`git status cleaned`);
1613
- }
1614
- await guardAgainstLocalChanges(git);
1615
- await git.checkout(branchOrHash);
1616
- }
1617
-
1618
- // packages/utils/src/lib/git/git.commits-and-tags.ts
1619
- import { simpleGit as simpleGit2 } from "simple-git";
1620
-
1621
- // packages/utils/src/lib/semver.ts
1622
- import { rcompare, valid } from "semver";
1623
- function normalizeSemver(semverString) {
1624
- if (semverString.startsWith("v") || semverString.startsWith("V")) {
1625
- return semverString.slice(1);
1626
- }
1627
- if (semverString.includes("@")) {
1628
- return semverString.split("@").at(-1) ?? "";
1629
- }
1630
- return semverString;
1631
- }
1632
- function isSemver(semverString = "") {
1633
- return valid(normalizeSemver(semverString)) != null;
1634
- }
1635
-
1636
- // packages/utils/src/lib/git/git.commits-and-tags.ts
1637
- async function getLatestCommit(git = simpleGit2()) {
1638
- const log2 = await git.log({
1639
- maxCount: 1,
1640
- // git log -1 --pretty=format:"%H %s %an %aI" - See: https://git-scm.com/docs/pretty-formats
1641
- format: { hash: "%H", message: "%s", author: "%an", date: "%aI" }
1642
- });
1643
- return commitSchema.parse(log2.latest);
1644
- }
1645
- async function getCurrentBranchOrTag(git = simpleGit2()) {
1646
- return await git.branch().then((r) => r.current) || // If no current branch, try to get the tag
1647
- // @TODO use simple git
1648
- await git.raw(["describe", "--tags", "--exact-match"]).then((out) => out.trim());
1649
- }
1650
- function validateFilter({ from, to }) {
1651
- if (to && !from) {
1652
- throw new Error(
1653
- `filter needs the "from" option defined to accept the "to" option.
1654
- `
1655
- );
1656
- }
1657
- }
1658
- function filterLogs(allTags, opt) {
1659
- if (!opt) {
1660
- return allTags;
1661
- }
1662
- validateFilter(opt);
1663
- const { from, to, maxCount } = opt;
1664
- const finIndex = (tagName, fallback) => {
1665
- const idx = allTags.indexOf(tagName ?? "");
1666
- if (idx > -1) {
1667
- return idx;
1668
- }
1669
- return fallback;
1670
- };
1671
- const fromIndex = finIndex(from, 0);
1672
- const toIndex = finIndex(to, void 0);
1673
- return allTags.slice(fromIndex, toIndex ? toIndex + 1 : toIndex).slice(0, maxCount ?? void 0);
1674
- }
1675
- async function getHashFromTag(tag, git = simpleGit2()) {
1676
- const tagDetails = await git.show(["--no-patch", "--format=%H", tag]);
1677
- const hash = tagDetails.trim();
1678
- return {
1679
- hash: hash.split("\n").at(-1) ?? "",
1680
- message: tag
1681
- };
1682
- }
1683
- async function getSemverTags(opt = {}, git = simpleGit2()) {
1684
- validateFilter(opt);
1685
- const { targetBranch, ...options2 } = opt;
1686
- let currentBranch;
1687
- if (targetBranch) {
1688
- currentBranch = await getCurrentBranchOrTag(git);
1689
- await git.checkout(targetBranch);
1690
- }
1691
- const tagsRaw = await git.tag([
1692
- "--merged",
1693
- targetBranch ?? await getCurrentBranchOrTag(git)
1694
- ]);
1695
- const allTags = tagsRaw.split(/\n/).map((tag) => tag.trim()).filter(Boolean).filter(isSemver);
1696
- const relevantTags = filterLogs(allTags, options2);
1697
- const tagsWithHashes = await Promise.all(
1698
- relevantTags.map((tag) => getHashFromTag(tag, git))
1699
- );
1700
- if (currentBranch) {
1701
- await git.checkout(currentBranch);
1702
- }
1703
- return tagsWithHashes;
1704
- }
1705
- async function getHashes(options2 = {}, git = simpleGit2()) {
1706
- const { targetBranch, from, to, maxCount, ...opt } = options2;
1707
- validateFilter({ from, to });
1708
- let currentBranch;
1709
- if (targetBranch) {
1710
- currentBranch = await getCurrentBranchOrTag(git);
1711
- await git.checkout(targetBranch);
1712
- }
1713
- const logs = await git.log({
1714
- ...opt,
1715
- format: {
1716
- hash: "%H",
1717
- message: "%s"
1718
- },
1719
- from,
1720
- to,
1721
- maxCount
1722
- });
1723
- if (targetBranch) {
1724
- await git.checkout(currentBranch);
1725
- }
1726
- return [...logs.all];
1727
- }
1728
-
1729
- // packages/utils/src/lib/group-by-status.ts
1730
- function groupByStatus(results) {
1731
- return results.reduce(
1732
- (acc, result) => result.status === "fulfilled" ? { ...acc, fulfilled: [...acc.fulfilled, result] } : { ...acc, rejected: [...acc.rejected, result] },
1733
- { fulfilled: [], rejected: [] }
1734
- );
1735
- }
1736
-
1737
- // packages/utils/src/lib/progress.ts
1738
- import chalk3 from "chalk";
1739
- import { MultiProgressBars } from "multi-progress-bars";
1740
- var barStyles = {
1741
- active: (s) => chalk3.green(s),
1742
- done: (s) => chalk3.gray(s),
1743
- idle: (s) => chalk3.gray(s)
1744
- };
1745
- var messageStyles = {
1746
- active: (s) => chalk3.black(s),
1747
- done: (s) => chalk3.green(chalk3.bold(s)),
1748
- idle: (s) => chalk3.gray(s)
1749
- };
1750
- var mpb;
1751
- function getSingletonProgressBars(options2) {
1752
- if (!mpb) {
1753
- mpb = new MultiProgressBars({
1754
- progressWidth: TERMINAL_WIDTH,
1755
- initMessage: "",
1756
- border: true,
1757
- ...options2
1758
- });
1759
- }
1760
- return mpb;
1761
- }
1762
- function getProgressBar(taskName) {
1763
- const tasks = getSingletonProgressBars();
1764
- tasks.addTask(taskName, {
1765
- type: "percentage",
1766
- percentage: 0,
1767
- message: "",
1768
- barTransformFn: barStyles.idle
1769
- });
1770
- return {
1771
- incrementInSteps: (numPlugins) => {
1772
- tasks.incrementTask(taskName, {
1773
- percentage: 1 / numPlugins,
1774
- barTransformFn: barStyles.active
1775
- });
1776
- },
1777
- updateTitle: (title) => {
1778
- tasks.updateTask(taskName, {
1779
- message: title,
1780
- barTransformFn: barStyles.active
1781
- });
1782
- },
1783
- endProgress: (message = "") => {
1784
- tasks.done(taskName, {
1785
- message: messageStyles.done(message),
1786
- barTransformFn: barStyles.done
1787
- });
1788
- }
1789
- };
1790
- }
1791
-
1792
- // packages/utils/src/lib/reports/flatten-plugins.ts
1793
- function listGroupsFromAllPlugins(report) {
1794
- return report.plugins.flatMap(
1795
- (plugin) => plugin.groups?.map((group) => ({ plugin, group })) ?? []
1796
- );
1797
- }
1798
- function listAuditsFromAllPlugins(report) {
1799
- return report.plugins.flatMap(
1800
- (plugin) => plugin.audits.map((audit) => ({ plugin, audit }))
1801
- );
1802
- }
1803
-
1804
- // packages/utils/src/lib/reports/formatting.ts
1805
- var { headline: headline2, lines: lines2, link: link4, section: section2, table: table3 } = md;
1806
- function tableSection(tableData, options2) {
1807
- if (tableData.rows.length === 0) {
1808
- return "";
1809
- }
1810
- const { level = 4 } = options2 ?? {};
1811
- const render = (h7, l) => l === 0 ? h7 : headline2(h7, l);
1812
- return lines2(
1813
- tableData.title && render(tableData.title, level),
1814
- table3(tableData)
1815
- );
1816
- }
1817
- function metaDescription({
1818
- docsUrl,
1819
- description
1820
- }) {
1821
- if (docsUrl) {
1822
- const docsLink = link4(docsUrl, "\u{1F4D6} Docs");
1823
- if (!description) {
1824
- return section2(docsLink);
1825
- }
1826
- const parsedDescription = description.toString().endsWith("```") ? `${description}${NEW_LINE + NEW_LINE}` : `${description}${SPACE}`;
1827
- return section2(`${parsedDescription}${docsLink}`);
1828
- }
1829
- if (description && description.trim().length > 0) {
1830
- return section2(description);
1831
- }
1832
- return "";
1833
- }
1834
-
1835
- // packages/utils/src/lib/reports/generate-md-report-categoy-section.ts
1836
- var { link: link5, section: section3, h2: h22, lines: lines3, li: li2, bold: boldMd2, h3: h32, indentation: indentation2 } = md;
1837
- function categoriesOverviewSection(report) {
1838
- const { categories, plugins } = report;
1839
- if (categories.length > 0 && plugins.length > 0) {
1840
- const tableContent = {
1841
- columns: reportOverviewTableHeaders,
1842
- rows: categories.map(({ title, refs, score }) => ({
1843
- // The heading "ID" is inferred from the heading text in Markdown.
1844
- category: link5(`#${slugify(title)}`, title),
1845
- score: `${scoreMarker(score)}${SPACE}${boldMd2(
1846
- formatReportScore(score)
1847
- )}`,
1848
- audits: countCategoryAudits(refs, plugins).toString()
1849
- }))
1850
- };
1851
- return tableSection(tableContent);
1852
- }
1853
- return "";
1854
- }
1855
- function categoriesDetailsSection(report) {
1856
- const { categories, plugins } = report;
1857
- const categoryDetails = categories.flatMap((category) => {
1858
- const categoryTitle = h32(category.title);
1859
- const categoryScore = `${scoreMarker(
1860
- category.score
1861
- )}${SPACE}Score: ${boldMd2(formatReportScore(category.score))}`;
1862
- const categoryMDItems = category.refs.map((ref) => {
1863
- if (ref.type === "group") {
1864
- const group = getSortableGroupByRef(ref, plugins);
1865
- const groupAudits = group.refs.map(
1866
- (groupRef) => getSortableAuditByRef(
1867
- { ...groupRef, plugin: group.plugin, type: "audit" },
1868
- plugins
1869
- )
1870
- );
1871
- const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
1872
- return categoryGroupItem(group, groupAudits, pluginTitle);
1873
- } else {
1874
- const audit = getSortableAuditByRef(ref, plugins);
1875
- const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
1876
- return categoryRef(audit, pluginTitle);
1877
- }
1878
- });
1879
- return section3(
1880
- categoryTitle,
1881
- metaDescription(category),
1882
- categoryScore,
1883
- ...categoryMDItems
1884
- );
1885
- });
1886
- return lines3(h22(CATEGORIES_TITLE), ...categoryDetails);
1763
+ function categoriesDetailsSection(report) {
1764
+ const { categories, plugins } = report;
1765
+ return new MarkdownDocument2().heading(HIERARCHY.level_2, "\u{1F3F7} Categories").$foreach(
1766
+ categories,
1767
+ (doc, category) => doc.heading(HIERARCHY.level_3, category.title).paragraph(metaDescription(category)).paragraph(
1768
+ md3`${scoreMarker(category.score)} Score: ${md3.bold(
1769
+ formatReportScore(category.score)
1770
+ )}${binaryIconSuffix(category.score, category.isBinary)}`
1771
+ ).list(
1772
+ category.refs.map((ref) => {
1773
+ if (ref.type === "group") {
1774
+ const group = getSortableGroupByRef(ref, plugins);
1775
+ const groupAudits = group.refs.map(
1776
+ (groupRef) => getSortableAuditByRef(
1777
+ { ...groupRef, plugin: group.plugin, type: "audit" },
1778
+ plugins
1779
+ )
1780
+ );
1781
+ const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
1782
+ return categoryGroupItem(group, groupAudits, pluginTitle);
1783
+ } else {
1784
+ const audit = getSortableAuditByRef(ref, plugins);
1785
+ const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
1786
+ return categoryRef(audit, pluginTitle);
1787
+ }
1788
+ })
1789
+ )
1790
+ );
1887
1791
  }
1888
1792
  function categoryRef({ title, score, value, displayValue }, pluginTitle) {
1889
- const auditTitleAsLink = link5(
1793
+ const auditTitleAsLink = md3.link(
1890
1794
  `#${slugify(title)}-${slugify(pluginTitle)}`,
1891
1795
  title
1892
1796
  );
1893
1797
  const marker = scoreMarker(score, "square");
1894
- return li2(
1895
- `${marker}${SPACE}${auditTitleAsLink}${SPACE}(_${pluginTitle}_) - ${boldMd2(
1896
- (displayValue || value).toString()
1897
- )}`
1898
- );
1798
+ return md3`${marker} ${auditTitleAsLink} (${md3.italic(
1799
+ pluginTitle
1800
+ )}) - ${md3.bold((displayValue || value).toString())}`;
1899
1801
  }
1900
1802
  function categoryGroupItem({ score = 0, title }, groupAudits, pluginTitle) {
1901
- const groupTitle = li2(
1902
- `${scoreMarker(score)}${SPACE}${title}${SPACE}(_${pluginTitle}_)`
1903
- );
1904
- const auditTitles = groupAudits.map(
1905
- ({ title: auditTitle, score: auditScore, value, displayValue }) => {
1906
- const auditTitleLink = link5(
1907
- `#${slugify(auditTitle)}-${slugify(pluginTitle)}`,
1908
- auditTitle
1909
- );
1910
- const marker = scoreMarker(auditScore, "square");
1911
- return indentation2(
1912
- li2(
1913
- `${marker}${SPACE}${auditTitleLink} - ${boldMd2(
1914
- String(displayValue ?? value)
1915
- )}`
1916
- )
1917
- );
1918
- }
1803
+ const groupTitle = md3`${scoreMarker(score)} ${title} (${md3.italic(
1804
+ pluginTitle
1805
+ )})`;
1806
+ const auditsList = md3.list(
1807
+ groupAudits.map(
1808
+ ({ title: auditTitle, score: auditScore, value, displayValue }) => {
1809
+ const auditTitleLink = md3.link(
1810
+ `#${slugify(auditTitle)}-${slugify(pluginTitle)}`,
1811
+ auditTitle
1812
+ );
1813
+ const marker = scoreMarker(auditScore, "square");
1814
+ return md3`${marker} ${auditTitleLink} - ${md3.bold(
1815
+ String(displayValue ?? value)
1816
+ )}`;
1817
+ }
1818
+ )
1919
1819
  );
1920
- return lines3(groupTitle, ...auditTitles);
1820
+ return md3`${groupTitle}${auditsList}`;
1821
+ }
1822
+ function binaryIconSuffix(score, isBinary) {
1823
+ return targetScoreIcon(score, isBinary ? 1 : void 0, { prefix: " " });
1921
1824
  }
1922
1825
 
1923
1826
  // packages/utils/src/lib/reports/generate-md-report.ts
1924
- var { h1: h12, h2: h23, h3: h33, lines: lines4, link: link6, section: section4, code: codeMd } = md;
1925
- var { bold: boldHtml, details: details2 } = html;
1926
1827
  function auditDetailsAuditValue({
1927
1828
  score,
1928
1829
  value,
1929
1830
  displayValue
1930
1831
  }) {
1931
- return `${scoreMarker(score, "square")} ${boldHtml(
1832
+ return md4`${scoreMarker(score, "square")} ${md4.bold(
1932
1833
  String(displayValue ?? value)
1933
1834
  )} (score: ${formatReportScore(score)})`;
1934
1835
  }
1935
1836
  function generateMdReport(report) {
1936
- const printCategories = report.categories.length > 0;
1937
- return lines4(
1938
- h12(reportHeadlineText),
1939
- printCategories ? categoriesOverviewSection(report) : "",
1940
- printCategories ? categoriesDetailsSection(report) : "",
1941
- auditsSection(report),
1942
- aboutSection(report),
1943
- `${FOOTER_PREFIX}${SPACE}${link6(README_LINK, "Code PushUp")}`
1944
- );
1945
- }
1946
- function auditDetailsIssues(issues = []) {
1947
- if (issues.length === 0) {
1948
- return "";
1949
- }
1950
- const detailsTableData = {
1951
- title: "Issues",
1952
- columns: issuesTableHeadings,
1953
- rows: issues.map(
1954
- ({ severity: severityVal, message, source: sourceVal }) => {
1955
- const severity = `${severityMarker(severityVal)} <i>${severityVal}</i>`;
1956
- if (!sourceVal) {
1957
- return { severity, message, file: "", line: "" };
1958
- }
1959
- const file = `<code>${sourceVal.file}</code>`;
1960
- if (!sourceVal.position) {
1961
- return { severity, message, file, line: "" };
1962
- }
1963
- const { startLine, endLine } = sourceVal.position;
1964
- const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
1965
- return { severity, message, file, line };
1966
- }
1967
- )
1968
- };
1969
- return tableSection(detailsTableData);
1970
- }
1971
- function auditDetails(audit) {
1972
- const { table: table5, issues = [] } = audit.details ?? {};
1973
- const detailsValue = auditDetailsAuditValue(audit);
1974
- if (issues.length === 0 && table5 == null) {
1975
- return section4(detailsValue);
1976
- }
1977
- const tableSectionContent = table5 == null ? "" : tableSection(table5);
1978
- const issuesSectionContent = issues.length > 0 ? auditDetailsIssues(issues) : "";
1979
- return details2(
1980
- detailsValue,
1981
- lines4(tableSectionContent, issuesSectionContent)
1982
- );
1983
- }
1984
- function auditsSection({
1985
- plugins
1986
- }) {
1987
- const content = plugins.flatMap(
1988
- ({ slug, audits }) => audits.flatMap((audit) => {
1989
- const auditTitle = `${audit.title}${SPACE}(${getPluginNameFromSlug(
1990
- slug,
1991
- plugins
1992
- )})`;
1993
- const detailsContent = auditDetails(audit);
1994
- const descriptionContent = metaDescription(audit);
1995
- return [h33(auditTitle), detailsContent, descriptionContent];
1996
- })
1997
- );
1998
- return section4(h23("\u{1F6E1}\uFE0F Audits"), ...content);
1999
- }
2000
- function aboutSection(report) {
2001
- const { date, plugins } = report;
2002
- const reportMetaTable = reportMetaData(report);
2003
- const pluginMetaTable = reportPluginMeta({ plugins });
2004
- return lines4(
2005
- h23("About"),
2006
- section4(
2007
- `Report was created by [Code PushUp](${README_LINK}) on ${formatDate(
2008
- new Date(date)
2009
- )}.`
2010
- ),
2011
- tableSection(pluginMetaTable),
2012
- tableSection(reportMetaTable)
2013
- );
2014
- }
2015
- function reportPluginMeta({ plugins }) {
2016
- return {
2017
- columns: [
2018
- {
2019
- key: "plugin",
2020
- align: "left"
2021
- },
2022
- {
2023
- key: "audits"
2024
- },
2025
- {
2026
- key: "version"
2027
- },
2028
- {
2029
- key: "duration"
2030
- }
2031
- ],
2032
- rows: plugins.map(
2033
- ({
2034
- title: pluginTitle,
2035
- audits,
2036
- version: pluginVersion,
2037
- duration: pluginDuration
2038
- }) => ({
2039
- plugin: pluginTitle,
2040
- audits: audits.length.toString(),
2041
- version: codeMd(pluginVersion || ""),
2042
- duration: formatDuration(pluginDuration)
2043
- })
1837
+ return new MarkdownDocument3().heading(HIERARCHY.level_1, REPORT_HEADLINE_TEXT).$if(
1838
+ report.categories.length > 0,
1839
+ (doc) => doc.$concat(
1840
+ categoriesOverviewSection(report),
1841
+ categoriesDetailsSection(report)
2044
1842
  )
2045
- };
2046
- }
2047
- function reportMetaData({
2048
- commit,
2049
- version: version2,
2050
- duration,
2051
- plugins,
2052
- categories
2053
- }) {
2054
- const commitInfo = commit ? `${commit.message}${SPACE}(${commit.hash})` : "N/A";
2055
- return {
2056
- columns: [
2057
- {
2058
- key: "commit",
2059
- align: "left"
2060
- },
2061
- {
2062
- key: "version"
2063
- },
2064
- {
2065
- key: "duration"
2066
- },
2067
- {
2068
- key: "plugins"
2069
- },
2070
- {
2071
- key: "categories"
2072
- },
2073
- {
2074
- key: "audits"
2075
- }
2076
- ],
2077
- rows: [
2078
- {
2079
- commit: commitInfo,
2080
- version: codeMd(version2 || ""),
2081
- duration: formatDuration(duration),
2082
- plugins: plugins.length,
2083
- categories: categories.length,
2084
- audits: plugins.reduce((acc, { audits }) => acc + audits.length, 0).toString()
2085
- }
2086
- ]
2087
- };
2088
- }
2089
-
2090
- // packages/utils/src/lib/reports/generate-md-reports-diff.ts
2091
- var {
2092
- h1: h13,
2093
- h2: h24,
2094
- lines: lines5,
2095
- link: link7,
2096
- bold: boldMd3,
2097
- italic: italicMd,
2098
- table: table4,
2099
- section: section5
2100
- } = md;
2101
- var { details: details3 } = html;
2102
- var MAX_ROWS = 100;
2103
- function generateMdReportsDiff(diff) {
2104
- return lines5(
2105
- section5(formatDiffHeaderSection(diff)),
2106
- formatDiffCategoriesSection(diff),
2107
- formatDiffGroupsSection(diff),
2108
- formatDiffAuditsSection(diff)
2109
- );
2110
- }
2111
- function formatDiffHeaderSection(diff) {
2112
- const outcomeTexts = {
2113
- positive: `\u{1F973} Code PushUp report has ${boldMd3("improved")}`,
2114
- negative: `\u{1F61F} Code PushUp report has ${boldMd3("regressed")}`,
2115
- mixed: `\u{1F928} Code PushUp report has both ${boldMd3(
2116
- "improvements and regressions"
2117
- )}`,
2118
- unchanged: `\u{1F610} Code PushUp report is ${boldMd3("unchanged")}`
2119
- };
2120
- const outcome = mergeDiffOutcomes(
2121
- changesToDiffOutcomes([
2122
- ...diff.categories.changed,
2123
- ...diff.groups.changed,
2124
- ...diff.audits.changed
2125
- ])
2126
- );
2127
- const styleCommits = (commits) => `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
2128
- return lines5(
2129
- h13("Code PushUp"),
2130
- diff.commits ? `${outcomeTexts[outcome]} \u2013 ${styleCommits(diff.commits)}.` : `${outcomeTexts[outcome]}.`
2131
- );
2132
- }
2133
- function formatDiffCategoriesSection(diff) {
2134
- const { changed, unchanged, added } = diff.categories;
2135
- const categoriesCount = changed.length + unchanged.length + added.length;
2136
- const hasChanges = unchanged.length < categoriesCount;
2137
- if (categoriesCount === 0) {
2138
- return "";
2139
- }
2140
- const columns = [
2141
- { key: "category", label: "\u{1F3F7}\uFE0F Category", align: "left" },
2142
- { key: "before", label: hasChanges ? "\u2B50 Previous score" : "\u2B50 Score" },
2143
- { key: "after", label: "\u2B50 Current score" },
2144
- { key: "change", label: "\u{1F504} Score change" }
2145
- ];
2146
- return lines5(
2147
- h24("\u{1F3F7}\uFE0F Categories"),
2148
- categoriesCount > 0 && table4({
2149
- columns: hasChanges ? columns : columns.slice(0, 2),
2150
- rows: [
2151
- ...sortChanges(changed).map((category) => ({
2152
- category: formatTitle(category),
2153
- after: formatScoreWithColor(category.scores.after),
2154
- before: formatScoreWithColor(category.scores.before, {
2155
- skipBold: true
2156
- }),
2157
- change: formatScoreChange(category.scores.diff)
2158
- })),
2159
- ...added.map((category) => ({
2160
- category: formatTitle(category),
2161
- after: formatScoreWithColor(category.score),
2162
- before: italicMd("n/a (\\*)"),
2163
- change: italicMd("n/a (\\*)")
2164
- })),
2165
- ...unchanged.map((category) => ({
2166
- category: formatTitle(category),
2167
- after: formatScoreWithColor(category.score),
2168
- before: formatScoreWithColor(category.score, { skipBold: true }),
2169
- change: "\u2013"
2170
- }))
2171
- ].map(
2172
- (row) => hasChanges ? row : { category: row.category, before: row.before }
2173
- )
2174
- }),
2175
- added.length > 0 && section5(italicMd("(\\*) New category."))
2176
- );
2177
- }
2178
- function formatDiffGroupsSection(diff) {
2179
- if (diff.groups.changed.length + diff.groups.unchanged.length === 0) {
2180
- return "";
2181
- }
2182
- return lines5(
2183
- h24("\u{1F5C3}\uFE0F Groups"),
2184
- formatGroupsOrAuditsDetails("group", diff.groups, {
2185
- columns: [
2186
- { key: "plugin", label: "\u{1F50C} Plugin", align: "left" },
2187
- { key: "group", label: "\u{1F5C3}\uFE0F Group", align: "left" },
2188
- { key: "before", label: "\u2B50 Previous score" },
2189
- { key: "after", label: "\u2B50 Current score" },
2190
- { key: "change", label: "\u{1F504} Score change" }
2191
- ],
2192
- rows: sortChanges(diff.groups.changed).map((group) => ({
2193
- plugin: formatTitle(group.plugin),
2194
- group: formatTitle(group),
2195
- after: formatScoreWithColor(group.scores.after),
2196
- before: formatScoreWithColor(group.scores.before, { skipBold: true }),
2197
- change: formatScoreChange(group.scores.diff)
2198
- }))
1843
+ ).$concat(auditsSection(report), aboutSection(report)).rule().paragraph(md4`${FOOTER_PREFIX} ${md4.link(README_LINK, "Code PushUp")}`).toString();
1844
+ }
1845
+ function auditDetailsIssues(issues = []) {
1846
+ if (issues.length === 0) {
1847
+ return null;
1848
+ }
1849
+ return new MarkdownDocument3().heading(HIERARCHY.level_4, "Issues").table(
1850
+ [
1851
+ { heading: "Severity", alignment: "center" },
1852
+ { heading: "Message", alignment: "left" },
1853
+ { heading: "Source file", alignment: "left" },
1854
+ { heading: "Line(s)", alignment: "center" }
1855
+ ],
1856
+ issues.map(({ severity: level, message, source }) => {
1857
+ const severity = md4`${severityMarker(level)} ${md4.italic(level)}`;
1858
+ if (!source) {
1859
+ return [severity, message];
1860
+ }
1861
+ const file = md4.code(source.file);
1862
+ if (!source.position) {
1863
+ return [severity, message, file];
1864
+ }
1865
+ const { startLine, endLine } = source.position;
1866
+ const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
1867
+ return [severity, message, file, line];
2199
1868
  })
2200
1869
  );
2201
1870
  }
2202
- function formatDiffAuditsSection(diff) {
2203
- return lines5(
2204
- h24("\u{1F6E1}\uFE0F Audits"),
2205
- formatGroupsOrAuditsDetails("audit", diff.audits, {
2206
- columns: [
2207
- { key: "plugin", label: "\u{1F50C} Plugin", align: "left" },
2208
- { key: "audit", label: "\u{1F6E1}\uFE0F Audit", align: "left" },
2209
- { key: "before", label: "\u{1F4CF} Previous value" },
2210
- { key: "after", label: "\u{1F4CF} Current value" },
2211
- { key: "change", label: "\u{1F504} Value change" }
2212
- ],
2213
- rows: sortChanges(diff.audits.changed).map((audit) => ({
2214
- plugin: formatTitle(audit.plugin),
2215
- audit: formatTitle(audit),
2216
- after: `${scoreMarker(audit.scores.after, "square")} ${boldMd3(
2217
- audit.displayValues.after || audit.values.after.toString()
2218
- )}`,
2219
- before: `${scoreMarker(audit.scores.before, "square")} ${audit.displayValues.before || audit.values.before.toString()}`,
2220
- change: formatValueChange(audit)
2221
- }))
2222
- })
1871
+ function auditDetails(audit) {
1872
+ const { table: table2, issues = [] } = audit.details ?? {};
1873
+ const detailsValue = auditDetailsAuditValue(audit);
1874
+ if (issues.length === 0 && !table2?.rows.length) {
1875
+ return new MarkdownDocument3().paragraph(detailsValue);
1876
+ }
1877
+ const tableSectionContent = table2 && tableSection(table2);
1878
+ const issuesSectionContent = issues.length > 0 && auditDetailsIssues(issues);
1879
+ return new MarkdownDocument3().details(
1880
+ detailsValue,
1881
+ new MarkdownDocument3().$concat(tableSectionContent, issuesSectionContent)
2223
1882
  );
2224
1883
  }
2225
- function formatGroupsOrAuditsDetails(token, { changed, unchanged }, tableData) {
2226
- return changed.length === 0 ? summarizeUnchanged(token, { changed, unchanged }) : details3(
2227
- summarizeDiffOutcomes(changesToDiffOutcomes(changed), token),
2228
- lines5(
2229
- table4({
2230
- ...tableData,
2231
- rows: tableData.rows.slice(0, MAX_ROWS)
2232
- // use never to avoid typing problem
2233
- }),
2234
- changed.length > MAX_ROWS && italicMd(
2235
- `Only the ${MAX_ROWS} most affected ${pluralize(
2236
- token
2237
- )} are listed above for brevity.`
2238
- ),
2239
- unchanged.length > 0 && summarizeUnchanged(token, { changed, unchanged })
2240
- )
1884
+ function auditsSection({
1885
+ plugins
1886
+ }) {
1887
+ return new MarkdownDocument3().heading(HIERARCHY.level_2, "\u{1F6E1}\uFE0F Audits").$foreach(
1888
+ plugins.flatMap(
1889
+ (plugin) => plugin.audits.map((audit) => ({ ...audit, plugin }))
1890
+ ),
1891
+ (doc, { plugin, ...audit }) => {
1892
+ const auditTitle = `${audit.title} (${plugin.title})`;
1893
+ const detailsContent = auditDetails(audit);
1894
+ const descriptionContent = metaDescription(audit);
1895
+ return doc.heading(HIERARCHY.level_3, auditTitle).$concat(detailsContent).paragraph(descriptionContent);
1896
+ }
2241
1897
  );
2242
1898
  }
2243
- function formatScoreChange(diff) {
2244
- const marker = getDiffMarker(diff);
2245
- const text = formatDiffNumber(Math.round(diff * 1e3) / 10);
2246
- return colorByScoreDiff(`${marker} ${text}`, diff);
1899
+ function aboutSection(report) {
1900
+ const { date, plugins } = report;
1901
+ return new MarkdownDocument3().heading(HIERARCHY.level_2, "About").paragraph(
1902
+ md4`Report was created by ${md4.link(
1903
+ README_LINK,
1904
+ "Code PushUp"
1905
+ )} on ${formatDate(new Date(date))}.`
1906
+ ).table(...pluginMetaTable({ plugins })).table(...reportMetaTable(report));
1907
+ }
1908
+ function pluginMetaTable({
1909
+ plugins
1910
+ }) {
1911
+ return [
1912
+ [
1913
+ { heading: "Plugin", alignment: "left" },
1914
+ { heading: "Audits", alignment: "center" },
1915
+ { heading: "Version", alignment: "center" },
1916
+ { heading: "Duration", alignment: "right" }
1917
+ ],
1918
+ plugins.map(({ title, audits, version: version3 = "", duration }) => [
1919
+ title,
1920
+ audits.length.toString(),
1921
+ version3 && md4.code(version3),
1922
+ formatDuration(duration)
1923
+ ])
1924
+ ];
2247
1925
  }
2248
- function formatValueChange({
2249
- values,
2250
- scores
1926
+ function reportMetaTable({
1927
+ commit,
1928
+ version: version3,
1929
+ duration,
1930
+ plugins,
1931
+ categories
2251
1932
  }) {
2252
- const marker = getDiffMarker(values.diff);
2253
- const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : Math.round(100 * values.diff / values.before);
2254
- const text = `${formatDiffNumber(percentage)}\u2009%`;
2255
- return colorByScoreDiff(`${marker} ${text}`, scores.diff);
1933
+ return [
1934
+ [
1935
+ { heading: "Commit", alignment: "left" },
1936
+ { heading: "Version", alignment: "center" },
1937
+ { heading: "Duration", alignment: "right" },
1938
+ { heading: "Plugins", alignment: "center" },
1939
+ { heading: "Categories", alignment: "center" },
1940
+ { heading: "Audits", alignment: "center" }
1941
+ ],
1942
+ [
1943
+ [
1944
+ commit ? `${commit.message} (${commit.hash})` : "N/A",
1945
+ md4.code(version3),
1946
+ formatDuration(duration),
1947
+ plugins.length.toString(),
1948
+ categories.length.toString(),
1949
+ plugins.reduce((acc, { audits }) => acc + audits.length, 0).toString()
1950
+ ]
1951
+ ]
1952
+ ];
2256
1953
  }
1954
+
1955
+ // packages/utils/src/lib/reports/generate-md-reports-diff.ts
1956
+ import {
1957
+ MarkdownDocument as MarkdownDocument5,
1958
+ md as md6
1959
+ } from "build-md";
1960
+
1961
+ // packages/utils/src/lib/reports/generate-md-reports-diff-utils.ts
1962
+ import { MarkdownDocument as MarkdownDocument4, md as md5 } from "build-md";
1963
+ var MAX_ROWS = 100;
2257
1964
  function summarizeUnchanged(token, { changed, unchanged }) {
2258
- return section5(
2259
- [
2260
- changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`,
2261
- unchanged.length === 1 ? "is" : "are",
2262
- "unchanged."
2263
- ].join(" ")
2264
- );
1965
+ const pluralizedCount = changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`;
1966
+ const pluralizedVerb = unchanged.length === 1 ? "is" : "are";
1967
+ return `${pluralizedCount} ${pluralizedVerb} unchanged.`;
2265
1968
  }
2266
1969
  function summarizeDiffOutcomes(outcomes, token) {
2267
1970
  return objectToEntries(countDiffOutcomes(outcomes)).filter(
@@ -2281,20 +1984,46 @@ function summarizeDiffOutcomes(outcomes, token) {
2281
1984
  }
2282
1985
  }).join(", ");
2283
1986
  }
1987
+ function createGroupsOrAuditsDetails(token, { changed, unchanged }, ...[columns, rows]) {
1988
+ if (changed.length === 0) {
1989
+ return new MarkdownDocument4().paragraph(
1990
+ summarizeUnchanged(token, { changed, unchanged })
1991
+ );
1992
+ }
1993
+ return new MarkdownDocument4().table(columns, rows.slice(0, MAX_ROWS)).paragraph(
1994
+ changed.length > MAX_ROWS && md5.italic(
1995
+ `Only the ${MAX_ROWS} most affected ${pluralize(
1996
+ token
1997
+ )} are listed above for brevity.`
1998
+ )
1999
+ ).paragraph(
2000
+ unchanged.length > 0 && summarizeUnchanged(token, { changed, unchanged })
2001
+ );
2002
+ }
2284
2003
  function formatTitle({
2285
2004
  title,
2286
2005
  docsUrl
2287
2006
  }) {
2288
2007
  if (docsUrl) {
2289
- return link7(docsUrl, title);
2008
+ return md5.link(docsUrl, title);
2290
2009
  }
2291
2010
  return title;
2292
2011
  }
2012
+ function formatPortalLink(portalUrl) {
2013
+ return portalUrl && md5.link(portalUrl, "\u{1F575}\uFE0F See full comparison in Code PushUp portal \u{1F50D}");
2014
+ }
2293
2015
  function sortChanges(changes) {
2294
2016
  return [...changes].sort(
2295
2017
  (a, b) => Math.abs(b.scores.diff) - Math.abs(a.scores.diff) || Math.abs(b.values?.diff ?? 0) - Math.abs(a.values?.diff ?? 0)
2296
2018
  );
2297
2019
  }
2020
+ function getDiffChanges(diff) {
2021
+ return [
2022
+ ...diff.categories.changed,
2023
+ ...diff.groups.changed,
2024
+ ...diff.audits.changed
2025
+ ];
2026
+ }
2298
2027
  function changesToDiffOutcomes(changes) {
2299
2028
  return changes.map((change) => {
2300
2029
  if (change.scores.diff > 0) {
@@ -2329,9 +2058,218 @@ function countDiffOutcomes(outcomes) {
2329
2058
  unchanged: outcomes.filter((outcome) => outcome === "unchanged").length
2330
2059
  };
2331
2060
  }
2061
+ function formatReportOutcome(outcome, commits) {
2062
+ const outcomeTexts = {
2063
+ positive: md5`🥳 Code PushUp report has ${md5.bold("improved")}`,
2064
+ negative: md5`😟 Code PushUp report has ${md5.bold("regressed")}`,
2065
+ mixed: md5`🤨 Code PushUp report has both ${md5.bold(
2066
+ "improvements and regressions"
2067
+ )}`,
2068
+ unchanged: md5`😐 Code PushUp report is ${md5.bold("unchanged")}`
2069
+ };
2070
+ if (commits) {
2071
+ const commitsText = `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
2072
+ return md5`${outcomeTexts[outcome]} – ${commitsText}.`;
2073
+ }
2074
+ return md5`${outcomeTexts[outcome]}.`;
2075
+ }
2076
+ function compareDiffsBy(type, a, b) {
2077
+ return sumScoreChanges(b[type].changed) - sumScoreChanges(a[type].changed) || sumConfigChanges(b[type]) - sumConfigChanges(a[type]);
2078
+ }
2079
+ function sumScoreChanges(changes) {
2080
+ return changes.reduce(
2081
+ (acc, { scores }) => acc + Math.abs(scores.diff),
2082
+ 0
2083
+ );
2084
+ }
2085
+ function sumConfigChanges({
2086
+ added,
2087
+ removed
2088
+ }) {
2089
+ return added.length + removed.length;
2090
+ }
2091
+
2092
+ // packages/utils/src/lib/reports/generate-md-reports-diff.ts
2093
+ function generateMdReportsDiff(diff) {
2094
+ return new MarkdownDocument5().$concat(
2095
+ createDiffHeaderSection(diff),
2096
+ createDiffCategoriesSection(diff),
2097
+ createDiffDetailsSection(diff)
2098
+ ).toString();
2099
+ }
2100
+ function generateMdReportsDiffForMonorepo(diffs) {
2101
+ const diffsWithOutcomes = diffs.map((diff) => ({
2102
+ ...diff,
2103
+ outcome: mergeDiffOutcomes(changesToDiffOutcomes(getDiffChanges(diff)))
2104
+ })).sort(
2105
+ (a, b) => compareDiffsBy("categories", a, b) || compareDiffsBy("groups", a, b) || compareDiffsBy("audits", a, b) || a.label.localeCompare(b.label)
2106
+ );
2107
+ const unchanged = diffsWithOutcomes.filter(
2108
+ ({ outcome }) => outcome === "unchanged"
2109
+ );
2110
+ const changed = diffsWithOutcomes.filter((diff) => !unchanged.includes(diff));
2111
+ return new MarkdownDocument5().$concat(
2112
+ createDiffHeaderSection(diffs),
2113
+ ...changed.map(createDiffProjectSection)
2114
+ ).$if(
2115
+ unchanged.length > 0,
2116
+ (doc) => doc.rule().paragraph(summarizeUnchanged("project", { unchanged, changed }))
2117
+ ).toString();
2118
+ }
2119
+ function createDiffHeaderSection(diff) {
2120
+ const outcome = mergeDiffOutcomes(
2121
+ changesToDiffOutcomes(toArray(diff).flatMap(getDiffChanges))
2122
+ );
2123
+ const commits = Array.isArray(diff) ? diff[0]?.commits : diff.commits;
2124
+ const portalUrl = Array.isArray(diff) ? void 0 : diff.portalUrl;
2125
+ return new MarkdownDocument5().heading(HIERARCHY.level_1, "Code PushUp").paragraph(formatReportOutcome(outcome, commits)).paragraph(formatPortalLink(portalUrl));
2126
+ }
2127
+ function createDiffProjectSection(diff) {
2128
+ return new MarkdownDocument5().heading(HIERARCHY.level_2, md6`💼 Project ${md6.code(diff.label)}`).paragraph(formatReportOutcome(diff.outcome)).paragraph(formatPortalLink(diff.portalUrl)).$concat(
2129
+ createDiffCategoriesSection(diff, {
2130
+ skipHeading: true,
2131
+ skipUnchanged: true
2132
+ }),
2133
+ createDiffDetailsSection(diff, HIERARCHY.level_3)
2134
+ );
2135
+ }
2136
+ function createDiffCategoriesSection(diff, options2) {
2137
+ const { changed, unchanged, added } = diff.categories;
2138
+ const { skipHeading, skipUnchanged } = options2 ?? {};
2139
+ const categoriesCount = changed.length + unchanged.length + added.length;
2140
+ const hasChanges = unchanged.length < categoriesCount;
2141
+ if (categoriesCount === 0) {
2142
+ return null;
2143
+ }
2144
+ const [columns, rows] = createCategoriesTable(diff, {
2145
+ hasChanges,
2146
+ skipUnchanged
2147
+ });
2148
+ return new MarkdownDocument5().heading(HIERARCHY.level_2, !skipHeading && "\u{1F3F7}\uFE0F Categories").table(columns, rows).paragraph(added.length > 0 && md6.italic("(\\*) New category.")).paragraph(
2149
+ skipUnchanged && unchanged.length > 0 && summarizeUnchanged("category", { changed, unchanged })
2150
+ );
2151
+ }
2152
+ function createCategoriesTable(diff, options2) {
2153
+ const { changed, unchanged, added } = diff.categories;
2154
+ const { hasChanges, skipUnchanged } = options2;
2155
+ const columns = [
2156
+ { heading: "\u{1F3F7}\uFE0F Category", alignment: "left" },
2157
+ {
2158
+ heading: hasChanges ? "\u2B50 Previous score" : "\u2B50 Score",
2159
+ alignment: "center"
2160
+ },
2161
+ { heading: "\u2B50 Current score", alignment: "center" },
2162
+ { heading: "\u{1F504} Score change", alignment: "center" }
2163
+ ];
2164
+ const rows = [
2165
+ ...sortChanges(changed).map((category) => [
2166
+ formatTitle(category),
2167
+ formatScoreWithColor(category.scores.before, {
2168
+ skipBold: true
2169
+ }),
2170
+ formatScoreWithColor(category.scores.after),
2171
+ formatScoreChange(category.scores.diff)
2172
+ ]),
2173
+ ...added.map((category) => [
2174
+ formatTitle(category),
2175
+ md6.italic("n/a (\\*)"),
2176
+ formatScoreWithColor(category.score),
2177
+ md6.italic("n/a (\\*)")
2178
+ ]),
2179
+ ...skipUnchanged ? [] : unchanged.map((category) => [
2180
+ formatTitle(category),
2181
+ formatScoreWithColor(category.score, { skipBold: true }),
2182
+ formatScoreWithColor(category.score),
2183
+ "\u2013"
2184
+ ])
2185
+ ];
2186
+ return [
2187
+ hasChanges ? columns : columns.slice(0, 2),
2188
+ rows.map((row) => hasChanges ? row : row.slice(0, 2))
2189
+ ];
2190
+ }
2191
+ function createDiffDetailsSection(diff, level = HIERARCHY.level_2) {
2192
+ if (diff.groups.changed.length + diff.audits.changed.length === 0) {
2193
+ return null;
2194
+ }
2195
+ const summary = ["group", "audit"].map(
2196
+ (token) => summarizeDiffOutcomes(
2197
+ changesToDiffOutcomes(diff[`${token}s`].changed),
2198
+ token
2199
+ )
2200
+ ).filter(Boolean).join(", ");
2201
+ const details2 = new MarkdownDocument5().$concat(
2202
+ createDiffGroupsSection(diff, level),
2203
+ createDiffAuditsSection(diff, level)
2204
+ );
2205
+ return new MarkdownDocument5().details(summary, details2);
2206
+ }
2207
+ function createDiffGroupsSection(diff, level) {
2208
+ if (diff.groups.changed.length + diff.groups.unchanged.length === 0) {
2209
+ return null;
2210
+ }
2211
+ return new MarkdownDocument5().heading(level, "\u{1F5C3}\uFE0F Groups").$concat(
2212
+ createGroupsOrAuditsDetails(
2213
+ "group",
2214
+ diff.groups,
2215
+ [
2216
+ { heading: "\u{1F50C} Plugin", alignment: "left" },
2217
+ { heading: "\u{1F5C3}\uFE0F Group", alignment: "left" },
2218
+ { heading: "\u2B50 Previous score", alignment: "center" },
2219
+ { heading: "\u2B50 Current score", alignment: "center" },
2220
+ { heading: "\u{1F504} Score change", alignment: "center" }
2221
+ ],
2222
+ sortChanges(diff.groups.changed).map((group) => [
2223
+ formatTitle(group.plugin),
2224
+ formatTitle(group),
2225
+ formatScoreWithColor(group.scores.before, { skipBold: true }),
2226
+ formatScoreWithColor(group.scores.after),
2227
+ formatScoreChange(group.scores.diff)
2228
+ ])
2229
+ )
2230
+ );
2231
+ }
2232
+ function createDiffAuditsSection(diff, level) {
2233
+ return new MarkdownDocument5().heading(level, "\u{1F6E1}\uFE0F Audits").$concat(
2234
+ createGroupsOrAuditsDetails(
2235
+ "audit",
2236
+ diff.audits,
2237
+ [
2238
+ { heading: "\u{1F50C} Plugin", alignment: "left" },
2239
+ { heading: "\u{1F6E1}\uFE0F Audit", alignment: "left" },
2240
+ { heading: "\u{1F4CF} Previous value", alignment: "center" },
2241
+ { heading: "\u{1F4CF} Current value", alignment: "center" },
2242
+ { heading: "\u{1F504} Value change", alignment: "center" }
2243
+ ],
2244
+ sortChanges(diff.audits.changed).map((audit) => [
2245
+ formatTitle(audit.plugin),
2246
+ formatTitle(audit),
2247
+ `${scoreMarker(audit.scores.before, "square")} ${audit.displayValues.before || audit.values.before.toString()}`,
2248
+ md6`${scoreMarker(audit.scores.after, "square")} ${md6.bold(
2249
+ audit.displayValues.after || audit.values.after.toString()
2250
+ )}`,
2251
+ formatValueChange(audit)
2252
+ ])
2253
+ )
2254
+ );
2255
+ }
2256
+
2257
+ // packages/utils/src/lib/reports/load-report.ts
2258
+ import { join as join2 } from "node:path";
2259
+ async function loadReport(options2) {
2260
+ const { outputDir, filename, format } = options2;
2261
+ await ensureDirectoryExists(outputDir);
2262
+ const filePath = join2(outputDir, `${filename}.${format}`);
2263
+ if (format === "json") {
2264
+ const content = await readJsonFile(filePath);
2265
+ return reportSchema.parse(content);
2266
+ }
2267
+ const text = await readTextFile(filePath);
2268
+ return text;
2269
+ }
2332
2270
 
2333
2271
  // packages/utils/src/lib/reports/log-stdout-summary.ts
2334
- import chalk4 from "chalk";
2272
+ import { bold as bold4, cyan, cyanBright, green as green2, red } from "ansis";
2335
2273
  function log(msg = "") {
2336
2274
  ui().logger.log(msg);
2337
2275
  }
@@ -2347,15 +2285,15 @@ function logStdoutSummary(report) {
2347
2285
  log();
2348
2286
  }
2349
2287
  function reportToHeaderSection(report) {
2350
- const { packageName, version: version2 } = report;
2351
- return `${chalk4.bold(reportHeadlineText)} - ${packageName}@${version2}`;
2288
+ const { packageName, version: version3 } = report;
2289
+ return `${bold4(REPORT_HEADLINE_TEXT)} - ${packageName}@${version3}`;
2352
2290
  }
2353
2291
  function logPlugins(report) {
2354
2292
  const { plugins } = report;
2355
2293
  plugins.forEach((plugin) => {
2356
2294
  const { title, audits } = plugin;
2357
2295
  log();
2358
- log(chalk4.magentaBright.bold(`${title} audits`));
2296
+ log(bold4.magentaBright(`${title} audits`));
2359
2297
  log();
2360
2298
  audits.forEach((audit) => {
2361
2299
  ui().row([
@@ -2370,8 +2308,9 @@ function logPlugins(report) {
2370
2308
  padding: [0, 3, 0, 0]
2371
2309
  },
2372
2310
  {
2373
- text: chalk4.cyanBright(audit.displayValue || `${audit.value}`),
2374
- width: 10,
2311
+ text: cyanBright(audit.displayValue || `${audit.value}`),
2312
+ // eslint-disable-next-line no-magic-numbers
2313
+ width: 20,
2375
2314
  padding: [0, 0, 0, 0]
2376
2315
  }
2377
2316
  ]);
@@ -2381,42 +2320,38 @@ function logPlugins(report) {
2381
2320
  }
2382
2321
  function logCategories({ categories, plugins }) {
2383
2322
  const hAlign = (idx) => idx === 0 ? "left" : "right";
2384
- const rows = categories.map(({ title, score, refs }) => [
2323
+ const rows = categories.map(({ title, score, refs, isBinary }) => [
2385
2324
  title,
2386
- applyScoreColor({ score }),
2325
+ `${binaryIconPrefix(score, isBinary)}${applyScoreColor({ score })}`,
2387
2326
  countCategoryAudits(refs, plugins)
2388
2327
  ]);
2389
- const table5 = ui().table();
2390
- table5.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
2391
- table5.head(
2392
- reportRawOverviewTableHeaders.map((heading, idx) => ({
2393
- content: chalk4.cyan(heading),
2328
+ const table2 = ui().table();
2329
+ table2.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
2330
+ table2.head(
2331
+ REPORT_RAW_OVERVIEW_TABLE_HEADERS.map((heading, idx) => ({
2332
+ content: cyan(heading),
2394
2333
  hAlign: hAlign(idx)
2395
2334
  }))
2396
2335
  );
2397
2336
  rows.forEach(
2398
- (row) => table5.row(
2337
+ (row) => table2.row(
2399
2338
  row.map((content, idx) => ({
2400
2339
  content: content.toString(),
2401
2340
  hAlign: hAlign(idx)
2402
2341
  }))
2403
2342
  )
2404
2343
  );
2405
- log(chalk4.magentaBright.bold("Categories"));
2344
+ log(bold4.magentaBright("Categories"));
2406
2345
  log();
2407
- table5.render();
2346
+ table2.render();
2408
2347
  log();
2409
2348
  }
2410
- function applyScoreColor({ score, text }) {
2411
- const formattedScore = text ?? formatReportScore(score);
2412
- const style = text ? chalk4 : chalk4.bold;
2413
- if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
2414
- return style.green(formattedScore);
2415
- }
2416
- if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
2417
- return style.yellow(formattedScore);
2418
- }
2419
- return style.red(formattedScore);
2349
+ function binaryIconPrefix(score, isBinary) {
2350
+ return targetScoreIcon(score, isBinary ? 1 : void 0, {
2351
+ passIcon: bold4(green2("\u2713")),
2352
+ failIcon: bold4(red("\u2717")),
2353
+ postfix: " "
2354
+ });
2420
2355
  }
2421
2356
 
2422
2357
  // packages/utils/src/lib/reports/scoring.ts
@@ -2506,56 +2441,6 @@ function parseScoringParameters(refs, scoreFn) {
2506
2441
  return scoredRefs;
2507
2442
  }
2508
2443
 
2509
- // packages/utils/src/lib/reports/sorting.ts
2510
- function sortReport(report) {
2511
- const { categories, plugins } = report;
2512
- const sortedCategories = categories.map((category) => {
2513
- const { audits, groups: groups2 } = category.refs.reduce(
2514
- (acc, ref) => ({
2515
- ...acc,
2516
- ...ref.type === "group" ? {
2517
- groups: [...acc.groups, getSortableGroupByRef(ref, plugins)]
2518
- } : {
2519
- audits: [...acc.audits, getSortableAuditByRef(ref, plugins)]
2520
- }
2521
- }),
2522
- { groups: [], audits: [] }
2523
- );
2524
- const sortedAuditsAndGroups = [...audits, ...groups2].sort(
2525
- compareCategoryAuditsAndGroups
2526
- );
2527
- const sortedRefs = [...category.refs].sort((a, b) => {
2528
- const aIndex = sortedAuditsAndGroups.findIndex(
2529
- (ref) => ref.slug === a.slug && ref.plugin === a.plugin
2530
- );
2531
- const bIndex = sortedAuditsAndGroups.findIndex(
2532
- (ref) => ref.slug === b.slug && ref.plugin === b.plugin
2533
- );
2534
- return aIndex - bIndex;
2535
- });
2536
- return { ...category, refs: sortedRefs };
2537
- });
2538
- return {
2539
- ...report,
2540
- categories: sortedCategories,
2541
- plugins: sortPlugins(plugins)
2542
- };
2543
- }
2544
- function sortPlugins(plugins) {
2545
- return plugins.map((plugin) => ({
2546
- ...plugin,
2547
- audits: [...plugin.audits].sort(compareAudits).map(
2548
- (audit) => audit.details?.issues ? {
2549
- ...audit,
2550
- details: {
2551
- ...audit.details,
2552
- issues: [...audit.details.issues].sort(compareIssues)
2553
- }
2554
- } : audit
2555
- )
2556
- }));
2557
- }
2558
-
2559
2444
  // packages/utils/src/lib/verbose-utils.ts
2560
2445
  function getLogVerbose(verbose = false) {
2561
2446
  return (msg) => {
@@ -2578,10 +2463,10 @@ var verboseUtils = (verbose = false) => ({
2578
2463
 
2579
2464
  // packages/core/package.json
2580
2465
  var name = "@code-pushup/core";
2581
- var version = "0.48.0";
2466
+ var version = "0.50.0";
2582
2467
 
2583
2468
  // packages/core/src/lib/implementation/execute-plugin.ts
2584
- import chalk5 from "chalk";
2469
+ import { bold as bold5 } from "ansis";
2585
2470
 
2586
2471
  // packages/core/src/lib/normalize.ts
2587
2472
  function normalizeIssue(issue, gitRoot) {
@@ -2644,7 +2529,7 @@ async function executeRunnerFunction(runner, onProgress) {
2644
2529
  var PluginOutputMissingAuditError = class extends Error {
2645
2530
  constructor(auditSlug) {
2646
2531
  super(
2647
- `Audit metadata not present in plugin config. Missing slug: ${chalk5.bold(
2532
+ `Audit metadata not present in plugin config. Missing slug: ${bold5(
2648
2533
  auditSlug
2649
2534
  )}`
2650
2535
  );
@@ -2686,7 +2571,7 @@ async function executePlugin(pluginConfig, onProgress) {
2686
2571
  };
2687
2572
  }
2688
2573
  var wrapProgress = async (pluginCfg, steps, progressBar) => {
2689
- progressBar?.updateTitle(`Executing ${chalk5.bold(pluginCfg.title)}`);
2574
+ progressBar?.updateTitle(`Executing ${bold5(pluginCfg.title)}`);
2690
2575
  try {
2691
2576
  const pluginReport = await executePlugin(pluginCfg);
2692
2577
  progressBar?.incrementInSteps(steps);
@@ -2694,7 +2579,7 @@ var wrapProgress = async (pluginCfg, steps, progressBar) => {
2694
2579
  } catch (error) {
2695
2580
  progressBar?.incrementInSteps(steps);
2696
2581
  throw new Error(
2697
- error instanceof Error ? `- Plugin ${chalk5.bold(pluginCfg.title)} (${chalk5.bold(
2582
+ error instanceof Error ? `- Plugin ${bold5(pluginCfg.title)} (${bold5(
2698
2583
  pluginCfg.slug
2699
2584
  )}) produced the following error:
2700
2585
  - ${error.message}` : String(error)
@@ -2834,6 +2719,10 @@ async function collectAndPersistReports(options2) {
2834
2719
  // packages/core/src/lib/compare.ts
2835
2720
  import { writeFile as writeFile2 } from "node:fs/promises";
2836
2721
  import { join as join5 } from "node:path";
2722
+ import {
2723
+ PortalOperationError,
2724
+ getPortalComparisonLink
2725
+ } from "@code-pushup/portal-client";
2837
2726
 
2838
2727
  // packages/core/src/lib/implementation/compare-scorables.ts
2839
2728
  function compareCategories(reports) {
@@ -2970,7 +2859,7 @@ function selectMeta(meta) {
2970
2859
  }
2971
2860
 
2972
2861
  // packages/core/src/lib/compare.ts
2973
- async function compareReportFiles(inputPaths, persistConfig) {
2862
+ async function compareReportFiles(inputPaths, persistConfig, uploadConfig, label) {
2974
2863
  const { outputDir, filename, format } = persistConfig;
2975
2864
  const [reportBefore, reportAfter] = await Promise.all([
2976
2865
  readJsonFile(inputPaths.before),
@@ -2980,11 +2869,20 @@ async function compareReportFiles(inputPaths, persistConfig) {
2980
2869
  before: reportSchema.parse(reportBefore),
2981
2870
  after: reportSchema.parse(reportAfter)
2982
2871
  };
2983
- const reportsDiff = compareReports(reports);
2872
+ const diff = compareReports(reports);
2873
+ if (label) {
2874
+ diff.label = label;
2875
+ }
2876
+ if (uploadConfig && diff.commits) {
2877
+ diff.portalUrl = await fetchPortalComparisonLink(
2878
+ uploadConfig,
2879
+ diff.commits
2880
+ );
2881
+ }
2984
2882
  return Promise.all(
2985
2883
  format.map(async (fmt) => {
2986
2884
  const outputPath = join5(outputDir, `${filename}-diff.${fmt}`);
2987
- const content = reportsDiffToFileContent(reportsDiff, fmt);
2885
+ const content = reportsDiffToFileContent(diff, fmt);
2988
2886
  await ensureDirectoryExists(outputDir);
2989
2887
  await writeFile2(outputPath, content);
2990
2888
  return outputPath;
@@ -3022,6 +2920,29 @@ function reportsDiffToFileContent(reportsDiff, format) {
3022
2920
  return generateMdReportsDiff(reportsDiff);
3023
2921
  }
3024
2922
  }
2923
+ async function fetchPortalComparisonLink(uploadConfig, commits) {
2924
+ const { server, apiKey, organization, project } = uploadConfig;
2925
+ try {
2926
+ return await getPortalComparisonLink({
2927
+ server,
2928
+ apiKey,
2929
+ parameters: {
2930
+ organization,
2931
+ project,
2932
+ before: commits.before.hash,
2933
+ after: commits.after.hash
2934
+ }
2935
+ });
2936
+ } catch (error) {
2937
+ if (error instanceof PortalOperationError) {
2938
+ ui().logger.warning(
2939
+ `Failed to fetch portal comparison link - ${error.message}`
2940
+ );
2941
+ return void 0;
2942
+ }
2943
+ throw error;
2944
+ }
2945
+ }
3025
2946
 
3026
2947
  // packages/core/src/lib/upload.ts
3027
2948
  import {
@@ -3077,9 +2998,9 @@ function auditToGQL(audit) {
3077
2998
  score,
3078
2999
  value,
3079
3000
  displayValue: formattedValue,
3080
- details: details4
3001
+ details: details2
3081
3002
  } = audit;
3082
- const { issues, table: table5 } = details4 ?? {};
3003
+ const { issues, table: table2 } = details2 ?? {};
3083
3004
  return {
3084
3005
  slug,
3085
3006
  title,
@@ -3088,10 +3009,10 @@ function auditToGQL(audit) {
3088
3009
  score,
3089
3010
  value,
3090
3011
  formattedValue,
3091
- ...details4 && {
3012
+ ...details2 && {
3092
3013
  details: {
3093
3014
  ...issues && { issues: issues.map(issueToGQL) },
3094
- ...table5 && { tables: [tableToGQL(table5)] }
3015
+ ...table2 && { tables: [tableToGQL(table2)] }
3095
3016
  }
3096
3017
  }
3097
3018
  };
@@ -3110,11 +3031,11 @@ function issueToGQL(issue) {
3110
3031
  }
3111
3032
  };
3112
3033
  }
3113
- function tableToGQL(table5) {
3034
+ function tableToGQL(table2) {
3114
3035
  return {
3115
- ...table5.title && { title: table5.title },
3116
- ...table5.columns?.length && {
3117
- columns: table5.columns.map(
3036
+ ...table2.title && { title: table2.title },
3037
+ ...table2.columns?.length && {
3038
+ columns: table2.columns.map(
3118
3039
  (column) => typeof column === "string" ? { alignment: tableAlignmentToGQL(column) } : {
3119
3040
  key: column.key,
3120
3041
  label: column.label,
@@ -3122,7 +3043,7 @@ function tableToGQL(table5) {
3122
3043
  }
3123
3044
  )
3124
3045
  },
3125
- rows: table5.rows.map(
3046
+ rows: table2.rows.map(
3126
3047
  (row) => Array.isArray(row) ? row.map((content) => ({ content: content?.toString() ?? "" })) : Object.entries(row).map(([key, content]) => ({
3127
3048
  key,
3128
3049
  content: content?.toString() ?? ""
@@ -3269,15 +3190,54 @@ async function autoloadRc(tsconfig) {
3269
3190
  );
3270
3191
  }
3271
3192
 
3193
+ // packages/core/src/lib/merge-diffs.ts
3194
+ import { writeFile as writeFile3 } from "node:fs/promises";
3195
+ import { basename, dirname, join as join7 } from "node:path";
3196
+ async function mergeDiffs(files, persistConfig) {
3197
+ const results = await Promise.allSettled(
3198
+ files.map(async (file) => {
3199
+ const json = await readJsonFile(file).catch((error) => {
3200
+ throw new Error(
3201
+ `Failed to read JSON file ${file} - ${stringifyError(error)}`
3202
+ );
3203
+ });
3204
+ const result = await reportsDiffSchema.safeParseAsync(json);
3205
+ if (!result.success) {
3206
+ throw new Error(
3207
+ `Invalid reports diff in ${file} - ${result.error.message}`
3208
+ );
3209
+ }
3210
+ return { ...result.data, file };
3211
+ })
3212
+ );
3213
+ results.filter(isPromiseRejectedResult).forEach(({ reason }) => {
3214
+ ui().logger.warning(
3215
+ `Skipped invalid report diff - ${stringifyError(reason)}`
3216
+ );
3217
+ });
3218
+ const diffs = results.filter(isPromiseFulfilledResult).map(({ value }) => value);
3219
+ const labeledDiffs = diffs.map((diff) => ({
3220
+ ...diff,
3221
+ label: diff.label || basename(dirname(diff.file))
3222
+ // fallback is parent folder name
3223
+ }));
3224
+ const markdown = generateMdReportsDiffForMonorepo(labeledDiffs);
3225
+ const { outputDir, filename } = persistConfig;
3226
+ const outputPath = join7(outputDir, `${filename}-diff.md`);
3227
+ await ensureDirectoryExists(outputDir);
3228
+ await writeFile3(outputPath, markdown);
3229
+ return outputPath;
3230
+ }
3231
+
3272
3232
  // packages/cli/src/lib/constants.ts
3273
3233
  var CLI_NAME = "Code PushUp CLI";
3274
3234
  var CLI_SCRIPT_NAME = "code-pushup";
3275
3235
 
3276
3236
  // packages/cli/src/lib/implementation/logging.ts
3277
- import chalk6 from "chalk";
3237
+ import { bold as bold6, gray as gray3 } from "ansis";
3278
3238
  function renderConfigureCategoriesHint() {
3279
3239
  ui().logger.info(
3280
- chalk6.gray(
3240
+ gray3(
3281
3241
  `\u{1F4A1} Configure categories to see the scores in an overview table. See: ${link(
3282
3242
  "https://github.com/code-pushup/cli/blob/main/packages/cli/README.md"
3283
3243
  )}`
@@ -3292,8 +3252,8 @@ function collectSuccessfulLog() {
3292
3252
  ui().logger.success("Collecting report successful!");
3293
3253
  }
3294
3254
  function renderIntegratePortalHint() {
3295
- ui().sticker().add(chalk6.bold(chalk6.gray("\u{1F4A1} Integrate the portal"))).add("").add(
3296
- `${chalk6.gray("\u276F")} Upload a report to the server - ${chalk6.gray(
3255
+ ui().sticker().add(bold6.gray("\u{1F4A1} Integrate the portal")).add("").add(
3256
+ `${gray3("\u276F")} Upload a report to the server - ${gray3(
3297
3257
  "npx code-pushup upload"
3298
3258
  )}`
3299
3259
  ).add(
@@ -3301,11 +3261,11 @@ function renderIntegratePortalHint() {
3301
3261
  "https://github.com/code-pushup/cli/tree/main/packages/cli#upload-command"
3302
3262
  )}`
3303
3263
  ).add(
3304
- `${chalk6.gray("\u276F")} ${chalk6.gray("Portal Integration")} - ${link(
3264
+ `${gray3("\u276F")} ${gray3("Portal Integration")} - ${link(
3305
3265
  "https://github.com/code-pushup/cli/blob/main/packages/cli/README.md#portal-integration"
3306
3266
  )}`
3307
3267
  ).add(
3308
- `${chalk6.gray("\u276F")} ${chalk6.gray("Upload Command")} - ${link(
3268
+ `${gray3("\u276F")} ${gray3("Upload Command")} - ${link(
3309
3269
  "https://github.com/code-pushup/cli/blob/main/packages/cli/README.md#portal-integration"
3310
3270
  )}`
3311
3271
  ).render();
@@ -3318,8 +3278,8 @@ function yargsAutorunCommandObject() {
3318
3278
  command: command2,
3319
3279
  describe: "Shortcut for running collect followed by upload",
3320
3280
  handler: async (args) => {
3321
- ui().logger.log(chalk7.bold(CLI_NAME));
3322
- ui().logger.info(chalk7.gray(`Run ${command2}...`));
3281
+ ui().logger.log(bold7(CLI_NAME));
3282
+ ui().logger.info(gray4(`Run ${command2}...`));
3323
3283
  const options2 = args;
3324
3284
  const optionsWithFormat = {
3325
3285
  ...options2,
@@ -3347,7 +3307,7 @@ function yargsAutorunCommandObject() {
3347
3307
  }
3348
3308
 
3349
3309
  // packages/cli/src/lib/collect/collect-command.ts
3350
- import chalk8 from "chalk";
3310
+ import { bold as bold8, gray as gray5 } from "ansis";
3351
3311
  function yargsCollectCommandObject() {
3352
3312
  const command2 = "collect";
3353
3313
  return {
@@ -3355,8 +3315,8 @@ function yargsCollectCommandObject() {
3355
3315
  describe: "Run Plugins and collect results",
3356
3316
  handler: async (args) => {
3357
3317
  const options2 = args;
3358
- ui().logger.log(chalk8.bold(CLI_NAME));
3359
- ui().logger.info(chalk8.gray(`Run ${command2}...`));
3318
+ ui().logger.log(bold8(CLI_NAME));
3319
+ ui().logger.info(gray5(`Run ${command2}...`));
3360
3320
  await collectAndPersistReports(options2);
3361
3321
  collectSuccessfulLog();
3362
3322
  if (options2.categories.length === 0) {
@@ -3370,8 +3330,8 @@ function yargsCollectCommandObject() {
3370
3330
  };
3371
3331
  }
3372
3332
  function renderUploadAutorunHint() {
3373
- ui().sticker().add(chalk8.bold(chalk8.gray("\u{1F4A1} Visualize your reports"))).add("").add(
3374
- `${chalk8.gray("\u276F")} npx code-pushup upload - ${chalk8.gray(
3333
+ ui().sticker().add(bold8.gray("\u{1F4A1} Visualize your reports")).add("").add(
3334
+ `${gray5("\u276F")} npx code-pushup upload - ${gray5(
3375
3335
  "Run upload to upload the created report to the server"
3376
3336
  )}`
3377
3337
  ).add(
@@ -3379,9 +3339,7 @@ function renderUploadAutorunHint() {
3379
3339
  "https://github.com/code-pushup/cli/tree/main/packages/cli#upload-command"
3380
3340
  )}`
3381
3341
  ).add(
3382
- `${chalk8.gray("\u276F")} npx code-pushup autorun - ${chalk8.gray(
3383
- "Run collect & upload"
3384
- )}`
3342
+ `${gray5("\u276F")} npx code-pushup autorun - ${gray5("Run collect & upload")}`
3385
3343
  ).add(
3386
3344
  ` ${link(
3387
3345
  "https://github.com/code-pushup/cli/tree/main/packages/cli#autorun-command"
@@ -3390,7 +3348,7 @@ function renderUploadAutorunHint() {
3390
3348
  }
3391
3349
 
3392
3350
  // packages/cli/src/lib/compare/compare-command.ts
3393
- import chalk9 from "chalk";
3351
+ import { bold as bold9, gray as gray6 } from "ansis";
3394
3352
 
3395
3353
  // packages/cli/src/lib/implementation/compare.options.ts
3396
3354
  function yargsCompareOptionsDefinition() {
@@ -3404,6 +3362,10 @@ function yargsCompareOptionsDefinition() {
3404
3362
  describe: "Path to target report.json",
3405
3363
  type: "string",
3406
3364
  demandOption: true
3365
+ },
3366
+ label: {
3367
+ describe: "Label for diff (e.g. project name)",
3368
+ type: "string"
3407
3369
  }
3408
3370
  };
3409
3371
  }
@@ -3416,20 +3378,25 @@ function yargsCompareCommandObject() {
3416
3378
  describe: "Compare 2 report files and create a diff file",
3417
3379
  builder: yargsCompareOptionsDefinition(),
3418
3380
  handler: async (args) => {
3419
- ui().logger.log(chalk9.bold(CLI_NAME));
3420
- ui().logger.info(chalk9.gray(`Run ${command2}...`));
3381
+ ui().logger.log(bold9(CLI_NAME));
3382
+ ui().logger.info(gray6(`Run ${command2}...`));
3421
3383
  const options2 = args;
3422
- const { before, after, persist } = options2;
3423
- const outputPaths = await compareReportFiles({ before, after }, persist);
3384
+ const { before, after, label, persist, upload: upload2 } = options2;
3385
+ const outputPaths = await compareReportFiles(
3386
+ { before, after },
3387
+ persist,
3388
+ upload2,
3389
+ label
3390
+ );
3424
3391
  ui().logger.info(
3425
- `Reports diff written to ${outputPaths.map((path) => chalk9.bold(path)).join(" and ")}`
3392
+ `Reports diff written to ${outputPaths.map((path) => bold9(path)).join(" and ")}`
3426
3393
  );
3427
3394
  }
3428
3395
  };
3429
3396
  }
3430
3397
 
3431
3398
  // packages/cli/src/lib/history/history-command.ts
3432
- import chalk10 from "chalk";
3399
+ import { bold as bold10, gray as gray7 } from "ansis";
3433
3400
 
3434
3401
  // packages/cli/src/lib/implementation/global.utils.ts
3435
3402
  function filterKebabCaseKeys(obj) {
@@ -3555,8 +3522,8 @@ async function normalizeHashOptions(processArgs) {
3555
3522
  // packages/cli/src/lib/history/history-command.ts
3556
3523
  var command = "history";
3557
3524
  async function handler(args) {
3558
- ui().logger.info(chalk10.bold(CLI_NAME));
3559
- ui().logger.info(chalk10.gray(`Run ${command}`));
3525
+ ui().logger.info(bold10(CLI_NAME));
3526
+ ui().logger.info(gray7(`Run ${command}`));
3560
3527
  const currentBranch = await getCurrentBranchOrTag();
3561
3528
  const { targetBranch: rawTargetBranch, ...opt } = args;
3562
3529
  const {
@@ -3605,6 +3572,38 @@ function yargsHistoryCommandObject() {
3605
3572
  };
3606
3573
  }
3607
3574
 
3575
+ // packages/cli/src/lib/merge-diffs/merge-diffs-command.ts
3576
+ import { bold as bold11, gray as gray8 } from "ansis";
3577
+
3578
+ // packages/cli/src/lib/implementation/merge-diffs.options.ts
3579
+ function yargsMergeDiffsOptionsDefinition() {
3580
+ return {
3581
+ files: {
3582
+ describe: "List of report-diff.json paths",
3583
+ type: "array",
3584
+ demandOption: true
3585
+ }
3586
+ };
3587
+ }
3588
+
3589
+ // packages/cli/src/lib/merge-diffs/merge-diffs-command.ts
3590
+ function yargsMergeDiffsCommandObject() {
3591
+ const command2 = "merge-diffs";
3592
+ return {
3593
+ command: command2,
3594
+ describe: "Combine many report diffs into a single diff file",
3595
+ builder: yargsMergeDiffsOptionsDefinition(),
3596
+ handler: async (args) => {
3597
+ ui().logger.log(bold11(CLI_NAME));
3598
+ ui().logger.info(gray8(`Run ${command2}...`));
3599
+ const options2 = args;
3600
+ const { files, persist } = options2;
3601
+ const outputPath = await mergeDiffs(files, persist);
3602
+ ui().logger.info(`Reports diff written to ${bold11(outputPath)}`);
3603
+ }
3604
+ };
3605
+ }
3606
+
3608
3607
  // packages/cli/src/lib/print-config/print-config-command.ts
3609
3608
  function yargsConfigCommandObject() {
3610
3609
  const command2 = "print-config";
@@ -3620,15 +3619,15 @@ function yargsConfigCommandObject() {
3620
3619
  }
3621
3620
 
3622
3621
  // packages/cli/src/lib/upload/upload-command.ts
3623
- import chalk11 from "chalk";
3622
+ import { bold as bold12, gray as gray9 } from "ansis";
3624
3623
  function yargsUploadCommandObject() {
3625
3624
  const command2 = "upload";
3626
3625
  return {
3627
3626
  command: command2,
3628
3627
  describe: "Upload report results to the portal",
3629
3628
  handler: async (args) => {
3630
- ui().logger.log(chalk11.bold(CLI_NAME));
3631
- ui().logger.info(chalk11.gray(`Run ${command2}...`));
3629
+ ui().logger.log(bold12(CLI_NAME));
3630
+ ui().logger.info(gray9(`Run ${command2}...`));
3632
3631
  const options2 = args;
3633
3632
  if (options2.upload == null) {
3634
3633
  renderIntegratePortalHint();
@@ -3651,7 +3650,8 @@ var commands = [
3651
3650
  yargsUploadCommandObject(),
3652
3651
  yargsHistoryCommandObject(),
3653
3652
  yargsCompareCommandObject(),
3654
- yargsConfigCommandObject()
3653
+ yargsConfigCommandObject(),
3654
+ yargsMergeDiffsCommandObject()
3655
3655
  ];
3656
3656
 
3657
3657
  // packages/cli/src/lib/implementation/core-config.middleware.ts
@@ -3679,7 +3679,9 @@ async function coreConfigMiddleware(processArgs) {
3679
3679
  persist: {
3680
3680
  outputDir: cliPersist?.outputDir ?? rcPersist?.outputDir ?? DEFAULT_PERSIST_OUTPUT_DIR,
3681
3681
  filename: cliPersist?.filename ?? rcPersist?.filename ?? DEFAULT_PERSIST_FILENAME,
3682
- format: cliPersist?.format ?? rcPersist?.format ?? DEFAULT_PERSIST_FORMAT
3682
+ format: normalizeFormats(
3683
+ cliPersist?.format ?? rcPersist?.format ?? DEFAULT_PERSIST_FORMAT
3684
+ )
3683
3685
  },
3684
3686
  ...upload2 != null && { upload: upload2 },
3685
3687
  categories: rcCategories ?? [],
@@ -3687,9 +3689,10 @@ async function coreConfigMiddleware(processArgs) {
3687
3689
  ...remainingCliOptions
3688
3690
  };
3689
3691
  }
3692
+ var normalizeFormats = (formats) => (formats ?? []).flatMap((format) => format.split(","));
3690
3693
 
3691
3694
  // packages/cli/src/lib/implementation/validate-plugin-filter-options.utils.ts
3692
- import chalk12 from "chalk";
3695
+ import { yellow } from "ansis";
3693
3696
  function validatePluginFilterOption(filterOption, {
3694
3697
  plugins,
3695
3698
  categories
@@ -3705,7 +3708,7 @@ function validatePluginFilterOption(filterOption, {
3705
3708
  const filterFunction = (plugin) => isSkipOption ? pluginsToFilterSet.has(plugin) : !pluginsToFilterSet.has(plugin);
3706
3709
  if (missingPlugins.length > 0 && verbose) {
3707
3710
  ui().logger.info(
3708
- `${chalk12.yellow(
3711
+ `${yellow(
3709
3712
  "\u26A0"
3710
3713
  )} The --${filterOption} argument references plugins with "${missingPlugins.join(
3711
3714
  '", "'
@@ -3860,7 +3863,7 @@ function yargsGlobalOptionsDefinition() {
3860
3863
  default: false
3861
3864
  },
3862
3865
  config: {
3863
- describe: "Path to config file, e.g. code-pushup.config.ts. By default it loads code-pushup.config.(ts|mjs|js).",
3866
+ describe: "Path to config file. By default it loads code-pushup.config.(ts|mjs|js).",
3864
3867
  type: "string"
3865
3868
  },
3866
3869
  tsconfig: {
@@ -3888,8 +3891,55 @@ var groups = {
3888
3891
  };
3889
3892
 
3890
3893
  // packages/cli/src/lib/yargs-cli.ts
3891
- import chalk13 from "chalk";
3894
+ import { blue, dim as dim2, green as green4 } from "ansis";
3892
3895
  import yargs from "yargs";
3896
+
3897
+ // packages/cli/package.json
3898
+ var version2 = "0.50.0";
3899
+
3900
+ // packages/cli/src/lib/implementation/formatting.ts
3901
+ import { bold as bold13, dim, green as green3 } from "ansis";
3902
+ function titleStyle(title) {
3903
+ return `${bold13(title)}`;
3904
+ }
3905
+ function headerStyle(title) {
3906
+ return `${green3(title)}`;
3907
+ }
3908
+ function descriptionStyle(title) {
3909
+ return `${dim(title)}`;
3910
+ }
3911
+ function formatObjectValue(opts, propName) {
3912
+ const description = opts[propName];
3913
+ return {
3914
+ ...opts,
3915
+ ...typeof description === "string" && {
3916
+ [propName]: descriptionStyle(description)
3917
+ }
3918
+ };
3919
+ }
3920
+ function formatNestedValues(options2, propName) {
3921
+ return Object.fromEntries(
3922
+ Object.entries(options2).map(([key, opts]) => [
3923
+ key,
3924
+ formatObjectValue(opts, propName)
3925
+ ])
3926
+ );
3927
+ }
3928
+
3929
+ // packages/cli/src/lib/yargs-cli.ts
3930
+ var yargsDecorator = {
3931
+ "Commands:": `${green4("Commands")}:`,
3932
+ "Options:": `${green4("Options")}:`,
3933
+ "Examples:": `${green4("Examples")}:`,
3934
+ boolean: blue("boolean"),
3935
+ count: blue("count"),
3936
+ string: blue("string"),
3937
+ array: blue("array"),
3938
+ required: blue("required"),
3939
+ "default:": `${blue("default")}:`,
3940
+ "choices:": `${blue("choices")}:`,
3941
+ "aliases:": `${blue("aliases")}:`
3942
+ };
3893
3943
  function yargsCli(argv, cfg) {
3894
3944
  const { usageMessage, scriptName, noExitProcess } = cfg;
3895
3945
  const commands2 = cfg.commands ?? [];
@@ -3898,7 +3948,7 @@ function yargsCli(argv, cfg) {
3898
3948
  const groups2 = cfg.groups ?? {};
3899
3949
  const examples = cfg.examples ?? [];
3900
3950
  const cli2 = yargs(argv);
3901
- cli2.help().version(false).alias("h", "help").check((args) => {
3951
+ cli2.updateLocale(yargsDecorator).wrap(Math.max(TERMINAL_WIDTH, cli2.terminalWidth())).help("help", descriptionStyle("Show help")).alias("h", "help").showHelpOnFail(false).version("version", dim2`Show version`, version2).check((args) => {
3902
3952
  const persist = args["persist"];
3903
3953
  return persist == null || validatePersistFormat(persist);
3904
3954
  }).parserConfiguration({
@@ -3906,18 +3956,18 @@ function yargsCli(argv, cfg) {
3906
3956
  }).coerce(
3907
3957
  "config",
3908
3958
  (config) => Array.isArray(config) ? config.at(-1) : config
3909
- ).options(options2).wrap(TERMINAL_WIDTH);
3959
+ ).options(formatNestedValues(options2, "describe"));
3910
3960
  if (usageMessage) {
3911
- cli2.usage(chalk13.bold(usageMessage));
3961
+ cli2.usage(titleStyle(usageMessage));
3912
3962
  }
3913
3963
  if (scriptName) {
3914
3964
  cli2.scriptName(scriptName);
3915
3965
  }
3916
3966
  examples.forEach(
3917
- ([exampleName, description]) => cli2.example(exampleName, description)
3967
+ ([exampleName, description]) => cli2.example(exampleName, descriptionStyle(description))
3918
3968
  );
3919
3969
  Object.entries(groups2).forEach(
3920
- ([groupName, optionNames]) => cli2.group(optionNames, groupName)
3970
+ ([groupName, optionNames]) => cli2.group(optionNames, headerStyle(groupName))
3921
3971
  );
3922
3972
  middlewares2.forEach(({ middlewareFunction, applyBeforeValidation }) => {
3923
3973
  cli2.middleware(
@@ -3926,13 +3976,18 @@ function yargsCli(argv, cfg) {
3926
3976
  );
3927
3977
  });
3928
3978
  commands2.forEach((commandObj) => {
3929
- cli2.command({
3930
- ...commandObj,
3931
- handler: logErrorBeforeThrow(commandObj.handler),
3932
- ...typeof commandObj.builder === "function" && {
3933
- builder: logErrorBeforeThrow(commandObj.builder)
3934
- }
3935
- });
3979
+ cli2.command(
3980
+ formatObjectValue(
3981
+ {
3982
+ ...commandObj,
3983
+ handler: logErrorBeforeThrow(commandObj.handler),
3984
+ ...typeof commandObj.builder === "function" && {
3985
+ builder: logErrorBeforeThrow(commandObj.builder)
3986
+ }
3987
+ },
3988
+ "describe"
3989
+ )
3990
+ );
3936
3991
  });
3937
3992
  if (noExitProcess) {
3938
3993
  cli2.exitProcess(false);
@@ -3942,7 +3997,7 @@ function yargsCli(argv, cfg) {
3942
3997
  function validatePersistFormat(persist) {
3943
3998
  try {
3944
3999
  if (persist.format != null) {
3945
- persist.format.forEach((format) => formatSchema.parse(format));
4000
+ persist.format.flatMap((format) => format.split(",")).forEach((format) => formatSchema.parse(format));
3946
4001
  }
3947
4002
  return true;
3948
4003
  } catch {
@@ -3978,8 +4033,8 @@ var cli = (args) => yargsCli(args, {
3978
4033
  "Run collect skiping the coverage plugin, other plugins from config file will be included."
3979
4034
  ],
3980
4035
  [
3981
- "code-pushup upload --persist.outputDir=dist --persist.filename=cp-report --upload.apiKey=$CP_API_KEY",
3982
- "Upload dist/cp-report.json to portal using API key from environment variable"
4036
+ "code-pushup upload --persist.outputDir=dist --upload.apiKey=$CP_API_KEY",
4037
+ "Upload dist/report.json to portal using API key from environment variable"
3983
4038
  ],
3984
4039
  [
3985
4040
  "code-pushup print-config --config code-pushup.config.test.js",