@code-pushup/cli 0.27.1 → 0.28.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 +407 -109
  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) {
@@ -1624,6 +1594,319 @@ 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 styleCommit = (commit) => style(commit.hash.slice(0, 7), ["c"]);
1624
+ const styleCommits = (commits) => {
1625
+ const src = styleCommit(commits.before);
1626
+ const tgt = styleCommit(commits.after);
1627
+ return `compared target commit ${tgt} with source commit ${src}`;
1628
+ };
1629
+ return paragraphs(
1630
+ h1("Code PushUp"),
1631
+ diff.commits ? `${outcomeTexts[outcome]} \u2013 ${styleCommits(diff.commits)}.` : `${outcomeTexts[outcome]}.`
1632
+ );
1633
+ }
1634
+ function formatDiffCategoriesSection(diff) {
1635
+ const { changed, unchanged, added } = diff.categories;
1636
+ const categoriesCount = changed.length + unchanged.length + added.length;
1637
+ const hasChanges = unchanged.length < categoriesCount;
1638
+ if (categoriesCount === 0) {
1639
+ return "";
1640
+ }
1641
+ return paragraphs(
1642
+ h2("\u{1F3F7}\uFE0F Categories"),
1643
+ categoriesCount > 0 && tableMd(
1644
+ [
1645
+ [
1646
+ "\u{1F3F7}\uFE0F Category",
1647
+ hasChanges ? "\u2B50 Current score" : "\u2B50 Score",
1648
+ "\u2B50 Previous score",
1649
+ "\u{1F504} Score change"
1650
+ ],
1651
+ ...sortChanges(changed).map((category) => [
1652
+ category.title,
1653
+ formatScoreWithColor(category.scores.after),
1654
+ formatScoreWithColor(category.scores.before, { skipBold: true }),
1655
+ formatScoreChange(category.scores.diff)
1656
+ ]),
1657
+ ...added.map((category) => [
1658
+ category.title,
1659
+ formatScoreWithColor(category.score),
1660
+ style("n/a (\\*)", ["i"]),
1661
+ style("n/a (\\*)", ["i"])
1662
+ ]),
1663
+ ...unchanged.map((category) => [
1664
+ category.title,
1665
+ formatScoreWithColor(category.score),
1666
+ formatScoreWithColor(category.score, { skipBold: true }),
1667
+ "\u2013"
1668
+ ])
1669
+ ].map((row) => hasChanges ? row : row.slice(0, 2)),
1670
+ hasChanges ? ["l", "c", "c", "c"] : ["l", "c"]
1671
+ ),
1672
+ added.length > 0 && style("(\\*) New category.", ["i"])
1673
+ );
1674
+ }
1675
+ function formatDiffGroupsSection(diff) {
1676
+ if (diff.groups.changed.length + diff.groups.unchanged.length === 0) {
1677
+ return "";
1678
+ }
1679
+ return paragraphs(
1680
+ h2("\u{1F397}\uFE0F Groups"),
1681
+ formatGroupsOrAuditsDetails("group", diff.groups, {
1682
+ headings: [
1683
+ "\u{1F50C} Plugin",
1684
+ "\u{1F5C3}\uFE0F Group",
1685
+ "\u2B50 Current score",
1686
+ "\u2B50 Previous score",
1687
+ "\u{1F504} Score change"
1688
+ ],
1689
+ rows: sortChanges(diff.groups.changed).map((group) => [
1690
+ group.plugin.title,
1691
+ group.title,
1692
+ formatScoreWithColor(group.scores.after),
1693
+ formatScoreWithColor(group.scores.before, { skipBold: true }),
1694
+ formatScoreChange(group.scores.diff)
1695
+ ]),
1696
+ align: ["l", "l", "c", "c", "c"]
1697
+ })
1698
+ );
1699
+ }
1700
+ function formatDiffAuditsSection(diff) {
1701
+ return paragraphs(
1702
+ h2("\u{1F6E1}\uFE0F Audits"),
1703
+ formatGroupsOrAuditsDetails("audit", diff.audits, {
1704
+ headings: [
1705
+ "\u{1F50C} Plugin",
1706
+ "\u{1F6E1}\uFE0F Audit",
1707
+ "\u{1F4CF} Current value",
1708
+ "\u{1F4CF} Previous value",
1709
+ "\u{1F504} Value change"
1710
+ ],
1711
+ rows: sortChanges(diff.audits.changed).map((audit) => [
1712
+ audit.plugin.title,
1713
+ audit.title,
1714
+ `${getSquaredScoreMarker(audit.scores.after)} ${style(
1715
+ audit.displayValues.after || audit.values.after.toString()
1716
+ )}`,
1717
+ `${getSquaredScoreMarker(audit.scores.before)} ${audit.displayValues.before || audit.values.before.toString()}`,
1718
+ formatValueChange(audit)
1719
+ ]),
1720
+ align: ["l", "l", "c", "c", "c"]
1721
+ })
1722
+ );
1723
+ }
1724
+ function formatGroupsOrAuditsDetails(token, { changed, unchanged }, table) {
1725
+ return changed.length === 0 ? summarizeUnchanged(token, { changed, unchanged }) : details(
1726
+ summarizeDiffOutcomes(changesToDiffOutcomes(changed), token),
1727
+ paragraphs(
1728
+ tableMd(
1729
+ [table.headings, ...table.rows.slice(0, MAX_ROWS)],
1730
+ table.align
1731
+ ),
1732
+ changed.length > MAX_ROWS && style(
1733
+ `Only the ${MAX_ROWS} most affected ${pluralize(
1734
+ token
1735
+ )} are listed above for brevity.`,
1736
+ ["i"]
1737
+ ),
1738
+ unchanged.length > 0 && summarizeUnchanged(token, { changed, unchanged })
1739
+ )
1740
+ );
1741
+ }
1742
+ function formatScoreChange(diff) {
1743
+ const marker = getDiffMarker(diff);
1744
+ const text = formatDiffNumber(Math.round(diff * 100));
1745
+ return colorByScoreDiff(`${marker} ${text}`, diff);
1746
+ }
1747
+ function formatValueChange({
1748
+ values,
1749
+ scores
1750
+ }) {
1751
+ const marker = getDiffMarker(values.diff);
1752
+ const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : Math.round(100 * values.diff / values.before);
1753
+ const text = `${formatDiffNumber(percentage)}\u2009%`;
1754
+ return colorByScoreDiff(`${marker} ${text}`, scores.diff);
1755
+ }
1756
+ function summarizeUnchanged(token, { changed, unchanged }) {
1757
+ return [
1758
+ changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`,
1759
+ unchanged.length === 1 ? "is" : "are",
1760
+ "unchanged."
1761
+ ].join(" ");
1762
+ }
1763
+ function summarizeDiffOutcomes(outcomes, token) {
1764
+ return objectToEntries(countDiffOutcomes(outcomes)).filter(
1765
+ (entry) => entry[0] !== "unchanged" && entry[1] > 0
1766
+ ).map(([outcome, count]) => {
1767
+ const formattedCount = `<strong>${count}</strong> ${pluralize(
1768
+ token,
1769
+ count
1770
+ )}`;
1771
+ switch (outcome) {
1772
+ case "positive":
1773
+ return `\u{1F44D} ${formattedCount} improved`;
1774
+ case "negative":
1775
+ return `\u{1F44E} ${formattedCount} regressed`;
1776
+ case "mixed":
1777
+ return `${formattedCount} changed without impacting score`;
1778
+ }
1779
+ }).join(", ");
1780
+ }
1781
+ function sortChanges(changes) {
1782
+ return [...changes].sort(
1783
+ (a, b) => Math.abs(b.scores.diff) - Math.abs(a.scores.diff) || Math.abs(b.values?.diff ?? 0) - Math.abs(a.values?.diff ?? 0)
1784
+ );
1785
+ }
1786
+ function changesToDiffOutcomes(changes) {
1787
+ return changes.map((change) => {
1788
+ if (change.scores.diff > 0) {
1789
+ return "positive";
1790
+ }
1791
+ if (change.scores.diff < 0) {
1792
+ return "negative";
1793
+ }
1794
+ if (change.values != null && change.values.diff !== 0) {
1795
+ return "mixed";
1796
+ }
1797
+ return "unchanged";
1798
+ });
1799
+ }
1800
+ function mergeDiffOutcomes(outcomes) {
1801
+ if (outcomes.every((outcome) => outcome === "unchanged")) {
1802
+ return "unchanged";
1803
+ }
1804
+ if (outcomes.includes("positive") && !outcomes.includes("negative")) {
1805
+ return "positive";
1806
+ }
1807
+ if (outcomes.includes("negative") && !outcomes.includes("positive")) {
1808
+ return "negative";
1809
+ }
1810
+ return "mixed";
1811
+ }
1812
+ function countDiffOutcomes(outcomes) {
1813
+ return {
1814
+ positive: outcomes.filter((outcome) => outcome === "positive").length,
1815
+ negative: outcomes.filter((outcome) => outcome === "negative").length,
1816
+ mixed: outcomes.filter((outcome) => outcome === "mixed").length,
1817
+ unchanged: outcomes.filter((outcome) => outcome === "unchanged").length
1818
+ };
1819
+ }
1820
+
1821
+ // packages/utils/src/lib/reports/log-stdout-summary.ts
1822
+ import chalk4 from "chalk";
1823
+ function log(msg = "") {
1824
+ ui().logger.log(msg);
1825
+ }
1826
+ function logStdoutSummary(report) {
1827
+ const printCategories = report.categories.length > 0;
1828
+ log(reportToHeaderSection2(report));
1829
+ log();
1830
+ logPlugins(report);
1831
+ if (printCategories) {
1832
+ logCategories(report);
1833
+ }
1834
+ log(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`);
1835
+ log();
1836
+ }
1837
+ function reportToHeaderSection2(report) {
1838
+ const { packageName, version: version2 } = report;
1839
+ return `${chalk4.bold(reportHeadlineText)} - ${packageName}@${version2}`;
1840
+ }
1841
+ function logPlugins(report) {
1842
+ const { plugins } = report;
1843
+ plugins.forEach((plugin) => {
1844
+ const { title, audits } = plugin;
1845
+ log();
1846
+ log(chalk4.magentaBright.bold(`${title} audits`));
1847
+ log();
1848
+ audits.forEach((audit) => {
1849
+ ui().row([
1850
+ {
1851
+ text: applyScoreColor({ score: audit.score, text: "\u25CF" }),
1852
+ width: 2,
1853
+ padding: [0, 1, 0, 0]
1854
+ },
1855
+ {
1856
+ text: audit.title,
1857
+ // eslint-disable-next-line no-magic-numbers
1858
+ padding: [0, 3, 0, 0]
1859
+ },
1860
+ {
1861
+ text: chalk4.cyanBright(audit.displayValue || `${audit.value}`),
1862
+ width: 10,
1863
+ padding: [0, 0, 0, 0]
1864
+ }
1865
+ ]);
1866
+ });
1867
+ log();
1868
+ });
1869
+ }
1870
+ function logCategories({ categories, plugins }) {
1871
+ const hAlign = (idx) => idx === 0 ? "left" : "right";
1872
+ const rows = categories.map(({ title, score, refs }) => [
1873
+ title,
1874
+ applyScoreColor({ score }),
1875
+ countCategoryAudits(refs, plugins)
1876
+ ]);
1877
+ const table = ui().table();
1878
+ table.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
1879
+ table.head(
1880
+ reportRawOverviewTableHeaders.map((heading, idx) => ({
1881
+ content: chalk4.cyan(heading),
1882
+ hAlign: hAlign(idx)
1883
+ }))
1884
+ );
1885
+ rows.forEach(
1886
+ (row) => table.row(
1887
+ row.map((content, idx) => ({
1888
+ content: content.toString(),
1889
+ hAlign: hAlign(idx)
1890
+ }))
1891
+ )
1892
+ );
1893
+ log(chalk4.magentaBright.bold("Categories"));
1894
+ log();
1895
+ table.render();
1896
+ log();
1897
+ }
1898
+ function applyScoreColor({ score, text }) {
1899
+ const formattedScore = text ?? formatReportScore(score);
1900
+ const style2 = text ? chalk4 : chalk4.bold;
1901
+ if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
1902
+ return style2.green(formattedScore);
1903
+ }
1904
+ if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
1905
+ return style2.yellow(formattedScore);
1906
+ }
1907
+ return style2.red(formattedScore);
1908
+ }
1909
+
1627
1910
  // packages/utils/src/lib/reports/scoring.ts
1628
1911
  var GroupRefInvalidError = class extends Error {
1629
1912
  constructor(auditSlug, pluginSlug) {
@@ -1783,7 +2066,7 @@ var verboseUtils = (verbose = false) => ({
1783
2066
 
1784
2067
  // packages/core/package.json
1785
2068
  var name = "@code-pushup/core";
1786
- var version = "0.27.1";
2069
+ var version = "0.28.0";
1787
2070
 
1788
2071
  // packages/core/src/lib/implementation/execute-plugin.ts
1789
2072
  import chalk5 from "chalk";
@@ -2010,6 +2293,7 @@ async function collectAndPersistReports(options2) {
2010
2293
 
2011
2294
  // packages/core/src/lib/compare.ts
2012
2295
  import { writeFile as writeFile2 } from "node:fs/promises";
2296
+ import { join as join5 } from "node:path";
2013
2297
 
2014
2298
  // packages/core/src/lib/implementation/compare-scorables.ts
2015
2299
  function compareCategories(reports) {
@@ -2155,7 +2439,8 @@ function pluginAuditPairToDiff({
2155
2439
  }
2156
2440
 
2157
2441
  // packages/core/src/lib/compare.ts
2158
- async function compareReportFiles(inputPaths, outputPath) {
2442
+ async function compareReportFiles(inputPaths, persistConfig) {
2443
+ const { outputDir, filename, format } = persistConfig;
2159
2444
  const [reportBefore, reportAfter] = await Promise.all([
2160
2445
  readJsonFile(inputPaths.before),
2161
2446
  readJsonFile(inputPaths.after)
@@ -2165,7 +2450,15 @@ async function compareReportFiles(inputPaths, outputPath) {
2165
2450
  after: reportSchema.parse(reportAfter)
2166
2451
  };
2167
2452
  const reportsDiff = compareReports(reports);
2168
- await writeFile2(outputPath, JSON.stringify(reportsDiff, null, 2));
2453
+ return Promise.all(
2454
+ format.map(async (fmt) => {
2455
+ const outputPath = join5(outputDir, `${filename}-diff.${fmt}`);
2456
+ const content = reportsDiffToFileContent(reportsDiff, fmt);
2457
+ await ensureDirectoryExists(outputDir);
2458
+ await writeFile2(outputPath, content);
2459
+ return outputPath;
2460
+ })
2461
+ );
2169
2462
  }
2170
2463
  function compareReports(reports) {
2171
2464
  const start = performance.now();
@@ -2190,9 +2483,17 @@ function compareReports(reports) {
2190
2483
  duration
2191
2484
  };
2192
2485
  }
2486
+ function reportsDiffToFileContent(reportsDiff, format) {
2487
+ switch (format) {
2488
+ case "json":
2489
+ return JSON.stringify(reportsDiff, null, 2);
2490
+ case "md":
2491
+ return generateMdReportsDiff(reportsDiff);
2492
+ }
2493
+ }
2193
2494
 
2194
2495
  // packages/core/src/lib/implementation/read-rc-file.ts
2195
- import { join as join5 } from "node:path";
2496
+ import { join as join6 } from "node:path";
2196
2497
  var ConfigPathError = class extends Error {
2197
2498
  constructor(configPath) {
2198
2499
  super(`Provided path '${configPath}' is not valid.`);
@@ -2226,7 +2527,7 @@ async function autoloadRc(tsconfig) {
2226
2527
  );
2227
2528
  }
2228
2529
  return readRcByPath(
2229
- join5(process.cwd(), `${CONFIG_FILE_NAME}.${ext}`),
2530
+ join6(process.cwd(), `${CONFIG_FILE_NAME}.${ext}`),
2230
2531
  tsconfig
2231
2532
  );
2232
2533
  }
@@ -2481,7 +2782,6 @@ function renderUploadAutorunHint() {
2481
2782
 
2482
2783
  // packages/cli/src/lib/compare/compare-command.ts
2483
2784
  import chalk9 from "chalk";
2484
- import { join as join6 } from "node:path";
2485
2785
 
2486
2786
  // packages/cli/src/lib/implementation/compare.options.ts
2487
2787
  function yargsCompareOptionsDefinition() {
@@ -2511,12 +2811,10 @@ function yargsCompareCommandObject() {
2511
2811
  ui().logger.info(chalk9.gray(`Run ${command}...`));
2512
2812
  const options2 = args;
2513
2813
  const { before, after, persist } = options2;
2514
- const outputPath = join6(
2515
- persist.outputDir,
2516
- `${persist.filename}-diff.json`
2814
+ const outputPaths = await compareReportFiles({ before, after }, persist);
2815
+ ui().logger.info(
2816
+ `Reports diff written to ${outputPaths.map((path) => chalk9.bold(path)).join(" and ")}`
2517
2817
  );
2518
- await compareReportFiles({ before, after }, outputPath);
2519
- ui().logger.info(`Reports diff written to ${chalk9.bold(outputPath)}`);
2520
2818
  }
2521
2819
  };
2522
2820
  }
@@ -2617,9 +2915,9 @@ async function coreConfigMiddleware(processArgs) {
2617
2915
  return {
2618
2916
  ...config != null && { config },
2619
2917
  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
2918
+ outputDir: cliPersist?.outputDir ?? rcPersist?.outputDir ?? DEFAULT_PERSIST_OUTPUT_DIR,
2919
+ filename: cliPersist?.filename ?? rcPersist?.filename ?? DEFAULT_PERSIST_FILENAME,
2920
+ format: cliPersist?.format ?? rcPersist?.format ?? DEFAULT_PERSIST_FORMAT
2623
2921
  },
2624
2922
  ...upload2 != null && { upload: upload2 },
2625
2923
  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.28.0",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "code-pushup": "index.js"