@code-pushup/core 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 */
@@ -541,7 +544,7 @@ var CONFIG_FILE_NAME = "code-pushup.config";
541
544
  var SUPPORTED_CONFIG_FILE_FORMATS = ["ts", "mjs", "js"];
542
545
 
543
546
  // packages/models/src/lib/report.ts
544
- import { z as z14 } from "zod";
547
+ import { z as z15 } from "zod";
545
548
  var auditReportSchema = auditSchema.merge(auditOutputSchema);
546
549
  var pluginReportSchema = pluginMetaSchema.merge(
547
550
  executionMetaSchema({
@@ -549,9 +552,9 @@ var pluginReportSchema = pluginMetaSchema.merge(
549
552
  descriptionDuration: "Duration of the plugin run in ms"
550
553
  })
551
554
  ).merge(
552
- z14.object({
553
- audits: z14.array(auditReportSchema).min(1),
554
- groups: z14.array(groupSchema).optional()
555
+ z15.object({
556
+ audits: z15.array(auditReportSchema).min(1),
557
+ groups: z15.array(groupSchema).optional()
555
558
  })
556
559
  ).refine(
557
560
  (pluginReport) => !getMissingRefsFromGroups2(pluginReport.audits, pluginReport.groups ?? []),
@@ -585,10 +588,10 @@ var reportSchema = packageVersionSchema({
585
588
  descriptionDuration: "Duration of the collect run in ms"
586
589
  })
587
590
  ).merge(
588
- z14.object(
591
+ z15.object(
589
592
  {
590
- categories: z14.array(categoryConfigSchema),
591
- plugins: z14.array(pluginReportSchema).min(1),
593
+ categories: z15.array(categoryConfigSchema),
594
+ plugins: z15.array(pluginReportSchema).min(1),
592
595
  commit: commitSchema.describe("Git commit for which report was collected").nullable()
593
596
  },
594
597
  { description: "Collect output data" }
@@ -604,40 +607,40 @@ var reportSchema = packageVersionSchema({
604
607
  );
605
608
 
606
609
  // packages/models/src/lib/reports-diff.ts
607
- import { z as z15 } from "zod";
610
+ import { z as z16 } from "zod";
608
611
  function makeComparisonSchema(schema) {
609
612
  const sharedDescription = schema.description || "Result";
610
- return z15.object({
613
+ return z16.object({
611
614
  before: schema.describe(`${sharedDescription} (source commit)`),
612
615
  after: schema.describe(`${sharedDescription} (target commit)`)
613
616
  });
614
617
  }
615
618
  function makeArraysComparisonSchema(diffSchema, resultSchema, description) {
616
- return z15.object(
619
+ return z16.object(
617
620
  {
618
- changed: z15.array(diffSchema),
619
- unchanged: z15.array(resultSchema),
620
- added: z15.array(resultSchema),
621
- removed: z15.array(resultSchema)
621
+ changed: z16.array(diffSchema),
622
+ unchanged: z16.array(resultSchema),
623
+ added: z16.array(resultSchema),
624
+ removed: z16.array(resultSchema)
622
625
  },
623
626
  { description }
624
627
  );
625
628
  }
626
- var scorableMetaSchema = z15.object({
629
+ var scorableMetaSchema = z16.object({
627
630
  slug: slugSchema,
628
631
  title: titleSchema,
629
632
  docsUrl: docsUrlSchema
630
633
  });
631
634
  var scorableWithPluginMetaSchema = scorableMetaSchema.merge(
632
- z15.object({
635
+ z16.object({
633
636
  plugin: pluginMetaSchema.pick({ slug: true, title: true, docsUrl: true }).describe("Plugin which defines it")
634
637
  })
635
638
  );
636
639
  var scorableDiffSchema = scorableMetaSchema.merge(
637
- z15.object({
640
+ z16.object({
638
641
  scores: makeComparisonSchema(scoreSchema).merge(
639
- z15.object({
640
- diff: z15.number().min(-1).max(1).describe("Score change (`scores.after - scores.before`)")
642
+ z16.object({
643
+ diff: z16.number().min(-1).max(1).describe("Score change (`scores.after - scores.before`)")
641
644
  })
642
645
  ).describe("Score comparison")
643
646
  })
@@ -648,10 +651,10 @@ var scorableWithPluginDiffSchema = scorableDiffSchema.merge(
648
651
  var categoryDiffSchema = scorableDiffSchema;
649
652
  var groupDiffSchema = scorableWithPluginDiffSchema;
650
653
  var auditDiffSchema = scorableWithPluginDiffSchema.merge(
651
- z15.object({
654
+ z16.object({
652
655
  values: makeComparisonSchema(auditValueSchema).merge(
653
- z15.object({
654
- diff: z15.number().int().describe("Value change (`values.after - values.before`)")
656
+ z16.object({
657
+ diff: z16.number().int().describe("Value change (`values.after - values.before`)")
655
658
  })
656
659
  ).describe("Audit `value` comparison"),
657
660
  displayValues: makeComparisonSchema(auditDisplayValueSchema).describe(
@@ -660,16 +663,18 @@ var auditDiffSchema = scorableWithPluginDiffSchema.merge(
660
663
  })
661
664
  );
662
665
  var categoryResultSchema = scorableMetaSchema.merge(
663
- z15.object({ score: scoreSchema })
666
+ z16.object({ score: scoreSchema })
664
667
  );
665
668
  var groupResultSchema = scorableWithPluginMetaSchema.merge(
666
- z15.object({ score: scoreSchema })
669
+ z16.object({ score: scoreSchema })
667
670
  );
668
671
  var auditResultSchema = scorableWithPluginMetaSchema.merge(
669
672
  auditOutputSchema.pick({ score: true, value: true, displayValue: true })
670
673
  );
671
- var reportsDiffSchema = z15.object({
674
+ var reportsDiffSchema = z16.object({
672
675
  commits: makeComparisonSchema(commitSchema).nullable().describe("Commits identifying compared reports"),
676
+ portalUrl: urlSchema.optional().describe("Link to comparison page in Code PushUp portal"),
677
+ label: z16.string().optional().describe("Label (e.g. project name)"),
673
678
  categories: makeArraysComparisonSchema(
674
679
  categoryDiffSchema,
675
680
  categoryResultSchema,
@@ -739,8 +744,24 @@ function comparePairs(pairs, equalsFn) {
739
744
  );
740
745
  }
741
746
 
747
+ // packages/utils/src/lib/errors.ts
748
+ function stringifyError(error) {
749
+ if (error instanceof Error) {
750
+ if (error.name === "Error" || error.message.startsWith(error.name)) {
751
+ return error.message;
752
+ }
753
+ return `${error.name}: ${error.message}`;
754
+ }
755
+ if (typeof error === "string") {
756
+ return error;
757
+ }
758
+ return JSON.stringify(error);
759
+ }
760
+
742
761
  // packages/utils/src/lib/execute-process.ts
743
- import { spawn } from "node:child_process";
762
+ import {
763
+ spawn
764
+ } from "node:child_process";
744
765
 
745
766
  // packages/utils/src/lib/reports/utils.ts
746
767
  import ansis from "ansis";
@@ -826,9 +847,17 @@ function severityMarker(severity) {
826
847
  }
827
848
  return "\u2139\uFE0F";
828
849
  }
850
+ var MIN_NON_ZERO_RESULT = 0.1;
851
+ function roundValue(value) {
852
+ const roundedValue = Math.round(value * 10) / 10;
853
+ if (roundedValue === 0 && value !== 0) {
854
+ return MIN_NON_ZERO_RESULT * Math.sign(value);
855
+ }
856
+ return roundedValue;
857
+ }
829
858
  function formatScoreChange(diff) {
830
859
  const marker = getDiffMarker(diff);
831
- const text = formatDiffNumber(Math.round(diff * 1e3) / 10);
860
+ const text = formatDiffNumber(roundValue(diff * 100));
832
861
  return colorByScoreDiff(`${marker} ${text}`, diff);
833
862
  }
834
863
  function formatValueChange({
@@ -836,7 +865,7 @@ function formatValueChange({
836
865
  scores
837
866
  }) {
838
867
  const marker = getDiffMarker(values.diff);
839
- const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : Math.round(100 * values.diff / values.before);
868
+ const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : roundValue(values.diff / values.before * 100);
840
869
  const text = `${formatDiffNumber(percentage)}\u2009%`;
841
870
  return colorByScoreDiff(`${marker} ${text}`, scores.diff);
842
871
  }
@@ -867,12 +896,12 @@ function countCategoryAudits(refs, plugins) {
867
896
  }, 0);
868
897
  }
869
898
  function compareCategoryAuditsAndGroups(a, b) {
870
- if (a.weight !== b.weight) {
871
- return b.weight - a.weight;
872
- }
873
899
  if (a.score !== b.score) {
874
900
  return a.score - b.score;
875
901
  }
902
+ if (a.weight !== b.weight) {
903
+ return b.weight - a.weight;
904
+ }
876
905
  if ("value" in a && "value" in b && a.value !== b.value) {
877
906
  return b.value - a.value;
878
907
  }
@@ -964,25 +993,29 @@ var ProcessError = class extends Error {
964
993
  }
965
994
  };
966
995
  function executeProcess(cfg) {
967
- const { observer, cwd, command, args, ignoreExitCode = false } = cfg;
968
- const { onStdout, onError, onComplete } = observer ?? {};
996
+ const { command, args, observer, ignoreExitCode = false, ...options } = cfg;
997
+ const { onStdout, onStderr, onError, onComplete } = observer ?? {};
969
998
  const date = (/* @__PURE__ */ new Date()).toISOString();
970
999
  const start = performance.now();
971
1000
  return new Promise((resolve, reject) => {
972
- const process2 = spawn(command, args, { cwd, shell: true });
1001
+ const spawnedProcess = spawn(command, args ?? [], {
1002
+ shell: true,
1003
+ ...options
1004
+ });
973
1005
  let stdout = "";
974
1006
  let stderr = "";
975
- process2.stdout.on("data", (data) => {
1007
+ spawnedProcess.stdout.on("data", (data) => {
976
1008
  stdout += String(data);
977
- onStdout?.(String(data));
1009
+ onStdout?.(String(data), spawnedProcess);
978
1010
  });
979
- process2.stderr.on("data", (data) => {
1011
+ spawnedProcess.stderr.on("data", (data) => {
980
1012
  stderr += String(data);
1013
+ onStderr?.(String(data), spawnedProcess);
981
1014
  });
982
- process2.on("error", (err) => {
1015
+ spawnedProcess.on("error", (err) => {
983
1016
  stderr += err.toString();
984
1017
  });
985
- process2.on("close", (code2) => {
1018
+ spawnedProcess.on("close", (code2) => {
986
1019
  const timings = { date, duration: calcDuration(start) };
987
1020
  if (code2 === 0 || ignoreExitCode) {
988
1021
  onComplete?.();
@@ -1181,6 +1214,9 @@ import { isAbsolute, join, relative } from "node:path";
1181
1214
  import { simpleGit } from "simple-git";
1182
1215
 
1183
1216
  // packages/utils/src/lib/transform.ts
1217
+ function toArray(val) {
1218
+ return Array.isArray(val) ? val : [val];
1219
+ }
1184
1220
  function objectToEntries(obj) {
1185
1221
  return Object.entries(obj);
1186
1222
  }
@@ -1452,7 +1488,33 @@ function getColumnAlignments(tableData) {
1452
1488
  }
1453
1489
 
1454
1490
  // packages/utils/src/lib/reports/formatting.ts
1455
- import { MarkdownDocument, md as md2 } from "build-md";
1491
+ import {
1492
+ MarkdownDocument,
1493
+ md as md2
1494
+ } from "build-md";
1495
+ import { posix as pathPosix } from "node:path";
1496
+
1497
+ // packages/utils/src/lib/reports/ide-environment.ts
1498
+ function getEnvironmentType() {
1499
+ if (isVSCode()) {
1500
+ return "vscode";
1501
+ }
1502
+ if (isGitHub()) {
1503
+ return "github";
1504
+ }
1505
+ return "other";
1506
+ }
1507
+ function isVSCode() {
1508
+ return process.env["TERM_PROGRAM"] === "vscode";
1509
+ }
1510
+ function isGitHub() {
1511
+ return process.env["GITHUB_ACTIONS"] === "true";
1512
+ }
1513
+ function getGitHubBaseUrl() {
1514
+ return `${process.env["GITHUB_SERVER_URL"]}/${process.env["GITHUB_REPOSITORY"]}/blob/${process.env["GITHUB_SHA"]}`;
1515
+ }
1516
+
1517
+ // packages/utils/src/lib/reports/formatting.ts
1456
1518
  function tableSection(tableData, options) {
1457
1519
  if (tableData.rows.length === 0) {
1458
1520
  return null;
@@ -1490,6 +1552,44 @@ function metaDescription(audit) {
1490
1552
  }
1491
1553
  return "";
1492
1554
  }
1555
+ function linkToLocalSourceForIde(source, options) {
1556
+ const { file, position } = source;
1557
+ const { outputDir } = options ?? {};
1558
+ if (!outputDir) {
1559
+ return md2.code(file);
1560
+ }
1561
+ return md2.link(formatFileLink(file, position, outputDir), md2.code(file));
1562
+ }
1563
+ function formatSourceLine(position) {
1564
+ if (!position) {
1565
+ return "";
1566
+ }
1567
+ const { startLine, endLine } = position;
1568
+ return endLine && startLine !== endLine ? `${startLine}-${endLine}` : `${startLine}`;
1569
+ }
1570
+ function formatGitHubLink(file, position) {
1571
+ const baseUrl = getGitHubBaseUrl();
1572
+ if (!position) {
1573
+ return `${baseUrl}/${file}`;
1574
+ }
1575
+ const { startLine, endLine, startColumn, endColumn } = position;
1576
+ const start = startColumn ? `L${startLine}C${startColumn}` : `L${startLine}`;
1577
+ const end = endLine ? endColumn ? `L${endLine}C${endColumn}` : `L${endLine}` : "";
1578
+ const lineRange = end && start !== end ? `${start}-${end}` : start;
1579
+ return `${baseUrl}/${file}#${lineRange}`;
1580
+ }
1581
+ function formatFileLink(file, position, outputDir) {
1582
+ const relativePath = pathPosix.relative(outputDir, file);
1583
+ const env = getEnvironmentType();
1584
+ switch (env) {
1585
+ case "vscode":
1586
+ return position ? `${relativePath}#L${position.startLine}` : relativePath;
1587
+ case "github":
1588
+ return formatGitHubLink(file, position);
1589
+ default:
1590
+ return relativePath;
1591
+ }
1592
+ }
1493
1593
 
1494
1594
  // packages/utils/src/lib/reports/generate-md-report-categoy-section.ts
1495
1595
  import { MarkdownDocument as MarkdownDocument2, md as md3 } from "build-md";
@@ -1691,16 +1791,16 @@ function auditDetailsAuditValue({
1691
1791
  String(displayValue ?? value)
1692
1792
  )} (score: ${formatReportScore(score)})`;
1693
1793
  }
1694
- function generateMdReport(report) {
1794
+ function generateMdReport(report, options) {
1695
1795
  return new MarkdownDocument3().heading(HIERARCHY.level_1, REPORT_HEADLINE_TEXT).$if(
1696
1796
  report.categories.length > 0,
1697
1797
  (doc) => doc.$concat(
1698
1798
  categoriesOverviewSection(report),
1699
1799
  categoriesDetailsSection(report)
1700
1800
  )
1701
- ).$concat(auditsSection(report), aboutSection(report)).rule().paragraph(md4`${FOOTER_PREFIX} ${md4.link(README_LINK, "Code PushUp")}`).toString();
1801
+ ).$concat(auditsSection(report, options), aboutSection(report)).rule().paragraph(md4`${FOOTER_PREFIX} ${md4.link(README_LINK, "Code PushUp")}`).toString();
1702
1802
  }
1703
- function auditDetailsIssues(issues = []) {
1803
+ function auditDetailsIssues(issues = [], options) {
1704
1804
  if (issues.length === 0) {
1705
1805
  return null;
1706
1806
  }
@@ -1716,39 +1816,36 @@ function auditDetailsIssues(issues = []) {
1716
1816
  if (!source) {
1717
1817
  return [severity, message];
1718
1818
  }
1719
- const file = md4.code(source.file);
1819
+ const file = linkToLocalSourceForIde(source, options);
1720
1820
  if (!source.position) {
1721
1821
  return [severity, message, file];
1722
1822
  }
1723
- const { startLine, endLine } = source.position;
1724
- const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
1823
+ const line = formatSourceLine(source.position);
1725
1824
  return [severity, message, file, line];
1726
1825
  })
1727
1826
  );
1728
1827
  }
1729
- function auditDetails(audit) {
1828
+ function auditDetails(audit, options) {
1730
1829
  const { table: table2, issues = [] } = audit.details ?? {};
1731
1830
  const detailsValue = auditDetailsAuditValue(audit);
1732
1831
  if (issues.length === 0 && !table2?.rows.length) {
1733
1832
  return new MarkdownDocument3().paragraph(detailsValue);
1734
1833
  }
1735
1834
  const tableSectionContent = table2 && tableSection(table2);
1736
- const issuesSectionContent = issues.length > 0 && auditDetailsIssues(issues);
1835
+ const issuesSectionContent = issues.length > 0 && auditDetailsIssues(issues, options);
1737
1836
  return new MarkdownDocument3().details(
1738
1837
  detailsValue,
1739
1838
  new MarkdownDocument3().$concat(tableSectionContent, issuesSectionContent)
1740
1839
  );
1741
1840
  }
1742
- function auditsSection({
1743
- plugins
1744
- }) {
1841
+ function auditsSection({ plugins }, options) {
1745
1842
  return new MarkdownDocument3().heading(HIERARCHY.level_2, "\u{1F6E1}\uFE0F Audits").$foreach(
1746
1843
  plugins.flatMap(
1747
1844
  (plugin) => plugin.audits.map((audit) => ({ ...audit, plugin }))
1748
1845
  ),
1749
1846
  (doc, { plugin, ...audit }) => {
1750
1847
  const auditTitle = `${audit.title} (${plugin.title})`;
1751
- const detailsContent = auditDetails(audit);
1848
+ const detailsContent = auditDetails(audit, options);
1752
1849
  const descriptionContent = metaDescription(audit);
1753
1850
  return doc.heading(HIERARCHY.level_3, auditTitle).$concat(detailsContent).paragraph(descriptionContent);
1754
1851
  }
@@ -1812,19 +1909,111 @@ function reportMetaTable({
1812
1909
 
1813
1910
  // packages/utils/src/lib/reports/generate-md-reports-diff.ts
1814
1911
  import {
1815
- MarkdownDocument as MarkdownDocument4,
1816
- md as md5
1912
+ MarkdownDocument as MarkdownDocument5,
1913
+ md as md6
1817
1914
  } from "build-md";
1915
+
1916
+ // packages/utils/src/lib/reports/generate-md-reports-diff-utils.ts
1917
+ import { MarkdownDocument as MarkdownDocument4, md as md5 } from "build-md";
1818
1918
  var MAX_ROWS = 100;
1819
- function generateMdReportsDiff(diff, portalUrl) {
1820
- return new MarkdownDocument4().$concat(
1821
- createDiffHeaderSection(diff, portalUrl),
1822
- createDiffCategoriesSection(diff),
1823
- createDiffGroupsSection(diff),
1824
- createDiffAuditsSection(diff)
1825
- ).toString();
1919
+ function summarizeUnchanged(token, { changed, unchanged }) {
1920
+ const pluralizedCount = changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`;
1921
+ const pluralizedVerb = unchanged.length === 1 ? "is" : "are";
1922
+ return `${pluralizedCount} ${pluralizedVerb} unchanged.`;
1826
1923
  }
1827
- function createDiffHeaderSection(diff, portalUrl) {
1924
+ function summarizeDiffOutcomes(outcomes, token) {
1925
+ return objectToEntries(countDiffOutcomes(outcomes)).filter(
1926
+ (entry) => entry[0] !== "unchanged" && entry[1] > 0
1927
+ ).map(([outcome, count]) => {
1928
+ const formattedCount = `<strong>${count}</strong> ${pluralize(
1929
+ token,
1930
+ count
1931
+ )}`;
1932
+ switch (outcome) {
1933
+ case "positive":
1934
+ return `\u{1F44D} ${formattedCount} improved`;
1935
+ case "negative":
1936
+ return `\u{1F44E} ${formattedCount} regressed`;
1937
+ case "mixed":
1938
+ return `${formattedCount} changed without impacting score`;
1939
+ }
1940
+ }).join(", ");
1941
+ }
1942
+ function createGroupsOrAuditsDetails(token, { changed, unchanged }, ...[columns, rows]) {
1943
+ if (changed.length === 0) {
1944
+ return new MarkdownDocument4().paragraph(
1945
+ summarizeUnchanged(token, { changed, unchanged })
1946
+ );
1947
+ }
1948
+ return new MarkdownDocument4().table(columns, rows.slice(0, MAX_ROWS)).paragraph(
1949
+ changed.length > MAX_ROWS && md5.italic(
1950
+ `Only the ${MAX_ROWS} most affected ${pluralize(
1951
+ token
1952
+ )} are listed above for brevity.`
1953
+ )
1954
+ ).paragraph(
1955
+ unchanged.length > 0 && summarizeUnchanged(token, { changed, unchanged })
1956
+ );
1957
+ }
1958
+ function formatTitle({
1959
+ title,
1960
+ docsUrl
1961
+ }) {
1962
+ if (docsUrl) {
1963
+ return md5.link(docsUrl, title);
1964
+ }
1965
+ return title;
1966
+ }
1967
+ function formatPortalLink(portalUrl) {
1968
+ return portalUrl && md5.link(portalUrl, "\u{1F575}\uFE0F See full comparison in Code PushUp portal \u{1F50D}");
1969
+ }
1970
+ function sortChanges(changes) {
1971
+ return [...changes].sort(
1972
+ (a, b) => Math.abs(b.scores.diff) - Math.abs(a.scores.diff) || Math.abs(b.values?.diff ?? 0) - Math.abs(a.values?.diff ?? 0)
1973
+ );
1974
+ }
1975
+ function getDiffChanges(diff) {
1976
+ return [
1977
+ ...diff.categories.changed,
1978
+ ...diff.groups.changed,
1979
+ ...diff.audits.changed
1980
+ ];
1981
+ }
1982
+ function changesToDiffOutcomes(changes) {
1983
+ return changes.map((change) => {
1984
+ if (change.scores.diff > 0) {
1985
+ return "positive";
1986
+ }
1987
+ if (change.scores.diff < 0) {
1988
+ return "negative";
1989
+ }
1990
+ if (change.values != null && change.values.diff !== 0) {
1991
+ return "mixed";
1992
+ }
1993
+ return "unchanged";
1994
+ });
1995
+ }
1996
+ function mergeDiffOutcomes(outcomes) {
1997
+ if (outcomes.every((outcome) => outcome === "unchanged")) {
1998
+ return "unchanged";
1999
+ }
2000
+ if (outcomes.includes("positive") && !outcomes.includes("negative")) {
2001
+ return "positive";
2002
+ }
2003
+ if (outcomes.includes("negative") && !outcomes.includes("positive")) {
2004
+ return "negative";
2005
+ }
2006
+ return "mixed";
2007
+ }
2008
+ function countDiffOutcomes(outcomes) {
2009
+ return {
2010
+ positive: outcomes.filter((outcome) => outcome === "positive").length,
2011
+ negative: outcomes.filter((outcome) => outcome === "negative").length,
2012
+ mixed: outcomes.filter((outcome) => outcome === "mixed").length,
2013
+ unchanged: outcomes.filter((outcome) => outcome === "unchanged").length
2014
+ };
2015
+ }
2016
+ function formatReportOutcome(outcome, commits) {
1828
2017
  const outcomeTexts = {
1829
2018
  positive: md5`🥳 Code PushUp report has ${md5.bold("improved")}`,
1830
2019
  negative: md5`😟 Code PushUp report has ${md5.bold("regressed")}`,
@@ -1833,36 +2022,91 @@ function createDiffHeaderSection(diff, portalUrl) {
1833
2022
  )}`,
1834
2023
  unchanged: md5`😐 Code PushUp report is ${md5.bold("unchanged")}`
1835
2024
  };
2025
+ if (commits) {
2026
+ const commitsText = `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
2027
+ return md5`${outcomeTexts[outcome]} – ${commitsText}.`;
2028
+ }
2029
+ return md5`${outcomeTexts[outcome]}.`;
2030
+ }
2031
+ function compareDiffsBy(type, a, b) {
2032
+ return sumScoreChanges(b[type].changed) - sumScoreChanges(a[type].changed) || sumConfigChanges(b[type]) - sumConfigChanges(a[type]);
2033
+ }
2034
+ function sumScoreChanges(changes) {
2035
+ return changes.reduce(
2036
+ (acc, { scores }) => acc + Math.abs(scores.diff),
2037
+ 0
2038
+ );
2039
+ }
2040
+ function sumConfigChanges({
2041
+ added,
2042
+ removed
2043
+ }) {
2044
+ return added.length + removed.length;
2045
+ }
2046
+
2047
+ // packages/utils/src/lib/reports/generate-md-reports-diff.ts
2048
+ function generateMdReportsDiff(diff) {
2049
+ return new MarkdownDocument5().$concat(
2050
+ createDiffHeaderSection(diff),
2051
+ createDiffCategoriesSection(diff),
2052
+ createDiffDetailsSection(diff)
2053
+ ).toString();
2054
+ }
2055
+ function generateMdReportsDiffForMonorepo(diffs) {
2056
+ const diffsWithOutcomes = diffs.map((diff) => ({
2057
+ ...diff,
2058
+ outcome: mergeDiffOutcomes(changesToDiffOutcomes(getDiffChanges(diff)))
2059
+ })).sort(
2060
+ (a, b) => compareDiffsBy("categories", a, b) || compareDiffsBy("groups", a, b) || compareDiffsBy("audits", a, b) || a.label.localeCompare(b.label)
2061
+ );
2062
+ const unchanged = diffsWithOutcomes.filter(
2063
+ ({ outcome }) => outcome === "unchanged"
2064
+ );
2065
+ const changed = diffsWithOutcomes.filter((diff) => !unchanged.includes(diff));
2066
+ return new MarkdownDocument5().$concat(
2067
+ createDiffHeaderSection(diffs),
2068
+ ...changed.map(createDiffProjectSection)
2069
+ ).$if(
2070
+ unchanged.length > 0,
2071
+ (doc) => doc.rule().paragraph(summarizeUnchanged("project", { unchanged, changed }))
2072
+ ).toString();
2073
+ }
2074
+ function createDiffHeaderSection(diff) {
1836
2075
  const outcome = mergeDiffOutcomes(
1837
- changesToDiffOutcomes([
1838
- ...diff.categories.changed,
1839
- ...diff.groups.changed,
1840
- ...diff.audits.changed
1841
- ])
2076
+ changesToDiffOutcomes(toArray(diff).flatMap(getDiffChanges))
1842
2077
  );
1843
- const styleCommits = (commits) => `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
1844
- return new MarkdownDocument4().heading(HIERARCHY.level_1, "Code PushUp").paragraph(
1845
- diff.commits ? md5`${outcomeTexts[outcome]} – ${styleCommits(diff.commits)}.` : outcomeTexts[outcome]
1846
- ).paragraph(
1847
- portalUrl && md5.link(portalUrl, "\u{1F575}\uFE0F See full comparison in Code PushUp portal \u{1F50D}")
2078
+ const commits = Array.isArray(diff) ? diff[0]?.commits : diff.commits;
2079
+ const portalUrl = Array.isArray(diff) ? void 0 : diff.portalUrl;
2080
+ return new MarkdownDocument5().heading(HIERARCHY.level_1, "Code PushUp").paragraph(formatReportOutcome(outcome, commits)).paragraph(formatPortalLink(portalUrl));
2081
+ }
2082
+ function createDiffProjectSection(diff) {
2083
+ return new MarkdownDocument5().heading(HIERARCHY.level_2, md6`💼 Project ${md6.code(diff.label)}`).paragraph(formatReportOutcome(diff.outcome)).paragraph(formatPortalLink(diff.portalUrl)).$concat(
2084
+ createDiffCategoriesSection(diff, {
2085
+ skipHeading: true,
2086
+ skipUnchanged: true
2087
+ }),
2088
+ createDiffDetailsSection(diff, HIERARCHY.level_3)
1848
2089
  );
1849
2090
  }
1850
- function createDiffCategoriesSection(diff) {
2091
+ function createDiffCategoriesSection(diff, options) {
1851
2092
  const { changed, unchanged, added } = diff.categories;
2093
+ const { skipHeading, skipUnchanged } = options ?? {};
1852
2094
  const categoriesCount = changed.length + unchanged.length + added.length;
1853
2095
  const hasChanges = unchanged.length < categoriesCount;
1854
2096
  if (categoriesCount === 0) {
1855
2097
  return null;
1856
2098
  }
1857
- const columns = [
1858
- { heading: "\u{1F3F7}\uFE0F Category", alignment: "left" },
1859
- {
1860
- heading: hasChanges ? "\u2B50 Previous score" : "\u2B50 Score",
1861
- alignment: "center"
1862
- },
1863
- { heading: "\u2B50 Current score", alignment: "center" },
1864
- { heading: "\u{1F504} Score change", alignment: "center" }
1865
- ];
2099
+ const [columns, rows] = createCategoriesTable(diff, {
2100
+ hasChanges,
2101
+ skipUnchanged
2102
+ });
2103
+ return new MarkdownDocument5().heading(HIERARCHY.level_2, !skipHeading && "\u{1F3F7}\uFE0F Categories").table(columns, rows).paragraph(added.length > 0 && md6.italic("(\\*) New category.")).paragraph(
2104
+ skipUnchanged && unchanged.length > 0 && summarizeUnchanged("category", { changed, unchanged })
2105
+ );
2106
+ }
2107
+ function createCategoriesTable(diff, options) {
2108
+ const { changed, unchanged, added } = diff.categories;
2109
+ const { hasChanges, skipUnchanged } = options;
1866
2110
  const rows = [
1867
2111
  ...sortChanges(changed).map((category) => [
1868
2112
  formatTitle(category),
@@ -1874,27 +2118,55 @@ function createDiffCategoriesSection(diff) {
1874
2118
  ]),
1875
2119
  ...added.map((category) => [
1876
2120
  formatTitle(category),
1877
- md5.italic("n/a (\\*)"),
2121
+ md6.italic("n/a (\\*)"),
1878
2122
  formatScoreWithColor(category.score),
1879
- md5.italic("n/a (\\*)")
2123
+ md6.italic("n/a (\\*)")
1880
2124
  ]),
1881
- ...unchanged.map((category) => [
2125
+ ...skipUnchanged ? [] : unchanged.map((category) => [
1882
2126
  formatTitle(category),
1883
2127
  formatScoreWithColor(category.score, { skipBold: true }),
1884
2128
  formatScoreWithColor(category.score),
1885
2129
  "\u2013"
1886
2130
  ])
1887
2131
  ];
1888
- return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F3F7}\uFE0F Categories").table(
2132
+ if (rows.length === 0) {
2133
+ return [[], []];
2134
+ }
2135
+ const columns = [
2136
+ { heading: "\u{1F3F7}\uFE0F Category", alignment: "left" },
2137
+ {
2138
+ heading: hasChanges ? "\u2B50 Previous score" : "\u2B50 Score",
2139
+ alignment: "center"
2140
+ },
2141
+ { heading: "\u2B50 Current score", alignment: "center" },
2142
+ { heading: "\u{1F504} Score change", alignment: "center" }
2143
+ ];
2144
+ return [
1889
2145
  hasChanges ? columns : columns.slice(0, 2),
1890
2146
  rows.map((row) => hasChanges ? row : row.slice(0, 2))
1891
- ).paragraph(added.length > 0 && md5.italic("(\\*) New category."));
2147
+ ];
2148
+ }
2149
+ function createDiffDetailsSection(diff, level = HIERARCHY.level_2) {
2150
+ if (diff.groups.changed.length + diff.audits.changed.length === 0) {
2151
+ return null;
2152
+ }
2153
+ const summary = ["group", "audit"].map(
2154
+ (token) => summarizeDiffOutcomes(
2155
+ changesToDiffOutcomes(diff[`${token}s`].changed),
2156
+ token
2157
+ )
2158
+ ).filter(Boolean).join(", ");
2159
+ const details2 = new MarkdownDocument5().$concat(
2160
+ createDiffGroupsSection(diff, level),
2161
+ createDiffAuditsSection(diff, level)
2162
+ );
2163
+ return new MarkdownDocument5().details(summary, details2);
1892
2164
  }
1893
- function createDiffGroupsSection(diff) {
2165
+ function createDiffGroupsSection(diff, level) {
1894
2166
  if (diff.groups.changed.length + diff.groups.unchanged.length === 0) {
1895
2167
  return null;
1896
2168
  }
1897
- return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F5C3}\uFE0F Groups").$concat(
2169
+ return new MarkdownDocument5().heading(level, "\u{1F5C3}\uFE0F Groups").$concat(
1898
2170
  createGroupsOrAuditsDetails(
1899
2171
  "group",
1900
2172
  diff.groups,
@@ -1915,8 +2187,8 @@ function createDiffGroupsSection(diff) {
1915
2187
  )
1916
2188
  );
1917
2189
  }
1918
- function createDiffAuditsSection(diff) {
1919
- return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F6E1}\uFE0F Audits").$concat(
2190
+ function createDiffAuditsSection(diff, level) {
2191
+ return new MarkdownDocument5().heading(level, "\u{1F6E1}\uFE0F Audits").$concat(
1920
2192
  createGroupsOrAuditsDetails(
1921
2193
  "audit",
1922
2194
  diff.audits,
@@ -1931,7 +2203,7 @@ function createDiffAuditsSection(diff) {
1931
2203
  formatTitle(audit.plugin),
1932
2204
  formatTitle(audit),
1933
2205
  `${scoreMarker(audit.scores.before, "square")} ${audit.displayValues.before || audit.values.before.toString()}`,
1934
- md5`${scoreMarker(audit.scores.after, "square")} ${md5.bold(
2206
+ md6`${scoreMarker(audit.scores.after, "square")} ${md6.bold(
1935
2207
  audit.displayValues.after || audit.values.after.toString()
1936
2208
  )}`,
1937
2209
  formatValueChange(audit)
@@ -1939,96 +2211,6 @@ function createDiffAuditsSection(diff) {
1939
2211
  )
1940
2212
  );
1941
2213
  }
1942
- function createGroupsOrAuditsDetails(token, { changed, unchanged }, ...[columns, rows]) {
1943
- if (changed.length === 0) {
1944
- return new MarkdownDocument4().paragraph(
1945
- summarizeUnchanged(token, { changed, unchanged })
1946
- );
1947
- }
1948
- return new MarkdownDocument4().details(
1949
- summarizeDiffOutcomes(changesToDiffOutcomes(changed), token),
1950
- md5`${md5.table(columns, rows.slice(0, MAX_ROWS))}${changed.length > MAX_ROWS ? md5.paragraph(
1951
- md5.italic(
1952
- `Only the ${MAX_ROWS} most affected ${pluralize(
1953
- token
1954
- )} are listed above for brevity.`
1955
- )
1956
- ) : ""}${unchanged.length > 0 ? md5.paragraph(summarizeUnchanged(token, { changed, unchanged })) : ""}`
1957
- );
1958
- }
1959
- function summarizeUnchanged(token, { changed, unchanged }) {
1960
- return [
1961
- changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`,
1962
- unchanged.length === 1 ? "is" : "are",
1963
- "unchanged."
1964
- ].join(" ");
1965
- }
1966
- function summarizeDiffOutcomes(outcomes, token) {
1967
- return objectToEntries(countDiffOutcomes(outcomes)).filter(
1968
- (entry) => entry[0] !== "unchanged" && entry[1] > 0
1969
- ).map(([outcome, count]) => {
1970
- const formattedCount = `<strong>${count}</strong> ${pluralize(
1971
- token,
1972
- count
1973
- )}`;
1974
- switch (outcome) {
1975
- case "positive":
1976
- return `\u{1F44D} ${formattedCount} improved`;
1977
- case "negative":
1978
- return `\u{1F44E} ${formattedCount} regressed`;
1979
- case "mixed":
1980
- return `${formattedCount} changed without impacting score`;
1981
- }
1982
- }).join(", ");
1983
- }
1984
- function formatTitle({
1985
- title,
1986
- docsUrl
1987
- }) {
1988
- if (docsUrl) {
1989
- return md5.link(docsUrl, title);
1990
- }
1991
- return title;
1992
- }
1993
- function sortChanges(changes) {
1994
- return [...changes].sort(
1995
- (a, b) => Math.abs(b.scores.diff) - Math.abs(a.scores.diff) || Math.abs(b.values?.diff ?? 0) - Math.abs(a.values?.diff ?? 0)
1996
- );
1997
- }
1998
- function changesToDiffOutcomes(changes) {
1999
- return changes.map((change) => {
2000
- if (change.scores.diff > 0) {
2001
- return "positive";
2002
- }
2003
- if (change.scores.diff < 0) {
2004
- return "negative";
2005
- }
2006
- if (change.values != null && change.values.diff !== 0) {
2007
- return "mixed";
2008
- }
2009
- return "unchanged";
2010
- });
2011
- }
2012
- function mergeDiffOutcomes(outcomes) {
2013
- if (outcomes.every((outcome) => outcome === "unchanged")) {
2014
- return "unchanged";
2015
- }
2016
- if (outcomes.includes("positive") && !outcomes.includes("negative")) {
2017
- return "positive";
2018
- }
2019
- if (outcomes.includes("negative") && !outcomes.includes("positive")) {
2020
- return "negative";
2021
- }
2022
- return "mixed";
2023
- }
2024
- function countDiffOutcomes(outcomes) {
2025
- return {
2026
- positive: outcomes.filter((outcome) => outcome === "positive").length,
2027
- negative: outcomes.filter((outcome) => outcome === "negative").length,
2028
- mixed: outcomes.filter((outcome) => outcome === "mixed").length,
2029
- unchanged: outcomes.filter((outcome) => outcome === "unchanged").length
2030
- };
2031
- }
2032
2214
 
2033
2215
  // packages/utils/src/lib/reports/load-report.ts
2034
2216
  import { join as join2 } from "node:path";
@@ -2085,7 +2267,8 @@ function logPlugins(report) {
2085
2267
  },
2086
2268
  {
2087
2269
  text: cyanBright(audit.displayValue || `${audit.value}`),
2088
- width: 10,
2270
+ // eslint-disable-next-line no-magic-numbers
2271
+ width: 20,
2089
2272
  padding: [0, 0, 0, 0]
2090
2273
  }
2091
2274
  ]);
@@ -2238,7 +2421,7 @@ var verboseUtils = (verbose = false) => ({
2238
2421
 
2239
2422
  // packages/core/package.json
2240
2423
  var name = "@code-pushup/core";
2241
- var version = "0.49.0";
2424
+ var version = "0.51.0";
2242
2425
 
2243
2426
  // packages/core/src/lib/implementation/execute-plugin.ts
2244
2427
  import { bold as bold5 } from "ansis";
@@ -2447,7 +2630,7 @@ async function persistReport(report, options) {
2447
2630
  case "md":
2448
2631
  return {
2449
2632
  format: "md",
2450
- content: generateMdReport(sortedScoredReport)
2633
+ content: generateMdReport(sortedScoredReport, { outputDir })
2451
2634
  };
2452
2635
  }
2453
2636
  });
@@ -2634,7 +2817,7 @@ function selectMeta(meta) {
2634
2817
  }
2635
2818
 
2636
2819
  // packages/core/src/lib/compare.ts
2637
- async function compareReportFiles(inputPaths, persistConfig, uploadConfig) {
2820
+ async function compareReportFiles(inputPaths, persistConfig, uploadConfig, label) {
2638
2821
  const { outputDir, filename, format } = persistConfig;
2639
2822
  const [reportBefore, reportAfter] = await Promise.all([
2640
2823
  readJsonFile(inputPaths.before),
@@ -2644,12 +2827,20 @@ async function compareReportFiles(inputPaths, persistConfig, uploadConfig) {
2644
2827
  before: reportSchema.parse(reportBefore),
2645
2828
  after: reportSchema.parse(reportAfter)
2646
2829
  };
2647
- const reportsDiff = compareReports(reports);
2648
- const portalUrl = uploadConfig && reportsDiff.commits && format.includes("md") ? await fetchPortalComparisonLink(uploadConfig, reportsDiff.commits) : void 0;
2830
+ const diff = compareReports(reports);
2831
+ if (label) {
2832
+ diff.label = label;
2833
+ }
2834
+ if (uploadConfig && diff.commits) {
2835
+ diff.portalUrl = await fetchPortalComparisonLink(
2836
+ uploadConfig,
2837
+ diff.commits
2838
+ );
2839
+ }
2649
2840
  return Promise.all(
2650
2841
  format.map(async (fmt) => {
2651
2842
  const outputPath = join5(outputDir, `${filename}-diff.${fmt}`);
2652
- const content = reportsDiffToFileContent(reportsDiff, fmt, portalUrl);
2843
+ const content = reportsDiffToFileContent(diff, fmt);
2653
2844
  await ensureDirectoryExists(outputDir);
2654
2845
  await writeFile2(outputPath, content);
2655
2846
  return outputPath;
@@ -2679,12 +2870,12 @@ function compareReports(reports) {
2679
2870
  duration
2680
2871
  };
2681
2872
  }
2682
- function reportsDiffToFileContent(reportsDiff, format, portalUrl) {
2873
+ function reportsDiffToFileContent(reportsDiff, format) {
2683
2874
  switch (format) {
2684
2875
  case "json":
2685
2876
  return JSON.stringify(reportsDiff, null, 2);
2686
2877
  case "md":
2687
- return generateMdReportsDiff(reportsDiff, portalUrl ?? void 0);
2878
+ return generateMdReportsDiff(reportsDiff);
2688
2879
  }
2689
2880
  }
2690
2881
  async function fetchPortalComparisonLink(uploadConfig, commits) {
@@ -2956,6 +3147,45 @@ async function autoloadRc(tsconfig) {
2956
3147
  tsconfig
2957
3148
  );
2958
3149
  }
3150
+
3151
+ // packages/core/src/lib/merge-diffs.ts
3152
+ import { writeFile as writeFile3 } from "node:fs/promises";
3153
+ import { basename, dirname, join as join7 } from "node:path";
3154
+ async function mergeDiffs(files, persistConfig) {
3155
+ const results = await Promise.allSettled(
3156
+ files.map(async (file) => {
3157
+ const json = await readJsonFile(file).catch((error) => {
3158
+ throw new Error(
3159
+ `Failed to read JSON file ${file} - ${stringifyError(error)}`
3160
+ );
3161
+ });
3162
+ const result = await reportsDiffSchema.safeParseAsync(json);
3163
+ if (!result.success) {
3164
+ throw new Error(
3165
+ `Invalid reports diff in ${file} - ${result.error.message}`
3166
+ );
3167
+ }
3168
+ return { ...result.data, file };
3169
+ })
3170
+ );
3171
+ results.filter(isPromiseRejectedResult).forEach(({ reason }) => {
3172
+ ui().logger.warning(
3173
+ `Skipped invalid report diff - ${stringifyError(reason)}`
3174
+ );
3175
+ });
3176
+ const diffs = results.filter(isPromiseFulfilledResult).map(({ value }) => value);
3177
+ const labeledDiffs = diffs.map((diff) => ({
3178
+ ...diff,
3179
+ label: diff.label || basename(dirname(diff.file))
3180
+ // fallback is parent folder name
3181
+ }));
3182
+ const markdown = generateMdReportsDiffForMonorepo(labeledDiffs);
3183
+ const { outputDir, filename } = persistConfig;
3184
+ const outputPath = join7(outputDir, `${filename}-diff.md`);
3185
+ await ensureDirectoryExists(outputDir);
3186
+ await writeFile3(outputPath, markdown);
3187
+ return outputPath;
3188
+ }
2959
3189
  export {
2960
3190
  ConfigPathError,
2961
3191
  PersistDirError,
@@ -2969,6 +3199,7 @@ export {
2969
3199
  executePlugin,
2970
3200
  executePlugins,
2971
3201
  history,
3202
+ mergeDiffs,
2972
3203
  persistReport,
2973
3204
  readRcByPath,
2974
3205
  upload