@code-pushup/cli 0.27.1 → 0.29.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 (2) hide show
  1. package/index.js +403 -110
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -504,9 +504,9 @@ var CONFIG_FILE_NAME = "code-pushup.config";
504
504
  var SUPPORTED_CONFIG_FILE_FORMATS = ["ts", "mjs", "js"];
505
505
 
506
506
  // packages/models/src/lib/implementation/constants.ts
507
- var PERSIST_OUTPUT_DIR = ".code-pushup";
508
- var PERSIST_FORMAT = ["json"];
509
- var PERSIST_FILENAME = "report";
507
+ var DEFAULT_PERSIST_OUTPUT_DIR = ".code-pushup";
508
+ var DEFAULT_PERSIST_FILENAME = "report";
509
+ var DEFAULT_PERSIST_FORMAT = ["json", "md"];
510
510
 
511
511
  // packages/models/src/lib/report.ts
512
512
  import { z as z13 } from "zod";
@@ -718,6 +718,18 @@ import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
718
718
  function slugify(text) {
719
719
  return text.trim().toLowerCase().replace(/\s+|\//g, "-").replace(/[^a-z\d-]/g, "");
720
720
  }
721
+ function pluralize(text, amount) {
722
+ if (amount != null && Math.abs(amount) === 1) {
723
+ return text;
724
+ }
725
+ if (text.endsWith("y")) {
726
+ return `${text.slice(0, -1)}ies`;
727
+ }
728
+ if (text.endsWith("s")) {
729
+ return `${text}es`;
730
+ }
731
+ return `${text}s`;
732
+ }
721
733
  function formatBytes(bytes, decimals = 2) {
722
734
  const positiveBytes = Math.max(bytes, 0);
723
735
  if (positiveBytes === 0) {
@@ -729,6 +741,9 @@ function formatBytes(bytes, decimals = 2) {
729
741
  const i = Math.floor(Math.log(positiveBytes) / Math.log(k));
730
742
  return `${Number.parseFloat((positiveBytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
731
743
  }
744
+ function pluralizeToken(token, times) {
745
+ return `${times} ${Math.abs(times) === 1 ? token : pluralize(token)}`;
746
+ }
732
747
  function formatDuration(duration) {
733
748
  if (duration < 1e3) {
734
749
  return `${duration} ms`;
@@ -955,6 +970,9 @@ function style(text, styles = ["b"]) {
955
970
  function headline(text, hierarchy = 1) {
956
971
  return `${"#".repeat(hierarchy)} ${text}`;
957
972
  }
973
+ function h1(text) {
974
+ return headline(text, 1);
975
+ }
958
976
  function h2(text) {
959
977
  return headline(text, 2);
960
978
  }
@@ -962,6 +980,11 @@ function h3(text) {
962
980
  return headline(text, 3);
963
981
  }
964
982
 
983
+ // packages/utils/src/lib/reports/md/image.ts
984
+ function image(src, alt) {
985
+ return `![${alt}](${src})`;
986
+ }
987
+
965
988
  // packages/utils/src/lib/reports/md/link.ts
966
989
  function link2(href, text) {
967
990
  return `[${text || href}](${href})`;
@@ -973,6 +996,11 @@ function li(text, order = "unordered") {
973
996
  return `${style2} ${text}`;
974
997
  }
975
998
 
999
+ // packages/utils/src/lib/reports/md/paragraphs.ts
1000
+ function paragraphs(...sections) {
1001
+ return sections.filter(Boolean).join("\n\n");
1002
+ }
1003
+
976
1004
  // packages/utils/src/lib/reports/md/table.ts
977
1005
  var alignString = /* @__PURE__ */ new Map([
978
1006
  ["l", ":--"],
@@ -1007,6 +1035,10 @@ function tableHtml(data) {
1007
1035
  function formatReportScore(score) {
1008
1036
  return Math.round(score * 100).toString();
1009
1037
  }
1038
+ function formatScoreWithColor(score, options2) {
1039
+ const styledNumber = options2?.skipBold ? formatReportScore(score) : style(formatReportScore(score));
1040
+ return `${getRoundScoreMarker(score)} ${styledNumber}`;
1041
+ }
1010
1042
  function getRoundScoreMarker(score) {
1011
1043
  if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
1012
1044
  return "\u{1F7E2}";
@@ -1025,6 +1057,30 @@ function getSquaredScoreMarker(score) {
1025
1057
  }
1026
1058
  return "\u{1F7E5}";
1027
1059
  }
1060
+ function getDiffMarker(diff) {
1061
+ if (diff > 0) {
1062
+ return "\u2191";
1063
+ }
1064
+ if (diff < 0) {
1065
+ return "\u2193";
1066
+ }
1067
+ return "";
1068
+ }
1069
+ function colorByScoreDiff(text, diff) {
1070
+ const color = diff > 0 ? "green" : diff < 0 ? "red" : "gray";
1071
+ return shieldsBadge(text, color);
1072
+ }
1073
+ function shieldsBadge(text, color) {
1074
+ return image(
1075
+ `https://img.shields.io/badge/${encodeURIComponent(text)}-${color}`,
1076
+ text
1077
+ );
1078
+ }
1079
+ function formatDiffNumber(diff) {
1080
+ const number = Math.abs(diff) === Number.POSITIVE_INFINITY ? "\u221E" : `${Math.abs(diff)}`;
1081
+ const sign = diff < 0 ? "\u2212" : "+";
1082
+ return `${sign}${number}`;
1083
+ }
1028
1084
  function getSeverityIcon(severity) {
1029
1085
  if (severity === "error") {
1030
1086
  return "\u{1F6A8}";
@@ -1035,7 +1091,7 @@ function getSeverityIcon(severity) {
1035
1091
  return "\u2139\uFE0F";
1036
1092
  }
1037
1093
  function calcDuration(start, stop) {
1038
- return Math.floor((stop ?? performance.now()) - start);
1094
+ return Math.round((stop ?? performance.now()) - start);
1039
1095
  }
1040
1096
  function countCategoryAudits(refs, plugins) {
1041
1097
  const groupLookup = plugins.reduce(
@@ -1246,6 +1302,9 @@ import { simpleGit } from "simple-git";
1246
1302
  function toArray(val) {
1247
1303
  return Array.isArray(val) ? val : [val];
1248
1304
  }
1305
+ function objectToEntries(obj) {
1306
+ return Object.entries(obj);
1307
+ }
1249
1308
  function deepClone(obj) {
1250
1309
  return obj == null || typeof obj !== "object" ? obj : structuredClone(obj);
1251
1310
  }
@@ -1336,95 +1395,6 @@ function getProgressBar(taskName) {
1336
1395
  };
1337
1396
  }
1338
1397
 
1339
- // packages/utils/src/lib/reports/log-stdout-summary.ts
1340
- import chalk4 from "chalk";
1341
- function log(msg = "") {
1342
- ui().logger.log(msg);
1343
- }
1344
- function logStdoutSummary(report) {
1345
- const printCategories = report.categories.length > 0;
1346
- log(reportToHeaderSection(report));
1347
- log();
1348
- logPlugins(report);
1349
- if (printCategories) {
1350
- logCategories(report);
1351
- }
1352
- log(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`);
1353
- log();
1354
- }
1355
- function reportToHeaderSection(report) {
1356
- const { packageName, version: version2 } = report;
1357
- return `${chalk4.bold(reportHeadlineText)} - ${packageName}@${version2}`;
1358
- }
1359
- function logPlugins(report) {
1360
- const { plugins } = report;
1361
- plugins.forEach((plugin) => {
1362
- const { title, audits } = plugin;
1363
- log();
1364
- log(chalk4.magentaBright.bold(`${title} audits`));
1365
- log();
1366
- audits.forEach((audit) => {
1367
- ui().row([
1368
- {
1369
- text: applyScoreColor({ score: audit.score, text: "\u25CF" }),
1370
- width: 2,
1371
- padding: [0, 1, 0, 0]
1372
- },
1373
- {
1374
- text: audit.title,
1375
- // eslint-disable-next-line no-magic-numbers
1376
- padding: [0, 3, 0, 0]
1377
- },
1378
- {
1379
- text: chalk4.cyanBright(audit.displayValue || `${audit.value}`),
1380
- width: 10,
1381
- padding: [0, 0, 0, 0]
1382
- }
1383
- ]);
1384
- });
1385
- log();
1386
- });
1387
- }
1388
- function logCategories({ categories, plugins }) {
1389
- const hAlign = (idx) => idx === 0 ? "left" : "right";
1390
- const rows = categories.map(({ title, score, refs }) => [
1391
- title,
1392
- applyScoreColor({ score }),
1393
- countCategoryAudits(refs, plugins)
1394
- ]);
1395
- const table = ui().table();
1396
- table.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
1397
- table.head(
1398
- reportRawOverviewTableHeaders.map((heading, idx) => ({
1399
- content: chalk4.cyan(heading),
1400
- hAlign: hAlign(idx)
1401
- }))
1402
- );
1403
- rows.forEach(
1404
- (row) => table.row(
1405
- row.map((content, idx) => ({
1406
- content: content.toString(),
1407
- hAlign: hAlign(idx)
1408
- }))
1409
- )
1410
- );
1411
- log(chalk4.magentaBright.bold("Categories"));
1412
- log();
1413
- table.render();
1414
- log();
1415
- }
1416
- function applyScoreColor({ score, text }) {
1417
- const formattedScore = text ?? formatReportScore(score);
1418
- const style2 = text ? chalk4 : chalk4.bold;
1419
- if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
1420
- return style2.green(formattedScore);
1421
- }
1422
- if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
1423
- return style2.yellow(formattedScore);
1424
- }
1425
- return style2.red(formattedScore);
1426
- }
1427
-
1428
1398
  // packages/utils/src/lib/reports/flatten-plugins.ts
1429
1399
  function listGroupsFromAllPlugins(report) {
1430
1400
  return report.plugins.flatMap(
@@ -1443,7 +1413,7 @@ function generateMdReport(report) {
1443
1413
  return (
1444
1414
  // header section
1445
1415
  // eslint-disable-next-line prefer-template
1446
- reportToHeaderSection2() + NEW_LINE + // categories overview section
1416
+ reportToHeaderSection() + NEW_LINE + // categories overview section
1447
1417
  (printCategories ? reportToOverviewSection(report) + NEW_LINE + NEW_LINE : "") + // categories section
1448
1418
  (printCategories ? reportToCategoriesSection(report) + NEW_LINE + NEW_LINE : "") + // audits section
1449
1419
  reportToAuditsSection(report) + NEW_LINE + NEW_LINE + // about section
@@ -1451,7 +1421,7 @@ function generateMdReport(report) {
1451
1421
  `${FOOTER_PREFIX} ${link2(README_LINK, "Code PushUp")}`
1452
1422
  );
1453
1423
  }
1454
- function reportToHeaderSection2() {
1424
+ function reportToHeaderSection() {
1455
1425
  return headline(reportHeadlineText) + NEW_LINE;
1456
1426
  }
1457
1427
  function reportToOverviewSection(report) {
@@ -1574,7 +1544,7 @@ function reportToDetailsSection(audit) {
1574
1544
  function reportToAboutSection(report) {
1575
1545
  const date = formatDate(/* @__PURE__ */ new Date());
1576
1546
  const { duration, version: version2, commit, plugins, categories } = report;
1577
- const commitInfo = commit ? `${commit.message} (${commit.hash.slice(0, 7)})` : "N/A";
1547
+ const commitInfo = commit ? `${commit.message} (${commit.hash})` : "N/A";
1578
1548
  const reportMetaTable = [
1579
1549
  reportMetaTableHeaders,
1580
1550
  [
@@ -1624,6 +1594,314 @@ function getAuditResult(audit, isHtml = false) {
1624
1594
  return isHtml ? `<b>${displayValue || value}</b>` : style(String(displayValue || value));
1625
1595
  }
1626
1596
 
1597
+ // packages/utils/src/lib/reports/generate-md-reports-diff.ts
1598
+ var MAX_ROWS = 100;
1599
+ function generateMdReportsDiff(diff) {
1600
+ return paragraphs(
1601
+ formatDiffHeaderSection(diff),
1602
+ formatDiffCategoriesSection(diff),
1603
+ formatDiffGroupsSection(diff),
1604
+ formatDiffAuditsSection(diff)
1605
+ );
1606
+ }
1607
+ function formatDiffHeaderSection(diff) {
1608
+ const outcomeTexts = {
1609
+ positive: `\u{1F973} Code PushUp report has ${style("improved")}`,
1610
+ negative: `\u{1F61F} Code PushUp report has ${style("regressed")}`,
1611
+ mixed: `\u{1F928} Code PushUp report has both ${style(
1612
+ "improvements and regressions"
1613
+ )}`,
1614
+ unchanged: `\u{1F610} Code PushUp report is ${style("unchanged")}`
1615
+ };
1616
+ const outcome = mergeDiffOutcomes(
1617
+ changesToDiffOutcomes([
1618
+ ...diff.categories.changed,
1619
+ ...diff.groups.changed,
1620
+ ...diff.audits.changed
1621
+ ])
1622
+ );
1623
+ const styleCommits = (commits) => `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
1624
+ return paragraphs(
1625
+ h1("Code PushUp"),
1626
+ diff.commits ? `${outcomeTexts[outcome]} \u2013 ${styleCommits(diff.commits)}.` : `${outcomeTexts[outcome]}.`
1627
+ );
1628
+ }
1629
+ function formatDiffCategoriesSection(diff) {
1630
+ const { changed, unchanged, added } = diff.categories;
1631
+ const categoriesCount = changed.length + unchanged.length + added.length;
1632
+ const hasChanges = unchanged.length < categoriesCount;
1633
+ if (categoriesCount === 0) {
1634
+ return "";
1635
+ }
1636
+ return paragraphs(
1637
+ h2("\u{1F3F7}\uFE0F Categories"),
1638
+ categoriesCount > 0 && tableMd(
1639
+ [
1640
+ [
1641
+ "\u{1F3F7}\uFE0F Category",
1642
+ hasChanges ? "\u2B50 Current score" : "\u2B50 Score",
1643
+ "\u2B50 Previous score",
1644
+ "\u{1F504} Score change"
1645
+ ],
1646
+ ...sortChanges(changed).map((category) => [
1647
+ category.title,
1648
+ formatScoreWithColor(category.scores.after),
1649
+ formatScoreWithColor(category.scores.before, { skipBold: true }),
1650
+ formatScoreChange(category.scores.diff)
1651
+ ]),
1652
+ ...added.map((category) => [
1653
+ category.title,
1654
+ formatScoreWithColor(category.score),
1655
+ style("n/a (\\*)", ["i"]),
1656
+ style("n/a (\\*)", ["i"])
1657
+ ]),
1658
+ ...unchanged.map((category) => [
1659
+ category.title,
1660
+ formatScoreWithColor(category.score),
1661
+ formatScoreWithColor(category.score, { skipBold: true }),
1662
+ "\u2013"
1663
+ ])
1664
+ ].map((row) => hasChanges ? row : row.slice(0, 2)),
1665
+ hasChanges ? ["l", "c", "c", "c"] : ["l", "c"]
1666
+ ),
1667
+ added.length > 0 && style("(\\*) New category.", ["i"])
1668
+ );
1669
+ }
1670
+ function formatDiffGroupsSection(diff) {
1671
+ if (diff.groups.changed.length + diff.groups.unchanged.length === 0) {
1672
+ return "";
1673
+ }
1674
+ return paragraphs(
1675
+ h2("\u{1F397}\uFE0F Groups"),
1676
+ formatGroupsOrAuditsDetails("group", diff.groups, {
1677
+ headings: [
1678
+ "\u{1F50C} Plugin",
1679
+ "\u{1F5C3}\uFE0F Group",
1680
+ "\u2B50 Current score",
1681
+ "\u2B50 Previous score",
1682
+ "\u{1F504} Score change"
1683
+ ],
1684
+ rows: sortChanges(diff.groups.changed).map((group) => [
1685
+ group.plugin.title,
1686
+ group.title,
1687
+ formatScoreWithColor(group.scores.after),
1688
+ formatScoreWithColor(group.scores.before, { skipBold: true }),
1689
+ formatScoreChange(group.scores.diff)
1690
+ ]),
1691
+ align: ["l", "l", "c", "c", "c"]
1692
+ })
1693
+ );
1694
+ }
1695
+ function formatDiffAuditsSection(diff) {
1696
+ return paragraphs(
1697
+ h2("\u{1F6E1}\uFE0F Audits"),
1698
+ formatGroupsOrAuditsDetails("audit", diff.audits, {
1699
+ headings: [
1700
+ "\u{1F50C} Plugin",
1701
+ "\u{1F6E1}\uFE0F Audit",
1702
+ "\u{1F4CF} Current value",
1703
+ "\u{1F4CF} Previous value",
1704
+ "\u{1F504} Value change"
1705
+ ],
1706
+ rows: sortChanges(diff.audits.changed).map((audit) => [
1707
+ audit.plugin.title,
1708
+ audit.title,
1709
+ `${getSquaredScoreMarker(audit.scores.after)} ${style(
1710
+ audit.displayValues.after || audit.values.after.toString()
1711
+ )}`,
1712
+ `${getSquaredScoreMarker(audit.scores.before)} ${audit.displayValues.before || audit.values.before.toString()}`,
1713
+ formatValueChange(audit)
1714
+ ]),
1715
+ align: ["l", "l", "c", "c", "c"]
1716
+ })
1717
+ );
1718
+ }
1719
+ function formatGroupsOrAuditsDetails(token, { changed, unchanged }, table) {
1720
+ return changed.length === 0 ? summarizeUnchanged(token, { changed, unchanged }) : details(
1721
+ summarizeDiffOutcomes(changesToDiffOutcomes(changed), token),
1722
+ paragraphs(
1723
+ tableMd(
1724
+ [table.headings, ...table.rows.slice(0, MAX_ROWS)],
1725
+ table.align
1726
+ ),
1727
+ changed.length > MAX_ROWS && style(
1728
+ `Only the ${MAX_ROWS} most affected ${pluralize(
1729
+ token
1730
+ )} are listed above for brevity.`,
1731
+ ["i"]
1732
+ ),
1733
+ unchanged.length > 0 && summarizeUnchanged(token, { changed, unchanged })
1734
+ )
1735
+ );
1736
+ }
1737
+ function formatScoreChange(diff) {
1738
+ const marker = getDiffMarker(diff);
1739
+ const text = formatDiffNumber(Math.round(diff * 100));
1740
+ return colorByScoreDiff(`${marker} ${text}`, diff);
1741
+ }
1742
+ function formatValueChange({
1743
+ values,
1744
+ scores
1745
+ }) {
1746
+ const marker = getDiffMarker(values.diff);
1747
+ const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : Math.round(100 * values.diff / values.before);
1748
+ const text = `${formatDiffNumber(percentage)}\u2009%`;
1749
+ return colorByScoreDiff(`${marker} ${text}`, scores.diff);
1750
+ }
1751
+ function summarizeUnchanged(token, { changed, unchanged }) {
1752
+ return [
1753
+ changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`,
1754
+ unchanged.length === 1 ? "is" : "are",
1755
+ "unchanged."
1756
+ ].join(" ");
1757
+ }
1758
+ function summarizeDiffOutcomes(outcomes, token) {
1759
+ return objectToEntries(countDiffOutcomes(outcomes)).filter(
1760
+ (entry) => entry[0] !== "unchanged" && entry[1] > 0
1761
+ ).map(([outcome, count]) => {
1762
+ const formattedCount = `<strong>${count}</strong> ${pluralize(
1763
+ token,
1764
+ count
1765
+ )}`;
1766
+ switch (outcome) {
1767
+ case "positive":
1768
+ return `\u{1F44D} ${formattedCount} improved`;
1769
+ case "negative":
1770
+ return `\u{1F44E} ${formattedCount} regressed`;
1771
+ case "mixed":
1772
+ return `${formattedCount} changed without impacting score`;
1773
+ }
1774
+ }).join(", ");
1775
+ }
1776
+ function sortChanges(changes) {
1777
+ return [...changes].sort(
1778
+ (a, b) => Math.abs(b.scores.diff) - Math.abs(a.scores.diff) || Math.abs(b.values?.diff ?? 0) - Math.abs(a.values?.diff ?? 0)
1779
+ );
1780
+ }
1781
+ function changesToDiffOutcomes(changes) {
1782
+ return changes.map((change) => {
1783
+ if (change.scores.diff > 0) {
1784
+ return "positive";
1785
+ }
1786
+ if (change.scores.diff < 0) {
1787
+ return "negative";
1788
+ }
1789
+ if (change.values != null && change.values.diff !== 0) {
1790
+ return "mixed";
1791
+ }
1792
+ return "unchanged";
1793
+ });
1794
+ }
1795
+ function mergeDiffOutcomes(outcomes) {
1796
+ if (outcomes.every((outcome) => outcome === "unchanged")) {
1797
+ return "unchanged";
1798
+ }
1799
+ if (outcomes.includes("positive") && !outcomes.includes("negative")) {
1800
+ return "positive";
1801
+ }
1802
+ if (outcomes.includes("negative") && !outcomes.includes("positive")) {
1803
+ return "negative";
1804
+ }
1805
+ return "mixed";
1806
+ }
1807
+ function countDiffOutcomes(outcomes) {
1808
+ return {
1809
+ positive: outcomes.filter((outcome) => outcome === "positive").length,
1810
+ negative: outcomes.filter((outcome) => outcome === "negative").length,
1811
+ mixed: outcomes.filter((outcome) => outcome === "mixed").length,
1812
+ unchanged: outcomes.filter((outcome) => outcome === "unchanged").length
1813
+ };
1814
+ }
1815
+
1816
+ // packages/utils/src/lib/reports/log-stdout-summary.ts
1817
+ import chalk4 from "chalk";
1818
+ function log(msg = "") {
1819
+ ui().logger.log(msg);
1820
+ }
1821
+ function logStdoutSummary(report) {
1822
+ const printCategories = report.categories.length > 0;
1823
+ log(reportToHeaderSection2(report));
1824
+ log();
1825
+ logPlugins(report);
1826
+ if (printCategories) {
1827
+ logCategories(report);
1828
+ }
1829
+ log(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`);
1830
+ log();
1831
+ }
1832
+ function reportToHeaderSection2(report) {
1833
+ const { packageName, version: version2 } = report;
1834
+ return `${chalk4.bold(reportHeadlineText)} - ${packageName}@${version2}`;
1835
+ }
1836
+ function logPlugins(report) {
1837
+ const { plugins } = report;
1838
+ plugins.forEach((plugin) => {
1839
+ const { title, audits } = plugin;
1840
+ log();
1841
+ log(chalk4.magentaBright.bold(`${title} audits`));
1842
+ log();
1843
+ audits.forEach((audit) => {
1844
+ ui().row([
1845
+ {
1846
+ text: applyScoreColor({ score: audit.score, text: "\u25CF" }),
1847
+ width: 2,
1848
+ padding: [0, 1, 0, 0]
1849
+ },
1850
+ {
1851
+ text: audit.title,
1852
+ // eslint-disable-next-line no-magic-numbers
1853
+ padding: [0, 3, 0, 0]
1854
+ },
1855
+ {
1856
+ text: chalk4.cyanBright(audit.displayValue || `${audit.value}`),
1857
+ width: 10,
1858
+ padding: [0, 0, 0, 0]
1859
+ }
1860
+ ]);
1861
+ });
1862
+ log();
1863
+ });
1864
+ }
1865
+ function logCategories({ categories, plugins }) {
1866
+ const hAlign = (idx) => idx === 0 ? "left" : "right";
1867
+ const rows = categories.map(({ title, score, refs }) => [
1868
+ title,
1869
+ applyScoreColor({ score }),
1870
+ countCategoryAudits(refs, plugins)
1871
+ ]);
1872
+ const table = ui().table();
1873
+ table.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
1874
+ table.head(
1875
+ reportRawOverviewTableHeaders.map((heading, idx) => ({
1876
+ content: chalk4.cyan(heading),
1877
+ hAlign: hAlign(idx)
1878
+ }))
1879
+ );
1880
+ rows.forEach(
1881
+ (row) => table.row(
1882
+ row.map((content, idx) => ({
1883
+ content: content.toString(),
1884
+ hAlign: hAlign(idx)
1885
+ }))
1886
+ )
1887
+ );
1888
+ log(chalk4.magentaBright.bold("Categories"));
1889
+ log();
1890
+ table.render();
1891
+ log();
1892
+ }
1893
+ function applyScoreColor({ score, text }) {
1894
+ const formattedScore = text ?? formatReportScore(score);
1895
+ const style2 = text ? chalk4 : chalk4.bold;
1896
+ if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
1897
+ return style2.green(formattedScore);
1898
+ }
1899
+ if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
1900
+ return style2.yellow(formattedScore);
1901
+ }
1902
+ return style2.red(formattedScore);
1903
+ }
1904
+
1627
1905
  // packages/utils/src/lib/reports/scoring.ts
1628
1906
  var GroupRefInvalidError = class extends Error {
1629
1907
  constructor(auditSlug, pluginSlug) {
@@ -1783,7 +2061,7 @@ var verboseUtils = (verbose = false) => ({
1783
2061
 
1784
2062
  // packages/core/package.json
1785
2063
  var name = "@code-pushup/core";
1786
- var version = "0.27.1";
2064
+ var version = "0.29.0";
1787
2065
 
1788
2066
  // packages/core/src/lib/implementation/execute-plugin.ts
1789
2067
  import chalk5 from "chalk";
@@ -2010,6 +2288,7 @@ async function collectAndPersistReports(options2) {
2010
2288
 
2011
2289
  // packages/core/src/lib/compare.ts
2012
2290
  import { writeFile as writeFile2 } from "node:fs/promises";
2291
+ import { join as join5 } from "node:path";
2013
2292
 
2014
2293
  // packages/core/src/lib/implementation/compare-scorables.ts
2015
2294
  function compareCategories(reports) {
@@ -2155,7 +2434,8 @@ function pluginAuditPairToDiff({
2155
2434
  }
2156
2435
 
2157
2436
  // packages/core/src/lib/compare.ts
2158
- async function compareReportFiles(inputPaths, outputPath) {
2437
+ async function compareReportFiles(inputPaths, persistConfig) {
2438
+ const { outputDir, filename, format } = persistConfig;
2159
2439
  const [reportBefore, reportAfter] = await Promise.all([
2160
2440
  readJsonFile(inputPaths.before),
2161
2441
  readJsonFile(inputPaths.after)
@@ -2165,7 +2445,15 @@ async function compareReportFiles(inputPaths, outputPath) {
2165
2445
  after: reportSchema.parse(reportAfter)
2166
2446
  };
2167
2447
  const reportsDiff = compareReports(reports);
2168
- await writeFile2(outputPath, JSON.stringify(reportsDiff, null, 2));
2448
+ return Promise.all(
2449
+ format.map(async (fmt) => {
2450
+ const outputPath = join5(outputDir, `${filename}-diff.${fmt}`);
2451
+ const content = reportsDiffToFileContent(reportsDiff, fmt);
2452
+ await ensureDirectoryExists(outputDir);
2453
+ await writeFile2(outputPath, content);
2454
+ return outputPath;
2455
+ })
2456
+ );
2169
2457
  }
2170
2458
  function compareReports(reports) {
2171
2459
  const start = performance.now();
@@ -2190,9 +2478,17 @@ function compareReports(reports) {
2190
2478
  duration
2191
2479
  };
2192
2480
  }
2481
+ function reportsDiffToFileContent(reportsDiff, format) {
2482
+ switch (format) {
2483
+ case "json":
2484
+ return JSON.stringify(reportsDiff, null, 2);
2485
+ case "md":
2486
+ return generateMdReportsDiff(reportsDiff);
2487
+ }
2488
+ }
2193
2489
 
2194
2490
  // packages/core/src/lib/implementation/read-rc-file.ts
2195
- import { join as join5 } from "node:path";
2491
+ import { join as join6 } from "node:path";
2196
2492
  var ConfigPathError = class extends Error {
2197
2493
  constructor(configPath) {
2198
2494
  super(`Provided path '${configPath}' is not valid.`);
@@ -2226,7 +2522,7 @@ async function autoloadRc(tsconfig) {
2226
2522
  );
2227
2523
  }
2228
2524
  return readRcByPath(
2229
- join5(process.cwd(), `${CONFIG_FILE_NAME}.${ext}`),
2525
+ join6(process.cwd(), `${CONFIG_FILE_NAME}.${ext}`),
2230
2526
  tsconfig
2231
2527
  );
2232
2528
  }
@@ -2481,7 +2777,6 @@ function renderUploadAutorunHint() {
2481
2777
 
2482
2778
  // packages/cli/src/lib/compare/compare-command.ts
2483
2779
  import chalk9 from "chalk";
2484
- import { join as join6 } from "node:path";
2485
2780
 
2486
2781
  // packages/cli/src/lib/implementation/compare.options.ts
2487
2782
  function yargsCompareOptionsDefinition() {
@@ -2511,12 +2806,10 @@ function yargsCompareCommandObject() {
2511
2806
  ui().logger.info(chalk9.gray(`Run ${command}...`));
2512
2807
  const options2 = args;
2513
2808
  const { before, after, persist } = options2;
2514
- const outputPath = join6(
2515
- persist.outputDir,
2516
- `${persist.filename}-diff.json`
2809
+ const outputPaths = await compareReportFiles({ before, after }, persist);
2810
+ ui().logger.info(
2811
+ `Reports diff written to ${outputPaths.map((path) => chalk9.bold(path)).join(" and ")}`
2517
2812
  );
2518
- await compareReportFiles({ before, after }, outputPath);
2519
- ui().logger.info(`Reports diff written to ${chalk9.bold(outputPath)}`);
2520
2813
  }
2521
2814
  };
2522
2815
  }
@@ -2617,9 +2910,9 @@ async function coreConfigMiddleware(processArgs) {
2617
2910
  return {
2618
2911
  ...config != null && { config },
2619
2912
  persist: {
2620
- outputDir: cliPersist?.outputDir ?? rcPersist?.outputDir ?? PERSIST_OUTPUT_DIR,
2621
- format: cliPersist?.format ?? rcPersist?.format ?? PERSIST_FORMAT,
2622
- filename: cliPersist?.filename ?? rcPersist?.filename ?? PERSIST_FILENAME
2913
+ outputDir: cliPersist?.outputDir ?? rcPersist?.outputDir ?? DEFAULT_PERSIST_OUTPUT_DIR,
2914
+ filename: cliPersist?.filename ?? rcPersist?.filename ?? DEFAULT_PERSIST_FILENAME,
2915
+ format: cliPersist?.format ?? rcPersist?.format ?? DEFAULT_PERSIST_FORMAT
2623
2916
  },
2624
2917
  ...upload2 != null && { upload: upload2 },
2625
2918
  categories: rcCategories ?? [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-pushup/cli",
3
- "version": "0.27.1",
3
+ "version": "0.29.0",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "code-pushup": "index.js"