@code-pushup/core 0.26.1 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -84,6 +84,9 @@ var descriptionSchema = z.string({ description: "Description (markdown)" }).max(
84
84
  var urlSchema = z.string().url();
85
85
  var docsUrlSchema = urlSchema.optional().or(z.literal("")).describe("Documentation site");
86
86
  var titleSchema = z.string({ description: "Descriptive name" }).max(MAX_TITLE_LENGTH);
87
+ var scoreSchema = z.number({
88
+ description: "Value between 0 and 1"
89
+ }).min(0).max(1);
87
90
  function metaSchema(options) {
88
91
  const {
89
92
  descriptionDescription,
@@ -215,6 +218,8 @@ var issueSchema = z3.object(
215
218
  );
216
219
 
217
220
  // packages/models/src/lib/audit-output.ts
221
+ var auditValueSchema = nonnegativeIntSchema.describe("Raw numeric value");
222
+ var auditDisplayValueSchema = z4.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }).optional();
218
223
  var auditDetailsSchema = z4.object(
219
224
  {
220
225
  issues: z4.array(issueSchema, { description: "List of findings" })
@@ -224,11 +229,9 @@ var auditDetailsSchema = z4.object(
224
229
  var auditOutputSchema = z4.object(
225
230
  {
226
231
  slug: slugSchema.describe("Reference to audit"),
227
- displayValue: z4.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }).optional(),
228
- value: nonnegativeIntSchema.describe("Raw numeric value"),
229
- score: z4.number({
230
- description: "Value between 0 and 1"
231
- }).min(0).max(1),
232
+ displayValue: auditDisplayValueSchema,
233
+ value: auditValueSchema,
234
+ score: scoreSchema,
232
235
  details: auditDetailsSchema.optional()
233
236
  },
234
237
  { description: "Audit information" }
@@ -555,6 +558,138 @@ var reportSchema = packageVersionSchema({
555
558
  })
556
559
  );
557
560
 
561
+ // packages/models/src/lib/reports-diff.ts
562
+ import { z as z14 } from "zod";
563
+ function makeComparisonSchema(schema) {
564
+ const sharedDescription = schema.description || "Result";
565
+ return z14.object({
566
+ before: schema.describe(`${sharedDescription} (source commit)`),
567
+ after: schema.describe(`${sharedDescription} (target commit)`)
568
+ });
569
+ }
570
+ function makeArraysComparisonSchema(diffSchema, resultSchema, description) {
571
+ return z14.object(
572
+ {
573
+ changed: z14.array(diffSchema),
574
+ unchanged: z14.array(resultSchema),
575
+ added: z14.array(resultSchema),
576
+ removed: z14.array(resultSchema)
577
+ },
578
+ { description }
579
+ );
580
+ }
581
+ var scorableMetaSchema = z14.object({ slug: slugSchema, title: titleSchema });
582
+ var scorableWithPluginMetaSchema = scorableMetaSchema.merge(
583
+ z14.object({
584
+ plugin: pluginMetaSchema.pick({ slug: true, title: true }).describe("Plugin which defines it")
585
+ })
586
+ );
587
+ var scorableDiffSchema = scorableMetaSchema.merge(
588
+ z14.object({
589
+ scores: makeComparisonSchema(scoreSchema).merge(
590
+ z14.object({
591
+ diff: z14.number().min(-1).max(1).describe("Score change (`scores.after - scores.before`)")
592
+ })
593
+ ).describe("Score comparison")
594
+ })
595
+ );
596
+ var scorableWithPluginDiffSchema = scorableDiffSchema.merge(
597
+ scorableWithPluginMetaSchema
598
+ );
599
+ var categoryDiffSchema = scorableDiffSchema;
600
+ var groupDiffSchema = scorableWithPluginDiffSchema;
601
+ var auditDiffSchema = scorableWithPluginDiffSchema.merge(
602
+ z14.object({
603
+ values: makeComparisonSchema(auditValueSchema).merge(
604
+ z14.object({
605
+ diff: z14.number().int().describe("Value change (`values.after - values.before`)")
606
+ })
607
+ ).describe("Audit `value` comparison"),
608
+ displayValues: makeComparisonSchema(auditDisplayValueSchema).describe(
609
+ "Audit `displayValue` comparison"
610
+ )
611
+ })
612
+ );
613
+ var categoryResultSchema = scorableMetaSchema.merge(
614
+ z14.object({ score: scoreSchema })
615
+ );
616
+ var groupResultSchema = scorableWithPluginMetaSchema.merge(
617
+ z14.object({ score: scoreSchema })
618
+ );
619
+ var auditResultSchema = scorableWithPluginMetaSchema.merge(
620
+ auditOutputSchema.pick({ score: true, value: true, displayValue: true })
621
+ );
622
+ var reportsDiffSchema = z14.object({
623
+ commits: makeComparisonSchema(commitSchema).nullable().describe("Commits identifying compared reports"),
624
+ categories: makeArraysComparisonSchema(
625
+ categoryDiffSchema,
626
+ categoryResultSchema,
627
+ "Changes affecting categories"
628
+ ),
629
+ groups: makeArraysComparisonSchema(
630
+ groupDiffSchema,
631
+ groupResultSchema,
632
+ "Changes affecting groups"
633
+ ),
634
+ audits: makeArraysComparisonSchema(
635
+ auditDiffSchema,
636
+ auditResultSchema,
637
+ "Changes affecting audits"
638
+ )
639
+ }).merge(
640
+ packageVersionSchema({
641
+ versionDescription: "NPM version of the CLI (when `compare` was run)",
642
+ required: true
643
+ })
644
+ ).merge(
645
+ executionMetaSchema({
646
+ descriptionDate: "Start date and time of the compare run",
647
+ descriptionDuration: "Duration of the compare run in ms"
648
+ })
649
+ );
650
+
651
+ // packages/utils/src/lib/diff.ts
652
+ function matchArrayItemsByKey({
653
+ before,
654
+ after,
655
+ key
656
+ }) {
657
+ const pairs = [];
658
+ const added = [];
659
+ const afterKeys = /* @__PURE__ */ new Set();
660
+ const keyFn = typeof key === "function" ? key : (item) => item[key];
661
+ for (const afterItem of after) {
662
+ const afterKey = keyFn(afterItem);
663
+ afterKeys.add(afterKey);
664
+ const match = before.find((beforeItem) => keyFn(beforeItem) === afterKey);
665
+ if (match) {
666
+ pairs.push({ before: match, after: afterItem });
667
+ } else {
668
+ added.push(afterItem);
669
+ }
670
+ }
671
+ const removed = before.filter(
672
+ (beforeItem) => !afterKeys.has(keyFn(beforeItem))
673
+ );
674
+ return {
675
+ pairs,
676
+ added,
677
+ removed
678
+ };
679
+ }
680
+ function comparePairs(pairs, equalsFn) {
681
+ return pairs.reduce(
682
+ (acc, pair) => ({
683
+ ...acc,
684
+ ...equalsFn(pair) ? { unchanged: [...acc.unchanged, pair.after] } : { changed: [...acc.changed, pair] }
685
+ }),
686
+ {
687
+ changed: [],
688
+ unchanged: []
689
+ }
690
+ );
691
+ }
692
+
558
693
  // packages/utils/src/lib/execute-process.ts
559
694
  import { spawn } from "node:child_process";
560
695
 
@@ -563,7 +698,7 @@ import { join } from "node:path";
563
698
 
564
699
  // packages/utils/src/lib/file-system.ts
565
700
  import { bundleRequire } from "bundle-require";
566
- import chalk from "chalk";
701
+ import chalk2 from "chalk";
567
702
  import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
568
703
 
569
704
  // packages/utils/src/lib/formatting.ts
@@ -608,33 +743,103 @@ function isPromiseRejectedResult(result) {
608
743
  return result.status === "rejected";
609
744
  }
610
745
 
746
+ // packages/utils/src/lib/logging.ts
747
+ import isaacs_cliui from "@isaacs/cliui";
748
+ import { cliui } from "@poppinss/cliui";
749
+ import chalk from "chalk";
750
+
751
+ // packages/utils/src/lib/reports/constants.ts
752
+ var TERMINAL_WIDTH = 80;
753
+ var NEW_LINE = "\n";
754
+ var SCORE_COLOR_RANGE = {
755
+ GREEN_MIN: 0.9,
756
+ YELLOW_MIN: 0.5
757
+ };
758
+ var FOOTER_PREFIX = "Made with \u2764 by";
759
+ var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
760
+ var README_LINK = "https://github.com/flowup/quality-metrics-cli#readme";
761
+ var reportHeadlineText = "Code PushUp Report";
762
+ var reportOverviewTableHeaders = [
763
+ "\u{1F3F7} Category",
764
+ "\u2B50 Score",
765
+ "\u{1F6E1} Audits"
766
+ ];
767
+ var reportRawOverviewTableHeaders = ["Category", "Score", "Audits"];
768
+ var reportMetaTableHeaders = [
769
+ "Commit",
770
+ "Version",
771
+ "Duration",
772
+ "Plugins",
773
+ "Categories",
774
+ "Audits"
775
+ ];
776
+ var pluginMetaTableHeaders = [
777
+ "Plugin",
778
+ "Audits",
779
+ "Version",
780
+ "Duration"
781
+ ];
782
+ var detailsTableHeaders = [
783
+ "Severity",
784
+ "Message",
785
+ "Source file",
786
+ "Line(s)"
787
+ ];
788
+
789
+ // packages/utils/src/lib/logging.ts
790
+ var singletonUiInstance;
791
+ function ui() {
792
+ if (singletonUiInstance === void 0) {
793
+ singletonUiInstance = cliui();
794
+ }
795
+ return {
796
+ ...singletonUiInstance,
797
+ row: (args) => {
798
+ logListItem(args);
799
+ }
800
+ };
801
+ }
802
+ var singletonisaacUi;
803
+ function logListItem(args) {
804
+ if (singletonisaacUi === void 0) {
805
+ singletonisaacUi = isaacs_cliui({ width: TERMINAL_WIDTH });
806
+ }
807
+ singletonisaacUi.div(...args);
808
+ const content = singletonisaacUi.toString();
809
+ singletonisaacUi.rows = [];
810
+ singletonUiInstance?.logger.log(content);
811
+ }
812
+
611
813
  // packages/utils/src/lib/log-results.ts
612
- function logMultipleResults(results, messagePrefix, succeededCallback, failedCallback) {
613
- if (succeededCallback) {
814
+ function logMultipleResults(results, messagePrefix, succeededTransform, failedTransform) {
815
+ if (succeededTransform) {
614
816
  const succeededResults = results.filter(isPromiseFulfilledResult);
615
817
  logPromiseResults(
616
818
  succeededResults,
617
819
  `${messagePrefix} successfully: `,
618
- succeededCallback
820
+ succeededTransform
619
821
  );
620
822
  }
621
- if (failedCallback) {
823
+ if (failedTransform) {
622
824
  const failedResults = results.filter(isPromiseRejectedResult);
623
825
  logPromiseResults(
624
826
  failedResults,
625
827
  `${messagePrefix} failed: `,
626
- failedCallback
828
+ failedTransform
627
829
  );
628
830
  }
629
831
  }
630
- function logPromiseResults(results, logMessage, callback) {
832
+ function logPromiseResults(results, logMessage, getMsg) {
631
833
  if (results.length > 0) {
632
- if (results[0]?.status === "fulfilled") {
633
- console.info(logMessage);
634
- } else {
635
- console.warn(logMessage);
636
- }
637
- results.forEach(callback);
834
+ const log2 = results[0]?.status === "fulfilled" ? (m) => {
835
+ ui().logger.success(m);
836
+ } : (m) => {
837
+ ui().logger.warning(m);
838
+ };
839
+ log2(logMessage);
840
+ results.forEach((result) => {
841
+ log2(getMsg(result));
842
+ });
638
843
  }
639
844
  }
640
845
 
@@ -668,26 +873,24 @@ async function ensureDirectoryExists(baseDir) {
668
873
  await mkdir(baseDir, { recursive: true });
669
874
  return;
670
875
  } catch (error) {
671
- console.error(error.message);
876
+ ui().logger.error(error.message);
672
877
  if (error.code !== "EEXIST") {
673
878
  throw error;
674
879
  }
675
880
  }
676
881
  }
677
882
  function logMultipleFileResults(fileResults, messagePrefix) {
678
- const succeededCallback = (result) => {
883
+ const succeededTransform = (result) => {
679
884
  const [fileName, size] = result.value;
680
- const formattedSize = size ? ` (${chalk.gray(formatBytes(size))})` : "";
681
- console.info(`- ${chalk.bold(fileName)}${formattedSize}`);
682
- };
683
- const failedCallback = (result) => {
684
- console.warn(`- ${chalk.bold(result.reason)}`);
885
+ const formattedSize = size ? ` (${chalk2.gray(formatBytes(size))})` : "";
886
+ return `- ${chalk2.bold(fileName)}${formattedSize}`;
685
887
  };
888
+ const failedTransform = (result) => `- ${chalk2.bold(result.reason)}`;
686
889
  logMultipleResults(
687
890
  fileResults,
688
891
  messagePrefix,
689
- succeededCallback,
690
- failedCallback
892
+ succeededTransform,
893
+ failedTransform
691
894
  );
692
895
  }
693
896
  var NoExportError = class extends Error {
@@ -706,43 +909,83 @@ async function importEsmModule(options) {
706
909
  return mod.default;
707
910
  }
708
911
 
709
- // packages/utils/src/lib/reports/constants.ts
710
- var TERMINAL_WIDTH = 80;
711
- var NEW_LINE = "\n";
712
- var SCORE_COLOR_RANGE = {
713
- GREEN_MIN: 0.9,
714
- YELLOW_MIN: 0.5
912
+ // packages/utils/src/lib/reports/md/details.ts
913
+ function details(title, content, cfg = { open: false }) {
914
+ return `<details${cfg.open ? " open" : ""}>
915
+ <summary>${title}</summary>
916
+
917
+ ${content}
918
+
919
+ </details>
920
+ `;
921
+ }
922
+
923
+ // packages/utils/src/lib/reports/md/font-style.ts
924
+ var stylesMap = {
925
+ i: "_",
926
+ // italic
927
+ b: "**",
928
+ // bold
929
+ s: "~",
930
+ // strike through
931
+ c: "`"
932
+ // code
715
933
  };
716
- var FOOTER_PREFIX = "Made with \u2764 by";
717
- var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
718
- var README_LINK = "https://github.com/flowup/quality-metrics-cli#readme";
719
- var reportHeadlineText = "Code PushUp Report";
720
- var reportOverviewTableHeaders = [
721
- "\u{1F3F7} Category",
722
- "\u2B50 Score",
723
- "\u{1F6E1} Audits"
724
- ];
725
- var reportRawOverviewTableHeaders = ["Category", "Score", "Audits"];
726
- var reportMetaTableHeaders = [
727
- "Commit",
728
- "Version",
729
- "Duration",
730
- "Plugins",
731
- "Categories",
732
- "Audits"
733
- ];
734
- var pluginMetaTableHeaders = [
735
- "Plugin",
736
- "Audits",
737
- "Version",
738
- "Duration"
739
- ];
740
- var detailsTableHeaders = [
741
- "Severity",
742
- "Message",
743
- "Source file",
744
- "Line(s)"
745
- ];
934
+ function style(text, styles = ["b"]) {
935
+ return styles.reduce((t, s) => `${stylesMap[s]}${t}${stylesMap[s]}`, text);
936
+ }
937
+
938
+ // packages/utils/src/lib/reports/md/headline.ts
939
+ function headline(text, hierarchy = 1) {
940
+ return `${"#".repeat(hierarchy)} ${text}`;
941
+ }
942
+ function h2(text) {
943
+ return headline(text, 2);
944
+ }
945
+ function h3(text) {
946
+ return headline(text, 3);
947
+ }
948
+
949
+ // packages/utils/src/lib/reports/md/link.ts
950
+ function link(href, text) {
951
+ return `[${text || href}](${href})`;
952
+ }
953
+
954
+ // packages/utils/src/lib/reports/md/list.ts
955
+ function li(text, order = "unordered") {
956
+ const style2 = order === "unordered" ? "-" : "- [ ]";
957
+ return `${style2} ${text}`;
958
+ }
959
+
960
+ // packages/utils/src/lib/reports/md/table.ts
961
+ var alignString = /* @__PURE__ */ new Map([
962
+ ["l", ":--"],
963
+ ["c", ":--:"],
964
+ ["r", "--:"]
965
+ ]);
966
+ function tableMd(data, align) {
967
+ if (data.length === 0) {
968
+ throw new Error("Data can't be empty");
969
+ }
970
+ const alignmentSetting = align ?? data[0]?.map(() => "c");
971
+ const tableContent = data.map((arr) => `|${arr.join("|")}|`);
972
+ const alignmentRow = `|${alignmentSetting?.map((s) => alignString.get(s)).join("|")}|`;
973
+ return tableContent[0] + NEW_LINE + alignmentRow + NEW_LINE + tableContent.slice(1).join(NEW_LINE);
974
+ }
975
+ function tableHtml(data) {
976
+ if (data.length === 0) {
977
+ throw new Error("Data can't be empty");
978
+ }
979
+ const tableContent = data.map((arr, index) => {
980
+ if (index === 0) {
981
+ const headerRow = arr.map((s) => `<th>${s}</th>`).join("");
982
+ return `<tr>${headerRow}</tr>`;
983
+ }
984
+ const row = arr.map((s) => `<td>${s}</td>`).join("");
985
+ return `<tr>${row}</tr>`;
986
+ });
987
+ return `<table>${tableContent.join("")}</table>`;
988
+ }
746
989
 
747
990
  // packages/utils/src/lib/reports/utils.ts
748
991
  function formatReportScore(score) {
@@ -939,7 +1182,7 @@ var ProcessError = class extends Error {
939
1182
  }
940
1183
  };
941
1184
  function executeProcess(cfg) {
942
- const { observer, cwd, command, args } = cfg;
1185
+ const { observer, cwd, command, args, alwaysResolve = false } = cfg;
943
1186
  const { onStdout, onError, onComplete } = observer ?? {};
944
1187
  const date = (/* @__PURE__ */ new Date()).toISOString();
945
1188
  const start = performance.now();
@@ -959,7 +1202,7 @@ function executeProcess(cfg) {
959
1202
  });
960
1203
  process2.on("close", (code) => {
961
1204
  const timings = { date, duration: calcDuration(start) };
962
- if (code === 0) {
1205
+ if (code === 0 || alwaysResolve) {
963
1206
  onComplete?.();
964
1207
  resolve({ code, stdout, stderr, ...timings });
965
1208
  } else {
@@ -985,14 +1228,14 @@ function toUnixPath(path) {
985
1228
 
986
1229
  // packages/utils/src/lib/git.ts
987
1230
  async function getLatestCommit(git = simpleGit()) {
988
- const log = await git.log({
1231
+ const log2 = await git.log({
989
1232
  maxCount: 1,
990
1233
  format: { hash: "%H", message: "%s", author: "%an", date: "%aI" }
991
1234
  });
992
- if (!log.latest) {
1235
+ if (!log2.latest) {
993
1236
  return null;
994
1237
  }
995
- return commitSchema.parse(log.latest);
1238
+ return commitSchema.parse(log2.latest);
996
1239
  }
997
1240
  function getGitRoot(git = simpleGit()) {
998
1241
  return git.revparse("--show-toplevel");
@@ -1011,9 +1254,6 @@ function groupByStatus(results) {
1011
1254
  );
1012
1255
  }
1013
1256
 
1014
- // packages/utils/src/lib/logging.ts
1015
- import chalk2 from "chalk";
1016
-
1017
1257
  // packages/utils/src/lib/progress.ts
1018
1258
  import chalk3 from "chalk";
1019
1259
  import { MultiProgressBars } from "multi-progress-bars";
@@ -1069,80 +1309,105 @@ function getProgressBar(taskName) {
1069
1309
  };
1070
1310
  }
1071
1311
 
1072
- // packages/utils/src/lib/reports/md/details.ts
1073
- function details(title, content, cfg = { open: false }) {
1074
- return `<details${cfg.open ? " open" : ""}>
1075
- <summary>${title}</summary>
1076
- ${content}
1077
- </details>
1078
- `;
1079
- }
1080
-
1081
- // packages/utils/src/lib/reports/md/font-style.ts
1082
- var stylesMap = {
1083
- i: "_",
1084
- // italic
1085
- b: "**",
1086
- // bold
1087
- s: "~",
1088
- // strike through
1089
- c: "`"
1090
- // code
1091
- };
1092
- function style(text, styles = ["b"]) {
1093
- return styles.reduce((t, s) => `${stylesMap[s]}${t}${stylesMap[s]}`, text);
1312
+ // packages/utils/src/lib/reports/log-stdout-summary.ts
1313
+ import chalk4 from "chalk";
1314
+ function log(msg = "") {
1315
+ ui().logger.log(msg);
1094
1316
  }
1095
-
1096
- // packages/utils/src/lib/reports/md/headline.ts
1097
- function headline(text, hierarchy = 1) {
1098
- return `${"#".repeat(hierarchy)} ${text}`;
1317
+ function logStdoutSummary(report) {
1318
+ const printCategories = report.categories.length > 0;
1319
+ log(reportToHeaderSection(report));
1320
+ log();
1321
+ logPlugins(report);
1322
+ if (printCategories) {
1323
+ logCategories(report);
1324
+ }
1325
+ log(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`);
1326
+ log();
1099
1327
  }
1100
- function h2(text) {
1101
- return headline(text, 2);
1328
+ function reportToHeaderSection(report) {
1329
+ const { packageName, version: version2 } = report;
1330
+ return `${chalk4.bold(reportHeadlineText)} - ${packageName}@${version2}`;
1102
1331
  }
1103
- function h3(text) {
1104
- return headline(text, 3);
1332
+ function logPlugins(report) {
1333
+ const { plugins } = report;
1334
+ plugins.forEach((plugin) => {
1335
+ const { title, audits } = plugin;
1336
+ log();
1337
+ log(chalk4.magentaBright.bold(`${title} audits`));
1338
+ log();
1339
+ audits.forEach((audit) => {
1340
+ ui().row([
1341
+ {
1342
+ text: applyScoreColor({ score: audit.score, text: "\u25CF" }),
1343
+ width: 2,
1344
+ padding: [0, 1, 0, 0]
1345
+ },
1346
+ {
1347
+ text: audit.title,
1348
+ // eslint-disable-next-line no-magic-numbers
1349
+ padding: [0, 3, 0, 0]
1350
+ },
1351
+ {
1352
+ text: chalk4.cyanBright(audit.displayValue || `${audit.value}`),
1353
+ width: 10,
1354
+ padding: [0, 0, 0, 0]
1355
+ }
1356
+ ]);
1357
+ });
1358
+ log();
1359
+ });
1105
1360
  }
1106
-
1107
- // packages/utils/src/lib/reports/md/link.ts
1108
- function link(href, text) {
1109
- return `[${text || href}](${href})`;
1361
+ function logCategories({ categories, plugins }) {
1362
+ const hAlign = (idx) => idx === 0 ? "left" : "right";
1363
+ const rows = categories.map(({ title, score, refs }) => [
1364
+ title,
1365
+ applyScoreColor({ score }),
1366
+ countCategoryAudits(refs, plugins)
1367
+ ]);
1368
+ const table = ui().table();
1369
+ table.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
1370
+ table.head(
1371
+ reportRawOverviewTableHeaders.map((heading, idx) => ({
1372
+ content: chalk4.cyan(heading),
1373
+ hAlign: hAlign(idx)
1374
+ }))
1375
+ );
1376
+ rows.forEach(
1377
+ (row) => table.row(
1378
+ row.map((content, idx) => ({
1379
+ content: content.toString(),
1380
+ hAlign: hAlign(idx)
1381
+ }))
1382
+ )
1383
+ );
1384
+ log(chalk4.magentaBright.bold("Categories"));
1385
+ log();
1386
+ table.render();
1387
+ log();
1110
1388
  }
1111
-
1112
- // packages/utils/src/lib/reports/md/list.ts
1113
- function li(text, order = "unordered") {
1114
- const style2 = order === "unordered" ? "-" : "- [ ]";
1115
- return `${style2} ${text}`;
1389
+ function applyScoreColor({ score, text }) {
1390
+ const formattedScore = text ?? formatReportScore(score);
1391
+ const style2 = text ? chalk4 : chalk4.bold;
1392
+ if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
1393
+ return style2.green(formattedScore);
1394
+ }
1395
+ if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
1396
+ return style2.yellow(formattedScore);
1397
+ }
1398
+ return style2.red(formattedScore);
1116
1399
  }
1117
1400
 
1118
- // packages/utils/src/lib/reports/md/table.ts
1119
- var alignString = /* @__PURE__ */ new Map([
1120
- ["l", ":--"],
1121
- ["c", ":--:"],
1122
- ["r", "--:"]
1123
- ]);
1124
- function tableMd(data, align) {
1125
- if (data.length === 0) {
1126
- throw new Error("Data can't be empty");
1127
- }
1128
- const alignmentSetting = align ?? data[0]?.map(() => "c");
1129
- const tableContent = data.map((arr) => `|${arr.join("|")}|`);
1130
- const alignmentRow = `|${alignmentSetting?.map((s) => alignString.get(s)).join("|")}|`;
1131
- return tableContent[0] + NEW_LINE + alignmentRow + NEW_LINE + tableContent.slice(1).join(NEW_LINE);
1401
+ // packages/utils/src/lib/reports/flatten-plugins.ts
1402
+ function listGroupsFromAllPlugins(report) {
1403
+ return report.plugins.flatMap(
1404
+ (plugin) => plugin.groups?.map((group) => ({ plugin, group })) ?? []
1405
+ );
1132
1406
  }
1133
- function tableHtml(data) {
1134
- if (data.length === 0) {
1135
- throw new Error("Data can't be empty");
1136
- }
1137
- const tableContent = data.map((arr, index) => {
1138
- if (index === 0) {
1139
- const headerRow = arr.map((s) => `<th>${s}</th>`).join("");
1140
- return `<tr>${headerRow}</tr>`;
1141
- }
1142
- const row = arr.map((s) => `<td>${s}</td>`).join("");
1143
- return `<tr>${row}</tr>`;
1144
- });
1145
- return `<table>${tableContent.join("")}</table>`;
1407
+ function listAuditsFromAllPlugins(report) {
1408
+ return report.plugins.flatMap(
1409
+ (plugin) => plugin.audits.map((audit) => ({ plugin, audit }))
1410
+ );
1146
1411
  }
1147
1412
 
1148
1413
  // packages/utils/src/lib/reports/generate-md-report.ts
@@ -1151,7 +1416,7 @@ function generateMdReport(report) {
1151
1416
  return (
1152
1417
  // header section
1153
1418
  // eslint-disable-next-line prefer-template
1154
- reportToHeaderSection() + NEW_LINE + // categories overview section
1419
+ reportToHeaderSection2() + NEW_LINE + // categories overview section
1155
1420
  (printCategories ? reportToOverviewSection(report) + NEW_LINE + NEW_LINE : "") + // categories section
1156
1421
  (printCategories ? reportToCategoriesSection(report) + NEW_LINE + NEW_LINE : "") + // audits section
1157
1422
  reportToAuditsSection(report) + NEW_LINE + NEW_LINE + // about section
@@ -1159,7 +1424,7 @@ function generateMdReport(report) {
1159
1424
  `${FOOTER_PREFIX} ${link(README_LINK, "Code PushUp")}`
1160
1425
  );
1161
1426
  }
1162
- function reportToHeaderSection() {
1427
+ function reportToHeaderSection2() {
1163
1428
  return headline(reportHeadlineText) + NEW_LINE;
1164
1429
  }
1165
1430
  function reportToOverviewSection(report) {
@@ -1332,82 +1597,6 @@ function getAuditResult(audit, isHtml = false) {
1332
1597
  return isHtml ? `<b>${displayValue || value}</b>` : style(String(displayValue || value));
1333
1598
  }
1334
1599
 
1335
- // packages/utils/src/lib/reports/generate-stdout-summary.ts
1336
- import cliui from "@isaacs/cliui";
1337
- import chalk4 from "chalk";
1338
- import CliTable3 from "cli-table3";
1339
- function addLine(line = "") {
1340
- return line + NEW_LINE;
1341
- }
1342
- function generateStdoutSummary(report) {
1343
- const printCategories = report.categories.length > 0;
1344
- return addLine(reportToHeaderSection2(report)) + addLine() + addLine(reportToDetailSection(report)) + (printCategories ? addLine(reportToOverviewSection2(report)) : "") + addLine(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`);
1345
- }
1346
- function reportToHeaderSection2(report) {
1347
- const { packageName, version: version2 } = report;
1348
- return `${chalk4.bold(reportHeadlineText)} - ${packageName}@${version2}`;
1349
- }
1350
- function reportToDetailSection(report) {
1351
- const { plugins } = report;
1352
- return plugins.reduce((acc, plugin) => {
1353
- const { title, audits } = plugin;
1354
- const ui = cliui({ width: TERMINAL_WIDTH });
1355
- audits.forEach((audit) => {
1356
- ui.div(
1357
- {
1358
- text: withColor({ score: audit.score, text: "\u25CF" }),
1359
- width: 2,
1360
- padding: [0, 1, 0, 0]
1361
- },
1362
- {
1363
- text: audit.title,
1364
- // eslint-disable-next-line no-magic-numbers
1365
- padding: [0, 3, 0, 0]
1366
- },
1367
- {
1368
- text: chalk4.cyanBright(audit.displayValue || `${audit.value}`),
1369
- width: 10,
1370
- padding: [0, 0, 0, 0]
1371
- }
1372
- );
1373
- });
1374
- return acc + addLine() + addLine(chalk4.magentaBright.bold(`${title} audits`)) + addLine() + addLine(ui.toString()) + addLine();
1375
- }, "");
1376
- }
1377
- function reportToOverviewSection2({
1378
- categories,
1379
- plugins
1380
- }) {
1381
- const table = new CliTable3({
1382
- // eslint-disable-next-line no-magic-numbers
1383
- colWidths: [TERMINAL_WIDTH - 7 - 8 - 4, 7, 8],
1384
- head: reportRawOverviewTableHeaders,
1385
- colAligns: ["left", "right", "right"],
1386
- style: {
1387
- head: ["cyan"]
1388
- }
1389
- });
1390
- table.push(
1391
- ...categories.map(({ title, score, refs }) => [
1392
- title,
1393
- withColor({ score }),
1394
- countCategoryAudits(refs, plugins)
1395
- ])
1396
- );
1397
- return addLine(chalk4.magentaBright.bold("Categories")) + addLine() + addLine(table.toString());
1398
- }
1399
- function withColor({ score, text }) {
1400
- const formattedScore = text ?? formatReportScore(score);
1401
- const style2 = text ? chalk4 : chalk4.bold;
1402
- if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
1403
- return style2.green(formattedScore);
1404
- }
1405
- if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
1406
- return style2.yellow(formattedScore);
1407
- }
1408
- return style2.red(formattedScore);
1409
- }
1410
-
1411
1600
  // packages/utils/src/lib/reports/scoring.ts
1412
1601
  var GroupRefInvalidError = class extends Error {
1413
1602
  constructor(auditSlug, pluginSlug) {
@@ -1547,9 +1736,9 @@ function sortPlugins(plugins) {
1547
1736
 
1548
1737
  // packages/utils/src/lib/verbose-utils.ts
1549
1738
  function getLogVerbose(verbose = false) {
1550
- return (...args) => {
1739
+ return (msg) => {
1551
1740
  if (verbose) {
1552
- console.info(...args);
1741
+ ui().logger.info(msg);
1553
1742
  }
1554
1743
  };
1555
1744
  }
@@ -1567,7 +1756,7 @@ var verboseUtils = (verbose = false) => ({
1567
1756
 
1568
1757
  // packages/core/package.json
1569
1758
  var name = "@code-pushup/core";
1570
- var version = "0.26.1";
1759
+ var version = "0.27.1";
1571
1760
 
1572
1761
  // packages/core/src/lib/implementation/execute-plugin.ts
1573
1762
  import chalk5 from "chalk";
@@ -1680,11 +1869,9 @@ async function executePlugins(plugins, options) {
1680
1869
  }
1681
1870
  }, Promise.resolve([]));
1682
1871
  progressBar?.endProgress("Done running plugins");
1683
- const errorsCallback = ({ reason }) => {
1684
- console.error(reason);
1685
- };
1872
+ const errorsTransform = ({ reason }) => String(reason);
1686
1873
  const results = await Promise.allSettled(pluginsResult);
1687
- logMultipleResults(results, "Plugins", void 0, errorsCallback);
1874
+ logMultipleResults(results, "Plugins", void 0, errorsTransform);
1688
1875
  const { fulfilled, rejected } = groupByStatus(results);
1689
1876
  if (rejected.length > 0) {
1690
1877
  const errorMessages = rejected.map(({ reason }) => String(reason)).join(", ");
@@ -1739,7 +1926,7 @@ var PersistError = class extends Error {
1739
1926
  async function persistReport(report, options) {
1740
1927
  const { outputDir, filename, format } = options;
1741
1928
  const sortedScoredReport = sortReport(scoreReport(report));
1742
- console.info(generateStdoutSummary(sortedScoredReport));
1929
+ logStdoutSummary(sortedScoredReport);
1743
1930
  const results = format.map((reportType) => {
1744
1931
  switch (reportType) {
1745
1932
  case "json":
@@ -1758,7 +1945,7 @@ async function persistReport(report, options) {
1758
1945
  try {
1759
1946
  await mkdir2(outputDir, { recursive: true });
1760
1947
  } catch (error) {
1761
- console.warn(error);
1948
+ ui().logger.warning(error.toString());
1762
1949
  throw new PersistDirError(outputDir);
1763
1950
  }
1764
1951
  }
@@ -1773,7 +1960,7 @@ async function persistReport(report, options) {
1773
1960
  }
1774
1961
  async function persistResult(reportPath, content) {
1775
1962
  return writeFile(reportPath, content).then(() => stat2(reportPath)).then((stats) => [reportPath, stats.size]).catch((error) => {
1776
- console.warn(error);
1963
+ ui().logger.warning(error.toString());
1777
1964
  throw new PersistError(reportPath);
1778
1965
  });
1779
1966
  }
@@ -1794,6 +1981,189 @@ async function collectAndPersistReports(options) {
1794
1981
  });
1795
1982
  }
1796
1983
 
1984
+ // packages/core/src/lib/compare.ts
1985
+ import { writeFile as writeFile2 } from "node:fs/promises";
1986
+
1987
+ // packages/core/src/lib/implementation/compare-scorables.ts
1988
+ function compareCategories(reports) {
1989
+ const { pairs, added, removed } = matchArrayItemsByKey({
1990
+ before: reports.before.categories,
1991
+ after: reports.after.categories,
1992
+ key: "slug"
1993
+ });
1994
+ const { changed, unchanged } = comparePairs(
1995
+ pairs,
1996
+ ({ before, after }) => before.score === after.score
1997
+ );
1998
+ return {
1999
+ changed: changed.map(categoryPairToDiff),
2000
+ unchanged: unchanged.map(categoryToResult),
2001
+ added: added.map(categoryToResult),
2002
+ removed: removed.map(categoryToResult)
2003
+ };
2004
+ }
2005
+ function compareGroups(reports) {
2006
+ const { pairs, added, removed } = matchArrayItemsByKey({
2007
+ before: listGroupsFromAllPlugins(reports.before),
2008
+ after: listGroupsFromAllPlugins(reports.after),
2009
+ key: ({ plugin, group }) => `${plugin.slug}/${group.slug}`
2010
+ });
2011
+ const { changed, unchanged } = comparePairs(
2012
+ pairs,
2013
+ ({ before, after }) => before.group.score === after.group.score
2014
+ );
2015
+ return {
2016
+ changed: changed.map(pluginGroupPairToDiff),
2017
+ unchanged: unchanged.map(pluginGroupToResult),
2018
+ added: added.map(pluginGroupToResult),
2019
+ removed: removed.map(pluginGroupToResult)
2020
+ };
2021
+ }
2022
+ function compareAudits2(reports) {
2023
+ const { pairs, added, removed } = matchArrayItemsByKey({
2024
+ before: listAuditsFromAllPlugins(reports.before),
2025
+ after: listAuditsFromAllPlugins(reports.after),
2026
+ key: ({ plugin, audit }) => `${plugin.slug}/${audit.slug}`
2027
+ });
2028
+ const { changed, unchanged } = comparePairs(
2029
+ pairs,
2030
+ ({ before, after }) => before.audit.value === after.audit.value && before.audit.score === after.audit.score
2031
+ );
2032
+ return {
2033
+ changed: changed.map(pluginAuditPairToDiff),
2034
+ unchanged: unchanged.map(pluginAuditToResult),
2035
+ added: added.map(pluginAuditToResult),
2036
+ removed: removed.map(pluginAuditToResult)
2037
+ };
2038
+ }
2039
+ function categoryToResult(category) {
2040
+ return {
2041
+ slug: category.slug,
2042
+ title: category.title,
2043
+ score: category.score
2044
+ };
2045
+ }
2046
+ function categoryPairToDiff({
2047
+ before,
2048
+ after
2049
+ }) {
2050
+ return {
2051
+ slug: after.slug,
2052
+ title: after.title,
2053
+ scores: {
2054
+ before: before.score,
2055
+ after: after.score,
2056
+ diff: after.score - before.score
2057
+ }
2058
+ };
2059
+ }
2060
+ function pluginGroupToResult({ group, plugin }) {
2061
+ return {
2062
+ slug: group.slug,
2063
+ title: group.title,
2064
+ plugin: {
2065
+ slug: plugin.slug,
2066
+ title: plugin.title
2067
+ },
2068
+ score: group.score
2069
+ };
2070
+ }
2071
+ function pluginGroupPairToDiff({
2072
+ before,
2073
+ after
2074
+ }) {
2075
+ return {
2076
+ slug: after.group.slug,
2077
+ title: after.group.title,
2078
+ plugin: {
2079
+ slug: after.plugin.slug,
2080
+ title: after.plugin.title
2081
+ },
2082
+ scores: {
2083
+ before: before.group.score,
2084
+ after: after.group.score,
2085
+ diff: after.group.score - before.group.score
2086
+ }
2087
+ };
2088
+ }
2089
+ function pluginAuditToResult({ audit, plugin }) {
2090
+ return {
2091
+ slug: audit.slug,
2092
+ title: audit.title,
2093
+ plugin: {
2094
+ slug: plugin.slug,
2095
+ title: plugin.title
2096
+ },
2097
+ score: audit.score,
2098
+ value: audit.value,
2099
+ displayValue: audit.displayValue
2100
+ };
2101
+ }
2102
+ function pluginAuditPairToDiff({
2103
+ before,
2104
+ after
2105
+ }) {
2106
+ return {
2107
+ slug: after.audit.slug,
2108
+ title: after.audit.title,
2109
+ plugin: {
2110
+ slug: after.plugin.slug,
2111
+ title: after.plugin.title
2112
+ },
2113
+ scores: {
2114
+ before: before.audit.score,
2115
+ after: after.audit.score,
2116
+ diff: after.audit.score - before.audit.score
2117
+ },
2118
+ values: {
2119
+ before: before.audit.value,
2120
+ after: after.audit.value,
2121
+ diff: after.audit.value - before.audit.value
2122
+ },
2123
+ displayValues: {
2124
+ before: before.audit.displayValue,
2125
+ after: after.audit.displayValue
2126
+ }
2127
+ };
2128
+ }
2129
+
2130
+ // packages/core/src/lib/compare.ts
2131
+ async function compareReportFiles(inputPaths, outputPath) {
2132
+ const [reportBefore, reportAfter] = await Promise.all([
2133
+ readJsonFile(inputPaths.before),
2134
+ readJsonFile(inputPaths.after)
2135
+ ]);
2136
+ const reports = {
2137
+ before: reportSchema.parse(reportBefore),
2138
+ after: reportSchema.parse(reportAfter)
2139
+ };
2140
+ const reportsDiff = compareReports(reports);
2141
+ await writeFile2(outputPath, JSON.stringify(reportsDiff, null, 2));
2142
+ }
2143
+ function compareReports(reports) {
2144
+ const start = performance.now();
2145
+ const date = (/* @__PURE__ */ new Date()).toISOString();
2146
+ const commits = reports.before.commit != null && reports.after.commit != null ? { before: reports.before.commit, after: reports.after.commit } : null;
2147
+ const scoredReports = {
2148
+ before: scoreReport(reports.before),
2149
+ after: scoreReport(reports.after)
2150
+ };
2151
+ const categories = compareCategories(scoredReports);
2152
+ const groups = compareGroups(scoredReports);
2153
+ const audits = compareAudits2(scoredReports);
2154
+ const duration = calcDuration(start);
2155
+ return {
2156
+ commits,
2157
+ categories,
2158
+ groups,
2159
+ audits,
2160
+ packageName: name,
2161
+ version,
2162
+ date,
2163
+ duration
2164
+ };
2165
+ }
2166
+
1797
2167
  // packages/core/src/lib/implementation/read-rc-file.ts
1798
2168
  import { join as join5 } from "node:path";
1799
2169
  var ConfigPathError = class extends Error {
@@ -1969,6 +2339,8 @@ export {
1969
2339
  autoloadRc,
1970
2340
  collect,
1971
2341
  collectAndPersistReports,
2342
+ compareReportFiles,
2343
+ compareReports,
1972
2344
  executePlugin,
1973
2345
  executePlugins,
1974
2346
  persistReport,
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@code-pushup/core",
3
- "version": "0.26.1",
3
+ "version": "0.27.1",
4
4
  "license": "MIT",
5
5
  "dependencies": {
6
6
  "@code-pushup/models": "*",
7
7
  "@code-pushup/utils": "*",
8
8
  "@code-pushup/portal-client": "^0.6.1",
9
- "chalk": "^5.3.0"
9
+ "chalk": "^5.3.0",
10
+ "simple-git": "^3.20.0"
10
11
  },
11
12
  "type": "module",
12
13
  "main": "./index.js",
package/src/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { CollectAndPersistReportsOptions, collectAndPersistReports, } from './lib/collect-and-persist';
2
+ export { compareReportFiles, compareReports } from './lib/compare';
2
3
  export { CollectOptions, collect } from './lib/implementation/collect';
4
+ export { ReportsToCompare } from './lib/implementation/compare-scorables';
3
5
  export { PluginOutputMissingAuditError, executePlugin, executePlugins, } from './lib/implementation/execute-plugin';
4
6
  export { PersistDirError, PersistError, persistReport, } from './lib/implementation/persist';
5
7
  export { ConfigPathError, autoloadRc, readRcByPath, } from './lib/implementation/read-rc-file';
@@ -2,5 +2,5 @@ import { CoreConfig, PersistConfig } from '@code-pushup/models';
2
2
  import { GlobalOptions } from './types';
3
3
  export type CollectAndPersistReportsOptions = Required<Pick<CoreConfig, 'plugins' | 'categories'>> & {
4
4
  persist: Required<PersistConfig>;
5
- } & GlobalOptions;
5
+ } & Partial<GlobalOptions>;
6
6
  export declare function collectAndPersistReports(options: CollectAndPersistReportsOptions): Promise<void>;
@@ -0,0 +1,4 @@
1
+ import { Report, ReportsDiff } from '@code-pushup/models';
2
+ import { Diff } from '@code-pushup/utils';
3
+ export declare function compareReportFiles(inputPaths: Diff<string>, outputPath: string): Promise<void>;
4
+ export declare function compareReports(reports: Diff<Report>): ReportsDiff;
@@ -0,0 +1,15 @@
1
+ import { LogOptions, LogResult } from 'simple-git';
2
+ import { CoreConfig, PersistConfig, UploadConfig } from '@code-pushup/models';
3
+ import { GlobalOptions } from './types';
4
+ export type HistoryOnlyOptions = {
5
+ targetBranch?: string;
6
+ skipUploads?: boolean;
7
+ forceCleanStatus?: boolean;
8
+ };
9
+ export type HistoryOptions = Required<Pick<CoreConfig, 'plugins' | 'categories'>> & {
10
+ persist: Required<PersistConfig>;
11
+ upload?: Required<UploadConfig>;
12
+ } & HistoryOnlyOptions & Partial<GlobalOptions>;
13
+ export declare function history(config: HistoryOptions, commits: string[]): Promise<string[]>;
14
+ export declare function getHashes(options: LogOptions, git?: import("simple-git").SimpleGit): Promise<string[]>;
15
+ export declare function prepareHashes(logs: LogResult): string[];
@@ -1,6 +1,6 @@
1
1
  import { CoreConfig, Report } from '@code-pushup/models';
2
2
  import { GlobalOptions } from '../types';
3
- export type CollectOptions = Required<Pick<CoreConfig, 'plugins' | 'categories'>> & GlobalOptions;
3
+ export type CollectOptions = Required<Pick<CoreConfig, 'plugins' | 'categories'>> & Partial<GlobalOptions>;
4
4
  /**
5
5
  * Run audits, collect plugin output and aggregate it into a JSON object
6
6
  * @param options
@@ -0,0 +1,6 @@
1
+ import { ReportsDiff } from '@code-pushup/models';
2
+ import { Diff, ScoredReport } from '@code-pushup/utils';
3
+ export type ReportsToCompare = Diff<ScoredReport>;
4
+ export declare function compareCategories(reports: ReportsToCompare): ReportsDiff['categories'];
5
+ export declare function compareGroups(reports: ReportsToCompare): ReportsDiff['groups'];
6
+ export declare function compareAudits(reports: ReportsToCompare): ReportsDiff['audits'];
@@ -50,5 +50,5 @@ export declare function executePlugin(pluginConfig: PluginConfig, onProgress?: O
50
50
  *
51
51
  */
52
52
  export declare function executePlugins(plugins: PluginConfig[], options?: {
53
- progress: boolean;
53
+ progress?: boolean;
54
54
  }): Promise<PluginReport[]>;
@@ -5,7 +5,7 @@ export type UploadOptions = {
5
5
  upload?: UploadConfig;
6
6
  } & {
7
7
  persist: Required<PersistConfig>;
8
- } & GlobalOptions;
8
+ } & Partial<GlobalOptions>;
9
9
  /**
10
10
  * Uploads collected audits to the portal
11
11
  * @param options