@code-pushup/cli 0.26.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.
package/index.js CHANGED
@@ -57,7 +57,7 @@ function getMissingRefsForCategories(categories, plugins) {
57
57
  ({ refs }) => refs.filter(({ type }) => type === "group").map(({ plugin, slug }) => `${plugin}#${slug} (group)`)
58
58
  );
59
59
  const groupRefsFromPlugins = plugins.flatMap(
60
- ({ groups, slug: pluginSlug }) => Array.isArray(groups) ? groups.map(({ slug }) => `${pluginSlug}#${slug} (group)`) : []
60
+ ({ groups: groups2, slug: pluginSlug }) => Array.isArray(groups2) ? groups2.map(({ slug }) => `${pluginSlug}#${slug} (group)`) : []
61
61
  );
62
62
  const missingGroupRefs = hasMissingStrings(
63
63
  groupRefsFromCategory,
@@ -92,6 +92,9 @@ var descriptionSchema = z.string({ description: "Description (markdown)" }).max(
92
92
  var urlSchema = z.string().url();
93
93
  var docsUrlSchema = urlSchema.optional().or(z.literal("")).describe("Documentation site");
94
94
  var titleSchema = z.string({ description: "Descriptive name" }).max(MAX_TITLE_LENGTH);
95
+ var scoreSchema = z.number({
96
+ description: "Value between 0 and 1"
97
+ }).min(0).max(1);
95
98
  function metaSchema(options2) {
96
99
  const {
97
100
  descriptionDescription,
@@ -223,6 +226,8 @@ var issueSchema = z3.object(
223
226
  );
224
227
 
225
228
  // packages/models/src/lib/audit-output.ts
229
+ var auditValueSchema = nonnegativeIntSchema.describe("Raw numeric value");
230
+ var auditDisplayValueSchema = z4.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }).optional();
226
231
  var auditDetailsSchema = z4.object(
227
232
  {
228
233
  issues: z4.array(issueSchema, { description: "List of findings" })
@@ -232,11 +237,9 @@ var auditDetailsSchema = z4.object(
232
237
  var auditOutputSchema = z4.object(
233
238
  {
234
239
  slug: slugSchema.describe("Reference to audit"),
235
- displayValue: z4.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }).optional(),
236
- value: nonnegativeIntSchema.describe("Raw numeric value"),
237
- score: z4.number({
238
- description: "Value between 0 and 1"
239
- }).min(0).max(1),
240
+ displayValue: auditDisplayValueSchema,
241
+ value: auditValueSchema,
242
+ score: scoreSchema,
240
243
  details: auditDetailsSchema.optional()
241
244
  },
242
245
  { description: "Audit information" }
@@ -375,28 +378,28 @@ var groupSchema = scorableSchema(
375
378
  var groupsSchema = z8.array(groupSchema, {
376
379
  description: "List of groups"
377
380
  }).optional().refine(
378
- (groups) => !getDuplicateSlugsInGroups(groups),
379
- (groups) => ({
380
- message: duplicateSlugsInGroupsErrorMsg(groups)
381
+ (groups2) => !getDuplicateSlugsInGroups(groups2),
382
+ (groups2) => ({
383
+ message: duplicateSlugsInGroupsErrorMsg(groups2)
381
384
  })
382
385
  );
383
- function duplicateRefsInGroupsErrorMsg(groups) {
384
- const duplicateRefs = getDuplicateRefsInGroups(groups);
386
+ function duplicateRefsInGroupsErrorMsg(groups2) {
387
+ const duplicateRefs = getDuplicateRefsInGroups(groups2);
385
388
  return `In plugin groups the following references are not unique: ${errorItems(
386
389
  duplicateRefs
387
390
  )}`;
388
391
  }
389
- function getDuplicateRefsInGroups(groups) {
390
- return hasDuplicateStrings(groups.map(({ slug: ref }) => ref).filter(exists));
392
+ function getDuplicateRefsInGroups(groups2) {
393
+ return hasDuplicateStrings(groups2.map(({ slug: ref }) => ref).filter(exists));
391
394
  }
392
- function duplicateSlugsInGroupsErrorMsg(groups) {
393
- const duplicateRefs = getDuplicateSlugsInGroups(groups);
395
+ function duplicateSlugsInGroupsErrorMsg(groups2) {
396
+ const duplicateRefs = getDuplicateSlugsInGroups(groups2);
394
397
  return `In groups the following slugs are not unique: ${errorItems(
395
398
  duplicateRefs
396
399
  )}`;
397
400
  }
398
- function getDuplicateSlugsInGroups(groups) {
399
- return Array.isArray(groups) ? hasDuplicateStrings(groups.map(({ slug }) => slug)) : false;
401
+ function getDuplicateSlugsInGroups(groups2) {
402
+ return Array.isArray(groups2) ? hasDuplicateStrings(groups2.map(({ slug }) => slug)) : false;
400
403
  }
401
404
 
402
405
  // packages/models/src/lib/runner-config.ts
@@ -501,9 +504,9 @@ var CONFIG_FILE_NAME = "code-pushup.config";
501
504
  var SUPPORTED_CONFIG_FILE_FORMATS = ["ts", "mjs", "js"];
502
505
 
503
506
  // packages/models/src/lib/implementation/constants.ts
504
- var PERSIST_OUTPUT_DIR = ".code-pushup";
505
- var PERSIST_FORMAT = ["json"];
506
- 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"];
507
510
 
508
511
  // packages/models/src/lib/report.ts
509
512
  import { z as z13 } from "zod";
@@ -527,15 +530,15 @@ var pluginReportSchema = pluginMetaSchema.merge(
527
530
  )
528
531
  })
529
532
  );
530
- function missingRefsFromGroupsErrorMsg2(audits, groups) {
531
- const missingRefs = getMissingRefsFromGroups2(audits, groups);
533
+ function missingRefsFromGroupsErrorMsg2(audits, groups2) {
534
+ const missingRefs = getMissingRefsFromGroups2(audits, groups2);
532
535
  return `group references need to point to an existing audit in this plugin report: ${errorItems(
533
536
  missingRefs
534
537
  )}`;
535
538
  }
536
- function getMissingRefsFromGroups2(audits, groups) {
539
+ function getMissingRefsFromGroups2(audits, groups2) {
537
540
  return hasMissingStrings(
538
- groups.flatMap(
541
+ groups2.flatMap(
539
542
  ({ refs: auditRefs }) => auditRefs.map(({ slug: ref }) => ref)
540
543
  ),
541
544
  audits.map(({ slug }) => slug)
@@ -568,6 +571,138 @@ var reportSchema = packageVersionSchema({
568
571
  })
569
572
  );
570
573
 
574
+ // packages/models/src/lib/reports-diff.ts
575
+ import { z as z14 } from "zod";
576
+ function makeComparisonSchema(schema) {
577
+ const sharedDescription = schema.description || "Result";
578
+ return z14.object({
579
+ before: schema.describe(`${sharedDescription} (source commit)`),
580
+ after: schema.describe(`${sharedDescription} (target commit)`)
581
+ });
582
+ }
583
+ function makeArraysComparisonSchema(diffSchema, resultSchema, description) {
584
+ return z14.object(
585
+ {
586
+ changed: z14.array(diffSchema),
587
+ unchanged: z14.array(resultSchema),
588
+ added: z14.array(resultSchema),
589
+ removed: z14.array(resultSchema)
590
+ },
591
+ { description }
592
+ );
593
+ }
594
+ var scorableMetaSchema = z14.object({ slug: slugSchema, title: titleSchema });
595
+ var scorableWithPluginMetaSchema = scorableMetaSchema.merge(
596
+ z14.object({
597
+ plugin: pluginMetaSchema.pick({ slug: true, title: true }).describe("Plugin which defines it")
598
+ })
599
+ );
600
+ var scorableDiffSchema = scorableMetaSchema.merge(
601
+ z14.object({
602
+ scores: makeComparisonSchema(scoreSchema).merge(
603
+ z14.object({
604
+ diff: z14.number().min(-1).max(1).describe("Score change (`scores.after - scores.before`)")
605
+ })
606
+ ).describe("Score comparison")
607
+ })
608
+ );
609
+ var scorableWithPluginDiffSchema = scorableDiffSchema.merge(
610
+ scorableWithPluginMetaSchema
611
+ );
612
+ var categoryDiffSchema = scorableDiffSchema;
613
+ var groupDiffSchema = scorableWithPluginDiffSchema;
614
+ var auditDiffSchema = scorableWithPluginDiffSchema.merge(
615
+ z14.object({
616
+ values: makeComparisonSchema(auditValueSchema).merge(
617
+ z14.object({
618
+ diff: z14.number().int().describe("Value change (`values.after - values.before`)")
619
+ })
620
+ ).describe("Audit `value` comparison"),
621
+ displayValues: makeComparisonSchema(auditDisplayValueSchema).describe(
622
+ "Audit `displayValue` comparison"
623
+ )
624
+ })
625
+ );
626
+ var categoryResultSchema = scorableMetaSchema.merge(
627
+ z14.object({ score: scoreSchema })
628
+ );
629
+ var groupResultSchema = scorableWithPluginMetaSchema.merge(
630
+ z14.object({ score: scoreSchema })
631
+ );
632
+ var auditResultSchema = scorableWithPluginMetaSchema.merge(
633
+ auditOutputSchema.pick({ score: true, value: true, displayValue: true })
634
+ );
635
+ var reportsDiffSchema = z14.object({
636
+ commits: makeComparisonSchema(commitSchema).nullable().describe("Commits identifying compared reports"),
637
+ categories: makeArraysComparisonSchema(
638
+ categoryDiffSchema,
639
+ categoryResultSchema,
640
+ "Changes affecting categories"
641
+ ),
642
+ groups: makeArraysComparisonSchema(
643
+ groupDiffSchema,
644
+ groupResultSchema,
645
+ "Changes affecting groups"
646
+ ),
647
+ audits: makeArraysComparisonSchema(
648
+ auditDiffSchema,
649
+ auditResultSchema,
650
+ "Changes affecting audits"
651
+ )
652
+ }).merge(
653
+ packageVersionSchema({
654
+ versionDescription: "NPM version of the CLI (when `compare` was run)",
655
+ required: true
656
+ })
657
+ ).merge(
658
+ executionMetaSchema({
659
+ descriptionDate: "Start date and time of the compare run",
660
+ descriptionDuration: "Duration of the compare run in ms"
661
+ })
662
+ );
663
+
664
+ // packages/utils/src/lib/diff.ts
665
+ function matchArrayItemsByKey({
666
+ before,
667
+ after,
668
+ key
669
+ }) {
670
+ const pairs = [];
671
+ const added = [];
672
+ const afterKeys = /* @__PURE__ */ new Set();
673
+ const keyFn = typeof key === "function" ? key : (item) => item[key];
674
+ for (const afterItem of after) {
675
+ const afterKey = keyFn(afterItem);
676
+ afterKeys.add(afterKey);
677
+ const match = before.find((beforeItem) => keyFn(beforeItem) === afterKey);
678
+ if (match) {
679
+ pairs.push({ before: match, after: afterItem });
680
+ } else {
681
+ added.push(afterItem);
682
+ }
683
+ }
684
+ const removed = before.filter(
685
+ (beforeItem) => !afterKeys.has(keyFn(beforeItem))
686
+ );
687
+ return {
688
+ pairs,
689
+ added,
690
+ removed
691
+ };
692
+ }
693
+ function comparePairs(pairs, equalsFn) {
694
+ return pairs.reduce(
695
+ (acc, pair) => ({
696
+ ...acc,
697
+ ...equalsFn(pair) ? { unchanged: [...acc.unchanged, pair.after] } : { changed: [...acc.changed, pair] }
698
+ }),
699
+ {
700
+ changed: [],
701
+ unchanged: []
702
+ }
703
+ );
704
+ }
705
+
571
706
  // packages/utils/src/lib/execute-process.ts
572
707
  import { spawn } from "node:child_process";
573
708
 
@@ -576,13 +711,25 @@ import { join } from "node:path";
576
711
 
577
712
  // packages/utils/src/lib/file-system.ts
578
713
  import { bundleRequire } from "bundle-require";
579
- import chalk from "chalk";
714
+ import chalk2 from "chalk";
580
715
  import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
581
716
 
582
717
  // packages/utils/src/lib/formatting.ts
583
718
  function slugify(text) {
584
719
  return text.trim().toLowerCase().replace(/\s+|\//g, "-").replace(/[^a-z\d-]/g, "");
585
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
+ }
586
733
  function formatBytes(bytes, decimals = 2) {
587
734
  const positiveBytes = Math.max(bytes, 0);
588
735
  if (positiveBytes === 0) {
@@ -594,6 +741,9 @@ function formatBytes(bytes, decimals = 2) {
594
741
  const i = Math.floor(Math.log(positiveBytes) / Math.log(k));
595
742
  return `${Number.parseFloat((positiveBytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
596
743
  }
744
+ function pluralizeToken(token, times) {
745
+ return `${times} ${Math.abs(times) === 1 ? token : pluralize(token)}`;
746
+ }
597
747
  function formatDuration(duration) {
598
748
  if (duration < 1e3) {
599
749
  return `${duration} ms`;
@@ -621,33 +771,106 @@ function isPromiseRejectedResult(result) {
621
771
  return result.status === "rejected";
622
772
  }
623
773
 
774
+ // packages/utils/src/lib/logging.ts
775
+ import isaacs_cliui from "@isaacs/cliui";
776
+ import { cliui } from "@poppinss/cliui";
777
+ import chalk from "chalk";
778
+
779
+ // packages/utils/src/lib/reports/constants.ts
780
+ var TERMINAL_WIDTH = 80;
781
+ var NEW_LINE = "\n";
782
+ var SCORE_COLOR_RANGE = {
783
+ GREEN_MIN: 0.9,
784
+ YELLOW_MIN: 0.5
785
+ };
786
+ var FOOTER_PREFIX = "Made with \u2764 by";
787
+ var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
788
+ var README_LINK = "https://github.com/flowup/quality-metrics-cli#readme";
789
+ var reportHeadlineText = "Code PushUp Report";
790
+ var reportOverviewTableHeaders = [
791
+ "\u{1F3F7} Category",
792
+ "\u2B50 Score",
793
+ "\u{1F6E1} Audits"
794
+ ];
795
+ var reportRawOverviewTableHeaders = ["Category", "Score", "Audits"];
796
+ var reportMetaTableHeaders = [
797
+ "Commit",
798
+ "Version",
799
+ "Duration",
800
+ "Plugins",
801
+ "Categories",
802
+ "Audits"
803
+ ];
804
+ var pluginMetaTableHeaders = [
805
+ "Plugin",
806
+ "Audits",
807
+ "Version",
808
+ "Duration"
809
+ ];
810
+ var detailsTableHeaders = [
811
+ "Severity",
812
+ "Message",
813
+ "Source file",
814
+ "Line(s)"
815
+ ];
816
+
817
+ // packages/utils/src/lib/logging.ts
818
+ var singletonUiInstance;
819
+ function ui() {
820
+ if (singletonUiInstance === void 0) {
821
+ singletonUiInstance = cliui();
822
+ }
823
+ return {
824
+ ...singletonUiInstance,
825
+ row: (args) => {
826
+ logListItem(args);
827
+ }
828
+ };
829
+ }
830
+ var singletonisaacUi;
831
+ function logListItem(args) {
832
+ if (singletonisaacUi === void 0) {
833
+ singletonisaacUi = isaacs_cliui({ width: TERMINAL_WIDTH });
834
+ }
835
+ singletonisaacUi.div(...args);
836
+ const content = singletonisaacUi.toString();
837
+ singletonisaacUi.rows = [];
838
+ singletonUiInstance?.logger.log(content);
839
+ }
840
+ function link(text) {
841
+ return chalk.underline(chalk.blueBright(text));
842
+ }
843
+
624
844
  // packages/utils/src/lib/log-results.ts
625
- function logMultipleResults(results, messagePrefix, succeededCallback, failedCallback) {
626
- if (succeededCallback) {
845
+ function logMultipleResults(results, messagePrefix, succeededTransform, failedTransform) {
846
+ if (succeededTransform) {
627
847
  const succeededResults = results.filter(isPromiseFulfilledResult);
628
848
  logPromiseResults(
629
849
  succeededResults,
630
850
  `${messagePrefix} successfully: `,
631
- succeededCallback
851
+ succeededTransform
632
852
  );
633
853
  }
634
- if (failedCallback) {
854
+ if (failedTransform) {
635
855
  const failedResults = results.filter(isPromiseRejectedResult);
636
856
  logPromiseResults(
637
857
  failedResults,
638
858
  `${messagePrefix} failed: `,
639
- failedCallback
859
+ failedTransform
640
860
  );
641
861
  }
642
862
  }
643
- function logPromiseResults(results, logMessage, callback) {
863
+ function logPromiseResults(results, logMessage, getMsg) {
644
864
  if (results.length > 0) {
645
- if (results[0]?.status === "fulfilled") {
646
- console.info(logMessage);
647
- } else {
648
- console.warn(logMessage);
649
- }
650
- results.forEach(callback);
865
+ const log2 = results[0]?.status === "fulfilled" ? (m) => {
866
+ ui().logger.success(m);
867
+ } : (m) => {
868
+ ui().logger.warning(m);
869
+ };
870
+ log2(logMessage);
871
+ results.forEach((result) => {
872
+ log2(getMsg(result));
873
+ });
651
874
  }
652
875
  }
653
876
 
@@ -681,26 +904,24 @@ async function ensureDirectoryExists(baseDir) {
681
904
  await mkdir(baseDir, { recursive: true });
682
905
  return;
683
906
  } catch (error) {
684
- console.error(error.message);
907
+ ui().logger.error(error.message);
685
908
  if (error.code !== "EEXIST") {
686
909
  throw error;
687
910
  }
688
911
  }
689
912
  }
690
913
  function logMultipleFileResults(fileResults, messagePrefix) {
691
- const succeededCallback = (result) => {
914
+ const succeededTransform = (result) => {
692
915
  const [fileName, size] = result.value;
693
- const formattedSize = size ? ` (${chalk.gray(formatBytes(size))})` : "";
694
- console.info(`- ${chalk.bold(fileName)}${formattedSize}`);
695
- };
696
- const failedCallback = (result) => {
697
- console.warn(`- ${chalk.bold(result.reason)}`);
916
+ const formattedSize = size ? ` (${chalk2.gray(formatBytes(size))})` : "";
917
+ return `- ${chalk2.bold(fileName)}${formattedSize}`;
698
918
  };
919
+ const failedTransform = (result) => `- ${chalk2.bold(result.reason)}`;
699
920
  logMultipleResults(
700
921
  fileResults,
701
922
  messagePrefix,
702
- succeededCallback,
703
- failedCallback
923
+ succeededTransform,
924
+ failedTransform
704
925
  );
705
926
  }
706
927
  var NoExportError = class extends Error {
@@ -719,48 +940,105 @@ async function importEsmModule(options2) {
719
940
  return mod.default;
720
941
  }
721
942
 
722
- // packages/utils/src/lib/reports/constants.ts
723
- var TERMINAL_WIDTH = 80;
724
- var NEW_LINE = "\n";
725
- var SCORE_COLOR_RANGE = {
726
- GREEN_MIN: 0.9,
727
- YELLOW_MIN: 0.5
943
+ // packages/utils/src/lib/reports/md/details.ts
944
+ function details(title, content, cfg = { open: false }) {
945
+ return `<details${cfg.open ? " open" : ""}>
946
+ <summary>${title}</summary>
947
+
948
+ ${content}
949
+
950
+ </details>
951
+ `;
952
+ }
953
+
954
+ // packages/utils/src/lib/reports/md/font-style.ts
955
+ var stylesMap = {
956
+ i: "_",
957
+ // italic
958
+ b: "**",
959
+ // bold
960
+ s: "~",
961
+ // strike through
962
+ c: "`"
963
+ // code
728
964
  };
729
- var FOOTER_PREFIX = "Made with \u2764 by";
730
- var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
731
- var README_LINK = "https://github.com/flowup/quality-metrics-cli#readme";
732
- var reportHeadlineText = "Code PushUp Report";
733
- var reportOverviewTableHeaders = [
734
- "\u{1F3F7} Category",
735
- "\u2B50 Score",
736
- "\u{1F6E1} Audits"
737
- ];
738
- var reportRawOverviewTableHeaders = ["Category", "Score", "Audits"];
739
- var reportMetaTableHeaders = [
740
- "Commit",
741
- "Version",
742
- "Duration",
743
- "Plugins",
744
- "Categories",
745
- "Audits"
746
- ];
747
- var pluginMetaTableHeaders = [
748
- "Plugin",
749
- "Audits",
750
- "Version",
751
- "Duration"
752
- ];
753
- var detailsTableHeaders = [
754
- "Severity",
755
- "Message",
756
- "Source file",
757
- "Line(s)"
758
- ];
965
+ function style(text, styles = ["b"]) {
966
+ return styles.reduce((t, s) => `${stylesMap[s]}${t}${stylesMap[s]}`, text);
967
+ }
968
+
969
+ // packages/utils/src/lib/reports/md/headline.ts
970
+ function headline(text, hierarchy = 1) {
971
+ return `${"#".repeat(hierarchy)} ${text}`;
972
+ }
973
+ function h1(text) {
974
+ return headline(text, 1);
975
+ }
976
+ function h2(text) {
977
+ return headline(text, 2);
978
+ }
979
+ function h3(text) {
980
+ return headline(text, 3);
981
+ }
982
+
983
+ // packages/utils/src/lib/reports/md/image.ts
984
+ function image(src, alt) {
985
+ return `![${alt}](${src})`;
986
+ }
987
+
988
+ // packages/utils/src/lib/reports/md/link.ts
989
+ function link2(href, text) {
990
+ return `[${text || href}](${href})`;
991
+ }
992
+
993
+ // packages/utils/src/lib/reports/md/list.ts
994
+ function li(text, order = "unordered") {
995
+ const style2 = order === "unordered" ? "-" : "- [ ]";
996
+ return `${style2} ${text}`;
997
+ }
998
+
999
+ // packages/utils/src/lib/reports/md/paragraphs.ts
1000
+ function paragraphs(...sections) {
1001
+ return sections.filter(Boolean).join("\n\n");
1002
+ }
1003
+
1004
+ // packages/utils/src/lib/reports/md/table.ts
1005
+ var alignString = /* @__PURE__ */ new Map([
1006
+ ["l", ":--"],
1007
+ ["c", ":--:"],
1008
+ ["r", "--:"]
1009
+ ]);
1010
+ function tableMd(data, align) {
1011
+ if (data.length === 0) {
1012
+ throw new Error("Data can't be empty");
1013
+ }
1014
+ const alignmentSetting = align ?? data[0]?.map(() => "c");
1015
+ const tableContent = data.map((arr) => `|${arr.join("|")}|`);
1016
+ const alignmentRow = `|${alignmentSetting?.map((s) => alignString.get(s)).join("|")}|`;
1017
+ return tableContent[0] + NEW_LINE + alignmentRow + NEW_LINE + tableContent.slice(1).join(NEW_LINE);
1018
+ }
1019
+ function tableHtml(data) {
1020
+ if (data.length === 0) {
1021
+ throw new Error("Data can't be empty");
1022
+ }
1023
+ const tableContent = data.map((arr, index) => {
1024
+ if (index === 0) {
1025
+ const headerRow = arr.map((s) => `<th>${s}</th>`).join("");
1026
+ return `<tr>${headerRow}</tr>`;
1027
+ }
1028
+ const row = arr.map((s) => `<td>${s}</td>`).join("");
1029
+ return `<tr>${row}</tr>`;
1030
+ });
1031
+ return `<table>${tableContent.join("")}</table>`;
1032
+ }
759
1033
 
760
1034
  // packages/utils/src/lib/reports/utils.ts
761
1035
  function formatReportScore(score) {
762
1036
  return Math.round(score * 100).toString();
763
1037
  }
1038
+ function formatScoreWithColor(score, options2) {
1039
+ const styledNumber = options2?.skipBold ? formatReportScore(score) : style(formatReportScore(score));
1040
+ return `${getRoundScoreMarker(score)} ${styledNumber}`;
1041
+ }
764
1042
  function getRoundScoreMarker(score) {
765
1043
  if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
766
1044
  return "\u{1F7E2}";
@@ -779,6 +1057,30 @@ function getSquaredScoreMarker(score) {
779
1057
  }
780
1058
  return "\u{1F7E5}";
781
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
+ }
782
1084
  function getSeverityIcon(severity) {
783
1085
  if (severity === "error") {
784
1086
  return "\u{1F6A8}";
@@ -789,7 +1091,7 @@ function getSeverityIcon(severity) {
789
1091
  return "\u2139\uFE0F";
790
1092
  }
791
1093
  function calcDuration(start, stop) {
792
- return Math.floor((stop ?? performance.now()) - start);
1094
+ return Math.round((stop ?? performance.now()) - start);
793
1095
  }
794
1096
  function countCategoryAudits(refs, plugins) {
795
1097
  const groupLookup = plugins.reduce(
@@ -952,7 +1254,7 @@ var ProcessError = class extends Error {
952
1254
  }
953
1255
  };
954
1256
  function executeProcess(cfg) {
955
- const { observer, cwd, command, args } = cfg;
1257
+ const { observer, cwd, command, args, alwaysResolve = false } = cfg;
956
1258
  const { onStdout, onError, onComplete } = observer ?? {};
957
1259
  const date = (/* @__PURE__ */ new Date()).toISOString();
958
1260
  const start = performance.now();
@@ -972,7 +1274,7 @@ function executeProcess(cfg) {
972
1274
  });
973
1275
  process2.on("close", (code) => {
974
1276
  const timings = { date, duration: calcDuration(start) };
975
- if (code === 0) {
1277
+ if (code === 0 || alwaysResolve) {
976
1278
  onComplete?.();
977
1279
  resolve({ code, stdout, stderr, ...timings });
978
1280
  } else {
@@ -984,6 +1286,14 @@ function executeProcess(cfg) {
984
1286
  });
985
1287
  }
986
1288
 
1289
+ // packages/utils/src/lib/filter.ts
1290
+ function filterItemRefsBy(items, refFilterFn) {
1291
+ return items.map((item) => ({
1292
+ ...item,
1293
+ refs: item.refs.filter(refFilterFn)
1294
+ })).filter((item) => item.refs.length);
1295
+ }
1296
+
987
1297
  // packages/utils/src/lib/git.ts
988
1298
  import { isAbsolute, join as join2, relative } from "node:path";
989
1299
  import { simpleGit } from "simple-git";
@@ -992,6 +1302,9 @@ import { simpleGit } from "simple-git";
992
1302
  function toArray(val) {
993
1303
  return Array.isArray(val) ? val : [val];
994
1304
  }
1305
+ function objectToEntries(obj) {
1306
+ return Object.entries(obj);
1307
+ }
995
1308
  function deepClone(obj) {
996
1309
  return obj == null || typeof obj !== "object" ? obj : structuredClone(obj);
997
1310
  }
@@ -1001,14 +1314,14 @@ function toUnixPath(path) {
1001
1314
 
1002
1315
  // packages/utils/src/lib/git.ts
1003
1316
  async function getLatestCommit(git = simpleGit()) {
1004
- const log = await git.log({
1317
+ const log2 = await git.log({
1005
1318
  maxCount: 1,
1006
1319
  format: { hash: "%H", message: "%s", author: "%an", date: "%aI" }
1007
1320
  });
1008
- if (!log.latest) {
1321
+ if (!log2.latest) {
1009
1322
  return null;
1010
1323
  }
1011
- return commitSchema.parse(log.latest);
1324
+ return commitSchema.parse(log2.latest);
1012
1325
  }
1013
1326
  function getGitRoot(git = simpleGit()) {
1014
1327
  return git.revparse("--show-toplevel");
@@ -1027,12 +1340,6 @@ function groupByStatus(results) {
1027
1340
  );
1028
1341
  }
1029
1342
 
1030
- // packages/utils/src/lib/logging.ts
1031
- import chalk2 from "chalk";
1032
- function link(text) {
1033
- return chalk2.underline(chalk2.blueBright(text));
1034
- }
1035
-
1036
1343
  // packages/utils/src/lib/progress.ts
1037
1344
  import chalk3 from "chalk";
1038
1345
  import { MultiProgressBars } from "multi-progress-bars";
@@ -1088,80 +1395,16 @@ function getProgressBar(taskName) {
1088
1395
  };
1089
1396
  }
1090
1397
 
1091
- // packages/utils/src/lib/reports/md/details.ts
1092
- function details(title, content, cfg = { open: false }) {
1093
- return `<details${cfg.open ? " open" : ""}>
1094
- <summary>${title}</summary>
1095
- ${content}
1096
- </details>
1097
- `;
1098
- }
1099
-
1100
- // packages/utils/src/lib/reports/md/font-style.ts
1101
- var stylesMap = {
1102
- i: "_",
1103
- // italic
1104
- b: "**",
1105
- // bold
1106
- s: "~",
1107
- // strike through
1108
- c: "`"
1109
- // code
1110
- };
1111
- function style(text, styles = ["b"]) {
1112
- return styles.reduce((t, s) => `${stylesMap[s]}${t}${stylesMap[s]}`, text);
1113
- }
1114
-
1115
- // packages/utils/src/lib/reports/md/headline.ts
1116
- function headline(text, hierarchy = 1) {
1117
- return `${"#".repeat(hierarchy)} ${text}`;
1118
- }
1119
- function h2(text) {
1120
- return headline(text, 2);
1121
- }
1122
- function h3(text) {
1123
- return headline(text, 3);
1124
- }
1125
-
1126
- // packages/utils/src/lib/reports/md/link.ts
1127
- function link2(href, text) {
1128
- return `[${text || href}](${href})`;
1129
- }
1130
-
1131
- // packages/utils/src/lib/reports/md/list.ts
1132
- function li(text, order = "unordered") {
1133
- const style2 = order === "unordered" ? "-" : "- [ ]";
1134
- return `${style2} ${text}`;
1135
- }
1136
-
1137
- // packages/utils/src/lib/reports/md/table.ts
1138
- var alignString = /* @__PURE__ */ new Map([
1139
- ["l", ":--"],
1140
- ["c", ":--:"],
1141
- ["r", "--:"]
1142
- ]);
1143
- function tableMd(data, align) {
1144
- if (data.length === 0) {
1145
- throw new Error("Data can't be empty");
1146
- }
1147
- const alignmentSetting = align ?? data[0]?.map(() => "c");
1148
- const tableContent = data.map((arr) => `|${arr.join("|")}|`);
1149
- const alignmentRow = `|${alignmentSetting?.map((s) => alignString.get(s)).join("|")}|`;
1150
- return tableContent[0] + NEW_LINE + alignmentRow + NEW_LINE + tableContent.slice(1).join(NEW_LINE);
1398
+ // packages/utils/src/lib/reports/flatten-plugins.ts
1399
+ function listGroupsFromAllPlugins(report) {
1400
+ return report.plugins.flatMap(
1401
+ (plugin) => plugin.groups?.map((group) => ({ plugin, group })) ?? []
1402
+ );
1151
1403
  }
1152
- function tableHtml(data) {
1153
- if (data.length === 0) {
1154
- throw new Error("Data can't be empty");
1155
- }
1156
- const tableContent = data.map((arr, index) => {
1157
- if (index === 0) {
1158
- const headerRow = arr.map((s) => `<th>${s}</th>`).join("");
1159
- return `<tr>${headerRow}</tr>`;
1160
- }
1161
- const row = arr.map((s) => `<td>${s}</td>`).join("");
1162
- return `<tr>${row}</tr>`;
1163
- });
1164
- return `<table>${tableContent.join("")}</table>`;
1404
+ function listAuditsFromAllPlugins(report) {
1405
+ return report.plugins.flatMap(
1406
+ (plugin) => plugin.audits.map((audit) => ({ plugin, audit }))
1407
+ );
1165
1408
  }
1166
1409
 
1167
1410
  // packages/utils/src/lib/reports/generate-md-report.ts
@@ -1327,54 +1570,285 @@ function reportToAboutSection(report) {
1327
1570
  h2("About") + NEW_LINE + NEW_LINE + `Report was created by [Code PushUp](${README_LINK}) on ${date}.` + NEW_LINE + NEW_LINE + tableMd(reportMetaTable, ["l", "c", "c", "c", "c", "c"]) + NEW_LINE + NEW_LINE + "The following plugins were run:" + NEW_LINE + NEW_LINE + tableMd(pluginMetaTable, ["l", "c", "c", "c"])
1328
1571
  );
1329
1572
  }
1330
- function getDocsAndDescription({
1331
- docsUrl,
1332
- description
1573
+ function getDocsAndDescription({
1574
+ docsUrl,
1575
+ description
1576
+ }) {
1577
+ if (docsUrl) {
1578
+ const docsLink = link2(docsUrl, "\u{1F4D6} Docs");
1579
+ if (!description) {
1580
+ return docsLink + NEW_LINE + NEW_LINE;
1581
+ }
1582
+ if (description.endsWith("```")) {
1583
+ return description + NEW_LINE + NEW_LINE + docsLink + NEW_LINE + NEW_LINE;
1584
+ }
1585
+ return `${description} ${docsLink}${NEW_LINE}${NEW_LINE}`;
1586
+ }
1587
+ if (description) {
1588
+ return description + NEW_LINE + NEW_LINE;
1589
+ }
1590
+ return "";
1591
+ }
1592
+ function getAuditResult(audit, isHtml = false) {
1593
+ const { displayValue, value } = audit;
1594
+ return isHtml ? `<b>${displayValue || value}</b>` : style(String(displayValue || value));
1595
+ }
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
1333
1750
  }) {
1334
- if (docsUrl) {
1335
- const docsLink = link2(docsUrl, "\u{1F4D6} Docs");
1336
- if (!description) {
1337
- return docsLink + NEW_LINE + NEW_LINE;
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`;
1338
1778
  }
1339
- if (description.endsWith("```")) {
1340
- return description + NEW_LINE + NEW_LINE + docsLink + NEW_LINE + NEW_LINE;
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";
1341
1790
  }
1342
- return `${description} ${docsLink}${NEW_LINE}${NEW_LINE}`;
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";
1343
1803
  }
1344
- if (description) {
1345
- return description + NEW_LINE + NEW_LINE;
1804
+ if (outcomes.includes("positive") && !outcomes.includes("negative")) {
1805
+ return "positive";
1346
1806
  }
1347
- return "";
1807
+ if (outcomes.includes("negative") && !outcomes.includes("positive")) {
1808
+ return "negative";
1809
+ }
1810
+ return "mixed";
1348
1811
  }
1349
- function getAuditResult(audit, isHtml = false) {
1350
- const { displayValue, value } = audit;
1351
- return isHtml ? `<b>${displayValue || value}</b>` : style(String(displayValue || value));
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
+ };
1352
1819
  }
1353
1820
 
1354
- // packages/utils/src/lib/reports/generate-stdout-summary.ts
1355
- import cliui from "@isaacs/cliui";
1821
+ // packages/utils/src/lib/reports/log-stdout-summary.ts
1356
1822
  import chalk4 from "chalk";
1357
- import CliTable3 from "cli-table3";
1358
- function addLine(line = "") {
1359
- return line + NEW_LINE;
1823
+ function log(msg = "") {
1824
+ ui().logger.log(msg);
1360
1825
  }
1361
- function generateStdoutSummary(report) {
1826
+ function logStdoutSummary(report) {
1362
1827
  const printCategories = report.categories.length > 0;
1363
- return addLine(reportToHeaderSection2(report)) + addLine() + addLine(reportToDetailSection(report)) + (printCategories ? addLine(reportToOverviewSection2(report)) : "") + addLine(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`);
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();
1364
1836
  }
1365
1837
  function reportToHeaderSection2(report) {
1366
1838
  const { packageName, version: version2 } = report;
1367
1839
  return `${chalk4.bold(reportHeadlineText)} - ${packageName}@${version2}`;
1368
1840
  }
1369
- function reportToDetailSection(report) {
1841
+ function logPlugins(report) {
1370
1842
  const { plugins } = report;
1371
- return plugins.reduce((acc, plugin) => {
1843
+ plugins.forEach((plugin) => {
1372
1844
  const { title, audits } = plugin;
1373
- const ui2 = cliui({ width: TERMINAL_WIDTH });
1845
+ log();
1846
+ log(chalk4.magentaBright.bold(`${title} audits`));
1847
+ log();
1374
1848
  audits.forEach((audit) => {
1375
- ui2.div(
1849
+ ui().row([
1376
1850
  {
1377
- text: withColor({ score: audit.score, text: "\u25CF" }),
1851
+ text: applyScoreColor({ score: audit.score, text: "\u25CF" }),
1378
1852
  width: 2,
1379
1853
  padding: [0, 1, 0, 0]
1380
1854
  },
@@ -1388,34 +1862,40 @@ function reportToDetailSection(report) {
1388
1862
  width: 10,
1389
1863
  padding: [0, 0, 0, 0]
1390
1864
  }
1391
- );
1865
+ ]);
1392
1866
  });
1393
- return acc + addLine() + addLine(chalk4.magentaBright.bold(`${title} audits`)) + addLine() + addLine(ui2.toString()) + addLine();
1394
- }, "");
1395
- }
1396
- function reportToOverviewSection2({
1397
- categories,
1398
- plugins
1399
- }) {
1400
- const table = new CliTable3({
1401
- // eslint-disable-next-line no-magic-numbers
1402
- colWidths: [TERMINAL_WIDTH - 7 - 8 - 4, 7, 8],
1403
- head: reportRawOverviewTableHeaders,
1404
- colAligns: ["left", "right", "right"],
1405
- style: {
1406
- head: ["cyan"]
1407
- }
1867
+ log();
1408
1868
  });
1409
- table.push(
1410
- ...categories.map(({ title, score, refs }) => [
1411
- title,
1412
- withColor({ score }),
1413
- countCategoryAudits(refs, plugins)
1414
- ])
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
+ )
1415
1892
  );
1416
- return addLine(chalk4.magentaBright.bold("Categories")) + addLine() + addLine(table.toString());
1893
+ log(chalk4.magentaBright.bold("Categories"));
1894
+ log();
1895
+ table.render();
1896
+ log();
1417
1897
  }
1418
- function withColor({ score, text }) {
1898
+ function applyScoreColor({ score, text }) {
1419
1899
  const formattedScore = text ?? formatReportScore(score);
1420
1900
  const style2 = text ? chalk4 : chalk4.bold;
1421
1901
  if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
@@ -1438,7 +1918,7 @@ var GroupRefInvalidError = class extends Error {
1438
1918
  function scoreReport(report) {
1439
1919
  const allScoredAuditsAndGroups = /* @__PURE__ */ new Map();
1440
1920
  const scoredPlugins = report.plugins.map((plugin) => {
1441
- const { groups, ...pluginProps } = plugin;
1921
+ const { groups: groups2, ...pluginProps } = plugin;
1442
1922
  plugin.audits.forEach((audit) => {
1443
1923
  allScoredAuditsAndGroups.set(`${plugin.slug}-${audit.slug}-audit`, audit);
1444
1924
  });
@@ -1451,7 +1931,7 @@ function scoreReport(report) {
1451
1931
  }
1452
1932
  return score;
1453
1933
  }
1454
- const scoredGroups = groups?.map((group) => ({
1934
+ const scoredGroups = groups2?.map((group) => ({
1455
1935
  ...group,
1456
1936
  score: calculateScore(group.refs, groupScoreFn)
1457
1937
  })) ?? [];
@@ -1518,7 +1998,7 @@ function parseScoringParameters(refs, scoreFn) {
1518
1998
  function sortReport(report) {
1519
1999
  const { categories, plugins } = report;
1520
2000
  const sortedCategories = categories.map((category) => {
1521
- const { audits, groups } = category.refs.reduce(
2001
+ const { audits, groups: groups2 } = category.refs.reduce(
1522
2002
  (acc, ref) => ({
1523
2003
  ...acc,
1524
2004
  ...ref.type === "group" ? {
@@ -1529,7 +2009,7 @@ function sortReport(report) {
1529
2009
  }),
1530
2010
  { groups: [], audits: [] }
1531
2011
  );
1532
- const sortedAuditsAndGroups = [...audits, ...groups].sort(
2012
+ const sortedAuditsAndGroups = [...audits, ...groups2].sort(
1533
2013
  compareCategoryAuditsAndGroups
1534
2014
  );
1535
2015
  const sortedRefs = [...category.refs].sort((a, b) => {
@@ -1566,9 +2046,9 @@ function sortPlugins(plugins) {
1566
2046
 
1567
2047
  // packages/utils/src/lib/verbose-utils.ts
1568
2048
  function getLogVerbose(verbose = false) {
1569
- return (...args) => {
2049
+ return (msg) => {
1570
2050
  if (verbose) {
1571
- console.info(...args);
2051
+ ui().logger.info(msg);
1572
2052
  }
1573
2053
  };
1574
2054
  }
@@ -1586,7 +2066,7 @@ var verboseUtils = (verbose = false) => ({
1586
2066
 
1587
2067
  // packages/core/package.json
1588
2068
  var name = "@code-pushup/core";
1589
- var version = "0.26.1";
2069
+ var version = "0.28.0";
1590
2070
 
1591
2071
  // packages/core/src/lib/implementation/execute-plugin.ts
1592
2072
  import chalk5 from "chalk";
@@ -1656,7 +2136,7 @@ async function executePlugin(pluginConfig, onProgress) {
1656
2136
  audits: pluginConfigAudits,
1657
2137
  description,
1658
2138
  docsUrl,
1659
- groups,
2139
+ groups: groups2,
1660
2140
  ...pluginMeta
1661
2141
  } = pluginConfig;
1662
2142
  const runnerResult = typeof runner === "object" ? await executeRunnerConfig(runner, onProgress) : await executeRunnerFunction(runner, onProgress);
@@ -1678,7 +2158,7 @@ async function executePlugin(pluginConfig, onProgress) {
1678
2158
  audits: auditReports,
1679
2159
  ...description && { description },
1680
2160
  ...docsUrl && { docsUrl },
1681
- ...groups && { groups }
2161
+ ...groups2 && { groups: groups2 }
1682
2162
  };
1683
2163
  }
1684
2164
  async function executePlugins(plugins, options2) {
@@ -1699,11 +2179,9 @@ async function executePlugins(plugins, options2) {
1699
2179
  }
1700
2180
  }, Promise.resolve([]));
1701
2181
  progressBar?.endProgress("Done running plugins");
1702
- const errorsCallback = ({ reason }) => {
1703
- console.error(reason);
1704
- };
2182
+ const errorsTransform = ({ reason }) => String(reason);
1705
2183
  const results = await Promise.allSettled(pluginsResult);
1706
- logMultipleResults(results, "Plugins", void 0, errorsCallback);
2184
+ logMultipleResults(results, "Plugins", void 0, errorsTransform);
1707
2185
  const { fulfilled, rejected } = groupByStatus(results);
1708
2186
  if (rejected.length > 0) {
1709
2187
  const errorMessages = rejected.map(({ reason }) => String(reason)).join(", ");
@@ -1758,7 +2236,7 @@ var PersistError = class extends Error {
1758
2236
  async function persistReport(report, options2) {
1759
2237
  const { outputDir, filename, format } = options2;
1760
2238
  const sortedScoredReport = sortReport(scoreReport(report));
1761
- console.info(generateStdoutSummary(sortedScoredReport));
2239
+ logStdoutSummary(sortedScoredReport);
1762
2240
  const results = format.map((reportType) => {
1763
2241
  switch (reportType) {
1764
2242
  case "json":
@@ -1777,7 +2255,7 @@ async function persistReport(report, options2) {
1777
2255
  try {
1778
2256
  await mkdir2(outputDir, { recursive: true });
1779
2257
  } catch (error) {
1780
- console.warn(error);
2258
+ ui().logger.warning(error.toString());
1781
2259
  throw new PersistDirError(outputDir);
1782
2260
  }
1783
2261
  }
@@ -1792,7 +2270,7 @@ async function persistReport(report, options2) {
1792
2270
  }
1793
2271
  async function persistResult(reportPath, content) {
1794
2272
  return writeFile(reportPath, content).then(() => stat2(reportPath)).then((stats) => [reportPath, stats.size]).catch((error) => {
1795
- console.warn(error);
2273
+ ui().logger.warning(error.toString());
1796
2274
  throw new PersistError(reportPath);
1797
2275
  });
1798
2276
  }
@@ -1813,8 +2291,209 @@ async function collectAndPersistReports(options2) {
1813
2291
  });
1814
2292
  }
1815
2293
 
1816
- // packages/core/src/lib/implementation/read-rc-file.ts
2294
+ // packages/core/src/lib/compare.ts
2295
+ import { writeFile as writeFile2 } from "node:fs/promises";
1817
2296
  import { join as join5 } from "node:path";
2297
+
2298
+ // packages/core/src/lib/implementation/compare-scorables.ts
2299
+ function compareCategories(reports) {
2300
+ const { pairs, added, removed } = matchArrayItemsByKey({
2301
+ before: reports.before.categories,
2302
+ after: reports.after.categories,
2303
+ key: "slug"
2304
+ });
2305
+ const { changed, unchanged } = comparePairs(
2306
+ pairs,
2307
+ ({ before, after }) => before.score === after.score
2308
+ );
2309
+ return {
2310
+ changed: changed.map(categoryPairToDiff),
2311
+ unchanged: unchanged.map(categoryToResult),
2312
+ added: added.map(categoryToResult),
2313
+ removed: removed.map(categoryToResult)
2314
+ };
2315
+ }
2316
+ function compareGroups(reports) {
2317
+ const { pairs, added, removed } = matchArrayItemsByKey({
2318
+ before: listGroupsFromAllPlugins(reports.before),
2319
+ after: listGroupsFromAllPlugins(reports.after),
2320
+ key: ({ plugin, group }) => `${plugin.slug}/${group.slug}`
2321
+ });
2322
+ const { changed, unchanged } = comparePairs(
2323
+ pairs,
2324
+ ({ before, after }) => before.group.score === after.group.score
2325
+ );
2326
+ return {
2327
+ changed: changed.map(pluginGroupPairToDiff),
2328
+ unchanged: unchanged.map(pluginGroupToResult),
2329
+ added: added.map(pluginGroupToResult),
2330
+ removed: removed.map(pluginGroupToResult)
2331
+ };
2332
+ }
2333
+ function compareAudits2(reports) {
2334
+ const { pairs, added, removed } = matchArrayItemsByKey({
2335
+ before: listAuditsFromAllPlugins(reports.before),
2336
+ after: listAuditsFromAllPlugins(reports.after),
2337
+ key: ({ plugin, audit }) => `${plugin.slug}/${audit.slug}`
2338
+ });
2339
+ const { changed, unchanged } = comparePairs(
2340
+ pairs,
2341
+ ({ before, after }) => before.audit.value === after.audit.value && before.audit.score === after.audit.score
2342
+ );
2343
+ return {
2344
+ changed: changed.map(pluginAuditPairToDiff),
2345
+ unchanged: unchanged.map(pluginAuditToResult),
2346
+ added: added.map(pluginAuditToResult),
2347
+ removed: removed.map(pluginAuditToResult)
2348
+ };
2349
+ }
2350
+ function categoryToResult(category) {
2351
+ return {
2352
+ slug: category.slug,
2353
+ title: category.title,
2354
+ score: category.score
2355
+ };
2356
+ }
2357
+ function categoryPairToDiff({
2358
+ before,
2359
+ after
2360
+ }) {
2361
+ return {
2362
+ slug: after.slug,
2363
+ title: after.title,
2364
+ scores: {
2365
+ before: before.score,
2366
+ after: after.score,
2367
+ diff: after.score - before.score
2368
+ }
2369
+ };
2370
+ }
2371
+ function pluginGroupToResult({ group, plugin }) {
2372
+ return {
2373
+ slug: group.slug,
2374
+ title: group.title,
2375
+ plugin: {
2376
+ slug: plugin.slug,
2377
+ title: plugin.title
2378
+ },
2379
+ score: group.score
2380
+ };
2381
+ }
2382
+ function pluginGroupPairToDiff({
2383
+ before,
2384
+ after
2385
+ }) {
2386
+ return {
2387
+ slug: after.group.slug,
2388
+ title: after.group.title,
2389
+ plugin: {
2390
+ slug: after.plugin.slug,
2391
+ title: after.plugin.title
2392
+ },
2393
+ scores: {
2394
+ before: before.group.score,
2395
+ after: after.group.score,
2396
+ diff: after.group.score - before.group.score
2397
+ }
2398
+ };
2399
+ }
2400
+ function pluginAuditToResult({ audit, plugin }) {
2401
+ return {
2402
+ slug: audit.slug,
2403
+ title: audit.title,
2404
+ plugin: {
2405
+ slug: plugin.slug,
2406
+ title: plugin.title
2407
+ },
2408
+ score: audit.score,
2409
+ value: audit.value,
2410
+ displayValue: audit.displayValue
2411
+ };
2412
+ }
2413
+ function pluginAuditPairToDiff({
2414
+ before,
2415
+ after
2416
+ }) {
2417
+ return {
2418
+ slug: after.audit.slug,
2419
+ title: after.audit.title,
2420
+ plugin: {
2421
+ slug: after.plugin.slug,
2422
+ title: after.plugin.title
2423
+ },
2424
+ scores: {
2425
+ before: before.audit.score,
2426
+ after: after.audit.score,
2427
+ diff: after.audit.score - before.audit.score
2428
+ },
2429
+ values: {
2430
+ before: before.audit.value,
2431
+ after: after.audit.value,
2432
+ diff: after.audit.value - before.audit.value
2433
+ },
2434
+ displayValues: {
2435
+ before: before.audit.displayValue,
2436
+ after: after.audit.displayValue
2437
+ }
2438
+ };
2439
+ }
2440
+
2441
+ // packages/core/src/lib/compare.ts
2442
+ async function compareReportFiles(inputPaths, persistConfig) {
2443
+ const { outputDir, filename, format } = persistConfig;
2444
+ const [reportBefore, reportAfter] = await Promise.all([
2445
+ readJsonFile(inputPaths.before),
2446
+ readJsonFile(inputPaths.after)
2447
+ ]);
2448
+ const reports = {
2449
+ before: reportSchema.parse(reportBefore),
2450
+ after: reportSchema.parse(reportAfter)
2451
+ };
2452
+ const reportsDiff = compareReports(reports);
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
+ );
2462
+ }
2463
+ function compareReports(reports) {
2464
+ const start = performance.now();
2465
+ const date = (/* @__PURE__ */ new Date()).toISOString();
2466
+ const commits = reports.before.commit != null && reports.after.commit != null ? { before: reports.before.commit, after: reports.after.commit } : null;
2467
+ const scoredReports = {
2468
+ before: scoreReport(reports.before),
2469
+ after: scoreReport(reports.after)
2470
+ };
2471
+ const categories = compareCategories(scoredReports);
2472
+ const groups2 = compareGroups(scoredReports);
2473
+ const audits = compareAudits2(scoredReports);
2474
+ const duration = calcDuration(start);
2475
+ return {
2476
+ commits,
2477
+ categories,
2478
+ groups: groups2,
2479
+ audits,
2480
+ packageName: name,
2481
+ version,
2482
+ date,
2483
+ duration
2484
+ };
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
+ }
2494
+
2495
+ // packages/core/src/lib/implementation/read-rc-file.ts
2496
+ import { join as join6 } from "node:path";
1818
2497
  var ConfigPathError = class extends Error {
1819
2498
  constructor(configPath) {
1820
2499
  super(`Provided path '${configPath}' is not valid.`);
@@ -1848,7 +2527,7 @@ async function autoloadRc(tsconfig) {
1848
2527
  );
1849
2528
  }
1850
2529
  return readRcByPath(
1851
- join5(process.cwd(), `${CONFIG_FILE_NAME}.${ext}`),
2530
+ join6(process.cwd(), `${CONFIG_FILE_NAME}.${ext}`),
1852
2531
  tsconfig
1853
2532
  );
1854
2533
  }
@@ -1986,15 +2665,7 @@ var CLI_NAME = "Code PushUp CLI";
1986
2665
  var CLI_SCRIPT_NAME = "code-pushup";
1987
2666
 
1988
2667
  // packages/cli/src/lib/implementation/logging.ts
1989
- import { cliui as cliui2 } from "@poppinss/cliui";
1990
2668
  import chalk6 from "chalk";
1991
- var singletonUiInstance;
1992
- function ui() {
1993
- if (singletonUiInstance === void 0) {
1994
- singletonUiInstance = cliui2();
1995
- }
1996
- return singletonUiInstance;
1997
- }
1998
2669
  function renderConfigureCategoriesHint() {
1999
2670
  ui().logger.info(
2000
2671
  chalk6.gray(
@@ -2031,51 +2702,12 @@ function renderIntegratePortalHint() {
2031
2702
  ).render();
2032
2703
  }
2033
2704
 
2034
- // packages/cli/src/lib/implementation/global.utils.ts
2035
- function filterKebabCaseKeys(obj) {
2036
- return Object.entries(obj).filter(([key]) => !key.includes("-")).reduce(
2037
- (acc, [key, value]) => typeof value === "string" || typeof value === "object" && Array.isArray(obj[key]) ? { ...acc, [key]: value } : typeof value === "object" && !Array.isArray(value) && value != null ? {
2038
- ...acc,
2039
- [key]: filterKebabCaseKeys(value)
2040
- } : { ...acc, [key]: value },
2041
- {}
2042
- );
2043
- }
2044
- function logErrorBeforeThrow(fn) {
2045
- return async (...args) => {
2046
- try {
2047
- return await fn(...args);
2048
- } catch (error) {
2049
- console.error(error);
2050
- await new Promise((resolve) => process.stdout.write("", resolve));
2051
- throw error;
2052
- }
2053
- };
2054
- }
2055
- function coerceArray(param) {
2056
- return [...new Set(toArray(param).flatMap((f) => f.split(",")))];
2057
- }
2058
-
2059
- // packages/cli/src/lib/implementation/only-plugins.options.ts
2060
- var onlyPluginsOption = {
2061
- describe: "List of plugins to run. If not set all plugins are run.",
2062
- type: "array",
2063
- default: [],
2064
- coerce: coerceArray
2065
- };
2066
- function yargsOnlyPluginsOptionsDefinition() {
2067
- return {
2068
- onlyPlugins: onlyPluginsOption
2069
- };
2070
- }
2071
-
2072
2705
  // packages/cli/src/lib/autorun/autorun-command.ts
2073
2706
  function yargsAutorunCommandObject() {
2074
2707
  const command = "autorun";
2075
2708
  return {
2076
2709
  command,
2077
2710
  describe: "Shortcut for running collect followed by upload",
2078
- builder: yargsOnlyPluginsOptionsDefinition(),
2079
2711
  handler: async (args) => {
2080
2712
  ui().logger.log(chalk7.bold(CLI_NAME));
2081
2713
  ui().logger.info(chalk7.gray(`Run ${command}...`));
@@ -2112,7 +2744,6 @@ function yargsCollectCommandObject() {
2112
2744
  return {
2113
2745
  command,
2114
2746
  describe: "Run Plugins and collect results",
2115
- builder: yargsOnlyPluginsOptionsDefinition(),
2116
2747
  handler: async (args) => {
2117
2748
  const options2 = args;
2118
2749
  ui().logger.log(chalk8.bold(CLI_NAME));
@@ -2149,15 +2780,76 @@ function renderUploadAutorunHint() {
2149
2780
  ).render();
2150
2781
  }
2151
2782
 
2783
+ // packages/cli/src/lib/compare/compare-command.ts
2784
+ import chalk9 from "chalk";
2785
+
2786
+ // packages/cli/src/lib/implementation/compare.options.ts
2787
+ function yargsCompareOptionsDefinition() {
2788
+ return {
2789
+ before: {
2790
+ describe: "Path to source report.json",
2791
+ type: "string",
2792
+ demandOption: true
2793
+ },
2794
+ after: {
2795
+ describe: "Path to target report.json",
2796
+ type: "string",
2797
+ demandOption: true
2798
+ }
2799
+ };
2800
+ }
2801
+
2802
+ // packages/cli/src/lib/compare/compare-command.ts
2803
+ function yargsCompareCommandObject() {
2804
+ const command = "compare";
2805
+ return {
2806
+ command,
2807
+ describe: "Compare 2 report files and create a diff file",
2808
+ builder: yargsCompareOptionsDefinition(),
2809
+ handler: async (args) => {
2810
+ ui().logger.log(chalk9.bold(CLI_NAME));
2811
+ ui().logger.info(chalk9.gray(`Run ${command}...`));
2812
+ const options2 = args;
2813
+ const { before, after, persist } = options2;
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 ")}`
2817
+ );
2818
+ }
2819
+ };
2820
+ }
2821
+
2822
+ // packages/cli/src/lib/implementation/global.utils.ts
2823
+ function filterKebabCaseKeys(obj) {
2824
+ return Object.entries(obj).filter(([key]) => !key.includes("-")).reduce(
2825
+ (acc, [key, value]) => typeof value === "string" || typeof value === "object" && Array.isArray(obj[key]) ? { ...acc, [key]: value } : typeof value === "object" && !Array.isArray(value) && value != null ? {
2826
+ ...acc,
2827
+ [key]: filterKebabCaseKeys(value)
2828
+ } : { ...acc, [key]: value },
2829
+ {}
2830
+ );
2831
+ }
2832
+ function logErrorBeforeThrow(fn) {
2833
+ return async (...args) => {
2834
+ try {
2835
+ return await fn(...args);
2836
+ } catch (error) {
2837
+ console.error(error);
2838
+ await new Promise((resolve) => process.stdout.write("", resolve));
2839
+ throw error;
2840
+ }
2841
+ };
2842
+ }
2843
+ function coerceArray(param) {
2844
+ return [...new Set(toArray(param).flatMap((f) => f.split(",")))];
2845
+ }
2846
+
2152
2847
  // packages/cli/src/lib/print-config/print-config-command.ts
2153
2848
  function yargsConfigCommandObject() {
2154
2849
  const command = "print-config";
2155
2850
  return {
2156
2851
  command,
2157
2852
  describe: "Print config",
2158
- builder: {
2159
- onlyPlugins: onlyPluginsOption
2160
- },
2161
2853
  handler: (yargsArgs) => {
2162
2854
  const { _, $0, ...args } = yargsArgs;
2163
2855
  const cleanArgs = filterKebabCaseKeys(args);
@@ -2167,15 +2859,15 @@ function yargsConfigCommandObject() {
2167
2859
  }
2168
2860
 
2169
2861
  // packages/cli/src/lib/upload/upload-command.ts
2170
- import chalk9 from "chalk";
2862
+ import chalk10 from "chalk";
2171
2863
  function yargsUploadCommandObject() {
2172
2864
  const command = "upload";
2173
2865
  return {
2174
2866
  command,
2175
2867
  describe: "Upload report results to the portal",
2176
2868
  handler: async (args) => {
2177
- ui().logger.log(chalk9.bold(CLI_NAME));
2178
- ui().logger.info(chalk9.gray(`Run ${command}...`));
2869
+ ui().logger.log(chalk10.bold(CLI_NAME));
2870
+ ui().logger.info(chalk10.gray(`Run ${command}...`));
2179
2871
  const options2 = args;
2180
2872
  if (options2.upload == null) {
2181
2873
  renderIntegratePortalHint();
@@ -2196,6 +2888,7 @@ var commands = [
2196
2888
  yargsAutorunCommandObject(),
2197
2889
  yargsCollectCommandObject(),
2198
2890
  yargsUploadCommandObject(),
2891
+ yargsCompareCommandObject(),
2199
2892
  yargsConfigCommandObject()
2200
2893
  ];
2201
2894
 
@@ -2222,9 +2915,9 @@ async function coreConfigMiddleware(processArgs) {
2222
2915
  return {
2223
2916
  ...config != null && { config },
2224
2917
  persist: {
2225
- outputDir: cliPersist?.outputDir ?? rcPersist?.outputDir ?? PERSIST_OUTPUT_DIR,
2226
- format: cliPersist?.format ?? rcPersist?.format ?? PERSIST_FORMAT,
2227
- 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
2228
2921
  },
2229
2922
  ...upload2 != null && { upload: upload2 },
2230
2923
  categories: rcCategories ?? [],
@@ -2234,58 +2927,64 @@ async function coreConfigMiddleware(processArgs) {
2234
2927
  }
2235
2928
 
2236
2929
  // packages/cli/src/lib/implementation/only-plugins.utils.ts
2237
- import chalk10 from "chalk";
2238
- function filterPluginsBySlug(plugins, { onlyPlugins }) {
2239
- if (!onlyPlugins?.length) {
2240
- return plugins;
2241
- }
2242
- return plugins.filter((plugin) => onlyPlugins.includes(plugin.slug));
2243
- }
2244
- function filterCategoryByPluginSlug(categories, {
2245
- onlyPlugins,
2930
+ import chalk11 from "chalk";
2931
+ function validateOnlyPluginsOption({
2932
+ plugins,
2933
+ categories
2934
+ }, {
2935
+ onlyPlugins = [],
2246
2936
  verbose = false
2247
- }) {
2248
- if (categories.length === 0) {
2249
- return categories;
2250
- }
2251
- if (!onlyPlugins?.length) {
2252
- return categories;
2253
- }
2254
- return categories.filter(
2255
- (category) => category.refs.every((ref) => {
2256
- const isNotSkipped = onlyPlugins.includes(ref.plugin);
2257
- if (!isNotSkipped && verbose) {
2258
- console.info(
2259
- `${chalk10.yellow("\u26A0")} Category "${category.title}" is ignored because it references audits from skipped plugin "${ref.plugin}"`
2260
- );
2261
- }
2262
- return isNotSkipped;
2263
- })
2937
+ } = {}) {
2938
+ const onlyPluginsSet = new Set(onlyPlugins);
2939
+ const missingPlugins = onlyPlugins.filter(
2940
+ (plugin) => !plugins.some(({ slug }) => slug === plugin)
2264
2941
  );
2265
- }
2266
- function validateOnlyPluginsOption(plugins, {
2267
- onlyPlugins,
2268
- verbose = false
2269
- }) {
2270
- const missingPlugins = onlyPlugins?.length ? onlyPlugins.filter((plugin) => !plugins.some(({ slug }) => slug === plugin)) : [];
2271
2942
  if (missingPlugins.length > 0 && verbose) {
2272
- console.warn(
2273
- `${chalk10.yellow(
2943
+ ui().logger.info(
2944
+ `${chalk11.yellow(
2274
2945
  "\u26A0"
2275
2946
  )} The --onlyPlugin argument references plugins with "${missingPlugins.join(
2276
2947
  '", "'
2277
2948
  )}" slugs, but no such plugins are present in the configuration. Expected one of the following plugin slugs: "${plugins.map(({ slug }) => slug).join('", "')}".`
2278
2949
  );
2279
2950
  }
2951
+ if (categories.length > 0) {
2952
+ const removedCategorieSlugs = filterItemRefsBy(
2953
+ categories,
2954
+ ({ plugin }) => !onlyPluginsSet.has(plugin)
2955
+ ).map(({ slug }) => slug);
2956
+ ui().logger.info(
2957
+ `The --onlyPlugin argument removed categories with "${removedCategorieSlugs.join(
2958
+ '", "'
2959
+ )}" slugs.
2960
+ `
2961
+ );
2962
+ }
2280
2963
  }
2281
2964
 
2282
2965
  // packages/cli/src/lib/implementation/only-plugins.middleware.ts
2283
- function onlyPluginsMiddleware(processArgs) {
2284
- validateOnlyPluginsOption(processArgs.plugins, processArgs);
2966
+ function onlyPluginsMiddleware(originalProcessArgs) {
2967
+ const { categories = [], onlyPlugins: originalOnlyPlugins } = originalProcessArgs;
2968
+ if (originalOnlyPlugins && originalOnlyPlugins.length > 0) {
2969
+ const { verbose, plugins, onlyPlugins } = originalProcessArgs;
2970
+ validateOnlyPluginsOption(
2971
+ { plugins, categories },
2972
+ { onlyPlugins, verbose }
2973
+ );
2974
+ const onlyPluginsSet = new Set(onlyPlugins);
2975
+ return {
2976
+ ...originalProcessArgs,
2977
+ plugins: plugins.filter(({ slug }) => onlyPluginsSet.has(slug)),
2978
+ categories: filterItemRefsBy(
2979
+ categories,
2980
+ ({ plugin }) => onlyPluginsSet.has(plugin)
2981
+ )
2982
+ };
2983
+ }
2285
2984
  return {
2286
- ...processArgs,
2287
- plugins: filterPluginsBySlug(processArgs.plugins, processArgs),
2288
- categories: filterCategoryByPluginSlug(processArgs.categories, processArgs)
2985
+ ...originalProcessArgs,
2986
+ // if undefined fill categories with empty array
2987
+ categories
2289
2988
  };
2290
2989
  }
2291
2990
 
@@ -2369,20 +3068,44 @@ function yargsGlobalOptionsDefinition() {
2369
3068
  };
2370
3069
  }
2371
3070
 
3071
+ // packages/cli/src/lib/implementation/only-plugins.options.ts
3072
+ var onlyPluginsOption = {
3073
+ describe: "List of plugins to run. If not set all plugins are run.",
3074
+ type: "array",
3075
+ default: [],
3076
+ coerce: coerceArray
3077
+ };
3078
+ function yargsOnlyPluginsOptionsDefinition() {
3079
+ return {
3080
+ onlyPlugins: onlyPluginsOption
3081
+ };
3082
+ }
3083
+
2372
3084
  // packages/cli/src/lib/options.ts
2373
3085
  var options = {
2374
3086
  ...yargsGlobalOptionsDefinition(),
2375
- ...yargsCoreConfigOptionsDefinition()
3087
+ ...yargsCoreConfigOptionsDefinition(),
3088
+ ...yargsOnlyPluginsOptionsDefinition()
3089
+ };
3090
+ var groups = {
3091
+ "Global Options:": [
3092
+ ...Object.keys(yargsGlobalOptionsDefinition()),
3093
+ ...Object.keys(yargsOnlyPluginsOptionsDefinition())
3094
+ ],
3095
+ "Persist Options:": Object.keys(yargsPersistConfigOptionsDefinition()),
3096
+ "Upload Options:": Object.keys(yargsUploadConfigOptionsDefinition())
2376
3097
  };
2377
3098
 
2378
3099
  // packages/cli/src/lib/yargs-cli.ts
2379
- import chalk11 from "chalk";
3100
+ import chalk12 from "chalk";
2380
3101
  import yargs from "yargs";
2381
3102
  function yargsCli(argv, cfg) {
2382
3103
  const { usageMessage, scriptName, noExitProcess } = cfg;
2383
3104
  const commands2 = cfg.commands ?? [];
2384
3105
  const middlewares2 = cfg.middlewares ?? [];
2385
3106
  const options2 = cfg.options ?? {};
3107
+ const groups2 = cfg.groups ?? {};
3108
+ const examples = cfg.examples ?? [];
2386
3109
  const cli2 = yargs(argv);
2387
3110
  cli2.help().version(false).alias("h", "help").check((args) => {
2388
3111
  const persist = args["persist"];
@@ -2394,11 +3117,17 @@ function yargsCli(argv, cfg) {
2394
3117
  (config) => Array.isArray(config) ? config.at(-1) : config
2395
3118
  ).options(options2).wrap(TERMINAL_WIDTH);
2396
3119
  if (usageMessage) {
2397
- cli2.usage(chalk11.bold(usageMessage));
3120
+ cli2.usage(chalk12.bold(usageMessage));
2398
3121
  }
2399
3122
  if (scriptName) {
2400
3123
  cli2.scriptName(scriptName);
2401
3124
  }
3125
+ examples.forEach(
3126
+ ([exampleName, description]) => cli2.example(exampleName, description)
3127
+ );
3128
+ Object.entries(groups2).forEach(
3129
+ ([groupName, optionNames]) => cli2.group(optionNames, groupName)
3130
+ );
2402
3131
  middlewares2.forEach(({ middlewareFunction, applyBeforeValidation }) => {
2403
3132
  cli2.middleware(
2404
3133
  logErrorBeforeThrow(middlewareFunction),
@@ -2439,6 +3168,29 @@ var cli = (args) => yargsCli(args, {
2439
3168
  usageMessage: CLI_NAME,
2440
3169
  scriptName: CLI_SCRIPT_NAME,
2441
3170
  options,
3171
+ groups,
3172
+ examples: [
3173
+ [
3174
+ "code-pushup",
3175
+ "Run collect followed by upload based on configuration from code-pushup.config.* file."
3176
+ ],
3177
+ [
3178
+ "code-pushup collect --tsconfig=tsconfig.base.json",
3179
+ "Run collect using custom tsconfig to parse code-pushup.config.ts file."
3180
+ ],
3181
+ [
3182
+ "code-pushup collect --onlyPlugins=coverage",
3183
+ "Run collect with only coverage plugin, other plugins from config file will be skipped."
3184
+ ],
3185
+ [
3186
+ "code-pushup upload --persist.outputDir=dist --persist.filename=cp-report --upload.apiKey=$CP_API_KEY",
3187
+ "Upload dist/cp-report.json to portal using API key from environment variable"
3188
+ ],
3189
+ [
3190
+ "code-pushup print-config --config code-pushup.config.test.js",
3191
+ "Print resolved config object parsed from custom config location"
3192
+ ]
3193
+ ],
2442
3194
  middlewares,
2443
3195
  commands
2444
3196
  });