@code-pushup/utils 0.49.0 → 0.51.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
@@ -155,9 +155,27 @@ function hasNonZeroWeightedRef(refs) {
155
155
  return refs.reduce((acc, { weight }) => weight + acc, 0) !== 0;
156
156
  }
157
157
 
158
- // packages/models/src/lib/audit.ts
158
+ // packages/models/src/lib/source.ts
159
159
  import { z as z2 } from "zod";
160
- var auditSchema = z2.object({
160
+ var sourceFileLocationSchema = z2.object(
161
+ {
162
+ file: filePathSchema.describe("Relative path to source file in Git repo"),
163
+ position: z2.object(
164
+ {
165
+ startLine: positiveIntSchema.describe("Start line"),
166
+ startColumn: positiveIntSchema.describe("Start column").optional(),
167
+ endLine: positiveIntSchema.describe("End line").optional(),
168
+ endColumn: positiveIntSchema.describe("End column").optional()
169
+ },
170
+ { description: "Location in file" }
171
+ ).optional()
172
+ },
173
+ { description: "Source file location" }
174
+ );
175
+
176
+ // packages/models/src/lib/audit.ts
177
+ import { z as z3 } from "zod";
178
+ var auditSchema = z3.object({
161
179
  slug: slugSchema.describe("ID (unique within plugin)")
162
180
  }).merge(
163
181
  metaSchema({
@@ -167,7 +185,7 @@ var auditSchema = z2.object({
167
185
  description: "List of scorable metrics for the given plugin"
168
186
  })
169
187
  );
170
- var pluginAuditsSchema = z2.array(auditSchema, {
188
+ var pluginAuditsSchema = z3.array(auditSchema, {
171
189
  description: "List of audits maintained in a plugin"
172
190
  }).min(1).refine(
173
191
  (auditMetadata) => !getDuplicateSlugsInAudits(auditMetadata),
@@ -186,31 +204,16 @@ function getDuplicateSlugsInAudits(audits) {
186
204
  }
187
205
 
188
206
  // packages/models/src/lib/audit-output.ts
189
- import { z as z5 } from "zod";
207
+ import { z as z6 } from "zod";
190
208
 
191
209
  // packages/models/src/lib/issue.ts
192
- import { z as z3 } from "zod";
193
- var sourceFileLocationSchema = z3.object(
194
- {
195
- file: filePathSchema.describe("Relative path to source file in Git repo"),
196
- position: z3.object(
197
- {
198
- startLine: positiveIntSchema.describe("Start line"),
199
- startColumn: positiveIntSchema.describe("Start column").optional(),
200
- endLine: positiveIntSchema.describe("End line").optional(),
201
- endColumn: positiveIntSchema.describe("End column").optional()
202
- },
203
- { description: "Location in file" }
204
- ).optional()
205
- },
206
- { description: "Source file location" }
207
- );
208
- var issueSeveritySchema = z3.enum(["info", "warning", "error"], {
210
+ import { z as z4 } from "zod";
211
+ var issueSeveritySchema = z4.enum(["info", "warning", "error"], {
209
212
  description: "Severity level"
210
213
  });
211
- var issueSchema = z3.object(
214
+ var issueSchema = z4.object(
212
215
  {
213
- message: z3.string({ description: "Descriptive error message" }).max(MAX_ISSUE_MESSAGE_LENGTH),
216
+ message: z4.string({ description: "Descriptive error message" }).max(MAX_ISSUE_MESSAGE_LENGTH),
214
217
  severity: issueSeveritySchema,
215
218
  source: sourceFileLocationSchema.optional()
216
219
  },
@@ -218,60 +221,60 @@ var issueSchema = z3.object(
218
221
  );
219
222
 
220
223
  // packages/models/src/lib/table.ts
221
- import { z as z4 } from "zod";
222
- var tableAlignmentSchema = z4.enum(["left", "center", "right"], {
224
+ import { z as z5 } from "zod";
225
+ var tableAlignmentSchema = z5.enum(["left", "center", "right"], {
223
226
  description: "Cell alignment"
224
227
  });
225
- var tableColumnObjectSchema = z4.object({
226
- key: z4.string(),
227
- label: z4.string().optional(),
228
+ var tableColumnObjectSchema = z5.object({
229
+ key: z5.string(),
230
+ label: z5.string().optional(),
228
231
  align: tableAlignmentSchema.optional()
229
232
  });
230
- var tableRowObjectSchema = z4.record(tableCellValueSchema, {
233
+ var tableRowObjectSchema = z5.record(tableCellValueSchema, {
231
234
  description: "Object row"
232
235
  });
233
- var tableRowPrimitiveSchema = z4.array(tableCellValueSchema, {
236
+ var tableRowPrimitiveSchema = z5.array(tableCellValueSchema, {
234
237
  description: "Primitive row"
235
238
  });
236
- var tableSharedSchema = z4.object({
237
- title: z4.string().optional().describe("Display title for table")
239
+ var tableSharedSchema = z5.object({
240
+ title: z5.string().optional().describe("Display title for table")
238
241
  });
239
242
  var tablePrimitiveSchema = tableSharedSchema.merge(
240
- z4.object(
243
+ z5.object(
241
244
  {
242
- columns: z4.array(tableAlignmentSchema).optional(),
243
- rows: z4.array(tableRowPrimitiveSchema)
245
+ columns: z5.array(tableAlignmentSchema).optional(),
246
+ rows: z5.array(tableRowPrimitiveSchema)
244
247
  },
245
248
  { description: "Table with primitive rows and optional alignment columns" }
246
249
  )
247
250
  );
248
251
  var tableObjectSchema = tableSharedSchema.merge(
249
- z4.object(
252
+ z5.object(
250
253
  {
251
- columns: z4.union([
252
- z4.array(tableAlignmentSchema),
253
- z4.array(tableColumnObjectSchema)
254
+ columns: z5.union([
255
+ z5.array(tableAlignmentSchema),
256
+ z5.array(tableColumnObjectSchema)
254
257
  ]).optional(),
255
- rows: z4.array(tableRowObjectSchema)
258
+ rows: z5.array(tableRowObjectSchema)
256
259
  },
257
260
  {
258
261
  description: "Table with object rows and optional alignment or object columns"
259
262
  }
260
263
  )
261
264
  );
262
- var tableSchema = (description = "Table information") => z4.union([tablePrimitiveSchema, tableObjectSchema], { description });
265
+ var tableSchema = (description = "Table information") => z5.union([tablePrimitiveSchema, tableObjectSchema], { description });
263
266
 
264
267
  // packages/models/src/lib/audit-output.ts
265
268
  var auditValueSchema = nonnegativeNumberSchema.describe("Raw numeric value");
266
- var auditDisplayValueSchema = z5.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }).optional();
267
- var auditDetailsSchema = z5.object(
269
+ var auditDisplayValueSchema = z6.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }).optional();
270
+ var auditDetailsSchema = z6.object(
268
271
  {
269
- issues: z5.array(issueSchema, { description: "List of findings" }).optional(),
272
+ issues: z6.array(issueSchema, { description: "List of findings" }).optional(),
270
273
  table: tableSchema("Table of related findings").optional()
271
274
  },
272
275
  { description: "Detailed information" }
273
276
  );
274
- var auditOutputSchema = z5.object(
277
+ var auditOutputSchema = z6.object(
275
278
  {
276
279
  slug: slugSchema.describe("Reference to audit"),
277
280
  displayValue: auditDisplayValueSchema,
@@ -281,7 +284,7 @@ var auditOutputSchema = z5.object(
281
284
  },
282
285
  { description: "Audit information" }
283
286
  );
284
- var auditOutputsSchema = z5.array(auditOutputSchema, {
287
+ var auditOutputsSchema = z6.array(auditOutputSchema, {
285
288
  description: "List of JSON formatted audit output emitted by the runner process of a plugin"
286
289
  }).refine(
287
290
  (audits) => !getDuplicateSlugsInAudits2(audits),
@@ -298,13 +301,13 @@ function getDuplicateSlugsInAudits2(audits) {
298
301
  }
299
302
 
300
303
  // packages/models/src/lib/category-config.ts
301
- import { z as z6 } from "zod";
304
+ import { z as z7 } from "zod";
302
305
  var categoryRefSchema = weightedRefSchema(
303
306
  "Weighted references to audits and/or groups for the category",
304
307
  "Slug of an audit or group (depending on `type`)"
305
308
  ).merge(
306
- z6.object({
307
- type: z6.enum(["audit", "group"], {
309
+ z7.object({
310
+ type: z7.enum(["audit", "group"], {
308
311
  description: "Discriminant for reference kind, affects where `slug` is looked up"
309
312
  }),
310
313
  plugin: slugSchema.describe(
@@ -325,8 +328,8 @@ var categoryConfigSchema = scorableSchema(
325
328
  description: "Meta info for category"
326
329
  })
327
330
  ).merge(
328
- z6.object({
329
- isBinary: z6.boolean({
331
+ z7.object({
332
+ isBinary: z7.boolean({
330
333
  description: 'Is this a binary category (i.e. only a perfect score considered a "pass")?'
331
334
  }).optional()
332
335
  })
@@ -342,7 +345,7 @@ function getDuplicateRefsInCategoryMetrics(metrics) {
342
345
  metrics.map(({ slug, type, plugin }) => `${type} :: ${plugin} / ${slug}`)
343
346
  );
344
347
  }
345
- var categoriesSchema = z6.array(categoryConfigSchema, {
348
+ var categoriesSchema = z7.array(categoryConfigSchema, {
346
349
  description: "Categorization of individual audits"
347
350
  }).refine(
348
351
  (categoryCfg) => !getDuplicateSlugCategories(categoryCfg),
@@ -361,18 +364,18 @@ function getDuplicateSlugCategories(categories) {
361
364
  }
362
365
 
363
366
  // packages/models/src/lib/commit.ts
364
- import { z as z7 } from "zod";
365
- var commitSchema = z7.object(
367
+ import { z as z8 } from "zod";
368
+ var commitSchema = z8.object(
366
369
  {
367
- hash: z7.string({ description: "Commit SHA (full)" }).regex(
370
+ hash: z8.string({ description: "Commit SHA (full)" }).regex(
368
371
  /^[\da-f]{40}$/,
369
372
  "Commit SHA should be a 40-character hexadecimal string"
370
373
  ),
371
- message: z7.string({ description: "Commit message" }),
372
- date: z7.coerce.date({
374
+ message: z8.string({ description: "Commit message" }),
375
+ date: z8.coerce.date({
373
376
  description: "Date and time when commit was authored"
374
377
  }),
375
- author: z7.string({
378
+ author: z8.string({
376
379
  description: "Commit author name"
377
380
  }).trim()
378
381
  },
@@ -380,22 +383,22 @@ var commitSchema = z7.object(
380
383
  );
381
384
 
382
385
  // packages/models/src/lib/core-config.ts
383
- import { z as z13 } from "zod";
386
+ import { z as z14 } from "zod";
384
387
 
385
388
  // packages/models/src/lib/persist-config.ts
386
- import { z as z8 } from "zod";
387
- var formatSchema = z8.enum(["json", "md"]);
388
- var persistConfigSchema = z8.object({
389
+ import { z as z9 } from "zod";
390
+ var formatSchema = z9.enum(["json", "md"]);
391
+ var persistConfigSchema = z9.object({
389
392
  outputDir: filePathSchema.describe("Artifacts folder").optional(),
390
393
  filename: fileNameSchema.describe("Artifacts file name (without extension)").optional(),
391
- format: z8.array(formatSchema).optional()
394
+ format: z9.array(formatSchema).optional()
392
395
  });
393
396
 
394
397
  // packages/models/src/lib/plugin-config.ts
395
- import { z as z11 } from "zod";
398
+ import { z as z12 } from "zod";
396
399
 
397
400
  // packages/models/src/lib/group.ts
398
- import { z as z9 } from "zod";
401
+ import { z as z10 } from "zod";
399
402
  var groupRefSchema = weightedRefSchema(
400
403
  "Weighted reference to a group",
401
404
  "Reference slug to a group within this plugin (e.g. 'max-lines')"
@@ -412,7 +415,7 @@ var groupSchema = scorableSchema(
412
415
  getDuplicateRefsInGroups,
413
416
  duplicateRefsInGroupsErrorMsg
414
417
  ).merge(groupMetaSchema);
415
- var groupsSchema = z9.array(groupSchema, {
418
+ var groupsSchema = z10.array(groupSchema, {
416
419
  description: "List of groups"
417
420
  }).optional().refine(
418
421
  (groups) => !getDuplicateSlugsInGroups(groups),
@@ -440,14 +443,14 @@ function getDuplicateSlugsInGroups(groups) {
440
443
  }
441
444
 
442
445
  // packages/models/src/lib/runner-config.ts
443
- import { z as z10 } from "zod";
444
- var outputTransformSchema = z10.function().args(z10.unknown()).returns(z10.union([auditOutputsSchema, z10.promise(auditOutputsSchema)]));
445
- var runnerConfigSchema = z10.object(
446
+ import { z as z11 } from "zod";
447
+ var outputTransformSchema = z11.function().args(z11.unknown()).returns(z11.union([auditOutputsSchema, z11.promise(auditOutputsSchema)]));
448
+ var runnerConfigSchema = z11.object(
446
449
  {
447
- command: z10.string({
450
+ command: z11.string({
448
451
  description: "Shell command to execute"
449
452
  }),
450
- args: z10.array(z10.string({ description: "Command arguments" })).optional(),
453
+ args: z11.array(z11.string({ description: "Command arguments" })).optional(),
451
454
  outputFile: filePathSchema.describe("Output path"),
452
455
  outputTransform: outputTransformSchema.optional()
453
456
  },
@@ -455,8 +458,8 @@ var runnerConfigSchema = z10.object(
455
458
  description: "How to execute runner"
456
459
  }
457
460
  );
458
- var onProgressSchema = z10.function().args(z10.unknown()).returns(z10.void());
459
- var runnerFunctionSchema = z10.function().args(onProgressSchema.optional()).returns(z10.union([auditOutputsSchema, z10.promise(auditOutputsSchema)]));
461
+ var onProgressSchema = z11.function().args(z11.unknown()).returns(z11.void());
462
+ var runnerFunctionSchema = z11.function().args(onProgressSchema.optional()).returns(z11.union([auditOutputsSchema, z11.promise(auditOutputsSchema)]));
460
463
 
461
464
  // packages/models/src/lib/plugin-config.ts
462
465
  var pluginMetaSchema = packageVersionSchema().merge(
@@ -467,13 +470,13 @@ var pluginMetaSchema = packageVersionSchema().merge(
467
470
  description: "Plugin metadata"
468
471
  })
469
472
  ).merge(
470
- z11.object({
473
+ z12.object({
471
474
  slug: slugSchema.describe("Unique plugin slug within core config"),
472
475
  icon: materialIconSchema
473
476
  })
474
477
  );
475
- var pluginDataSchema = z11.object({
476
- runner: z11.union([runnerConfigSchema, runnerFunctionSchema]),
478
+ var pluginDataSchema = z12.object({
479
+ runner: z12.union([runnerConfigSchema, runnerFunctionSchema]),
477
480
  audits: pluginAuditsSchema,
478
481
  groups: groupsSchema
479
482
  });
@@ -499,22 +502,22 @@ function getMissingRefsFromGroups(pluginCfg) {
499
502
  }
500
503
 
501
504
  // packages/models/src/lib/upload-config.ts
502
- import { z as z12 } from "zod";
503
- var uploadConfigSchema = z12.object({
505
+ import { z as z13 } from "zod";
506
+ var uploadConfigSchema = z13.object({
504
507
  server: urlSchema.describe("URL of deployed portal API"),
505
- apiKey: z12.string({
508
+ apiKey: z13.string({
506
509
  description: "API key with write access to portal (use `process.env` for security)"
507
510
  }),
508
511
  organization: slugSchema.describe(
509
512
  "Organization slug from Code PushUp portal"
510
513
  ),
511
514
  project: slugSchema.describe("Project slug from Code PushUp portal"),
512
- timeout: z12.number({ description: "Request timeout in minutes (default is 5)" }).positive().int().optional()
515
+ timeout: z13.number({ description: "Request timeout in minutes (default is 5)" }).positive().int().optional()
513
516
  });
514
517
 
515
518
  // packages/models/src/lib/core-config.ts
516
- var unrefinedCoreConfigSchema = z13.object({
517
- plugins: z13.array(pluginConfigSchema, {
519
+ var unrefinedCoreConfigSchema = z14.object({
520
+ plugins: z14.array(pluginConfigSchema, {
518
521
  description: "List of plugins to be used (official, community-provided, or custom)"
519
522
  }).min(1),
520
523
  /** portal configuration for persisting results */
@@ -537,7 +540,7 @@ function refineCoreConfig(schema) {
537
540
  }
538
541
 
539
542
  // packages/models/src/lib/report.ts
540
- import { z as z14 } from "zod";
543
+ import { z as z15 } from "zod";
541
544
  var auditReportSchema = auditSchema.merge(auditOutputSchema);
542
545
  var pluginReportSchema = pluginMetaSchema.merge(
543
546
  executionMetaSchema({
@@ -545,9 +548,9 @@ var pluginReportSchema = pluginMetaSchema.merge(
545
548
  descriptionDuration: "Duration of the plugin run in ms"
546
549
  })
547
550
  ).merge(
548
- z14.object({
549
- audits: z14.array(auditReportSchema).min(1),
550
- groups: z14.array(groupSchema).optional()
551
+ z15.object({
552
+ audits: z15.array(auditReportSchema).min(1),
553
+ groups: z15.array(groupSchema).optional()
551
554
  })
552
555
  ).refine(
553
556
  (pluginReport) => !getMissingRefsFromGroups2(pluginReport.audits, pluginReport.groups ?? []),
@@ -581,10 +584,10 @@ var reportSchema = packageVersionSchema({
581
584
  descriptionDuration: "Duration of the collect run in ms"
582
585
  })
583
586
  ).merge(
584
- z14.object(
587
+ z15.object(
585
588
  {
586
- categories: z14.array(categoryConfigSchema),
587
- plugins: z14.array(pluginReportSchema).min(1),
589
+ categories: z15.array(categoryConfigSchema),
590
+ plugins: z15.array(pluginReportSchema).min(1),
588
591
  commit: commitSchema.describe("Git commit for which report was collected").nullable()
589
592
  },
590
593
  { description: "Collect output data" }
@@ -600,40 +603,40 @@ var reportSchema = packageVersionSchema({
600
603
  );
601
604
 
602
605
  // packages/models/src/lib/reports-diff.ts
603
- import { z as z15 } from "zod";
606
+ import { z as z16 } from "zod";
604
607
  function makeComparisonSchema(schema) {
605
608
  const sharedDescription = schema.description || "Result";
606
- return z15.object({
609
+ return z16.object({
607
610
  before: schema.describe(`${sharedDescription} (source commit)`),
608
611
  after: schema.describe(`${sharedDescription} (target commit)`)
609
612
  });
610
613
  }
611
614
  function makeArraysComparisonSchema(diffSchema, resultSchema, description) {
612
- return z15.object(
615
+ return z16.object(
613
616
  {
614
- changed: z15.array(diffSchema),
615
- unchanged: z15.array(resultSchema),
616
- added: z15.array(resultSchema),
617
- removed: z15.array(resultSchema)
617
+ changed: z16.array(diffSchema),
618
+ unchanged: z16.array(resultSchema),
619
+ added: z16.array(resultSchema),
620
+ removed: z16.array(resultSchema)
618
621
  },
619
622
  { description }
620
623
  );
621
624
  }
622
- var scorableMetaSchema = z15.object({
625
+ var scorableMetaSchema = z16.object({
623
626
  slug: slugSchema,
624
627
  title: titleSchema,
625
628
  docsUrl: docsUrlSchema
626
629
  });
627
630
  var scorableWithPluginMetaSchema = scorableMetaSchema.merge(
628
- z15.object({
631
+ z16.object({
629
632
  plugin: pluginMetaSchema.pick({ slug: true, title: true, docsUrl: true }).describe("Plugin which defines it")
630
633
  })
631
634
  );
632
635
  var scorableDiffSchema = scorableMetaSchema.merge(
633
- z15.object({
636
+ z16.object({
634
637
  scores: makeComparisonSchema(scoreSchema).merge(
635
- z15.object({
636
- diff: z15.number().min(-1).max(1).describe("Score change (`scores.after - scores.before`)")
638
+ z16.object({
639
+ diff: z16.number().min(-1).max(1).describe("Score change (`scores.after - scores.before`)")
637
640
  })
638
641
  ).describe("Score comparison")
639
642
  })
@@ -644,10 +647,10 @@ var scorableWithPluginDiffSchema = scorableDiffSchema.merge(
644
647
  var categoryDiffSchema = scorableDiffSchema;
645
648
  var groupDiffSchema = scorableWithPluginDiffSchema;
646
649
  var auditDiffSchema = scorableWithPluginDiffSchema.merge(
647
- z15.object({
650
+ z16.object({
648
651
  values: makeComparisonSchema(auditValueSchema).merge(
649
- z15.object({
650
- diff: z15.number().int().describe("Value change (`values.after - values.before`)")
652
+ z16.object({
653
+ diff: z16.number().int().describe("Value change (`values.after - values.before`)")
651
654
  })
652
655
  ).describe("Audit `value` comparison"),
653
656
  displayValues: makeComparisonSchema(auditDisplayValueSchema).describe(
@@ -656,16 +659,18 @@ var auditDiffSchema = scorableWithPluginDiffSchema.merge(
656
659
  })
657
660
  );
658
661
  var categoryResultSchema = scorableMetaSchema.merge(
659
- z15.object({ score: scoreSchema })
662
+ z16.object({ score: scoreSchema })
660
663
  );
661
664
  var groupResultSchema = scorableWithPluginMetaSchema.merge(
662
- z15.object({ score: scoreSchema })
665
+ z16.object({ score: scoreSchema })
663
666
  );
664
667
  var auditResultSchema = scorableWithPluginMetaSchema.merge(
665
668
  auditOutputSchema.pick({ score: true, value: true, displayValue: true })
666
669
  );
667
- var reportsDiffSchema = z15.object({
670
+ var reportsDiffSchema = z16.object({
668
671
  commits: makeComparisonSchema(commitSchema).nullable().describe("Commits identifying compared reports"),
672
+ portalUrl: urlSchema.optional().describe("Link to comparison page in Code PushUp portal"),
673
+ label: z16.string().optional().describe("Label (e.g. project name)"),
669
674
  categories: makeArraysComparisonSchema(
670
675
  categoryDiffSchema,
671
676
  categoryResultSchema,
@@ -735,8 +740,24 @@ function comparePairs(pairs, equalsFn) {
735
740
  );
736
741
  }
737
742
 
743
+ // packages/utils/src/lib/errors.ts
744
+ function stringifyError(error) {
745
+ if (error instanceof Error) {
746
+ if (error.name === "Error" || error.message.startsWith(error.name)) {
747
+ return error.message;
748
+ }
749
+ return `${error.name}: ${error.message}`;
750
+ }
751
+ if (typeof error === "string") {
752
+ return error;
753
+ }
754
+ return JSON.stringify(error);
755
+ }
756
+
738
757
  // packages/utils/src/lib/execute-process.ts
739
- import { spawn } from "node:child_process";
758
+ import {
759
+ spawn
760
+ } from "node:child_process";
740
761
 
741
762
  // packages/utils/src/lib/reports/utils.ts
742
763
  import ansis from "ansis";
@@ -752,6 +773,7 @@ var FOOTER_PREFIX = "Made with \u2764 by";
752
773
  var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
753
774
  var README_LINK = "https://github.com/code-pushup/cli#readme";
754
775
  var REPORT_HEADLINE_TEXT = "Code PushUp Report";
776
+ var CODE_PUSHUP_UNICODE_LOGO = "<\u2713>";
755
777
  var REPORT_RAW_OVERVIEW_TABLE_HEADERS = [
756
778
  "Category",
757
779
  "Score",
@@ -822,9 +844,17 @@ function severityMarker(severity) {
822
844
  }
823
845
  return "\u2139\uFE0F";
824
846
  }
847
+ var MIN_NON_ZERO_RESULT = 0.1;
848
+ function roundValue(value) {
849
+ const roundedValue = Math.round(value * 10) / 10;
850
+ if (roundedValue === 0 && value !== 0) {
851
+ return MIN_NON_ZERO_RESULT * Math.sign(value);
852
+ }
853
+ return roundedValue;
854
+ }
825
855
  function formatScoreChange(diff) {
826
856
  const marker = getDiffMarker(diff);
827
- const text = formatDiffNumber(Math.round(diff * 1e3) / 10);
857
+ const text = formatDiffNumber(roundValue(diff * 100));
828
858
  return colorByScoreDiff(`${marker} ${text}`, diff);
829
859
  }
830
860
  function formatValueChange({
@@ -832,7 +862,7 @@ function formatValueChange({
832
862
  scores
833
863
  }) {
834
864
  const marker = getDiffMarker(values.diff);
835
- const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : Math.round(100 * values.diff / values.before);
865
+ const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : roundValue(values.diff / values.before * 100);
836
866
  const text = `${formatDiffNumber(percentage)}\u2009%`;
837
867
  return colorByScoreDiff(`${marker} ${text}`, scores.diff);
838
868
  }
@@ -863,12 +893,12 @@ function countCategoryAudits(refs, plugins) {
863
893
  }, 0);
864
894
  }
865
895
  function compareCategoryAuditsAndGroups(a, b) {
866
- if (a.weight !== b.weight) {
867
- return b.weight - a.weight;
868
- }
869
896
  if (a.score !== b.score) {
870
897
  return a.score - b.score;
871
898
  }
899
+ if (a.weight !== b.weight) {
900
+ return b.weight - a.weight;
901
+ }
872
902
  if ("value" in a && "value" in b && a.value !== b.value) {
873
903
  return b.value - a.value;
874
904
  }
@@ -960,25 +990,29 @@ var ProcessError = class extends Error {
960
990
  }
961
991
  };
962
992
  function executeProcess(cfg) {
963
- const { observer, cwd, command, args, ignoreExitCode = false } = cfg;
964
- const { onStdout, onError, onComplete } = observer ?? {};
993
+ const { command, args, observer, ignoreExitCode = false, ...options } = cfg;
994
+ const { onStdout, onStderr, onError, onComplete } = observer ?? {};
965
995
  const date = (/* @__PURE__ */ new Date()).toISOString();
966
996
  const start = performance.now();
967
997
  return new Promise((resolve, reject) => {
968
- const process2 = spawn(command, args, { cwd, shell: true });
998
+ const spawnedProcess = spawn(command, args ?? [], {
999
+ shell: true,
1000
+ ...options
1001
+ });
969
1002
  let stdout = "";
970
1003
  let stderr = "";
971
- process2.stdout.on("data", (data) => {
1004
+ spawnedProcess.stdout.on("data", (data) => {
972
1005
  stdout += String(data);
973
- onStdout?.(String(data));
1006
+ onStdout?.(String(data), spawnedProcess);
974
1007
  });
975
- process2.stderr.on("data", (data) => {
1008
+ spawnedProcess.stderr.on("data", (data) => {
976
1009
  stderr += String(data);
1010
+ onStderr?.(String(data), spawnedProcess);
977
1011
  });
978
- process2.on("error", (err) => {
1012
+ spawnedProcess.on("error", (err) => {
979
1013
  stderr += err.toString();
980
1014
  });
981
- process2.on("close", (code2) => {
1015
+ spawnedProcess.on("close", (code2) => {
982
1016
  const timings = { date, duration: calcDuration(start) };
983
1017
  if (code2 === 0 || ignoreExitCode) {
984
1018
  onComplete?.();
@@ -1881,7 +1915,33 @@ var html = {
1881
1915
  };
1882
1916
 
1883
1917
  // packages/utils/src/lib/reports/formatting.ts
1884
- import { MarkdownDocument, md as md2 } from "build-md";
1918
+ import {
1919
+ MarkdownDocument,
1920
+ md as md2
1921
+ } from "build-md";
1922
+ import { posix as pathPosix } from "node:path";
1923
+
1924
+ // packages/utils/src/lib/reports/ide-environment.ts
1925
+ function getEnvironmentType() {
1926
+ if (isVSCode()) {
1927
+ return "vscode";
1928
+ }
1929
+ if (isGitHub()) {
1930
+ return "github";
1931
+ }
1932
+ return "other";
1933
+ }
1934
+ function isVSCode() {
1935
+ return process.env["TERM_PROGRAM"] === "vscode";
1936
+ }
1937
+ function isGitHub() {
1938
+ return process.env["GITHUB_ACTIONS"] === "true";
1939
+ }
1940
+ function getGitHubBaseUrl() {
1941
+ return `${process.env["GITHUB_SERVER_URL"]}/${process.env["GITHUB_REPOSITORY"]}/blob/${process.env["GITHUB_SHA"]}`;
1942
+ }
1943
+
1944
+ // packages/utils/src/lib/reports/formatting.ts
1885
1945
  function tableSection(tableData, options) {
1886
1946
  if (tableData.rows.length === 0) {
1887
1947
  return null;
@@ -1919,6 +1979,44 @@ function metaDescription(audit) {
1919
1979
  }
1920
1980
  return "";
1921
1981
  }
1982
+ function linkToLocalSourceForIde(source, options) {
1983
+ const { file, position } = source;
1984
+ const { outputDir } = options ?? {};
1985
+ if (!outputDir) {
1986
+ return md2.code(file);
1987
+ }
1988
+ return md2.link(formatFileLink(file, position, outputDir), md2.code(file));
1989
+ }
1990
+ function formatSourceLine(position) {
1991
+ if (!position) {
1992
+ return "";
1993
+ }
1994
+ const { startLine, endLine } = position;
1995
+ return endLine && startLine !== endLine ? `${startLine}-${endLine}` : `${startLine}`;
1996
+ }
1997
+ function formatGitHubLink(file, position) {
1998
+ const baseUrl = getGitHubBaseUrl();
1999
+ if (!position) {
2000
+ return `${baseUrl}/${file}`;
2001
+ }
2002
+ const { startLine, endLine, startColumn, endColumn } = position;
2003
+ const start = startColumn ? `L${startLine}C${startColumn}` : `L${startLine}`;
2004
+ const end = endLine ? endColumn ? `L${endLine}C${endColumn}` : `L${endLine}` : "";
2005
+ const lineRange = end && start !== end ? `${start}-${end}` : start;
2006
+ return `${baseUrl}/${file}#${lineRange}`;
2007
+ }
2008
+ function formatFileLink(file, position, outputDir) {
2009
+ const relativePath = pathPosix.relative(outputDir, file);
2010
+ const env = getEnvironmentType();
2011
+ switch (env) {
2012
+ case "vscode":
2013
+ return position ? `${relativePath}#L${position.startLine}` : relativePath;
2014
+ case "github":
2015
+ return formatGitHubLink(file, position);
2016
+ default:
2017
+ return relativePath;
2018
+ }
2019
+ }
1922
2020
 
1923
2021
  // packages/utils/src/lib/reports/generate-md-report-categoy-section.ts
1924
2022
  import { MarkdownDocument as MarkdownDocument2, md as md3 } from "build-md";
@@ -2120,16 +2218,16 @@ function auditDetailsAuditValue({
2120
2218
  String(displayValue ?? value)
2121
2219
  )} (score: ${formatReportScore(score)})`;
2122
2220
  }
2123
- function generateMdReport(report) {
2221
+ function generateMdReport(report, options) {
2124
2222
  return new MarkdownDocument3().heading(HIERARCHY.level_1, REPORT_HEADLINE_TEXT).$if(
2125
2223
  report.categories.length > 0,
2126
2224
  (doc) => doc.$concat(
2127
2225
  categoriesOverviewSection(report),
2128
2226
  categoriesDetailsSection(report)
2129
2227
  )
2130
- ).$concat(auditsSection(report), aboutSection(report)).rule().paragraph(md4`${FOOTER_PREFIX} ${md4.link(README_LINK, "Code PushUp")}`).toString();
2228
+ ).$concat(auditsSection(report, options), aboutSection(report)).rule().paragraph(md4`${FOOTER_PREFIX} ${md4.link(README_LINK, "Code PushUp")}`).toString();
2131
2229
  }
2132
- function auditDetailsIssues(issues = []) {
2230
+ function auditDetailsIssues(issues = [], options) {
2133
2231
  if (issues.length === 0) {
2134
2232
  return null;
2135
2233
  }
@@ -2145,39 +2243,36 @@ function auditDetailsIssues(issues = []) {
2145
2243
  if (!source) {
2146
2244
  return [severity, message];
2147
2245
  }
2148
- const file = md4.code(source.file);
2246
+ const file = linkToLocalSourceForIde(source, options);
2149
2247
  if (!source.position) {
2150
2248
  return [severity, message, file];
2151
2249
  }
2152
- const { startLine, endLine } = source.position;
2153
- const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
2250
+ const line = formatSourceLine(source.position);
2154
2251
  return [severity, message, file, line];
2155
2252
  })
2156
2253
  );
2157
2254
  }
2158
- function auditDetails(audit) {
2255
+ function auditDetails(audit, options) {
2159
2256
  const { table: table2, issues = [] } = audit.details ?? {};
2160
2257
  const detailsValue = auditDetailsAuditValue(audit);
2161
2258
  if (issues.length === 0 && !table2?.rows.length) {
2162
2259
  return new MarkdownDocument3().paragraph(detailsValue);
2163
2260
  }
2164
2261
  const tableSectionContent = table2 && tableSection(table2);
2165
- const issuesSectionContent = issues.length > 0 && auditDetailsIssues(issues);
2262
+ const issuesSectionContent = issues.length > 0 && auditDetailsIssues(issues, options);
2166
2263
  return new MarkdownDocument3().details(
2167
2264
  detailsValue,
2168
2265
  new MarkdownDocument3().$concat(tableSectionContent, issuesSectionContent)
2169
2266
  );
2170
2267
  }
2171
- function auditsSection({
2172
- plugins
2173
- }) {
2268
+ function auditsSection({ plugins }, options) {
2174
2269
  return new MarkdownDocument3().heading(HIERARCHY.level_2, "\u{1F6E1}\uFE0F Audits").$foreach(
2175
2270
  plugins.flatMap(
2176
2271
  (plugin) => plugin.audits.map((audit) => ({ ...audit, plugin }))
2177
2272
  ),
2178
2273
  (doc, { plugin, ...audit }) => {
2179
2274
  const auditTitle = `${audit.title} (${plugin.title})`;
2180
- const detailsContent = auditDetails(audit);
2275
+ const detailsContent = auditDetails(audit, options);
2181
2276
  const descriptionContent = metaDescription(audit);
2182
2277
  return doc.heading(HIERARCHY.level_3, auditTitle).$concat(detailsContent).paragraph(descriptionContent);
2183
2278
  }
@@ -2241,19 +2336,111 @@ function reportMetaTable({
2241
2336
 
2242
2337
  // packages/utils/src/lib/reports/generate-md-reports-diff.ts
2243
2338
  import {
2244
- MarkdownDocument as MarkdownDocument4,
2245
- md as md5
2339
+ MarkdownDocument as MarkdownDocument5,
2340
+ md as md6
2246
2341
  } from "build-md";
2342
+
2343
+ // packages/utils/src/lib/reports/generate-md-reports-diff-utils.ts
2344
+ import { MarkdownDocument as MarkdownDocument4, md as md5 } from "build-md";
2247
2345
  var MAX_ROWS = 100;
2248
- function generateMdReportsDiff(diff, portalUrl) {
2249
- return new MarkdownDocument4().$concat(
2250
- createDiffHeaderSection(diff, portalUrl),
2251
- createDiffCategoriesSection(diff),
2252
- createDiffGroupsSection(diff),
2253
- createDiffAuditsSection(diff)
2254
- ).toString();
2346
+ function summarizeUnchanged(token, { changed, unchanged }) {
2347
+ const pluralizedCount = changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`;
2348
+ const pluralizedVerb = unchanged.length === 1 ? "is" : "are";
2349
+ return `${pluralizedCount} ${pluralizedVerb} unchanged.`;
2350
+ }
2351
+ function summarizeDiffOutcomes(outcomes, token) {
2352
+ return objectToEntries(countDiffOutcomes(outcomes)).filter(
2353
+ (entry) => entry[0] !== "unchanged" && entry[1] > 0
2354
+ ).map(([outcome, count]) => {
2355
+ const formattedCount = `<strong>${count}</strong> ${pluralize(
2356
+ token,
2357
+ count
2358
+ )}`;
2359
+ switch (outcome) {
2360
+ case "positive":
2361
+ return `\u{1F44D} ${formattedCount} improved`;
2362
+ case "negative":
2363
+ return `\u{1F44E} ${formattedCount} regressed`;
2364
+ case "mixed":
2365
+ return `${formattedCount} changed without impacting score`;
2366
+ }
2367
+ }).join(", ");
2368
+ }
2369
+ function createGroupsOrAuditsDetails(token, { changed, unchanged }, ...[columns, rows]) {
2370
+ if (changed.length === 0) {
2371
+ return new MarkdownDocument4().paragraph(
2372
+ summarizeUnchanged(token, { changed, unchanged })
2373
+ );
2374
+ }
2375
+ return new MarkdownDocument4().table(columns, rows.slice(0, MAX_ROWS)).paragraph(
2376
+ changed.length > MAX_ROWS && md5.italic(
2377
+ `Only the ${MAX_ROWS} most affected ${pluralize(
2378
+ token
2379
+ )} are listed above for brevity.`
2380
+ )
2381
+ ).paragraph(
2382
+ unchanged.length > 0 && summarizeUnchanged(token, { changed, unchanged })
2383
+ );
2384
+ }
2385
+ function formatTitle({
2386
+ title,
2387
+ docsUrl
2388
+ }) {
2389
+ if (docsUrl) {
2390
+ return md5.link(docsUrl, title);
2391
+ }
2392
+ return title;
2393
+ }
2394
+ function formatPortalLink(portalUrl) {
2395
+ return portalUrl && md5.link(portalUrl, "\u{1F575}\uFE0F See full comparison in Code PushUp portal \u{1F50D}");
2396
+ }
2397
+ function sortChanges(changes) {
2398
+ return [...changes].sort(
2399
+ (a, b) => Math.abs(b.scores.diff) - Math.abs(a.scores.diff) || Math.abs(b.values?.diff ?? 0) - Math.abs(a.values?.diff ?? 0)
2400
+ );
2401
+ }
2402
+ function getDiffChanges(diff) {
2403
+ return [
2404
+ ...diff.categories.changed,
2405
+ ...diff.groups.changed,
2406
+ ...diff.audits.changed
2407
+ ];
2408
+ }
2409
+ function changesToDiffOutcomes(changes) {
2410
+ return changes.map((change) => {
2411
+ if (change.scores.diff > 0) {
2412
+ return "positive";
2413
+ }
2414
+ if (change.scores.diff < 0) {
2415
+ return "negative";
2416
+ }
2417
+ if (change.values != null && change.values.diff !== 0) {
2418
+ return "mixed";
2419
+ }
2420
+ return "unchanged";
2421
+ });
2422
+ }
2423
+ function mergeDiffOutcomes(outcomes) {
2424
+ if (outcomes.every((outcome) => outcome === "unchanged")) {
2425
+ return "unchanged";
2426
+ }
2427
+ if (outcomes.includes("positive") && !outcomes.includes("negative")) {
2428
+ return "positive";
2429
+ }
2430
+ if (outcomes.includes("negative") && !outcomes.includes("positive")) {
2431
+ return "negative";
2432
+ }
2433
+ return "mixed";
2255
2434
  }
2256
- function createDiffHeaderSection(diff, portalUrl) {
2435
+ function countDiffOutcomes(outcomes) {
2436
+ return {
2437
+ positive: outcomes.filter((outcome) => outcome === "positive").length,
2438
+ negative: outcomes.filter((outcome) => outcome === "negative").length,
2439
+ mixed: outcomes.filter((outcome) => outcome === "mixed").length,
2440
+ unchanged: outcomes.filter((outcome) => outcome === "unchanged").length
2441
+ };
2442
+ }
2443
+ function formatReportOutcome(outcome, commits) {
2257
2444
  const outcomeTexts = {
2258
2445
  positive: md5`🥳 Code PushUp report has ${md5.bold("improved")}`,
2259
2446
  negative: md5`😟 Code PushUp report has ${md5.bold("regressed")}`,
@@ -2262,36 +2449,91 @@ function createDiffHeaderSection(diff, portalUrl) {
2262
2449
  )}`,
2263
2450
  unchanged: md5`😐 Code PushUp report is ${md5.bold("unchanged")}`
2264
2451
  };
2452
+ if (commits) {
2453
+ const commitsText = `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
2454
+ return md5`${outcomeTexts[outcome]} – ${commitsText}.`;
2455
+ }
2456
+ return md5`${outcomeTexts[outcome]}.`;
2457
+ }
2458
+ function compareDiffsBy(type, a, b) {
2459
+ return sumScoreChanges(b[type].changed) - sumScoreChanges(a[type].changed) || sumConfigChanges(b[type]) - sumConfigChanges(a[type]);
2460
+ }
2461
+ function sumScoreChanges(changes) {
2462
+ return changes.reduce(
2463
+ (acc, { scores }) => acc + Math.abs(scores.diff),
2464
+ 0
2465
+ );
2466
+ }
2467
+ function sumConfigChanges({
2468
+ added,
2469
+ removed
2470
+ }) {
2471
+ return added.length + removed.length;
2472
+ }
2473
+
2474
+ // packages/utils/src/lib/reports/generate-md-reports-diff.ts
2475
+ function generateMdReportsDiff(diff) {
2476
+ return new MarkdownDocument5().$concat(
2477
+ createDiffHeaderSection(diff),
2478
+ createDiffCategoriesSection(diff),
2479
+ createDiffDetailsSection(diff)
2480
+ ).toString();
2481
+ }
2482
+ function generateMdReportsDiffForMonorepo(diffs) {
2483
+ const diffsWithOutcomes = diffs.map((diff) => ({
2484
+ ...diff,
2485
+ outcome: mergeDiffOutcomes(changesToDiffOutcomes(getDiffChanges(diff)))
2486
+ })).sort(
2487
+ (a, b) => compareDiffsBy("categories", a, b) || compareDiffsBy("groups", a, b) || compareDiffsBy("audits", a, b) || a.label.localeCompare(b.label)
2488
+ );
2489
+ const unchanged = diffsWithOutcomes.filter(
2490
+ ({ outcome }) => outcome === "unchanged"
2491
+ );
2492
+ const changed = diffsWithOutcomes.filter((diff) => !unchanged.includes(diff));
2493
+ return new MarkdownDocument5().$concat(
2494
+ createDiffHeaderSection(diffs),
2495
+ ...changed.map(createDiffProjectSection)
2496
+ ).$if(
2497
+ unchanged.length > 0,
2498
+ (doc) => doc.rule().paragraph(summarizeUnchanged("project", { unchanged, changed }))
2499
+ ).toString();
2500
+ }
2501
+ function createDiffHeaderSection(diff) {
2265
2502
  const outcome = mergeDiffOutcomes(
2266
- changesToDiffOutcomes([
2267
- ...diff.categories.changed,
2268
- ...diff.groups.changed,
2269
- ...diff.audits.changed
2270
- ])
2503
+ changesToDiffOutcomes(toArray(diff).flatMap(getDiffChanges))
2271
2504
  );
2272
- const styleCommits = (commits) => `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
2273
- return new MarkdownDocument4().heading(HIERARCHY.level_1, "Code PushUp").paragraph(
2274
- diff.commits ? md5`${outcomeTexts[outcome]} – ${styleCommits(diff.commits)}.` : outcomeTexts[outcome]
2275
- ).paragraph(
2276
- portalUrl && md5.link(portalUrl, "\u{1F575}\uFE0F See full comparison in Code PushUp portal \u{1F50D}")
2505
+ const commits = Array.isArray(diff) ? diff[0]?.commits : diff.commits;
2506
+ const portalUrl = Array.isArray(diff) ? void 0 : diff.portalUrl;
2507
+ return new MarkdownDocument5().heading(HIERARCHY.level_1, "Code PushUp").paragraph(formatReportOutcome(outcome, commits)).paragraph(formatPortalLink(portalUrl));
2508
+ }
2509
+ function createDiffProjectSection(diff) {
2510
+ return new MarkdownDocument5().heading(HIERARCHY.level_2, md6`💼 Project ${md6.code(diff.label)}`).paragraph(formatReportOutcome(diff.outcome)).paragraph(formatPortalLink(diff.portalUrl)).$concat(
2511
+ createDiffCategoriesSection(diff, {
2512
+ skipHeading: true,
2513
+ skipUnchanged: true
2514
+ }),
2515
+ createDiffDetailsSection(diff, HIERARCHY.level_3)
2277
2516
  );
2278
2517
  }
2279
- function createDiffCategoriesSection(diff) {
2518
+ function createDiffCategoriesSection(diff, options) {
2280
2519
  const { changed, unchanged, added } = diff.categories;
2520
+ const { skipHeading, skipUnchanged } = options ?? {};
2281
2521
  const categoriesCount = changed.length + unchanged.length + added.length;
2282
2522
  const hasChanges = unchanged.length < categoriesCount;
2283
2523
  if (categoriesCount === 0) {
2284
2524
  return null;
2285
2525
  }
2286
- const columns = [
2287
- { heading: "\u{1F3F7}\uFE0F Category", alignment: "left" },
2288
- {
2289
- heading: hasChanges ? "\u2B50 Previous score" : "\u2B50 Score",
2290
- alignment: "center"
2291
- },
2292
- { heading: "\u2B50 Current score", alignment: "center" },
2293
- { heading: "\u{1F504} Score change", alignment: "center" }
2294
- ];
2526
+ const [columns, rows] = createCategoriesTable(diff, {
2527
+ hasChanges,
2528
+ skipUnchanged
2529
+ });
2530
+ return new MarkdownDocument5().heading(HIERARCHY.level_2, !skipHeading && "\u{1F3F7}\uFE0F Categories").table(columns, rows).paragraph(added.length > 0 && md6.italic("(\\*) New category.")).paragraph(
2531
+ skipUnchanged && unchanged.length > 0 && summarizeUnchanged("category", { changed, unchanged })
2532
+ );
2533
+ }
2534
+ function createCategoriesTable(diff, options) {
2535
+ const { changed, unchanged, added } = diff.categories;
2536
+ const { hasChanges, skipUnchanged } = options;
2295
2537
  const rows = [
2296
2538
  ...sortChanges(changed).map((category) => [
2297
2539
  formatTitle(category),
@@ -2303,27 +2545,55 @@ function createDiffCategoriesSection(diff) {
2303
2545
  ]),
2304
2546
  ...added.map((category) => [
2305
2547
  formatTitle(category),
2306
- md5.italic("n/a (\\*)"),
2548
+ md6.italic("n/a (\\*)"),
2307
2549
  formatScoreWithColor(category.score),
2308
- md5.italic("n/a (\\*)")
2550
+ md6.italic("n/a (\\*)")
2309
2551
  ]),
2310
- ...unchanged.map((category) => [
2552
+ ...skipUnchanged ? [] : unchanged.map((category) => [
2311
2553
  formatTitle(category),
2312
2554
  formatScoreWithColor(category.score, { skipBold: true }),
2313
2555
  formatScoreWithColor(category.score),
2314
2556
  "\u2013"
2315
2557
  ])
2316
2558
  ];
2317
- return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F3F7}\uFE0F Categories").table(
2559
+ if (rows.length === 0) {
2560
+ return [[], []];
2561
+ }
2562
+ const columns = [
2563
+ { heading: "\u{1F3F7}\uFE0F Category", alignment: "left" },
2564
+ {
2565
+ heading: hasChanges ? "\u2B50 Previous score" : "\u2B50 Score",
2566
+ alignment: "center"
2567
+ },
2568
+ { heading: "\u2B50 Current score", alignment: "center" },
2569
+ { heading: "\u{1F504} Score change", alignment: "center" }
2570
+ ];
2571
+ return [
2318
2572
  hasChanges ? columns : columns.slice(0, 2),
2319
2573
  rows.map((row) => hasChanges ? row : row.slice(0, 2))
2320
- ).paragraph(added.length > 0 && md5.italic("(\\*) New category."));
2574
+ ];
2321
2575
  }
2322
- function createDiffGroupsSection(diff) {
2576
+ function createDiffDetailsSection(diff, level = HIERARCHY.level_2) {
2577
+ if (diff.groups.changed.length + diff.audits.changed.length === 0) {
2578
+ return null;
2579
+ }
2580
+ const summary = ["group", "audit"].map(
2581
+ (token) => summarizeDiffOutcomes(
2582
+ changesToDiffOutcomes(diff[`${token}s`].changed),
2583
+ token
2584
+ )
2585
+ ).filter(Boolean).join(", ");
2586
+ const details2 = new MarkdownDocument5().$concat(
2587
+ createDiffGroupsSection(diff, level),
2588
+ createDiffAuditsSection(diff, level)
2589
+ );
2590
+ return new MarkdownDocument5().details(summary, details2);
2591
+ }
2592
+ function createDiffGroupsSection(diff, level) {
2323
2593
  if (diff.groups.changed.length + diff.groups.unchanged.length === 0) {
2324
2594
  return null;
2325
2595
  }
2326
- return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F5C3}\uFE0F Groups").$concat(
2596
+ return new MarkdownDocument5().heading(level, "\u{1F5C3}\uFE0F Groups").$concat(
2327
2597
  createGroupsOrAuditsDetails(
2328
2598
  "group",
2329
2599
  diff.groups,
@@ -2344,8 +2614,8 @@ function createDiffGroupsSection(diff) {
2344
2614
  )
2345
2615
  );
2346
2616
  }
2347
- function createDiffAuditsSection(diff) {
2348
- return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F6E1}\uFE0F Audits").$concat(
2617
+ function createDiffAuditsSection(diff, level) {
2618
+ return new MarkdownDocument5().heading(level, "\u{1F6E1}\uFE0F Audits").$concat(
2349
2619
  createGroupsOrAuditsDetails(
2350
2620
  "audit",
2351
2621
  diff.audits,
@@ -2360,7 +2630,7 @@ function createDiffAuditsSection(diff) {
2360
2630
  formatTitle(audit.plugin),
2361
2631
  formatTitle(audit),
2362
2632
  `${scoreMarker(audit.scores.before, "square")} ${audit.displayValues.before || audit.values.before.toString()}`,
2363
- md5`${scoreMarker(audit.scores.after, "square")} ${md5.bold(
2633
+ md6`${scoreMarker(audit.scores.after, "square")} ${md6.bold(
2364
2634
  audit.displayValues.after || audit.values.after.toString()
2365
2635
  )}`,
2366
2636
  formatValueChange(audit)
@@ -2368,96 +2638,6 @@ function createDiffAuditsSection(diff) {
2368
2638
  )
2369
2639
  );
2370
2640
  }
2371
- function createGroupsOrAuditsDetails(token, { changed, unchanged }, ...[columns, rows]) {
2372
- if (changed.length === 0) {
2373
- return new MarkdownDocument4().paragraph(
2374
- summarizeUnchanged(token, { changed, unchanged })
2375
- );
2376
- }
2377
- return new MarkdownDocument4().details(
2378
- summarizeDiffOutcomes(changesToDiffOutcomes(changed), token),
2379
- md5`${md5.table(columns, rows.slice(0, MAX_ROWS))}${changed.length > MAX_ROWS ? md5.paragraph(
2380
- md5.italic(
2381
- `Only the ${MAX_ROWS} most affected ${pluralize(
2382
- token
2383
- )} are listed above for brevity.`
2384
- )
2385
- ) : ""}${unchanged.length > 0 ? md5.paragraph(summarizeUnchanged(token, { changed, unchanged })) : ""}`
2386
- );
2387
- }
2388
- function summarizeUnchanged(token, { changed, unchanged }) {
2389
- return [
2390
- changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`,
2391
- unchanged.length === 1 ? "is" : "are",
2392
- "unchanged."
2393
- ].join(" ");
2394
- }
2395
- function summarizeDiffOutcomes(outcomes, token) {
2396
- return objectToEntries(countDiffOutcomes(outcomes)).filter(
2397
- (entry) => entry[0] !== "unchanged" && entry[1] > 0
2398
- ).map(([outcome, count]) => {
2399
- const formattedCount = `<strong>${count}</strong> ${pluralize(
2400
- token,
2401
- count
2402
- )}`;
2403
- switch (outcome) {
2404
- case "positive":
2405
- return `\u{1F44D} ${formattedCount} improved`;
2406
- case "negative":
2407
- return `\u{1F44E} ${formattedCount} regressed`;
2408
- case "mixed":
2409
- return `${formattedCount} changed without impacting score`;
2410
- }
2411
- }).join(", ");
2412
- }
2413
- function formatTitle({
2414
- title,
2415
- docsUrl
2416
- }) {
2417
- if (docsUrl) {
2418
- return md5.link(docsUrl, title);
2419
- }
2420
- return title;
2421
- }
2422
- function sortChanges(changes) {
2423
- return [...changes].sort(
2424
- (a, b) => Math.abs(b.scores.diff) - Math.abs(a.scores.diff) || Math.abs(b.values?.diff ?? 0) - Math.abs(a.values?.diff ?? 0)
2425
- );
2426
- }
2427
- function changesToDiffOutcomes(changes) {
2428
- return changes.map((change) => {
2429
- if (change.scores.diff > 0) {
2430
- return "positive";
2431
- }
2432
- if (change.scores.diff < 0) {
2433
- return "negative";
2434
- }
2435
- if (change.values != null && change.values.diff !== 0) {
2436
- return "mixed";
2437
- }
2438
- return "unchanged";
2439
- });
2440
- }
2441
- function mergeDiffOutcomes(outcomes) {
2442
- if (outcomes.every((outcome) => outcome === "unchanged")) {
2443
- return "unchanged";
2444
- }
2445
- if (outcomes.includes("positive") && !outcomes.includes("negative")) {
2446
- return "positive";
2447
- }
2448
- if (outcomes.includes("negative") && !outcomes.includes("positive")) {
2449
- return "negative";
2450
- }
2451
- return "mixed";
2452
- }
2453
- function countDiffOutcomes(outcomes) {
2454
- return {
2455
- positive: outcomes.filter((outcome) => outcome === "positive").length,
2456
- negative: outcomes.filter((outcome) => outcome === "negative").length,
2457
- mixed: outcomes.filter((outcome) => outcome === "mixed").length,
2458
- unchanged: outcomes.filter((outcome) => outcome === "unchanged").length
2459
- };
2460
- }
2461
2641
 
2462
2642
  // packages/utils/src/lib/reports/load-report.ts
2463
2643
  import { join as join3 } from "node:path";
@@ -2514,7 +2694,8 @@ function logPlugins(report) {
2514
2694
  },
2515
2695
  {
2516
2696
  text: cyanBright(audit.displayValue || `${audit.value}`),
2517
- width: 10,
2697
+ // eslint-disable-next-line no-magic-numbers
2698
+ width: 20,
2518
2699
  padding: [0, 0, 0, 0]
2519
2700
  }
2520
2701
  ]);
@@ -2666,6 +2847,7 @@ var verboseUtils = (verbose = false) => ({
2666
2847
  });
2667
2848
  export {
2668
2849
  CODE_PUSHUP_DOMAIN,
2850
+ CODE_PUSHUP_UNICODE_LOGO,
2669
2851
  FOOTER_PREFIX,
2670
2852
  HIERARCHY,
2671
2853
  NEW_LINE,
@@ -2693,9 +2875,11 @@ export {
2693
2875
  formatBytes,
2694
2876
  formatDuration,
2695
2877
  formatGitPath,
2878
+ formatReportScore,
2696
2879
  fromJsonLines,
2697
2880
  generateMdReport,
2698
2881
  generateMdReportsDiff,
2882
+ generateMdReportsDiffForMonorepo,
2699
2883
  getCurrentBranchOrTag,
2700
2884
  getGitRoot,
2701
2885
  getHashFromTag,
@@ -2735,6 +2919,7 @@ export {
2735
2919
  slugify,
2736
2920
  sortReport,
2737
2921
  sortSemvers,
2922
+ stringifyError,
2738
2923
  toArray,
2739
2924
  toGitPath,
2740
2925
  toJsonLines,