@code-pushup/core 0.35.0 → 0.42.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
@@ -1,6 +1,3 @@
1
- // packages/models/src/lib/audit.ts
2
- import { z as z2 } from "zod";
3
-
4
1
  // packages/models/src/lib/implementation/schemas.ts
5
2
  import { MATERIAL_ICONS } from "vscode-material-icons";
6
3
  import { z } from "zod";
@@ -66,6 +63,7 @@ function missingRefsForCategoriesErrorMsg(categories, plugins) {
66
63
  }
67
64
 
68
65
  // packages/models/src/lib/implementation/schemas.ts
66
+ var primitiveValueSchema = z.union([z.string(), z.number()]);
69
67
  function executionMetaSchema(options = {
70
68
  descriptionDate: "Execution start date and time",
71
69
  descriptionDuration: "Execution duration in ms"
@@ -157,6 +155,7 @@ function hasNonZeroWeightedRef(refs) {
157
155
  }
158
156
 
159
157
  // packages/models/src/lib/audit.ts
158
+ import { z as z2 } from "zod";
160
159
  var auditSchema = z2.object({
161
160
  slug: slugSchema.describe("ID (unique within plugin)")
162
161
  }).merge(
@@ -186,7 +185,7 @@ function getDuplicateSlugsInAudits(audits) {
186
185
  }
187
186
 
188
187
  // packages/models/src/lib/audit-output.ts
189
- import { z as z4 } from "zod";
188
+ import { z as z5 } from "zod";
190
189
 
191
190
  // packages/models/src/lib/issue.ts
192
191
  import { z as z3 } from "zod";
@@ -217,16 +216,61 @@ var issueSchema = z3.object(
217
216
  { description: "Issue information" }
218
217
  );
219
218
 
219
+ // packages/models/src/lib/table.ts
220
+ import { z as z4 } from "zod";
221
+ var tableAlignmentSchema = z4.enum(["left", "center", "right"], {
222
+ description: "Cell alignment"
223
+ });
224
+ var tableColumnObjectSchema = z4.object({
225
+ key: z4.string(),
226
+ label: z4.string().optional(),
227
+ align: tableAlignmentSchema.optional()
228
+ });
229
+ var tableRowObjectSchema = z4.record(primitiveValueSchema, {
230
+ description: "Object row"
231
+ });
232
+ var tableRowPrimitiveSchema = z4.array(primitiveValueSchema, {
233
+ description: "Primitive row"
234
+ });
235
+ var tableSharedSchema = z4.object({
236
+ title: z4.string().optional().describe("Display title for table")
237
+ });
238
+ var tablePrimitiveSchema = tableSharedSchema.merge(
239
+ z4.object(
240
+ {
241
+ columns: z4.array(tableAlignmentSchema).optional(),
242
+ rows: z4.array(tableRowPrimitiveSchema)
243
+ },
244
+ { description: "Table with primitive rows and optional alignment columns" }
245
+ )
246
+ );
247
+ var tableObjectSchema = tableSharedSchema.merge(
248
+ z4.object(
249
+ {
250
+ columns: z4.union([
251
+ z4.array(tableAlignmentSchema),
252
+ z4.array(tableColumnObjectSchema)
253
+ ]).optional(),
254
+ rows: z4.array(tableRowObjectSchema)
255
+ },
256
+ {
257
+ description: "Table with object rows and optional alignment or object columns"
258
+ }
259
+ )
260
+ );
261
+ var tableSchema = (description = "Table information") => z4.union([tablePrimitiveSchema, tableObjectSchema], { description });
262
+
220
263
  // packages/models/src/lib/audit-output.ts
221
264
  var auditValueSchema = nonnegativeIntSchema.describe("Raw numeric value");
222
- var auditDisplayValueSchema = z4.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }).optional();
223
- var auditDetailsSchema = z4.object(
265
+ var auditDisplayValueSchema = z5.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }).optional();
266
+ var auditDetailsSchema = z5.object(
224
267
  {
225
- issues: z4.array(issueSchema, { description: "List of findings" })
268
+ issues: z5.array(issueSchema, { description: "List of findings" }).optional(),
269
+ table: tableSchema("Table of related findings").optional()
226
270
  },
227
271
  { description: "Detailed information" }
228
272
  );
229
- var auditOutputSchema = z4.object(
273
+ var auditOutputSchema = z5.object(
230
274
  {
231
275
  slug: slugSchema.describe("Reference to audit"),
232
276
  displayValue: auditDisplayValueSchema,
@@ -236,7 +280,7 @@ var auditOutputSchema = z4.object(
236
280
  },
237
281
  { description: "Audit information" }
238
282
  );
239
- var auditOutputsSchema = z4.array(auditOutputSchema, {
283
+ var auditOutputsSchema = z5.array(auditOutputSchema, {
240
284
  description: "List of JSON formatted audit output emitted by the runner process of a plugin"
241
285
  }).refine(
242
286
  (audits) => !getDuplicateSlugsInAudits2(audits),
@@ -253,13 +297,13 @@ function getDuplicateSlugsInAudits2(audits) {
253
297
  }
254
298
 
255
299
  // packages/models/src/lib/category-config.ts
256
- import { z as z5 } from "zod";
300
+ import { z as z6 } from "zod";
257
301
  var categoryRefSchema = weightedRefSchema(
258
302
  "Weighted references to audits and/or groups for the category",
259
303
  "Slug of an audit or group (depending on `type`)"
260
304
  ).merge(
261
- z5.object({
262
- type: z5.enum(["audit", "group"], {
305
+ z6.object({
306
+ type: z6.enum(["audit", "group"], {
263
307
  description: "Discriminant for reference kind, affects where `slug` is looked up"
264
308
  }),
265
309
  plugin: slugSchema.describe(
@@ -280,8 +324,8 @@ var categoryConfigSchema = scorableSchema(
280
324
  description: "Meta info for category"
281
325
  })
282
326
  ).merge(
283
- z5.object({
284
- isBinary: z5.boolean({
327
+ z6.object({
328
+ isBinary: z6.boolean({
285
329
  description: 'Is this a binary category (i.e. only a perfect score considered a "pass")?'
286
330
  }).optional()
287
331
  })
@@ -297,7 +341,7 @@ function getDuplicateRefsInCategoryMetrics(metrics) {
297
341
  metrics.map(({ slug, type, plugin }) => `${type} :: ${plugin} / ${slug}`)
298
342
  );
299
343
  }
300
- var categoriesSchema = z5.array(categoryConfigSchema, {
344
+ var categoriesSchema = z6.array(categoryConfigSchema, {
301
345
  description: "Categorization of individual audits"
302
346
  }).refine(
303
347
  (categoryCfg) => !getDuplicateSlugCategories(categoryCfg),
@@ -316,18 +360,18 @@ function getDuplicateSlugCategories(categories) {
316
360
  }
317
361
 
318
362
  // packages/models/src/lib/commit.ts
319
- import { z as z6 } from "zod";
320
- var commitSchema = z6.object(
363
+ import { z as z7 } from "zod";
364
+ var commitSchema = z7.object(
321
365
  {
322
- hash: z6.string({ description: "Commit SHA (full)" }).regex(
366
+ hash: z7.string({ description: "Commit SHA (full)" }).regex(
323
367
  /^[\da-f]{40}$/,
324
368
  "Commit SHA should be a 40-character hexadecimal string"
325
369
  ),
326
- message: z6.string({ description: "Commit message" }),
327
- date: z6.coerce.date({
370
+ message: z7.string({ description: "Commit message" }),
371
+ date: z7.coerce.date({
328
372
  description: "Date and time when commit was authored"
329
373
  }),
330
- author: z6.string({
374
+ author: z7.string({
331
375
  description: "Commit author name"
332
376
  }).trim()
333
377
  },
@@ -335,22 +379,22 @@ var commitSchema = z6.object(
335
379
  );
336
380
 
337
381
  // packages/models/src/lib/core-config.ts
338
- import { z as z12 } from "zod";
382
+ import { z as z13 } from "zod";
339
383
 
340
384
  // packages/models/src/lib/persist-config.ts
341
- import { z as z7 } from "zod";
342
- var formatSchema = z7.enum(["json", "md"]);
343
- var persistConfigSchema = z7.object({
385
+ import { z as z8 } from "zod";
386
+ var formatSchema = z8.enum(["json", "md"]);
387
+ var persistConfigSchema = z8.object({
344
388
  outputDir: filePathSchema.describe("Artifacts folder").optional(),
345
389
  filename: fileNameSchema.describe("Artifacts file name (without extension)").optional(),
346
- format: z7.array(formatSchema).optional()
390
+ format: z8.array(formatSchema).optional()
347
391
  });
348
392
 
349
393
  // packages/models/src/lib/plugin-config.ts
350
- import { z as z10 } from "zod";
394
+ import { z as z11 } from "zod";
351
395
 
352
396
  // packages/models/src/lib/group.ts
353
- import { z as z8 } from "zod";
397
+ import { z as z9 } from "zod";
354
398
  var groupRefSchema = weightedRefSchema(
355
399
  "Weighted reference to a group",
356
400
  "Reference slug to a group within this plugin (e.g. 'max-lines')"
@@ -367,7 +411,7 @@ var groupSchema = scorableSchema(
367
411
  getDuplicateRefsInGroups,
368
412
  duplicateRefsInGroupsErrorMsg
369
413
  ).merge(groupMetaSchema);
370
- var groupsSchema = z8.array(groupSchema, {
414
+ var groupsSchema = z9.array(groupSchema, {
371
415
  description: "List of groups"
372
416
  }).optional().refine(
373
417
  (groups) => !getDuplicateSlugsInGroups(groups),
@@ -395,14 +439,14 @@ function getDuplicateSlugsInGroups(groups) {
395
439
  }
396
440
 
397
441
  // packages/models/src/lib/runner-config.ts
398
- import { z as z9 } from "zod";
399
- var outputTransformSchema = z9.function().args(z9.unknown()).returns(z9.union([auditOutputsSchema, z9.promise(auditOutputsSchema)]));
400
- var runnerConfigSchema = z9.object(
442
+ import { z as z10 } from "zod";
443
+ var outputTransformSchema = z10.function().args(z10.unknown()).returns(z10.union([auditOutputsSchema, z10.promise(auditOutputsSchema)]));
444
+ var runnerConfigSchema = z10.object(
401
445
  {
402
- command: z9.string({
446
+ command: z10.string({
403
447
  description: "Shell command to execute"
404
448
  }),
405
- args: z9.array(z9.string({ description: "Command arguments" })).optional(),
449
+ args: z10.array(z10.string({ description: "Command arguments" })).optional(),
406
450
  outputFile: filePathSchema.describe("Output path"),
407
451
  outputTransform: outputTransformSchema.optional()
408
452
  },
@@ -410,8 +454,8 @@ var runnerConfigSchema = z9.object(
410
454
  description: "How to execute runner"
411
455
  }
412
456
  );
413
- var onProgressSchema = z9.function().args(z9.unknown()).returns(z9.void());
414
- var runnerFunctionSchema = z9.function().args(onProgressSchema.optional()).returns(z9.union([auditOutputsSchema, z9.promise(auditOutputsSchema)]));
457
+ var onProgressSchema = z10.function().args(z10.unknown()).returns(z10.void());
458
+ var runnerFunctionSchema = z10.function().args(onProgressSchema.optional()).returns(z10.union([auditOutputsSchema, z10.promise(auditOutputsSchema)]));
415
459
 
416
460
  // packages/models/src/lib/plugin-config.ts
417
461
  var pluginMetaSchema = packageVersionSchema().merge(
@@ -422,13 +466,13 @@ var pluginMetaSchema = packageVersionSchema().merge(
422
466
  description: "Plugin metadata"
423
467
  })
424
468
  ).merge(
425
- z10.object({
469
+ z11.object({
426
470
  slug: slugSchema.describe("Unique plugin slug within core config"),
427
471
  icon: materialIconSchema
428
472
  })
429
473
  );
430
- var pluginDataSchema = z10.object({
431
- runner: z10.union([runnerConfigSchema, runnerFunctionSchema]),
474
+ var pluginDataSchema = z11.object({
475
+ runner: z11.union([runnerConfigSchema, runnerFunctionSchema]),
432
476
  audits: pluginAuditsSchema,
433
477
  groups: groupsSchema
434
478
  });
@@ -454,22 +498,22 @@ function getMissingRefsFromGroups(pluginCfg) {
454
498
  }
455
499
 
456
500
  // packages/models/src/lib/upload-config.ts
457
- import { z as z11 } from "zod";
458
- var uploadConfigSchema = z11.object({
501
+ import { z as z12 } from "zod";
502
+ var uploadConfigSchema = z12.object({
459
503
  server: urlSchema.describe("URL of deployed portal API"),
460
- apiKey: z11.string({
504
+ apiKey: z12.string({
461
505
  description: "API key with write access to portal (use `process.env` for security)"
462
506
  }),
463
507
  organization: slugSchema.describe(
464
508
  "Organization slug from Code PushUp portal"
465
509
  ),
466
510
  project: slugSchema.describe("Project slug from Code PushUp portal"),
467
- timeout: z11.number({ description: "Request timeout in minutes (default is 5)" }).positive().int().optional()
511
+ timeout: z12.number({ description: "Request timeout in minutes (default is 5)" }).positive().int().optional()
468
512
  });
469
513
 
470
514
  // packages/models/src/lib/core-config.ts
471
- var unrefinedCoreConfigSchema = z12.object({
472
- plugins: z12.array(pluginConfigSchema, {
515
+ var unrefinedCoreConfigSchema = z13.object({
516
+ plugins: z13.array(pluginConfigSchema, {
473
517
  description: "List of plugins to be used (official, community-provided, or custom)"
474
518
  }).min(1),
475
519
  /** portal configuration for persisting results */
@@ -496,7 +540,7 @@ var CONFIG_FILE_NAME = "code-pushup.config";
496
540
  var SUPPORTED_CONFIG_FILE_FORMATS = ["ts", "mjs", "js"];
497
541
 
498
542
  // packages/models/src/lib/report.ts
499
- import { z as z13 } from "zod";
543
+ import { z as z14 } from "zod";
500
544
  var auditReportSchema = auditSchema.merge(auditOutputSchema);
501
545
  var pluginReportSchema = pluginMetaSchema.merge(
502
546
  executionMetaSchema({
@@ -504,9 +548,9 @@ var pluginReportSchema = pluginMetaSchema.merge(
504
548
  descriptionDuration: "Duration of the plugin run in ms"
505
549
  })
506
550
  ).merge(
507
- z13.object({
508
- audits: z13.array(auditReportSchema).min(1),
509
- groups: z13.array(groupSchema).optional()
551
+ z14.object({
552
+ audits: z14.array(auditReportSchema).min(1),
553
+ groups: z14.array(groupSchema).optional()
510
554
  })
511
555
  ).refine(
512
556
  (pluginReport) => !getMissingRefsFromGroups2(pluginReport.audits, pluginReport.groups ?? []),
@@ -540,10 +584,10 @@ var reportSchema = packageVersionSchema({
540
584
  descriptionDuration: "Duration of the collect run in ms"
541
585
  })
542
586
  ).merge(
543
- z13.object(
587
+ z14.object(
544
588
  {
545
- categories: z13.array(categoryConfigSchema),
546
- plugins: z13.array(pluginReportSchema).min(1),
589
+ categories: z14.array(categoryConfigSchema),
590
+ plugins: z14.array(pluginReportSchema).min(1),
547
591
  commit: commitSchema.describe("Git commit for which report was collected").nullable()
548
592
  },
549
593
  { description: "Collect output data" }
@@ -559,36 +603,40 @@ var reportSchema = packageVersionSchema({
559
603
  );
560
604
 
561
605
  // packages/models/src/lib/reports-diff.ts
562
- import { z as z14 } from "zod";
606
+ import { z as z15 } from "zod";
563
607
  function makeComparisonSchema(schema) {
564
608
  const sharedDescription = schema.description || "Result";
565
- return z14.object({
609
+ return z15.object({
566
610
  before: schema.describe(`${sharedDescription} (source commit)`),
567
611
  after: schema.describe(`${sharedDescription} (target commit)`)
568
612
  });
569
613
  }
570
614
  function makeArraysComparisonSchema(diffSchema, resultSchema, description) {
571
- return z14.object(
615
+ return z15.object(
572
616
  {
573
- changed: z14.array(diffSchema),
574
- unchanged: z14.array(resultSchema),
575
- added: z14.array(resultSchema),
576
- removed: z14.array(resultSchema)
617
+ changed: z15.array(diffSchema),
618
+ unchanged: z15.array(resultSchema),
619
+ added: z15.array(resultSchema),
620
+ removed: z15.array(resultSchema)
577
621
  },
578
622
  { description }
579
623
  );
580
624
  }
581
- var scorableMetaSchema = z14.object({ slug: slugSchema, title: titleSchema });
625
+ var scorableMetaSchema = z15.object({
626
+ slug: slugSchema,
627
+ title: titleSchema,
628
+ docsUrl: docsUrlSchema
629
+ });
582
630
  var scorableWithPluginMetaSchema = scorableMetaSchema.merge(
583
- z14.object({
584
- plugin: pluginMetaSchema.pick({ slug: true, title: true }).describe("Plugin which defines it")
631
+ z15.object({
632
+ plugin: pluginMetaSchema.pick({ slug: true, title: true, docsUrl: true }).describe("Plugin which defines it")
585
633
  })
586
634
  );
587
635
  var scorableDiffSchema = scorableMetaSchema.merge(
588
- z14.object({
636
+ z15.object({
589
637
  scores: makeComparisonSchema(scoreSchema).merge(
590
- z14.object({
591
- diff: z14.number().min(-1).max(1).describe("Score change (`scores.after - scores.before`)")
638
+ z15.object({
639
+ diff: z15.number().min(-1).max(1).describe("Score change (`scores.after - scores.before`)")
592
640
  })
593
641
  ).describe("Score comparison")
594
642
  })
@@ -599,10 +647,10 @@ var scorableWithPluginDiffSchema = scorableDiffSchema.merge(
599
647
  var categoryDiffSchema = scorableDiffSchema;
600
648
  var groupDiffSchema = scorableWithPluginDiffSchema;
601
649
  var auditDiffSchema = scorableWithPluginDiffSchema.merge(
602
- z14.object({
650
+ z15.object({
603
651
  values: makeComparisonSchema(auditValueSchema).merge(
604
- z14.object({
605
- diff: z14.number().int().describe("Value change (`values.after - values.before`)")
652
+ z15.object({
653
+ diff: z15.number().int().describe("Value change (`values.after - values.before`)")
606
654
  })
607
655
  ).describe("Audit `value` comparison"),
608
656
  displayValues: makeComparisonSchema(auditDisplayValueSchema).describe(
@@ -611,15 +659,15 @@ var auditDiffSchema = scorableWithPluginDiffSchema.merge(
611
659
  })
612
660
  );
613
661
  var categoryResultSchema = scorableMetaSchema.merge(
614
- z14.object({ score: scoreSchema })
662
+ z15.object({ score: scoreSchema })
615
663
  );
616
664
  var groupResultSchema = scorableWithPluginMetaSchema.merge(
617
- z14.object({ score: scoreSchema })
665
+ z15.object({ score: scoreSchema })
618
666
  );
619
667
  var auditResultSchema = scorableWithPluginMetaSchema.merge(
620
668
  auditOutputSchema.pick({ score: true, value: true, displayValue: true })
621
669
  );
622
- var reportsDiffSchema = z14.object({
670
+ var reportsDiffSchema = z15.object({
623
671
  commits: makeComparisonSchema(commitSchema).nullable().describe("Commits identifying compared reports"),
624
672
  categories: makeArraysComparisonSchema(
625
673
  categoryDiffSchema,
@@ -648,6 +696,288 @@ var reportsDiffSchema = z14.object({
648
696
  })
649
697
  );
650
698
 
699
+ // packages/utils/src/lib/text-formats/constants.ts
700
+ var NEW_LINE = "\n";
701
+ var TAB = " ";
702
+ var SPACE = " ";
703
+
704
+ // packages/utils/src/lib/text-formats/html/details.ts
705
+ function details(title, content, cfg = { open: false }) {
706
+ return `<details${cfg.open ? " open" : ""}>${NEW_LINE}<summary>${title}</summary>${NEW_LINE}${// ⚠️ The blank line is needed to ensure Markdown in content is rendered correctly.
707
+ NEW_LINE}${content}${NEW_LINE}${// @TODO in the future we could consider adding it only if the content ends with a code block
708
+ // ⚠️ The blank line ensure Markdown in content is rendered correctly.
709
+ NEW_LINE}</details>${// ⚠️ The blank line is needed to ensure Markdown after details is rendered correctly.
710
+ NEW_LINE}`;
711
+ }
712
+
713
+ // packages/utils/src/lib/text-formats/html/font-style.ts
714
+ var boldElement = "b";
715
+ function bold(text) {
716
+ return `<${boldElement}>${text}</${boldElement}>`;
717
+ }
718
+ var italicElement = "i";
719
+ function italic(text) {
720
+ return `<${italicElement}>${text}</${italicElement}>`;
721
+ }
722
+ var codeElement = "code";
723
+ function code(text) {
724
+ return `<${codeElement}>${text}</${codeElement}>`;
725
+ }
726
+
727
+ // packages/utils/src/lib/text-formats/html/link.ts
728
+ function link(href, text) {
729
+ return `<a href="${href}">${text || href}"</a>`;
730
+ }
731
+
732
+ // packages/utils/src/lib/transform.ts
733
+ function objectToEntries(obj) {
734
+ return Object.entries(obj);
735
+ }
736
+ function deepClone(obj) {
737
+ return obj == null || typeof obj !== "object" ? obj : structuredClone(obj);
738
+ }
739
+ function toUnixPath(path) {
740
+ return path.replace(/\\/g, "/");
741
+ }
742
+ function capitalize(text) {
743
+ return `${text.charAt(0).toLocaleUpperCase()}${text.slice(
744
+ 1
745
+ )}`;
746
+ }
747
+
748
+ // packages/utils/src/lib/table.ts
749
+ function rowToStringArray({ rows, columns = [] }) {
750
+ if (Array.isArray(rows.at(0)) && typeof columns.at(0) === "object") {
751
+ throw new TypeError(
752
+ "Column can`t be object when rows are primitive values"
753
+ );
754
+ }
755
+ return rows.map((row) => {
756
+ if (Array.isArray(row)) {
757
+ return row.map(String);
758
+ }
759
+ const objectRow = row;
760
+ if (columns.length === 0 || typeof columns.at(0) === "string") {
761
+ return Object.values(objectRow).map(String);
762
+ }
763
+ return columns.map(
764
+ ({ key }) => String(objectRow[key])
765
+ );
766
+ });
767
+ }
768
+ function columnsToStringArray({ rows, columns = [] }) {
769
+ const firstRow = rows.at(0);
770
+ const primitiveRows = Array.isArray(firstRow);
771
+ if (typeof columns.at(0) === "string" && !primitiveRows) {
772
+ throw new Error("invalid union type. Caught by model parsing.");
773
+ }
774
+ if (columns.length === 0) {
775
+ if (Array.isArray(firstRow)) {
776
+ return firstRow.map((_, idx) => String(idx));
777
+ }
778
+ return Object.keys(firstRow);
779
+ }
780
+ if (typeof columns.at(0) === "string") {
781
+ return columns.map(String);
782
+ }
783
+ const cols = columns;
784
+ return cols.map(({ label, key }) => label ?? capitalize(key));
785
+ }
786
+ function getColumnAlignmentForKeyAndIndex(targetKey, targetIdx, columns = []) {
787
+ const column = columns.at(targetIdx) ?? columns.find((col) => col.key === targetKey);
788
+ if (typeof column === "string") {
789
+ return column;
790
+ } else if (typeof column === "object") {
791
+ return column.align ?? "center";
792
+ } else {
793
+ return "center";
794
+ }
795
+ }
796
+ function getColumnAlignmentForIndex(targetIdx, columns = []) {
797
+ const column = columns.at(targetIdx);
798
+ if (column == null) {
799
+ return "center";
800
+ } else if (typeof column === "string") {
801
+ return column;
802
+ } else if (typeof column === "object") {
803
+ return column.align ?? "center";
804
+ } else {
805
+ return "center";
806
+ }
807
+ }
808
+ function getColumnAlignments({
809
+ rows,
810
+ columns = []
811
+ }) {
812
+ if (rows.at(0) == null) {
813
+ throw new Error("first row can`t be undefined.");
814
+ }
815
+ if (Array.isArray(rows.at(0))) {
816
+ const firstPrimitiveRow = rows.at(0);
817
+ return Array.from({ length: firstPrimitiveRow.length }).map(
818
+ (_, idx) => getColumnAlignmentForIndex(idx, columns)
819
+ );
820
+ }
821
+ const firstObject = rows.at(0);
822
+ return Object.keys(firstObject).map(
823
+ (key, idx) => getColumnAlignmentForKeyAndIndex(key, idx, columns)
824
+ );
825
+ }
826
+
827
+ // packages/utils/src/lib/text-formats/html/table.ts
828
+ function wrap(elem, content) {
829
+ return `<${elem}>${content}</${elem}>${NEW_LINE}`;
830
+ }
831
+ function wrapRow(content) {
832
+ const elem = "tr";
833
+ return `<${elem}>${NEW_LINE}${content}</${elem}>${NEW_LINE}`;
834
+ }
835
+ function table(tableData) {
836
+ if (tableData.rows.length === 0) {
837
+ throw new Error("Data can't be empty");
838
+ }
839
+ const tableHeaderCols = columnsToStringArray(tableData).map((s) => wrap("th", s)).join("");
840
+ const tableHeaderRow = wrapRow(tableHeaderCols);
841
+ const tableBody = rowToStringArray(tableData).map((arr) => {
842
+ const columns = arr.map((s) => wrap("td", s)).join("");
843
+ return wrapRow(columns);
844
+ }).join("");
845
+ return wrap("table", `${NEW_LINE}${tableHeaderRow}${tableBody}`);
846
+ }
847
+
848
+ // packages/utils/src/lib/text-formats/md/font-style.ts
849
+ var boldWrap = "**";
850
+ function bold2(text) {
851
+ return `${boldWrap}${text}${boldWrap}`;
852
+ }
853
+ var italicWrap = "_";
854
+ function italic2(text) {
855
+ return `${italicWrap}${text}${italicWrap}`;
856
+ }
857
+ var strikeThroughWrap = "~";
858
+ function strikeThrough(text) {
859
+ return `${strikeThroughWrap}${text}${strikeThroughWrap}`;
860
+ }
861
+ var codeWrap = "`";
862
+ function code2(text) {
863
+ return `${codeWrap}${text}${codeWrap}`;
864
+ }
865
+
866
+ // packages/utils/src/lib/text-formats/md/headline.ts
867
+ function headline(text, hierarchy = 1) {
868
+ return `${"#".repeat(hierarchy)} ${text}${NEW_LINE}`;
869
+ }
870
+ function h(text, hierarchy = 1) {
871
+ return headline(text, hierarchy);
872
+ }
873
+ function h1(text) {
874
+ return headline(text, 1);
875
+ }
876
+ function h2(text) {
877
+ return headline(text, 2);
878
+ }
879
+ function h3(text) {
880
+ return headline(text, 3);
881
+ }
882
+ function h4(text) {
883
+ return headline(text, 4);
884
+ }
885
+ function h5(text) {
886
+ return headline(text, 5);
887
+ }
888
+ function h6(text) {
889
+ return headline(text, 6);
890
+ }
891
+
892
+ // packages/utils/src/lib/text-formats/md/image.ts
893
+ function image(src, alt) {
894
+ return `![${alt}](${src})`;
895
+ }
896
+
897
+ // packages/utils/src/lib/text-formats/md/link.ts
898
+ function link2(href, text) {
899
+ return `[${text || href}](${href})`;
900
+ }
901
+
902
+ // packages/utils/src/lib/text-formats/md/list.ts
903
+ function li(text, order = "unordered") {
904
+ const style = order === "unordered" ? "-" : "- [ ]";
905
+ return `${style} ${text}`;
906
+ }
907
+ function indentation(text, level = 1) {
908
+ return `${TAB.repeat(level)}${text}`;
909
+ }
910
+
911
+ // packages/utils/src/lib/text-formats/md/paragraphs.ts
912
+ function paragraphs(...sections) {
913
+ return sections.filter(Boolean).join(`${NEW_LINE}${NEW_LINE}`);
914
+ }
915
+
916
+ // packages/utils/src/lib/text-formats/md/section.ts
917
+ function section(...contents) {
918
+ return `${lines(...contents)}${NEW_LINE}`;
919
+ }
920
+ function lines(...contents) {
921
+ return `${contents.filter(Boolean).join(NEW_LINE)}`;
922
+ }
923
+
924
+ // packages/utils/src/lib/text-formats/md/table.ts
925
+ var alignString = /* @__PURE__ */ new Map([
926
+ ["left", ":--"],
927
+ ["center", ":--:"],
928
+ ["right", "--:"]
929
+ ]);
930
+ function tableRow(rows) {
931
+ return `|${rows.join("|")}|`;
932
+ }
933
+ function table2(data) {
934
+ if (data.rows.length === 0) {
935
+ throw new Error("Data can't be empty");
936
+ }
937
+ const alignmentRow = getColumnAlignments(data).map(
938
+ (s) => alignString.get(s) ?? String(alignString.get("center"))
939
+ );
940
+ return section(
941
+ `${lines(
942
+ tableRow(columnsToStringArray(data)),
943
+ tableRow(alignmentRow),
944
+ ...rowToStringArray(data).map(tableRow)
945
+ )}`
946
+ );
947
+ }
948
+
949
+ // packages/utils/src/lib/text-formats/index.ts
950
+ var md = {
951
+ bold: bold2,
952
+ italic: italic2,
953
+ strikeThrough,
954
+ code: code2,
955
+ link: link2,
956
+ image,
957
+ headline,
958
+ h,
959
+ h1,
960
+ h2,
961
+ h3,
962
+ h4,
963
+ h5,
964
+ h6,
965
+ indentation,
966
+ lines,
967
+ li,
968
+ section,
969
+ paragraphs,
970
+ table: table2
971
+ };
972
+ var html = {
973
+ bold,
974
+ italic,
975
+ code,
976
+ link,
977
+ details,
978
+ table
979
+ };
980
+
651
981
  // packages/utils/src/lib/diff.ts
652
982
  function matchArrayItemsByKey({
653
983
  before,
@@ -765,40 +1095,48 @@ import chalk from "chalk";
765
1095
 
766
1096
  // packages/utils/src/lib/reports/constants.ts
767
1097
  var TERMINAL_WIDTH = 80;
768
- var NEW_LINE = "\n";
769
1098
  var SCORE_COLOR_RANGE = {
770
1099
  GREEN_MIN: 0.9,
771
1100
  YELLOW_MIN: 0.5
772
1101
  };
1102
+ var CATEGORIES_TITLE = "\u{1F3F7} Categories";
773
1103
  var FOOTER_PREFIX = "Made with \u2764 by";
774
1104
  var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
775
- var README_LINK = "https://github.com/flowup/quality-metrics-cli#readme";
1105
+ var README_LINK = "https://github.com/code-pushup/cli#readme";
776
1106
  var reportHeadlineText = "Code PushUp Report";
777
1107
  var reportOverviewTableHeaders = [
778
- "\u{1F3F7} Category",
779
- "\u2B50 Score",
780
- "\u{1F6E1} Audits"
1108
+ {
1109
+ key: "category",
1110
+ label: "\u{1F3F7} Category",
1111
+ align: "left"
1112
+ },
1113
+ {
1114
+ key: "score",
1115
+ label: "\u2B50 Score"
1116
+ },
1117
+ {
1118
+ key: "audits",
1119
+ label: "\u{1F6E1} Audits"
1120
+ }
781
1121
  ];
782
1122
  var reportRawOverviewTableHeaders = ["Category", "Score", "Audits"];
783
- var reportMetaTableHeaders = [
784
- "Commit",
785
- "Version",
786
- "Duration",
787
- "Plugins",
788
- "Categories",
789
- "Audits"
790
- ];
791
- var pluginMetaTableHeaders = [
792
- "Plugin",
793
- "Audits",
794
- "Version",
795
- "Duration"
796
- ];
797
- var detailsTableHeaders = [
798
- "Severity",
799
- "Message",
800
- "Source file",
801
- "Line(s)"
1123
+ var issuesTableHeadings = [
1124
+ {
1125
+ key: "severity",
1126
+ label: "Severity"
1127
+ },
1128
+ {
1129
+ key: "message",
1130
+ label: "Message"
1131
+ },
1132
+ {
1133
+ key: "file",
1134
+ label: "Source file"
1135
+ },
1136
+ {
1137
+ key: "line",
1138
+ label: "Line(s)"
1139
+ }
802
1140
  ];
803
1141
 
804
1142
  // packages/utils/src/lib/logging.ts
@@ -888,7 +1226,7 @@ async function ensureDirectoryExists(baseDir) {
888
1226
  await mkdir(baseDir, { recursive: true });
889
1227
  return;
890
1228
  } catch (error) {
891
- ui().logger.error(error.message);
1229
+ ui().logger.info(error.message);
892
1230
  if (error.code !== "EEXIST") {
893
1231
  throw error;
894
1232
  }
@@ -924,122 +1262,35 @@ async function importEsmModule(options) {
924
1262
  return mod.default;
925
1263
  }
926
1264
 
927
- // packages/utils/src/lib/reports/md/details.ts
928
- function details(title, content, cfg = { open: false }) {
929
- return `<details${cfg.open ? " open" : ""}>
930
- <summary>${title}</summary>
931
-
932
- ${content}
933
-
934
- </details>
935
- `;
936
- }
937
-
938
- // packages/utils/src/lib/reports/md/font-style.ts
939
- var stylesMap = {
940
- i: "_",
941
- // italic
942
- b: "**",
943
- // bold
944
- s: "~",
945
- // strike through
946
- c: "`"
947
- // code
948
- };
949
- function style(text, styles = ["b"]) {
950
- return styles.reduce((t, s) => `${stylesMap[s]}${t}${stylesMap[s]}`, text);
951
- }
952
-
953
- // packages/utils/src/lib/reports/md/headline.ts
954
- function headline(text, hierarchy = 1) {
955
- return `${"#".repeat(hierarchy)} ${text}`;
956
- }
957
- function h1(text) {
958
- return headline(text, 1);
959
- }
960
- function h2(text) {
961
- return headline(text, 2);
962
- }
963
- function h3(text) {
964
- return headline(text, 3);
965
- }
966
-
967
- // packages/utils/src/lib/reports/md/image.ts
968
- function image(src, alt) {
969
- return `![${alt}](${src})`;
970
- }
971
-
972
- // packages/utils/src/lib/reports/md/link.ts
973
- function link(href, text) {
974
- return `[${text || href}](${href})`;
975
- }
976
-
977
- // packages/utils/src/lib/reports/md/list.ts
978
- function li(text, order = "unordered") {
979
- const style2 = order === "unordered" ? "-" : "- [ ]";
980
- return `${style2} ${text}`;
981
- }
982
-
983
- // packages/utils/src/lib/reports/md/paragraphs.ts
984
- function paragraphs(...sections) {
985
- return sections.filter(Boolean).join("\n\n");
986
- }
987
-
988
- // packages/utils/src/lib/reports/md/table.ts
989
- var alignString = /* @__PURE__ */ new Map([
990
- ["l", ":--"],
991
- ["c", ":--:"],
992
- ["r", "--:"]
993
- ]);
994
- function tableMd(data, align) {
995
- if (data.length === 0) {
996
- throw new Error("Data can't be empty");
997
- }
998
- const alignmentSetting = align ?? data[0]?.map(() => "c");
999
- const tableContent = data.map((arr) => `|${arr.join("|")}|`);
1000
- const alignmentRow = `|${alignmentSetting?.map((s) => alignString.get(s)).join("|")}|`;
1001
- return tableContent[0] + NEW_LINE + alignmentRow + NEW_LINE + tableContent.slice(1).join(NEW_LINE);
1002
- }
1003
- function tableHtml(data) {
1004
- if (data.length === 0) {
1005
- throw new Error("Data can't be empty");
1006
- }
1007
- const tableContent = data.map((arr, index) => {
1008
- if (index === 0) {
1009
- const headerRow = arr.map((s) => `<th>${s}</th>`).join("");
1010
- return `<tr>${headerRow}</tr>`;
1011
- }
1012
- const row = arr.map((s) => `<td>${s}</td>`).join("");
1013
- return `<tr>${row}</tr>`;
1014
- });
1015
- return `<table>${tableContent.join("")}</table>`;
1016
- }
1017
-
1018
1265
  // packages/utils/src/lib/reports/utils.ts
1266
+ var { image: image2, bold: boldMd } = md;
1019
1267
  function formatReportScore(score) {
1020
1268
  return Math.round(score * 100).toString();
1021
1269
  }
1022
1270
  function formatScoreWithColor(score, options) {
1023
- const styledNumber = options?.skipBold ? formatReportScore(score) : style(formatReportScore(score));
1024
- return `${getRoundScoreMarker(score)} ${styledNumber}`;
1025
- }
1026
- function getRoundScoreMarker(score) {
1027
- if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
1028
- return "\u{1F7E2}";
1029
- }
1030
- if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
1031
- return "\u{1F7E1}";
1271
+ const styledNumber = options?.skipBold ? formatReportScore(score) : boldMd(formatReportScore(score));
1272
+ return `${scoreMarker(score)} ${styledNumber}`;
1273
+ }
1274
+ var MARKERS = {
1275
+ circle: {
1276
+ red: "\u{1F534}",
1277
+ yellow: "\u{1F7E1}",
1278
+ green: "\u{1F7E2}"
1279
+ },
1280
+ square: {
1281
+ red: "\u{1F7E5}",
1282
+ yellow: "\u{1F7E8}",
1283
+ green: "\u{1F7E9}"
1032
1284
  }
1033
- return "\u{1F534}";
1034
- }
1035
- function getSquaredScoreMarker(score) {
1285
+ };
1286
+ function scoreMarker(score, markerType = "circle") {
1036
1287
  if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
1037
- return "\u{1F7E9}";
1288
+ return MARKERS[markerType].green;
1038
1289
  }
1039
1290
  if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
1040
- return "\u{1F7E8}";
1291
+ return MARKERS[markerType].yellow;
1041
1292
  }
1042
- return "\u{1F7E5}";
1293
+ return MARKERS[markerType].red;
1043
1294
  }
1044
1295
  function getDiffMarker(diff) {
1045
1296
  if (diff > 0) {
@@ -1055,7 +1306,7 @@ function colorByScoreDiff(text, diff) {
1055
1306
  return shieldsBadge(text, color);
1056
1307
  }
1057
1308
  function shieldsBadge(text, color) {
1058
- return image(
1309
+ return image2(
1059
1310
  `https://img.shields.io/badge/${encodeURIComponent(text)}-${color}`,
1060
1311
  text
1061
1312
  );
@@ -1065,7 +1316,7 @@ function formatDiffNumber(diff) {
1065
1316
  const sign = diff < 0 ? "\u2212" : "+";
1066
1317
  return `${sign}${number}`;
1067
1318
  }
1068
- function getSeverityIcon(severity) {
1319
+ function severityMarker(severity) {
1069
1320
  if (severity === "error") {
1070
1321
  return "\u{1F6A8}";
1071
1322
  }
@@ -1256,13 +1507,13 @@ function executeProcess(cfg) {
1256
1507
  process2.on("error", (err) => {
1257
1508
  stderr += err.toString();
1258
1509
  });
1259
- process2.on("close", (code) => {
1510
+ process2.on("close", (code3) => {
1260
1511
  const timings = { date, duration: calcDuration(start) };
1261
- if (code === 0 || ignoreExitCode) {
1512
+ if (code3 === 0 || ignoreExitCode) {
1262
1513
  onComplete?.();
1263
- resolve({ code, stdout, stderr, ...timings });
1514
+ resolve({ code: code3, stdout, stderr, ...timings });
1264
1515
  } else {
1265
- const errorMsg = new ProcessError({ code, stdout, stderr, ...timings });
1516
+ const errorMsg = new ProcessError({ code: code3, stdout, stderr, ...timings });
1266
1517
  onError?.(errorMsg);
1267
1518
  reject(errorMsg);
1268
1519
  }
@@ -1270,39 +1521,84 @@ function executeProcess(cfg) {
1270
1521
  });
1271
1522
  }
1272
1523
 
1273
- // packages/utils/src/lib/git.ts
1524
+ // packages/utils/src/lib/git/git.ts
1274
1525
  import { isAbsolute, join as join2, relative } from "node:path";
1275
1526
  import { simpleGit } from "simple-git";
1276
-
1277
- // packages/utils/src/lib/transform.ts
1278
- function objectToEntries(obj) {
1279
- return Object.entries(obj);
1527
+ function getGitRoot(git = simpleGit()) {
1528
+ return git.revparse("--show-toplevel");
1280
1529
  }
1281
- function deepClone(obj) {
1282
- return obj == null || typeof obj !== "object" ? obj : structuredClone(obj);
1530
+ function formatGitPath(path, gitRoot) {
1531
+ const absolutePath = isAbsolute(path) ? path : join2(process.cwd(), path);
1532
+ const relativePath = relative(gitRoot, absolutePath);
1533
+ return toUnixPath(relativePath);
1283
1534
  }
1284
- function toUnixPath(path) {
1285
- return path.replace(/\\/g, "/");
1535
+ var GitStatusError = class _GitStatusError extends Error {
1536
+ static ignoredProps = /* @__PURE__ */ new Set(["current", "tracking"]);
1537
+ static getReducedStatus(status) {
1538
+ return Object.fromEntries(
1539
+ Object.entries(status).filter(([key]) => !this.ignoredProps.has(key)).filter(
1540
+ (entry) => {
1541
+ const value = entry[1];
1542
+ if (value == null) {
1543
+ return false;
1544
+ }
1545
+ if (Array.isArray(value) && value.length === 0) {
1546
+ return false;
1547
+ }
1548
+ if (typeof value === "number" && value === 0) {
1549
+ return false;
1550
+ }
1551
+ return !(typeof value === "boolean" && !value);
1552
+ }
1553
+ )
1554
+ );
1555
+ }
1556
+ constructor(status) {
1557
+ super(
1558
+ `Working directory needs to be clean before we you can proceed. Commit your local changes or stash them:
1559
+ ${JSON.stringify(
1560
+ _GitStatusError.getReducedStatus(status),
1561
+ null,
1562
+ 2
1563
+ )}`
1564
+ );
1565
+ }
1566
+ };
1567
+ async function guardAgainstLocalChanges(git = simpleGit()) {
1568
+ const status = await git.status(["-s"]);
1569
+ if (status.files.length > 0) {
1570
+ throw new GitStatusError(status);
1571
+ }
1286
1572
  }
1573
+ async function safeCheckout(branchOrHash, forceCleanStatus = false, git = simpleGit()) {
1574
+ if (forceCleanStatus) {
1575
+ await git.raw(["reset", "--hard"]);
1576
+ await git.clean(["f", "d"]);
1577
+ ui().logger.info(`git status cleaned`);
1578
+ }
1579
+ await guardAgainstLocalChanges(git);
1580
+ await git.checkout(branchOrHash);
1581
+ }
1582
+
1583
+ // packages/utils/src/lib/git/git.commits-and-tags.ts
1584
+ import { simpleGit as simpleGit2 } from "simple-git";
1585
+
1586
+ // packages/utils/src/lib/semver.ts
1587
+ import { rcompare, valid } from "semver";
1287
1588
 
1288
- // packages/utils/src/lib/git.ts
1289
- async function getLatestCommit(git = simpleGit()) {
1589
+ // packages/utils/src/lib/git/git.commits-and-tags.ts
1590
+ async function getLatestCommit(git = simpleGit2()) {
1290
1591
  const log2 = await git.log({
1291
1592
  maxCount: 1,
1593
+ // git log -1 --pretty=format:"%H %s %an %aI" - See: https://git-scm.com/docs/pretty-formats
1292
1594
  format: { hash: "%H", message: "%s", author: "%an", date: "%aI" }
1293
1595
  });
1294
- if (!log2.latest) {
1295
- return null;
1296
- }
1297
1596
  return commitSchema.parse(log2.latest);
1298
1597
  }
1299
- function getGitRoot(git = simpleGit()) {
1300
- return git.revparse("--show-toplevel");
1301
- }
1302
- function formatGitPath(path, gitRoot) {
1303
- const absolutePath = isAbsolute(path) ? path : join2(process.cwd(), path);
1304
- const relativePath = relative(gitRoot, absolutePath);
1305
- return toUnixPath(relativePath);
1598
+ async function getCurrentBranchOrTag(git = simpleGit2()) {
1599
+ return await git.branch().then((r) => r.current) || // If no current branch, try to get the tag
1600
+ // @TODO use simple git
1601
+ await git.raw(["describe", "--tags", "--exact-match"]).then((out) => out.trim());
1306
1602
  }
1307
1603
 
1308
1604
  // packages/utils/src/lib/group-by-status.ts
@@ -1380,44 +1676,65 @@ function listAuditsFromAllPlugins(report) {
1380
1676
  );
1381
1677
  }
1382
1678
 
1383
- // packages/utils/src/lib/reports/generate-md-report.ts
1384
- function generateMdReport(report) {
1385
- const printCategories = report.categories.length > 0;
1386
- return (
1387
- // header section
1388
- // eslint-disable-next-line prefer-template
1389
- reportToHeaderSection() + NEW_LINE + // categories overview section
1390
- (printCategories ? reportToOverviewSection(report) + NEW_LINE + NEW_LINE : "") + // categories section
1391
- (printCategories ? reportToCategoriesSection(report) + NEW_LINE + NEW_LINE : "") + // audits section
1392
- reportToAuditsSection(report) + NEW_LINE + NEW_LINE + // about section
1393
- reportToAboutSection(report) + NEW_LINE + NEW_LINE + // footer section
1394
- `${FOOTER_PREFIX} ${link(README_LINK, "Code PushUp")}`
1679
+ // packages/utils/src/lib/reports/formatting.ts
1680
+ var { headline: headline2, lines: lines2, link: link3, section: section2, table: table3 } = md;
1681
+ function tableSection(tableData, options) {
1682
+ if (tableData.rows.length === 0) {
1683
+ return "";
1684
+ }
1685
+ const { level = 4 } = options ?? {};
1686
+ const render = (h7, l) => l === 0 ? h7 : headline2(h7, l);
1687
+ return lines2(
1688
+ tableData.title && render(tableData.title, level),
1689
+ table3(tableData)
1395
1690
  );
1396
1691
  }
1397
- function reportToHeaderSection() {
1398
- return headline(reportHeadlineText) + NEW_LINE;
1692
+ function metaDescription({
1693
+ docsUrl,
1694
+ description
1695
+ }) {
1696
+ if (docsUrl) {
1697
+ const docsLink = link3(docsUrl, "\u{1F4D6} Docs");
1698
+ if (!description) {
1699
+ return section2(docsLink);
1700
+ }
1701
+ const parsedDescription = description.toString().endsWith("```") ? `${description}${NEW_LINE + NEW_LINE}` : `${description}${SPACE}`;
1702
+ return section2(`${parsedDescription}${docsLink}`);
1703
+ }
1704
+ if (description && description.trim().length > 0) {
1705
+ return section2(description);
1706
+ }
1707
+ return "";
1399
1708
  }
1400
- function reportToOverviewSection(report) {
1709
+
1710
+ // packages/utils/src/lib/reports/generate-md-report-categoy-section.ts
1711
+ var { link: link4, section: section3, h2: h22, lines: lines3, li: li2, bold: boldMd2, h3: h32, indentation: indentation2 } = md;
1712
+ function categoriesOverviewSection(report) {
1401
1713
  const { categories, plugins } = report;
1402
- const tableContent = [
1403
- reportOverviewTableHeaders,
1404
- ...categories.map(({ title, refs, score }) => [
1405
- link(`#${slugify(title)}`, title),
1406
- `${getRoundScoreMarker(score)} ${style(formatReportScore(score))}`,
1407
- countCategoryAudits(refs, plugins).toString()
1408
- ])
1409
- ];
1410
- return tableMd(tableContent, ["l", "c", "c"]);
1714
+ if (categories.length > 0 && plugins.length > 0) {
1715
+ const tableContent = {
1716
+ columns: reportOverviewTableHeaders,
1717
+ rows: categories.map(({ title, refs, score }) => ({
1718
+ // The heading "ID" is inferred from the heading text in Markdown.
1719
+ category: link4(`#${slugify(title)}`, title),
1720
+ score: `${scoreMarker(score)}${SPACE}${boldMd2(
1721
+ formatReportScore(score)
1722
+ )}`,
1723
+ audits: countCategoryAudits(refs, plugins).toString()
1724
+ }))
1725
+ };
1726
+ return tableSection(tableContent);
1727
+ }
1728
+ return "";
1411
1729
  }
1412
- function reportToCategoriesSection(report) {
1730
+ function categoriesDetailsSection(report) {
1413
1731
  const { categories, plugins } = report;
1414
- const categoryDetails = categories.reduce((acc, category) => {
1415
- const categoryTitle = h3(category.title);
1416
- const categoryScore = `${getRoundScoreMarker(
1732
+ const categoryDetails = categories.flatMap((category) => {
1733
+ const categoryTitle = h32(category.title);
1734
+ const categoryScore = `${scoreMarker(
1417
1735
  category.score
1418
- )} Score: ${style(formatReportScore(category.score))}`;
1419
- const categoryDocs = getDocsAndDescription(category);
1420
- const categoryMDItems = category.refs.reduce((refAcc, ref) => {
1736
+ )}${SPACE}Score: ${boldMd2(formatReportScore(category.score))}`;
1737
+ const categoryMDItems = category.refs.map((ref) => {
1421
1738
  if (ref.type === "group") {
1422
1739
  const group = getSortableGroupByRef(ref, plugins);
1423
1740
  const groupAudits = group.refs.map(
@@ -1427,151 +1744,240 @@ function reportToCategoriesSection(report) {
1427
1744
  )
1428
1745
  );
1429
1746
  const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
1430
- const mdGroupItem = groupItemToCategorySection(
1431
- group,
1432
- groupAudits,
1433
- pluginTitle
1434
- );
1435
- return refAcc + mdGroupItem + NEW_LINE;
1747
+ return categoryGroupItem(group, groupAudits, pluginTitle);
1436
1748
  } else {
1437
1749
  const audit = getSortableAuditByRef(ref, plugins);
1438
1750
  const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
1439
- const mdAuditItem = auditItemToCategorySection(audit, pluginTitle);
1440
- return refAcc + mdAuditItem + NEW_LINE;
1751
+ return categoryRef(audit, pluginTitle);
1441
1752
  }
1442
- }, "");
1443
- return acc + NEW_LINE + categoryTitle + NEW_LINE + NEW_LINE + categoryDocs + categoryScore + NEW_LINE + categoryMDItems;
1444
- }, "");
1445
- return h2("\u{1F3F7} Categories") + NEW_LINE + categoryDetails;
1446
- }
1447
- function auditItemToCategorySection(audit, pluginTitle) {
1448
- const auditTitle = link(
1449
- `#${slugify(audit.title)}-${slugify(pluginTitle)}`,
1450
- audit.title
1753
+ });
1754
+ return section3(
1755
+ categoryTitle,
1756
+ metaDescription(category),
1757
+ categoryScore,
1758
+ ...categoryMDItems
1759
+ );
1760
+ });
1761
+ return lines3(h22(CATEGORIES_TITLE), ...categoryDetails);
1762
+ }
1763
+ function categoryRef({ title, score, value, displayValue }, pluginTitle) {
1764
+ const auditTitleAsLink = link4(
1765
+ `#${slugify(title)}-${slugify(pluginTitle)}`,
1766
+ title
1451
1767
  );
1452
- return li(
1453
- `${getSquaredScoreMarker(
1454
- audit.score
1455
- )} ${auditTitle} (_${pluginTitle}_) - ${getAuditResult(audit)}`
1768
+ const marker = scoreMarker(score, "square");
1769
+ return li2(
1770
+ `${marker}${SPACE}${auditTitleAsLink}${SPACE}(_${pluginTitle}_) - ${boldMd2(
1771
+ (displayValue || value).toString()
1772
+ )}`
1456
1773
  );
1457
1774
  }
1458
- function groupItemToCategorySection(group, groupAudits, pluginTitle) {
1459
- const groupScore = Number(formatReportScore(group.score || 0));
1460
- const groupTitle = li(
1461
- `${getRoundScoreMarker(groupScore)} ${group.title} (_${pluginTitle}_)`
1775
+ function categoryGroupItem({ score = 0, title }, groupAudits, pluginTitle) {
1776
+ const groupTitle = li2(
1777
+ `${scoreMarker(score)}${SPACE}${title}${SPACE}(_${pluginTitle}_)`
1462
1778
  );
1463
- const auditTitles = groupAudits.reduce((acc, audit) => {
1464
- const auditTitle = link(
1465
- `#${slugify(audit.title)}-${slugify(pluginTitle)}`,
1466
- audit.title
1467
- );
1468
- return `${acc} ${li(
1469
- `${getSquaredScoreMarker(audit.score)} ${auditTitle} - ${getAuditResult(
1470
- audit
1471
- )}`
1472
- )}${NEW_LINE}`;
1473
- }, "");
1474
- return groupTitle + NEW_LINE + auditTitles;
1475
- }
1476
- function reportToAuditsSection(report) {
1477
- const auditsSection = report.plugins.reduce((pluginAcc, plugin) => {
1478
- const auditsData = plugin.audits.reduce((auditAcc, audit) => {
1479
- const auditTitle = `${audit.title} (${getPluginNameFromSlug(
1480
- plugin.slug,
1481
- report.plugins
1482
- )})`;
1483
- return auditAcc + h3(auditTitle) + NEW_LINE + NEW_LINE + reportToDetailsSection(audit) + NEW_LINE + NEW_LINE + getDocsAndDescription(audit);
1484
- }, "");
1485
- return pluginAcc + auditsData;
1486
- }, "");
1487
- return h2("\u{1F6E1}\uFE0F Audits") + NEW_LINE + NEW_LINE + auditsSection;
1488
- }
1489
- function reportToDetailsSection(audit) {
1490
- const detailsTitle = `${getSquaredScoreMarker(audit.score)} ${getAuditResult(
1491
- audit,
1492
- true
1493
- )} (score: ${formatReportScore(audit.score)})`;
1494
- if (!audit.details?.issues.length) {
1495
- return detailsTitle;
1496
- }
1497
- const detailsTableData = [
1498
- detailsTableHeaders,
1499
- ...audit.details.issues.map((issue) => {
1500
- const severity = `${getSeverityIcon(issue.severity)} <i>${issue.severity}</i>`;
1501
- const message = issue.message;
1502
- if (!issue.source) {
1503
- return [severity, message, "", ""];
1504
- }
1505
- const file = `<code>${issue.source.file}</code>`;
1506
- if (!issue.source.position) {
1507
- return [severity, message, file, ""];
1508
- }
1509
- const { startLine, endLine } = issue.source.position;
1510
- const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
1511
- return [severity, message, file, line];
1512
- })
1513
- ];
1514
- const detailsTable = `<h4>Issues</h4>${tableHtml(detailsTableData)}`;
1515
- return details(detailsTitle, detailsTable);
1516
- }
1517
- function reportToAboutSection(report) {
1518
- const date = formatDate(/* @__PURE__ */ new Date());
1519
- const { duration, version: version2, commit, plugins, categories } = report;
1520
- const commitInfo = commit ? `${commit.message} (${commit.hash})` : "N/A";
1521
- const reportMetaTable = [
1522
- reportMetaTableHeaders,
1523
- [
1524
- commitInfo,
1525
- style(version2 || "", ["c"]),
1526
- formatDuration(duration),
1527
- plugins.length.toString(),
1528
- categories.length.toString(),
1529
- plugins.reduce((acc, { audits }) => acc + audits.length, 0).toString()
1530
- ]
1531
- ];
1532
- const pluginMetaTable = [
1533
- pluginMetaTableHeaders,
1534
- ...plugins.map((plugin) => [
1535
- plugin.title,
1536
- plugin.audits.length.toString(),
1537
- style(plugin.version || "", ["c"]),
1538
- formatDuration(plugin.duration)
1539
- ])
1540
- ];
1541
- return (
1542
- // eslint-disable-next-line prefer-template
1543
- 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"])
1779
+ const auditTitles = groupAudits.map(
1780
+ ({ title: auditTitle, score: auditScore, value, displayValue }) => {
1781
+ const auditTitleLink = link4(
1782
+ `#${slugify(auditTitle)}-${slugify(pluginTitle)}`,
1783
+ auditTitle
1784
+ );
1785
+ const marker = scoreMarker(auditScore, "square");
1786
+ return indentation2(
1787
+ li2(
1788
+ `${marker}${SPACE}${auditTitleLink} - ${boldMd2(
1789
+ String(displayValue ?? value)
1790
+ )}`
1791
+ )
1792
+ );
1793
+ }
1544
1794
  );
1795
+ return lines3(groupTitle, ...auditTitles);
1545
1796
  }
1546
- function getDocsAndDescription({
1547
- docsUrl,
1548
- description
1797
+
1798
+ // packages/utils/src/lib/reports/generate-md-report.ts
1799
+ var { h1: h12, h2: h23, h3: h33, lines: lines4, link: link5, section: section4, code: codeMd } = md;
1800
+ var { bold: boldHtml, details: details2 } = html;
1801
+ function auditDetailsAuditValue({
1802
+ score,
1803
+ value,
1804
+ displayValue
1549
1805
  }) {
1550
- if (docsUrl) {
1551
- const docsLink = link(docsUrl, "\u{1F4D6} Docs");
1552
- if (!description) {
1553
- return docsLink + NEW_LINE + NEW_LINE;
1554
- }
1555
- if (description.endsWith("```")) {
1556
- return description + NEW_LINE + NEW_LINE + docsLink + NEW_LINE + NEW_LINE;
1557
- }
1558
- return `${description} ${docsLink}${NEW_LINE}${NEW_LINE}`;
1559
- }
1560
- if (description) {
1561
- return description + NEW_LINE + NEW_LINE;
1806
+ return `${scoreMarker(score, "square")} ${boldHtml(
1807
+ String(displayValue ?? value)
1808
+ )} (score: ${formatReportScore(score)})`;
1809
+ }
1810
+ function generateMdReport(report) {
1811
+ const printCategories = report.categories.length > 0;
1812
+ return lines4(
1813
+ h12(reportHeadlineText),
1814
+ printCategories ? categoriesOverviewSection(report) : "",
1815
+ printCategories ? categoriesDetailsSection(report) : "",
1816
+ auditsSection(report),
1817
+ aboutSection(report),
1818
+ `${FOOTER_PREFIX}${SPACE}${link5(README_LINK, "Code PushUp")}`
1819
+ );
1820
+ }
1821
+ function auditDetailsIssues(issues = []) {
1822
+ if (issues.length === 0) {
1823
+ return "";
1562
1824
  }
1563
- return "";
1825
+ const detailsTableData = {
1826
+ title: "Issues",
1827
+ columns: issuesTableHeadings,
1828
+ rows: issues.map(
1829
+ ({ severity: severityVal, message, source: sourceVal }) => {
1830
+ const severity = `${severityMarker(severityVal)} <i>${severityVal}</i>`;
1831
+ if (!sourceVal) {
1832
+ return { severity, message, file: "", line: "" };
1833
+ }
1834
+ const file = `<code>${sourceVal.file}</code>`;
1835
+ if (!sourceVal.position) {
1836
+ return { severity, message, file, line: "" };
1837
+ }
1838
+ const { startLine, endLine } = sourceVal.position;
1839
+ const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
1840
+ return { severity, message, file, line };
1841
+ }
1842
+ )
1843
+ };
1844
+ return tableSection(detailsTableData);
1845
+ }
1846
+ function auditDetails(audit) {
1847
+ const { table: table5, issues = [] } = audit.details ?? {};
1848
+ const detailsValue = auditDetailsAuditValue(audit);
1849
+ if (issues.length === 0 && table5 == null) {
1850
+ return section4(detailsValue);
1851
+ }
1852
+ const tableSectionContent = table5 == null ? "" : tableSection(table5);
1853
+ const issuesSectionContent = issues.length > 0 ? auditDetailsIssues(issues) : "";
1854
+ return details2(
1855
+ detailsValue,
1856
+ lines4(tableSectionContent, issuesSectionContent)
1857
+ );
1858
+ }
1859
+ function auditsSection({
1860
+ plugins
1861
+ }) {
1862
+ const content = plugins.flatMap(
1863
+ ({ slug, audits }) => audits.flatMap((audit) => {
1864
+ const auditTitle = `${audit.title}${SPACE}(${getPluginNameFromSlug(
1865
+ slug,
1866
+ plugins
1867
+ )})`;
1868
+ const detailsContent = auditDetails(audit);
1869
+ const descriptionContent = metaDescription(audit);
1870
+ return [h33(auditTitle), detailsContent, descriptionContent];
1871
+ })
1872
+ );
1873
+ return section4(h23("\u{1F6E1}\uFE0F Audits"), ...content);
1874
+ }
1875
+ function aboutSection(report) {
1876
+ const { date, plugins } = report;
1877
+ const reportMetaTable = reportMetaData(report);
1878
+ const pluginMetaTable = reportPluginMeta({ plugins });
1879
+ return lines4(
1880
+ h23("About"),
1881
+ section4(
1882
+ `Report was created by [Code PushUp](${README_LINK}) on ${formatDate(
1883
+ new Date(date)
1884
+ )}.`
1885
+ ),
1886
+ tableSection(pluginMetaTable),
1887
+ tableSection(reportMetaTable)
1888
+ );
1564
1889
  }
1565
- function getAuditResult(audit, isHtml = false) {
1566
- const { displayValue, value } = audit;
1567
- return isHtml ? `<b>${displayValue || value}</b>` : style(String(displayValue || value));
1890
+ function reportPluginMeta({ plugins }) {
1891
+ return {
1892
+ columns: [
1893
+ {
1894
+ key: "plugin",
1895
+ align: "left"
1896
+ },
1897
+ {
1898
+ key: "audits"
1899
+ },
1900
+ {
1901
+ key: "version"
1902
+ },
1903
+ {
1904
+ key: "duration"
1905
+ }
1906
+ ],
1907
+ rows: plugins.map(
1908
+ ({
1909
+ title: pluginTitle,
1910
+ audits,
1911
+ version: pluginVersion,
1912
+ duration: pluginDuration
1913
+ }) => ({
1914
+ plugin: pluginTitle,
1915
+ audits: audits.length.toString(),
1916
+ version: codeMd(pluginVersion || ""),
1917
+ duration: formatDuration(pluginDuration)
1918
+ })
1919
+ )
1920
+ };
1921
+ }
1922
+ function reportMetaData({
1923
+ commit,
1924
+ version: version2,
1925
+ duration,
1926
+ plugins,
1927
+ categories
1928
+ }) {
1929
+ const commitInfo = commit ? `${commit.message}${SPACE}(${commit.hash})` : "N/A";
1930
+ return {
1931
+ columns: [
1932
+ {
1933
+ key: "commit",
1934
+ align: "left"
1935
+ },
1936
+ {
1937
+ key: "version"
1938
+ },
1939
+ {
1940
+ key: "duration"
1941
+ },
1942
+ {
1943
+ key: "plugins"
1944
+ },
1945
+ {
1946
+ key: "categories"
1947
+ },
1948
+ {
1949
+ key: "audits"
1950
+ }
1951
+ ],
1952
+ rows: [
1953
+ {
1954
+ commit: commitInfo,
1955
+ version: codeMd(version2 || ""),
1956
+ duration: formatDuration(duration),
1957
+ plugins: plugins.length,
1958
+ categories: categories.length,
1959
+ audits: plugins.reduce((acc, { audits }) => acc + audits.length, 0).toString()
1960
+ }
1961
+ ]
1962
+ };
1568
1963
  }
1569
1964
 
1570
1965
  // packages/utils/src/lib/reports/generate-md-reports-diff.ts
1966
+ var {
1967
+ h1: h13,
1968
+ h2: h24,
1969
+ lines: lines5,
1970
+ link: link6,
1971
+ bold: boldMd3,
1972
+ italic: italicMd,
1973
+ table: table4,
1974
+ section: section5
1975
+ } = md;
1976
+ var { details: details3 } = html;
1571
1977
  var MAX_ROWS = 100;
1572
1978
  function generateMdReportsDiff(diff) {
1573
- return paragraphs(
1574
- formatDiffHeaderSection(diff),
1979
+ return lines5(
1980
+ section5(formatDiffHeaderSection(diff)),
1575
1981
  formatDiffCategoriesSection(diff),
1576
1982
  formatDiffGroupsSection(diff),
1577
1983
  formatDiffAuditsSection(diff)
@@ -1579,12 +1985,12 @@ function generateMdReportsDiff(diff) {
1579
1985
  }
1580
1986
  function formatDiffHeaderSection(diff) {
1581
1987
  const outcomeTexts = {
1582
- positive: `\u{1F973} Code PushUp report has ${style("improved")}`,
1583
- negative: `\u{1F61F} Code PushUp report has ${style("regressed")}`,
1584
- mixed: `\u{1F928} Code PushUp report has both ${style(
1988
+ positive: `\u{1F973} Code PushUp report has ${boldMd3("improved")}`,
1989
+ negative: `\u{1F61F} Code PushUp report has ${boldMd3("regressed")}`,
1990
+ mixed: `\u{1F928} Code PushUp report has both ${boldMd3(
1585
1991
  "improvements and regressions"
1586
1992
  )}`,
1587
- unchanged: `\u{1F610} Code PushUp report is ${style("unchanged")}`
1993
+ unchanged: `\u{1F610} Code PushUp report is ${boldMd3("unchanged")}`
1588
1994
  };
1589
1995
  const outcome = mergeDiffOutcomes(
1590
1996
  changesToDiffOutcomes([
@@ -1594,8 +2000,8 @@ function formatDiffHeaderSection(diff) {
1594
2000
  ])
1595
2001
  );
1596
2002
  const styleCommits = (commits) => `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
1597
- return paragraphs(
1598
- h1("Code PushUp"),
2003
+ return lines5(
2004
+ h13("Code PushUp"),
1599
2005
  diff.commits ? `${outcomeTexts[outcome]} \u2013 ${styleCommits(diff.commits)}.` : `${outcomeTexts[outcome]}.`
1600
2006
  );
1601
2007
  }
@@ -1606,102 +2012,104 @@ function formatDiffCategoriesSection(diff) {
1606
2012
  if (categoriesCount === 0) {
1607
2013
  return "";
1608
2014
  }
1609
- return paragraphs(
1610
- h2("\u{1F3F7}\uFE0F Categories"),
1611
- categoriesCount > 0 && tableMd(
1612
- [
1613
- [
1614
- "\u{1F3F7}\uFE0F Category",
1615
- hasChanges ? "\u2B50 Current score" : "\u2B50 Score",
1616
- "\u2B50 Previous score",
1617
- "\u{1F504} Score change"
1618
- ],
1619
- ...sortChanges(changed).map((category) => [
1620
- category.title,
1621
- formatScoreWithColor(category.scores.after),
1622
- formatScoreWithColor(category.scores.before, { skipBold: true }),
1623
- formatScoreChange(category.scores.diff)
1624
- ]),
1625
- ...added.map((category) => [
1626
- category.title,
1627
- formatScoreWithColor(category.score),
1628
- style("n/a (\\*)", ["i"]),
1629
- style("n/a (\\*)", ["i"])
1630
- ]),
1631
- ...unchanged.map((category) => [
1632
- category.title,
1633
- formatScoreWithColor(category.score),
1634
- formatScoreWithColor(category.score, { skipBold: true }),
1635
- "\u2013"
1636
- ])
1637
- ].map((row) => hasChanges ? row : row.slice(0, 2)),
1638
- hasChanges ? ["l", "c", "c", "c"] : ["l", "c"]
1639
- ),
1640
- added.length > 0 && style("(\\*) New category.", ["i"])
2015
+ const columns = [
2016
+ { key: "category", label: "\u{1F3F7}\uFE0F Category", align: "left" },
2017
+ { key: "after", label: hasChanges ? "\u2B50 Current score" : "\u2B50 Score" },
2018
+ { key: "before", label: "\u2B50 Previous score" },
2019
+ { key: "change", label: "\u{1F504} Score change" }
2020
+ ];
2021
+ return lines5(
2022
+ h24("\u{1F3F7}\uFE0F Categories"),
2023
+ categoriesCount > 0 && table4({
2024
+ columns: hasChanges ? columns : columns.slice(0, 2),
2025
+ rows: [
2026
+ ...sortChanges(changed).map((category) => ({
2027
+ category: formatTitle(category),
2028
+ after: formatScoreWithColor(category.scores.after),
2029
+ before: formatScoreWithColor(category.scores.before, {
2030
+ skipBold: true
2031
+ }),
2032
+ change: formatScoreChange(category.scores.diff)
2033
+ })),
2034
+ ...added.map((category) => ({
2035
+ category: formatTitle(category),
2036
+ after: formatScoreWithColor(category.score),
2037
+ before: italicMd("n/a (\\*)"),
2038
+ change: italicMd("n/a (\\*)")
2039
+ })),
2040
+ ...unchanged.map((category) => ({
2041
+ category: formatTitle(category),
2042
+ after: formatScoreWithColor(category.score),
2043
+ before: formatScoreWithColor(category.score, { skipBold: true }),
2044
+ change: "\u2013"
2045
+ }))
2046
+ ].map(
2047
+ (row) => hasChanges ? row : { category: row.category, after: row.after }
2048
+ )
2049
+ }),
2050
+ added.length > 0 && section5(italicMd("(\\*) New category."))
1641
2051
  );
1642
2052
  }
1643
2053
  function formatDiffGroupsSection(diff) {
1644
2054
  if (diff.groups.changed.length + diff.groups.unchanged.length === 0) {
1645
2055
  return "";
1646
2056
  }
1647
- return paragraphs(
1648
- h2("\u{1F5C3}\uFE0F Groups"),
2057
+ return lines5(
2058
+ h24("\u{1F5C3}\uFE0F Groups"),
1649
2059
  formatGroupsOrAuditsDetails("group", diff.groups, {
1650
- headings: [
1651
- "\u{1F50C} Plugin",
1652
- "\u{1F5C3}\uFE0F Group",
1653
- "\u2B50 Current score",
1654
- "\u2B50 Previous score",
1655
- "\u{1F504} Score change"
2060
+ columns: [
2061
+ { key: "plugin", label: "\u{1F50C} Plugin", align: "left" },
2062
+ { key: "group", label: "\u{1F5C3}\uFE0F Group", align: "left" },
2063
+ { key: "after", label: "\u2B50 Current score" },
2064
+ { key: "before", label: "\u2B50 Previous score" },
2065
+ { key: "change", label: "\u{1F504} Score change" }
1656
2066
  ],
1657
- rows: sortChanges(diff.groups.changed).map((group) => [
1658
- group.plugin.title,
1659
- group.title,
1660
- formatScoreWithColor(group.scores.after),
1661
- formatScoreWithColor(group.scores.before, { skipBold: true }),
1662
- formatScoreChange(group.scores.diff)
1663
- ]),
1664
- align: ["l", "l", "c", "c", "c"]
2067
+ rows: sortChanges(diff.groups.changed).map((group) => ({
2068
+ plugin: formatTitle(group.plugin),
2069
+ group: formatTitle(group),
2070
+ after: formatScoreWithColor(group.scores.after),
2071
+ before: formatScoreWithColor(group.scores.before, { skipBold: true }),
2072
+ change: formatScoreChange(group.scores.diff)
2073
+ }))
1665
2074
  })
1666
2075
  );
1667
2076
  }
1668
2077
  function formatDiffAuditsSection(diff) {
1669
- return paragraphs(
1670
- h2("\u{1F6E1}\uFE0F Audits"),
2078
+ return lines5(
2079
+ h24("\u{1F6E1}\uFE0F Audits"),
1671
2080
  formatGroupsOrAuditsDetails("audit", diff.audits, {
1672
- headings: [
1673
- "\u{1F50C} Plugin",
1674
- "\u{1F6E1}\uFE0F Audit",
1675
- "\u{1F4CF} Current value",
1676
- "\u{1F4CF} Previous value",
1677
- "\u{1F504} Value change"
2081
+ columns: [
2082
+ { key: "plugin", label: "\u{1F50C} Plugin", align: "left" },
2083
+ { key: "audit", label: "\u{1F6E1}\uFE0F Audit", align: "left" },
2084
+ { key: "after", label: "\u{1F4CF} Current value" },
2085
+ { key: "before", label: "\u{1F4CF} Previous value" },
2086
+ { key: "change", label: "\u{1F504} Value change" }
1678
2087
  ],
1679
- rows: sortChanges(diff.audits.changed).map((audit) => [
1680
- audit.plugin.title,
1681
- audit.title,
1682
- `${getSquaredScoreMarker(audit.scores.after)} ${style(
2088
+ rows: sortChanges(diff.audits.changed).map((audit) => ({
2089
+ plugin: formatTitle(audit.plugin),
2090
+ audit: formatTitle(audit),
2091
+ after: `${scoreMarker(audit.scores.after, "square")} ${boldMd3(
1683
2092
  audit.displayValues.after || audit.values.after.toString()
1684
2093
  )}`,
1685
- `${getSquaredScoreMarker(audit.scores.before)} ${audit.displayValues.before || audit.values.before.toString()}`,
1686
- formatValueChange(audit)
1687
- ]),
1688
- align: ["l", "l", "c", "c", "c"]
2094
+ before: `${scoreMarker(audit.scores.before, "square")} ${audit.displayValues.before || audit.values.before.toString()}`,
2095
+ change: formatValueChange(audit)
2096
+ }))
1689
2097
  })
1690
2098
  );
1691
2099
  }
1692
- function formatGroupsOrAuditsDetails(token, { changed, unchanged }, table) {
1693
- return changed.length === 0 ? summarizeUnchanged(token, { changed, unchanged }) : details(
2100
+ function formatGroupsOrAuditsDetails(token, { changed, unchanged }, tableData) {
2101
+ return changed.length === 0 ? summarizeUnchanged(token, { changed, unchanged }) : details3(
1694
2102
  summarizeDiffOutcomes(changesToDiffOutcomes(changed), token),
1695
- paragraphs(
1696
- tableMd(
1697
- [table.headings, ...table.rows.slice(0, MAX_ROWS)],
1698
- table.align
1699
- ),
1700
- changed.length > MAX_ROWS && style(
2103
+ lines5(
2104
+ table4({
2105
+ ...tableData,
2106
+ rows: tableData.rows.slice(0, MAX_ROWS)
2107
+ // use never to avoid typing problem
2108
+ }),
2109
+ changed.length > MAX_ROWS && italicMd(
1701
2110
  `Only the ${MAX_ROWS} most affected ${pluralize(
1702
2111
  token
1703
- )} are listed above for brevity.`,
1704
- ["i"]
2112
+ )} are listed above for brevity.`
1705
2113
  ),
1706
2114
  unchanged.length > 0 && summarizeUnchanged(token, { changed, unchanged })
1707
2115
  )
@@ -1722,11 +2130,13 @@ function formatValueChange({
1722
2130
  return colorByScoreDiff(`${marker} ${text}`, scores.diff);
1723
2131
  }
1724
2132
  function summarizeUnchanged(token, { changed, unchanged }) {
1725
- return [
1726
- changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`,
1727
- unchanged.length === 1 ? "is" : "are",
1728
- "unchanged."
1729
- ].join(" ");
2133
+ return section5(
2134
+ [
2135
+ changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`,
2136
+ unchanged.length === 1 ? "is" : "are",
2137
+ "unchanged."
2138
+ ].join(" ")
2139
+ );
1730
2140
  }
1731
2141
  function summarizeDiffOutcomes(outcomes, token) {
1732
2142
  return objectToEntries(countDiffOutcomes(outcomes)).filter(
@@ -1746,6 +2156,15 @@ function summarizeDiffOutcomes(outcomes, token) {
1746
2156
  }
1747
2157
  }).join(", ");
1748
2158
  }
2159
+ function formatTitle({
2160
+ title,
2161
+ docsUrl
2162
+ }) {
2163
+ if (docsUrl) {
2164
+ return link6(docsUrl, title);
2165
+ }
2166
+ return title;
2167
+ }
1749
2168
  function sortChanges(changes) {
1750
2169
  return [...changes].sort(
1751
2170
  (a, b) => Math.abs(b.scores.diff) - Math.abs(a.scores.diff) || Math.abs(b.values?.diff ?? 0) - Math.abs(a.values?.diff ?? 0)
@@ -1793,7 +2212,7 @@ function log(msg = "") {
1793
2212
  }
1794
2213
  function logStdoutSummary(report) {
1795
2214
  const printCategories = report.categories.length > 0;
1796
- log(reportToHeaderSection2(report));
2215
+ log(reportToHeaderSection(report));
1797
2216
  log();
1798
2217
  logPlugins(report);
1799
2218
  if (printCategories) {
@@ -1802,7 +2221,7 @@ function logStdoutSummary(report) {
1802
2221
  log(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`);
1803
2222
  log();
1804
2223
  }
1805
- function reportToHeaderSection2(report) {
2224
+ function reportToHeaderSection(report) {
1806
2225
  const { packageName, version: version2 } = report;
1807
2226
  return `${chalk4.bold(reportHeadlineText)} - ${packageName}@${version2}`;
1808
2227
  }
@@ -1842,16 +2261,16 @@ function logCategories({ categories, plugins }) {
1842
2261
  applyScoreColor({ score }),
1843
2262
  countCategoryAudits(refs, plugins)
1844
2263
  ]);
1845
- const table = ui().table();
1846
- table.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
1847
- table.head(
2264
+ const table5 = ui().table();
2265
+ table5.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
2266
+ table5.head(
1848
2267
  reportRawOverviewTableHeaders.map((heading, idx) => ({
1849
2268
  content: chalk4.cyan(heading),
1850
2269
  hAlign: hAlign(idx)
1851
2270
  }))
1852
2271
  );
1853
2272
  rows.forEach(
1854
- (row) => table.row(
2273
+ (row) => table5.row(
1855
2274
  row.map((content, idx) => ({
1856
2275
  content: content.toString(),
1857
2276
  hAlign: hAlign(idx)
@@ -1860,19 +2279,19 @@ function logCategories({ categories, plugins }) {
1860
2279
  );
1861
2280
  log(chalk4.magentaBright.bold("Categories"));
1862
2281
  log();
1863
- table.render();
2282
+ table5.render();
1864
2283
  log();
1865
2284
  }
1866
2285
  function applyScoreColor({ score, text }) {
1867
2286
  const formattedScore = text ?? formatReportScore(score);
1868
- const style2 = text ? chalk4 : chalk4.bold;
2287
+ const style = text ? chalk4 : chalk4.bold;
1869
2288
  if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
1870
- return style2.green(formattedScore);
2289
+ return style.green(formattedScore);
1871
2290
  }
1872
2291
  if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
1873
- return style2.yellow(formattedScore);
2292
+ return style.yellow(formattedScore);
1874
2293
  }
1875
- return style2.red(formattedScore);
2294
+ return style.red(formattedScore);
1876
2295
  }
1877
2296
 
1878
2297
  // packages/utils/src/lib/reports/scoring.ts
@@ -2034,12 +2453,22 @@ var verboseUtils = (verbose = false) => ({
2034
2453
 
2035
2454
  // packages/core/package.json
2036
2455
  var name = "@code-pushup/core";
2037
- var version = "0.35.0";
2456
+ var version = "0.42.0";
2038
2457
 
2039
2458
  // packages/core/src/lib/implementation/execute-plugin.ts
2040
2459
  import chalk5 from "chalk";
2041
2460
 
2042
2461
  // packages/core/src/lib/normalize.ts
2462
+ function normalizeIssue(issue, gitRoot) {
2463
+ const { source, ...issueWithoutSource } = issue;
2464
+ return source == null ? issue : {
2465
+ ...issueWithoutSource,
2466
+ source: {
2467
+ ...source,
2468
+ file: formatGitPath(source.file, gitRoot)
2469
+ }
2470
+ };
2471
+ }
2043
2472
  async function normalizeAuditOutputs(audits) {
2044
2473
  const gitRoot = await getGitRoot();
2045
2474
  return audits.map((audit) => {
@@ -2051,13 +2480,7 @@ async function normalizeAuditOutputs(audits) {
2051
2480
  details: {
2052
2481
  ...audit.details,
2053
2482
  issues: audit.details.issues.map(
2054
- (issue) => issue.source == null ? issue : {
2055
- ...issue,
2056
- source: {
2057
- ...issue.source,
2058
- file: formatGitPath(issue.source.file, gitRoot)
2059
- }
2060
- }
2483
+ (issue) => normalizeIssue(issue, gitRoot)
2061
2484
  )
2062
2485
  }
2063
2486
  };
@@ -2095,7 +2518,11 @@ async function executeRunnerFunction(runner, onProgress) {
2095
2518
  // packages/core/src/lib/implementation/execute-plugin.ts
2096
2519
  var PluginOutputMissingAuditError = class extends Error {
2097
2520
  constructor(auditSlug) {
2098
- super(`Audit metadata not found for slug ${auditSlug}`);
2521
+ super(
2522
+ `Audit metadata not present in plugin config. Missing slug: ${chalk5.bold(
2523
+ auditSlug
2524
+ )}`
2525
+ );
2099
2526
  }
2100
2527
  };
2101
2528
  async function executePlugin(pluginConfig, onProgress) {
@@ -2109,7 +2536,11 @@ async function executePlugin(pluginConfig, onProgress) {
2109
2536
  } = pluginConfig;
2110
2537
  const runnerResult = typeof runner === "object" ? await executeRunnerConfig(runner, onProgress) : await executeRunnerFunction(runner, onProgress);
2111
2538
  const { audits: unvalidatedAuditOutputs, ...executionMeta } = runnerResult;
2112
- const auditOutputs = auditOutputsSchema.parse(unvalidatedAuditOutputs);
2539
+ const result = auditOutputsSchema.safeParse(unvalidatedAuditOutputs);
2540
+ if (!result.success) {
2541
+ throw new Error(`Audit output is invalid: ${result.error.message}`);
2542
+ }
2543
+ const auditOutputs = result.data;
2113
2544
  auditOutputsCorrelateWithPluginOutput(auditOutputs, pluginConfigAudits);
2114
2545
  const normalizedAuditOutputs = await normalizeAuditOutputs(auditOutputs);
2115
2546
  const auditReports = normalizedAuditOutputs.map(
@@ -2129,32 +2560,48 @@ async function executePlugin(pluginConfig, onProgress) {
2129
2560
  ...groups && { groups }
2130
2561
  };
2131
2562
  }
2563
+ var wrapProgress = async (pluginCfg, steps, progressBar) => {
2564
+ progressBar?.updateTitle(`Executing ${chalk5.bold(pluginCfg.title)}`);
2565
+ try {
2566
+ const pluginReport = await executePlugin(pluginCfg);
2567
+ progressBar?.incrementInSteps(steps);
2568
+ return pluginReport;
2569
+ } catch (error) {
2570
+ progressBar?.incrementInSteps(steps);
2571
+ throw new Error(
2572
+ error instanceof Error ? `- Plugin ${chalk5.bold(pluginCfg.title)} (${chalk5.bold(
2573
+ pluginCfg.slug
2574
+ )}) produced the following error:
2575
+ - ${error.message}` : String(error)
2576
+ );
2577
+ }
2578
+ };
2132
2579
  async function executePlugins(plugins, options) {
2133
2580
  const { progress = false } = options ?? {};
2134
2581
  const progressBar = progress ? getProgressBar("Run plugins") : null;
2135
- const pluginsResult = await plugins.reduce(async (acc, pluginCfg) => {
2136
- progressBar?.updateTitle(`Executing ${chalk5.bold(pluginCfg.title)}`);
2137
- try {
2138
- const pluginReport = await executePlugin(pluginCfg);
2139
- progressBar?.incrementInSteps(plugins.length);
2140
- return [...await acc, Promise.resolve(pluginReport)];
2141
- } catch (error) {
2142
- progressBar?.incrementInSteps(plugins.length);
2143
- return [
2144
- ...await acc,
2145
- Promise.reject(error instanceof Error ? error.message : String(error))
2146
- ];
2147
- }
2148
- }, Promise.resolve([]));
2582
+ const pluginsResult = await plugins.reduce(
2583
+ async (acc, pluginCfg) => [
2584
+ ...await acc,
2585
+ wrapProgress(pluginCfg, plugins.length, progressBar)
2586
+ ],
2587
+ Promise.resolve([])
2588
+ );
2149
2589
  progressBar?.endProgress("Done running plugins");
2150
2590
  const errorsTransform = ({ reason }) => String(reason);
2151
2591
  const results = await Promise.allSettled(pluginsResult);
2152
2592
  logMultipleResults(results, "Plugins", void 0, errorsTransform);
2153
2593
  const { fulfilled, rejected } = groupByStatus(results);
2154
2594
  if (rejected.length > 0) {
2155
- const errorMessages = rejected.map(({ reason }) => String(reason)).join(", ");
2595
+ const errorMessages = rejected.map(({ reason }) => String(reason)).join("\n");
2156
2596
  throw new Error(
2157
- `Plugins failed: ${rejected.length} errors: ${errorMessages}`
2597
+ `Executing ${pluralizeToken(
2598
+ "plugin",
2599
+ rejected.length
2600
+ )} failed.
2601
+
2602
+ ${errorMessages}
2603
+
2604
+ `
2158
2605
  );
2159
2606
  }
2160
2607
  return fulfilled.map((result) => result.value);
@@ -2317,8 +2764,7 @@ function compareAudits2(reports) {
2317
2764
  }
2318
2765
  function categoryToResult(category) {
2319
2766
  return {
2320
- slug: category.slug,
2321
- title: category.title,
2767
+ ...selectMeta(category),
2322
2768
  score: category.score
2323
2769
  };
2324
2770
  }
@@ -2327,8 +2773,7 @@ function categoryPairToDiff({
2327
2773
  after
2328
2774
  }) {
2329
2775
  return {
2330
- slug: after.slug,
2331
- title: after.title,
2776
+ ...selectMeta(after),
2332
2777
  scores: {
2333
2778
  before: before.score,
2334
2779
  after: after.score,
@@ -2338,12 +2783,8 @@ function categoryPairToDiff({
2338
2783
  }
2339
2784
  function pluginGroupToResult({ group, plugin }) {
2340
2785
  return {
2341
- slug: group.slug,
2342
- title: group.title,
2343
- plugin: {
2344
- slug: plugin.slug,
2345
- title: plugin.title
2346
- },
2786
+ ...selectMeta(group),
2787
+ plugin: selectMeta(plugin),
2347
2788
  score: group.score
2348
2789
  };
2349
2790
  }
@@ -2352,12 +2793,8 @@ function pluginGroupPairToDiff({
2352
2793
  after
2353
2794
  }) {
2354
2795
  return {
2355
- slug: after.group.slug,
2356
- title: after.group.title,
2357
- plugin: {
2358
- slug: after.plugin.slug,
2359
- title: after.plugin.title
2360
- },
2796
+ ...selectMeta(after.group),
2797
+ plugin: selectMeta(after.plugin),
2361
2798
  scores: {
2362
2799
  before: before.group.score,
2363
2800
  after: after.group.score,
@@ -2367,12 +2804,8 @@ function pluginGroupPairToDiff({
2367
2804
  }
2368
2805
  function pluginAuditToResult({ audit, plugin }) {
2369
2806
  return {
2370
- slug: audit.slug,
2371
- title: audit.title,
2372
- plugin: {
2373
- slug: plugin.slug,
2374
- title: plugin.title
2375
- },
2807
+ ...selectMeta(audit),
2808
+ plugin: selectMeta(plugin),
2376
2809
  score: audit.score,
2377
2810
  value: audit.value,
2378
2811
  displayValue: audit.displayValue
@@ -2383,12 +2816,8 @@ function pluginAuditPairToDiff({
2383
2816
  after
2384
2817
  }) {
2385
2818
  return {
2386
- slug: after.audit.slug,
2387
- title: after.audit.title,
2388
- plugin: {
2389
- slug: after.plugin.slug,
2390
- title: after.plugin.title
2391
- },
2819
+ ...selectMeta(after.audit),
2820
+ plugin: selectMeta(after.plugin),
2392
2821
  scores: {
2393
2822
  before: before.audit.score,
2394
2823
  after: after.audit.score,
@@ -2405,6 +2834,15 @@ function pluginAuditPairToDiff({
2405
2834
  }
2406
2835
  };
2407
2836
  }
2837
+ function selectMeta(meta) {
2838
+ return {
2839
+ slug: meta.slug,
2840
+ title: meta.title,
2841
+ ...meta.docsUrl && {
2842
+ docsUrl: meta.docsUrl
2843
+ }
2844
+ };
2845
+ }
2408
2846
 
2409
2847
  // packages/core/src/lib/compare.ts
2410
2848
  async function compareReportFiles(inputPaths, persistConfig) {
@@ -2460,46 +2898,6 @@ function reportsDiffToFileContent(reportsDiff, format) {
2460
2898
  }
2461
2899
  }
2462
2900
 
2463
- // packages/core/src/lib/implementation/read-rc-file.ts
2464
- import { join as join6 } from "node:path";
2465
- var ConfigPathError = class extends Error {
2466
- constructor(configPath) {
2467
- super(`Provided path '${configPath}' is not valid.`);
2468
- }
2469
- };
2470
- async function readRcByPath(filepath, tsconfig) {
2471
- if (filepath.length === 0) {
2472
- throw new Error("The path to the configuration file is empty.");
2473
- }
2474
- if (!await fileExists(filepath)) {
2475
- throw new ConfigPathError(filepath);
2476
- }
2477
- const cfg = await importEsmModule({ filepath, tsconfig });
2478
- return coreConfigSchema.parse(cfg);
2479
- }
2480
- async function autoloadRc(tsconfig) {
2481
- let ext = "";
2482
- for (const extension of SUPPORTED_CONFIG_FILE_FORMATS) {
2483
- const path = `${CONFIG_FILE_NAME}.${extension}`;
2484
- const exists2 = await fileExists(path);
2485
- if (exists2) {
2486
- ext = extension;
2487
- break;
2488
- }
2489
- }
2490
- if (!ext) {
2491
- throw new Error(
2492
- `No file ${CONFIG_FILE_NAME}.(${SUPPORTED_CONFIG_FILE_FORMATS.join(
2493
- "|"
2494
- )}) present in ${process.cwd()}`
2495
- );
2496
- }
2497
- return readRcByPath(
2498
- join6(process.cwd(), `${CONFIG_FILE_NAME}.${ext}`),
2499
- tsconfig
2500
- );
2501
- }
2502
-
2503
2901
  // packages/core/src/lib/upload.ts
2504
2902
  import {
2505
2903
  uploadToPortal
@@ -2545,17 +2943,33 @@ function groupToGQL(group) {
2545
2943
  };
2546
2944
  }
2547
2945
  function auditToGQL(audit) {
2946
+ const {
2947
+ slug,
2948
+ title,
2949
+ description,
2950
+ docsUrl,
2951
+ score,
2952
+ value,
2953
+ displayValue: formattedValue,
2954
+ details: details4
2955
+ } = audit;
2956
+ const {
2957
+ issues
2958
+ /*, table */
2959
+ } = details4 ?? {};
2548
2960
  return {
2549
- slug: audit.slug,
2550
- title: audit.title,
2551
- description: audit.description,
2552
- docsUrl: audit.docsUrl,
2553
- score: audit.score,
2554
- value: audit.value,
2555
- formattedValue: audit.displayValue,
2556
- ...audit.details && {
2961
+ slug,
2962
+ title,
2963
+ description,
2964
+ docsUrl,
2965
+ score,
2966
+ value,
2967
+ formattedValue,
2968
+ ...details4 && {
2557
2969
  details: {
2558
- issues: audit.details.issues.map(issueToGQL)
2970
+ ...issues && { issues: issues.map(issueToGQL) }
2971
+ // @TODO add when https://github.com/code-pushup/cli/issues/530 is implemented
2972
+ // ...(table ? {table} : {}),
2559
2973
  }
2560
2974
  }
2561
2975
  };
@@ -2579,6 +2993,7 @@ function categoryToGQL(category) {
2579
2993
  slug: category.slug,
2580
2994
  title: category.title,
2581
2995
  description: category.description,
2996
+ isBinary: category.isBinary,
2582
2997
  refs: category.refs.map((ref) => ({
2583
2998
  plugin: ref.plugin,
2584
2999
  type: categoryRefTypeToGQL(ref.type),
@@ -2627,6 +3042,80 @@ async function upload(options, uploadFn = uploadToPortal) {
2627
3042
  };
2628
3043
  return uploadFn({ apiKey, server, data, timeout });
2629
3044
  }
3045
+
3046
+ // packages/core/src/lib/history.ts
3047
+ async function history(config, commits) {
3048
+ const initialBranch = await getCurrentBranchOrTag();
3049
+ const { skipUploads = false, forceCleanStatus, persist } = config;
3050
+ const reports = [];
3051
+ for (const commit of commits) {
3052
+ ui().logger.info(`Collect ${commit}`);
3053
+ await safeCheckout(commit, forceCleanStatus);
3054
+ const currentConfig = {
3055
+ ...config,
3056
+ persist: {
3057
+ ...persist,
3058
+ format: ["json"],
3059
+ filename: `${commit}-report`
3060
+ }
3061
+ };
3062
+ await collectAndPersistReports(currentConfig);
3063
+ if (skipUploads) {
3064
+ ui().logger.info("Upload is skipped because skipUploads is set to true.");
3065
+ } else {
3066
+ if (currentConfig.upload) {
3067
+ await upload(currentConfig);
3068
+ } else {
3069
+ ui().logger.info(
3070
+ "Upload is skipped because upload config is undefined."
3071
+ );
3072
+ }
3073
+ }
3074
+ reports.push(currentConfig.persist.filename);
3075
+ }
3076
+ await safeCheckout(initialBranch, forceCleanStatus);
3077
+ return reports;
3078
+ }
3079
+
3080
+ // packages/core/src/lib/implementation/read-rc-file.ts
3081
+ import { join as join6 } from "node:path";
3082
+ var ConfigPathError = class extends Error {
3083
+ constructor(configPath) {
3084
+ super(`Provided path '${configPath}' is not valid.`);
3085
+ }
3086
+ };
3087
+ async function readRcByPath(filepath, tsconfig) {
3088
+ if (filepath.length === 0) {
3089
+ throw new Error("The path to the configuration file is empty.");
3090
+ }
3091
+ if (!await fileExists(filepath)) {
3092
+ throw new ConfigPathError(filepath);
3093
+ }
3094
+ const cfg = await importEsmModule({ filepath, tsconfig });
3095
+ return coreConfigSchema.parse(cfg);
3096
+ }
3097
+ async function autoloadRc(tsconfig) {
3098
+ let ext = "";
3099
+ for (const extension of SUPPORTED_CONFIG_FILE_FORMATS) {
3100
+ const path = `${CONFIG_FILE_NAME}.${extension}`;
3101
+ const exists2 = await fileExists(path);
3102
+ if (exists2) {
3103
+ ext = extension;
3104
+ break;
3105
+ }
3106
+ }
3107
+ if (!ext) {
3108
+ throw new Error(
3109
+ `No file ${CONFIG_FILE_NAME}.(${SUPPORTED_CONFIG_FILE_FORMATS.join(
3110
+ "|"
3111
+ )}) present in ${process.cwd()}`
3112
+ );
3113
+ }
3114
+ return readRcByPath(
3115
+ join6(process.cwd(), `${CONFIG_FILE_NAME}.${ext}`),
3116
+ tsconfig
3117
+ );
3118
+ }
2630
3119
  export {
2631
3120
  ConfigPathError,
2632
3121
  PersistDirError,
@@ -2639,6 +3128,7 @@ export {
2639
3128
  compareReports,
2640
3129
  executePlugin,
2641
3130
  executePlugins,
3131
+ history,
2642
3132
  persistReport,
2643
3133
  readRcByPath,
2644
3134
  upload