@code-pushup/cli 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.
Files changed (30) hide show
  1. package/README.md +20 -5
  2. package/index.js +666 -374
  3. package/package.json +6 -32
  4. package/src/lib/autorun/autorun-command.d.ts +1 -1
  5. package/src/lib/collect/collect-command.d.ts +1 -1
  6. package/src/lib/commands.d.ts +1 -1
  7. package/src/lib/compare/compare-command.d.ts +1 -2
  8. package/src/lib/history/history.model.d.ts +2 -2
  9. package/src/lib/history/history.options.d.ts +2 -2
  10. package/src/lib/history/utils.d.ts +2 -2
  11. package/src/lib/implementation/compare.model.d.ts +4 -2
  12. package/src/lib/implementation/compare.options.d.ts +1 -1
  13. package/src/lib/implementation/core-config.middleware.d.ts +5 -5
  14. package/src/lib/implementation/core-config.options.d.ts +2 -2
  15. package/src/lib/implementation/filter-plugins.middleware.d.ts +3 -0
  16. package/src/lib/implementation/formatting.d.ts +11 -0
  17. package/src/lib/implementation/global.model.d.ts +2 -2
  18. package/src/lib/implementation/global.options.d.ts +2 -2
  19. package/src/lib/implementation/merge-diffs.model.d.ts +3 -0
  20. package/src/lib/implementation/merge-diffs.options.d.ts +3 -0
  21. package/src/lib/implementation/only-plugins.model.d.ts +2 -2
  22. package/src/lib/implementation/only-plugins.options.d.ts +3 -2
  23. package/src/lib/implementation/skip-plugins.model.d.ts +2 -2
  24. package/src/lib/implementation/skip-plugins.options.d.ts +3 -2
  25. package/src/lib/merge-diffs/merge-diffs-command.d.ts +6 -0
  26. package/src/lib/middlewares.d.ts +1 -1
  27. package/src/lib/upload/upload-command.d.ts +1 -1
  28. package/src/lib/yargs-cli.d.ts +14 -1
  29. package/src/lib/implementation/only-plugins.middleware.d.ts +0 -2
  30. package/src/lib/implementation/skip-plugins.middleware.d.ts +0 -2
package/index.js CHANGED
@@ -163,9 +163,27 @@ function hasNonZeroWeightedRef(refs) {
163
163
  return refs.reduce((acc, { weight }) => weight + acc, 0) !== 0;
164
164
  }
165
165
 
166
- // packages/models/src/lib/audit.ts
166
+ // packages/models/src/lib/source.ts
167
167
  import { z as z2 } from "zod";
168
- var auditSchema = z2.object({
168
+ var sourceFileLocationSchema = z2.object(
169
+ {
170
+ file: filePathSchema.describe("Relative path to source file in Git repo"),
171
+ position: z2.object(
172
+ {
173
+ startLine: positiveIntSchema.describe("Start line"),
174
+ startColumn: positiveIntSchema.describe("Start column").optional(),
175
+ endLine: positiveIntSchema.describe("End line").optional(),
176
+ endColumn: positiveIntSchema.describe("End column").optional()
177
+ },
178
+ { description: "Location in file" }
179
+ ).optional()
180
+ },
181
+ { description: "Source file location" }
182
+ );
183
+
184
+ // packages/models/src/lib/audit.ts
185
+ import { z as z3 } from "zod";
186
+ var auditSchema = z3.object({
169
187
  slug: slugSchema.describe("ID (unique within plugin)")
170
188
  }).merge(
171
189
  metaSchema({
@@ -175,7 +193,7 @@ var auditSchema = z2.object({
175
193
  description: "List of scorable metrics for the given plugin"
176
194
  })
177
195
  );
178
- var pluginAuditsSchema = z2.array(auditSchema, {
196
+ var pluginAuditsSchema = z3.array(auditSchema, {
179
197
  description: "List of audits maintained in a plugin"
180
198
  }).min(1).refine(
181
199
  (auditMetadata) => !getDuplicateSlugsInAudits(auditMetadata),
@@ -194,31 +212,16 @@ function getDuplicateSlugsInAudits(audits) {
194
212
  }
195
213
 
196
214
  // packages/models/src/lib/audit-output.ts
197
- import { z as z5 } from "zod";
215
+ import { z as z6 } from "zod";
198
216
 
199
217
  // packages/models/src/lib/issue.ts
200
- import { z as z3 } from "zod";
201
- var sourceFileLocationSchema = z3.object(
202
- {
203
- file: filePathSchema.describe("Relative path to source file in Git repo"),
204
- position: z3.object(
205
- {
206
- startLine: positiveIntSchema.describe("Start line"),
207
- startColumn: positiveIntSchema.describe("Start column").optional(),
208
- endLine: positiveIntSchema.describe("End line").optional(),
209
- endColumn: positiveIntSchema.describe("End column").optional()
210
- },
211
- { description: "Location in file" }
212
- ).optional()
213
- },
214
- { description: "Source file location" }
215
- );
216
- var issueSeveritySchema = z3.enum(["info", "warning", "error"], {
218
+ import { z as z4 } from "zod";
219
+ var issueSeveritySchema = z4.enum(["info", "warning", "error"], {
217
220
  description: "Severity level"
218
221
  });
219
- var issueSchema = z3.object(
222
+ var issueSchema = z4.object(
220
223
  {
221
- message: z3.string({ description: "Descriptive error message" }).max(MAX_ISSUE_MESSAGE_LENGTH),
224
+ message: z4.string({ description: "Descriptive error message" }).max(MAX_ISSUE_MESSAGE_LENGTH),
222
225
  severity: issueSeveritySchema,
223
226
  source: sourceFileLocationSchema.optional()
224
227
  },
@@ -226,60 +229,60 @@ var issueSchema = z3.object(
226
229
  );
227
230
 
228
231
  // packages/models/src/lib/table.ts
229
- import { z as z4 } from "zod";
230
- var tableAlignmentSchema = z4.enum(["left", "center", "right"], {
232
+ import { z as z5 } from "zod";
233
+ var tableAlignmentSchema = z5.enum(["left", "center", "right"], {
231
234
  description: "Cell alignment"
232
235
  });
233
- var tableColumnObjectSchema = z4.object({
234
- key: z4.string(),
235
- label: z4.string().optional(),
236
+ var tableColumnObjectSchema = z5.object({
237
+ key: z5.string(),
238
+ label: z5.string().optional(),
236
239
  align: tableAlignmentSchema.optional()
237
240
  });
238
- var tableRowObjectSchema = z4.record(tableCellValueSchema, {
241
+ var tableRowObjectSchema = z5.record(tableCellValueSchema, {
239
242
  description: "Object row"
240
243
  });
241
- var tableRowPrimitiveSchema = z4.array(tableCellValueSchema, {
244
+ var tableRowPrimitiveSchema = z5.array(tableCellValueSchema, {
242
245
  description: "Primitive row"
243
246
  });
244
- var tableSharedSchema = z4.object({
245
- title: z4.string().optional().describe("Display title for table")
247
+ var tableSharedSchema = z5.object({
248
+ title: z5.string().optional().describe("Display title for table")
246
249
  });
247
250
  var tablePrimitiveSchema = tableSharedSchema.merge(
248
- z4.object(
251
+ z5.object(
249
252
  {
250
- columns: z4.array(tableAlignmentSchema).optional(),
251
- rows: z4.array(tableRowPrimitiveSchema)
253
+ columns: z5.array(tableAlignmentSchema).optional(),
254
+ rows: z5.array(tableRowPrimitiveSchema)
252
255
  },
253
256
  { description: "Table with primitive rows and optional alignment columns" }
254
257
  )
255
258
  );
256
259
  var tableObjectSchema = tableSharedSchema.merge(
257
- z4.object(
260
+ z5.object(
258
261
  {
259
- columns: z4.union([
260
- z4.array(tableAlignmentSchema),
261
- z4.array(tableColumnObjectSchema)
262
+ columns: z5.union([
263
+ z5.array(tableAlignmentSchema),
264
+ z5.array(tableColumnObjectSchema)
262
265
  ]).optional(),
263
- rows: z4.array(tableRowObjectSchema)
266
+ rows: z5.array(tableRowObjectSchema)
264
267
  },
265
268
  {
266
269
  description: "Table with object rows and optional alignment or object columns"
267
270
  }
268
271
  )
269
272
  );
270
- var tableSchema = (description = "Table information") => z4.union([tablePrimitiveSchema, tableObjectSchema], { description });
273
+ var tableSchema = (description = "Table information") => z5.union([tablePrimitiveSchema, tableObjectSchema], { description });
271
274
 
272
275
  // packages/models/src/lib/audit-output.ts
273
276
  var auditValueSchema = nonnegativeNumberSchema.describe("Raw numeric value");
274
- var auditDisplayValueSchema = z5.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }).optional();
275
- var auditDetailsSchema = z5.object(
277
+ var auditDisplayValueSchema = z6.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }).optional();
278
+ var auditDetailsSchema = z6.object(
276
279
  {
277
- issues: z5.array(issueSchema, { description: "List of findings" }).optional(),
280
+ issues: z6.array(issueSchema, { description: "List of findings" }).optional(),
278
281
  table: tableSchema("Table of related findings").optional()
279
282
  },
280
283
  { description: "Detailed information" }
281
284
  );
282
- var auditOutputSchema = z5.object(
285
+ var auditOutputSchema = z6.object(
283
286
  {
284
287
  slug: slugSchema.describe("Reference to audit"),
285
288
  displayValue: auditDisplayValueSchema,
@@ -289,7 +292,7 @@ var auditOutputSchema = z5.object(
289
292
  },
290
293
  { description: "Audit information" }
291
294
  );
292
- var auditOutputsSchema = z5.array(auditOutputSchema, {
295
+ var auditOutputsSchema = z6.array(auditOutputSchema, {
293
296
  description: "List of JSON formatted audit output emitted by the runner process of a plugin"
294
297
  }).refine(
295
298
  (audits) => !getDuplicateSlugsInAudits2(audits),
@@ -306,13 +309,13 @@ function getDuplicateSlugsInAudits2(audits) {
306
309
  }
307
310
 
308
311
  // packages/models/src/lib/category-config.ts
309
- import { z as z6 } from "zod";
312
+ import { z as z7 } from "zod";
310
313
  var categoryRefSchema = weightedRefSchema(
311
314
  "Weighted references to audits and/or groups for the category",
312
315
  "Slug of an audit or group (depending on `type`)"
313
316
  ).merge(
314
- z6.object({
315
- type: z6.enum(["audit", "group"], {
317
+ z7.object({
318
+ type: z7.enum(["audit", "group"], {
316
319
  description: "Discriminant for reference kind, affects where `slug` is looked up"
317
320
  }),
318
321
  plugin: slugSchema.describe(
@@ -333,8 +336,8 @@ var categoryConfigSchema = scorableSchema(
333
336
  description: "Meta info for category"
334
337
  })
335
338
  ).merge(
336
- z6.object({
337
- isBinary: z6.boolean({
339
+ z7.object({
340
+ isBinary: z7.boolean({
338
341
  description: 'Is this a binary category (i.e. only a perfect score considered a "pass")?'
339
342
  }).optional()
340
343
  })
@@ -350,7 +353,7 @@ function getDuplicateRefsInCategoryMetrics(metrics) {
350
353
  metrics.map(({ slug, type, plugin }) => `${type} :: ${plugin} / ${slug}`)
351
354
  );
352
355
  }
353
- var categoriesSchema = z6.array(categoryConfigSchema, {
356
+ var categoriesSchema = z7.array(categoryConfigSchema, {
354
357
  description: "Categorization of individual audits"
355
358
  }).refine(
356
359
  (categoryCfg) => !getDuplicateSlugCategories(categoryCfg),
@@ -369,18 +372,18 @@ function getDuplicateSlugCategories(categories) {
369
372
  }
370
373
 
371
374
  // packages/models/src/lib/commit.ts
372
- import { z as z7 } from "zod";
373
- var commitSchema = z7.object(
375
+ import { z as z8 } from "zod";
376
+ var commitSchema = z8.object(
374
377
  {
375
- hash: z7.string({ description: "Commit SHA (full)" }).regex(
378
+ hash: z8.string({ description: "Commit SHA (full)" }).regex(
376
379
  /^[\da-f]{40}$/,
377
380
  "Commit SHA should be a 40-character hexadecimal string"
378
381
  ),
379
- message: z7.string({ description: "Commit message" }),
380
- date: z7.coerce.date({
382
+ message: z8.string({ description: "Commit message" }),
383
+ date: z8.coerce.date({
381
384
  description: "Date and time when commit was authored"
382
385
  }),
383
- author: z7.string({
386
+ author: z8.string({
384
387
  description: "Commit author name"
385
388
  }).trim()
386
389
  },
@@ -388,22 +391,22 @@ var commitSchema = z7.object(
388
391
  );
389
392
 
390
393
  // packages/models/src/lib/core-config.ts
391
- import { z as z13 } from "zod";
394
+ import { z as z14 } from "zod";
392
395
 
393
396
  // packages/models/src/lib/persist-config.ts
394
- import { z as z8 } from "zod";
395
- var formatSchema = z8.enum(["json", "md"]);
396
- var persistConfigSchema = z8.object({
397
+ import { z as z9 } from "zod";
398
+ var formatSchema = z9.enum(["json", "md"]);
399
+ var persistConfigSchema = z9.object({
397
400
  outputDir: filePathSchema.describe("Artifacts folder").optional(),
398
401
  filename: fileNameSchema.describe("Artifacts file name (without extension)").optional(),
399
- format: z8.array(formatSchema).optional()
402
+ format: z9.array(formatSchema).optional()
400
403
  });
401
404
 
402
405
  // packages/models/src/lib/plugin-config.ts
403
- import { z as z11 } from "zod";
406
+ import { z as z12 } from "zod";
404
407
 
405
408
  // packages/models/src/lib/group.ts
406
- import { z as z9 } from "zod";
409
+ import { z as z10 } from "zod";
407
410
  var groupRefSchema = weightedRefSchema(
408
411
  "Weighted reference to a group",
409
412
  "Reference slug to a group within this plugin (e.g. 'max-lines')"
@@ -420,7 +423,7 @@ var groupSchema = scorableSchema(
420
423
  getDuplicateRefsInGroups,
421
424
  duplicateRefsInGroupsErrorMsg
422
425
  ).merge(groupMetaSchema);
423
- var groupsSchema = z9.array(groupSchema, {
426
+ var groupsSchema = z10.array(groupSchema, {
424
427
  description: "List of groups"
425
428
  }).optional().refine(
426
429
  (groups2) => !getDuplicateSlugsInGroups(groups2),
@@ -448,14 +451,14 @@ function getDuplicateSlugsInGroups(groups2) {
448
451
  }
449
452
 
450
453
  // packages/models/src/lib/runner-config.ts
451
- import { z as z10 } from "zod";
452
- var outputTransformSchema = z10.function().args(z10.unknown()).returns(z10.union([auditOutputsSchema, z10.promise(auditOutputsSchema)]));
453
- var runnerConfigSchema = z10.object(
454
+ import { z as z11 } from "zod";
455
+ var outputTransformSchema = z11.function().args(z11.unknown()).returns(z11.union([auditOutputsSchema, z11.promise(auditOutputsSchema)]));
456
+ var runnerConfigSchema = z11.object(
454
457
  {
455
- command: z10.string({
458
+ command: z11.string({
456
459
  description: "Shell command to execute"
457
460
  }),
458
- args: z10.array(z10.string({ description: "Command arguments" })).optional(),
461
+ args: z11.array(z11.string({ description: "Command arguments" })).optional(),
459
462
  outputFile: filePathSchema.describe("Output path"),
460
463
  outputTransform: outputTransformSchema.optional()
461
464
  },
@@ -463,8 +466,8 @@ var runnerConfigSchema = z10.object(
463
466
  description: "How to execute runner"
464
467
  }
465
468
  );
466
- var onProgressSchema = z10.function().args(z10.unknown()).returns(z10.void());
467
- var runnerFunctionSchema = z10.function().args(onProgressSchema.optional()).returns(z10.union([auditOutputsSchema, z10.promise(auditOutputsSchema)]));
469
+ var onProgressSchema = z11.function().args(z11.unknown()).returns(z11.void());
470
+ var runnerFunctionSchema = z11.function().args(onProgressSchema.optional()).returns(z11.union([auditOutputsSchema, z11.promise(auditOutputsSchema)]));
468
471
 
469
472
  // packages/models/src/lib/plugin-config.ts
470
473
  var pluginMetaSchema = packageVersionSchema().merge(
@@ -475,13 +478,13 @@ var pluginMetaSchema = packageVersionSchema().merge(
475
478
  description: "Plugin metadata"
476
479
  })
477
480
  ).merge(
478
- z11.object({
481
+ z12.object({
479
482
  slug: slugSchema.describe("Unique plugin slug within core config"),
480
483
  icon: materialIconSchema
481
484
  })
482
485
  );
483
- var pluginDataSchema = z11.object({
484
- runner: z11.union([runnerConfigSchema, runnerFunctionSchema]),
486
+ var pluginDataSchema = z12.object({
487
+ runner: z12.union([runnerConfigSchema, runnerFunctionSchema]),
485
488
  audits: pluginAuditsSchema,
486
489
  groups: groupsSchema
487
490
  });
@@ -507,22 +510,22 @@ function getMissingRefsFromGroups(pluginCfg) {
507
510
  }
508
511
 
509
512
  // packages/models/src/lib/upload-config.ts
510
- import { z as z12 } from "zod";
511
- var uploadConfigSchema = z12.object({
513
+ import { z as z13 } from "zod";
514
+ var uploadConfigSchema = z13.object({
512
515
  server: urlSchema.describe("URL of deployed portal API"),
513
- apiKey: z12.string({
516
+ apiKey: z13.string({
514
517
  description: "API key with write access to portal (use `process.env` for security)"
515
518
  }),
516
519
  organization: slugSchema.describe(
517
520
  "Organization slug from Code PushUp portal"
518
521
  ),
519
522
  project: slugSchema.describe("Project slug from Code PushUp portal"),
520
- timeout: z12.number({ description: "Request timeout in minutes (default is 5)" }).positive().int().optional()
523
+ timeout: z13.number({ description: "Request timeout in minutes (default is 5)" }).positive().int().optional()
521
524
  });
522
525
 
523
526
  // packages/models/src/lib/core-config.ts
524
- var unrefinedCoreConfigSchema = z13.object({
525
- plugins: z13.array(pluginConfigSchema, {
527
+ var unrefinedCoreConfigSchema = z14.object({
528
+ plugins: z14.array(pluginConfigSchema, {
526
529
  description: "List of plugins to be used (official, community-provided, or custom)"
527
530
  }).min(1),
528
531
  /** portal configuration for persisting results */
@@ -554,7 +557,7 @@ var DEFAULT_PERSIST_FILENAME = "report";
554
557
  var DEFAULT_PERSIST_FORMAT = ["json", "md"];
555
558
 
556
559
  // packages/models/src/lib/report.ts
557
- import { z as z14 } from "zod";
560
+ import { z as z15 } from "zod";
558
561
  var auditReportSchema = auditSchema.merge(auditOutputSchema);
559
562
  var pluginReportSchema = pluginMetaSchema.merge(
560
563
  executionMetaSchema({
@@ -562,9 +565,9 @@ var pluginReportSchema = pluginMetaSchema.merge(
562
565
  descriptionDuration: "Duration of the plugin run in ms"
563
566
  })
564
567
  ).merge(
565
- z14.object({
566
- audits: z14.array(auditReportSchema).min(1),
567
- groups: z14.array(groupSchema).optional()
568
+ z15.object({
569
+ audits: z15.array(auditReportSchema).min(1),
570
+ groups: z15.array(groupSchema).optional()
568
571
  })
569
572
  ).refine(
570
573
  (pluginReport) => !getMissingRefsFromGroups2(pluginReport.audits, pluginReport.groups ?? []),
@@ -598,10 +601,10 @@ var reportSchema = packageVersionSchema({
598
601
  descriptionDuration: "Duration of the collect run in ms"
599
602
  })
600
603
  ).merge(
601
- z14.object(
604
+ z15.object(
602
605
  {
603
- categories: z14.array(categoryConfigSchema),
604
- plugins: z14.array(pluginReportSchema).min(1),
606
+ categories: z15.array(categoryConfigSchema),
607
+ plugins: z15.array(pluginReportSchema).min(1),
605
608
  commit: commitSchema.describe("Git commit for which report was collected").nullable()
606
609
  },
607
610
  { description: "Collect output data" }
@@ -617,40 +620,40 @@ var reportSchema = packageVersionSchema({
617
620
  );
618
621
 
619
622
  // packages/models/src/lib/reports-diff.ts
620
- import { z as z15 } from "zod";
623
+ import { z as z16 } from "zod";
621
624
  function makeComparisonSchema(schema) {
622
625
  const sharedDescription = schema.description || "Result";
623
- return z15.object({
626
+ return z16.object({
624
627
  before: schema.describe(`${sharedDescription} (source commit)`),
625
628
  after: schema.describe(`${sharedDescription} (target commit)`)
626
629
  });
627
630
  }
628
631
  function makeArraysComparisonSchema(diffSchema, resultSchema, description) {
629
- return z15.object(
632
+ return z16.object(
630
633
  {
631
- changed: z15.array(diffSchema),
632
- unchanged: z15.array(resultSchema),
633
- added: z15.array(resultSchema),
634
- removed: z15.array(resultSchema)
634
+ changed: z16.array(diffSchema),
635
+ unchanged: z16.array(resultSchema),
636
+ added: z16.array(resultSchema),
637
+ removed: z16.array(resultSchema)
635
638
  },
636
639
  { description }
637
640
  );
638
641
  }
639
- var scorableMetaSchema = z15.object({
642
+ var scorableMetaSchema = z16.object({
640
643
  slug: slugSchema,
641
644
  title: titleSchema,
642
645
  docsUrl: docsUrlSchema
643
646
  });
644
647
  var scorableWithPluginMetaSchema = scorableMetaSchema.merge(
645
- z15.object({
648
+ z16.object({
646
649
  plugin: pluginMetaSchema.pick({ slug: true, title: true, docsUrl: true }).describe("Plugin which defines it")
647
650
  })
648
651
  );
649
652
  var scorableDiffSchema = scorableMetaSchema.merge(
650
- z15.object({
653
+ z16.object({
651
654
  scores: makeComparisonSchema(scoreSchema).merge(
652
- z15.object({
653
- diff: z15.number().min(-1).max(1).describe("Score change (`scores.after - scores.before`)")
655
+ z16.object({
656
+ diff: z16.number().min(-1).max(1).describe("Score change (`scores.after - scores.before`)")
654
657
  })
655
658
  ).describe("Score comparison")
656
659
  })
@@ -661,10 +664,10 @@ var scorableWithPluginDiffSchema = scorableDiffSchema.merge(
661
664
  var categoryDiffSchema = scorableDiffSchema;
662
665
  var groupDiffSchema = scorableWithPluginDiffSchema;
663
666
  var auditDiffSchema = scorableWithPluginDiffSchema.merge(
664
- z15.object({
667
+ z16.object({
665
668
  values: makeComparisonSchema(auditValueSchema).merge(
666
- z15.object({
667
- diff: z15.number().int().describe("Value change (`values.after - values.before`)")
669
+ z16.object({
670
+ diff: z16.number().int().describe("Value change (`values.after - values.before`)")
668
671
  })
669
672
  ).describe("Audit `value` comparison"),
670
673
  displayValues: makeComparisonSchema(auditDisplayValueSchema).describe(
@@ -673,16 +676,18 @@ var auditDiffSchema = scorableWithPluginDiffSchema.merge(
673
676
  })
674
677
  );
675
678
  var categoryResultSchema = scorableMetaSchema.merge(
676
- z15.object({ score: scoreSchema })
679
+ z16.object({ score: scoreSchema })
677
680
  );
678
681
  var groupResultSchema = scorableWithPluginMetaSchema.merge(
679
- z15.object({ score: scoreSchema })
682
+ z16.object({ score: scoreSchema })
680
683
  );
681
684
  var auditResultSchema = scorableWithPluginMetaSchema.merge(
682
685
  auditOutputSchema.pick({ score: true, value: true, displayValue: true })
683
686
  );
684
- var reportsDiffSchema = z15.object({
687
+ var reportsDiffSchema = z16.object({
685
688
  commits: makeComparisonSchema(commitSchema).nullable().describe("Commits identifying compared reports"),
689
+ portalUrl: urlSchema.optional().describe("Link to comparison page in Code PushUp portal"),
690
+ label: z16.string().optional().describe("Label (e.g. project name)"),
686
691
  categories: makeArraysComparisonSchema(
687
692
  categoryDiffSchema,
688
693
  categoryResultSchema,
@@ -752,8 +757,24 @@ function comparePairs(pairs, equalsFn) {
752
757
  );
753
758
  }
754
759
 
760
+ // packages/utils/src/lib/errors.ts
761
+ function stringifyError(error) {
762
+ if (error instanceof Error) {
763
+ if (error.name === "Error" || error.message.startsWith(error.name)) {
764
+ return error.message;
765
+ }
766
+ return `${error.name}: ${error.message}`;
767
+ }
768
+ if (typeof error === "string") {
769
+ return error;
770
+ }
771
+ return JSON.stringify(error);
772
+ }
773
+
755
774
  // packages/utils/src/lib/execute-process.ts
756
- import { spawn } from "node:child_process";
775
+ import {
776
+ spawn
777
+ } from "node:child_process";
757
778
 
758
779
  // packages/utils/src/lib/reports/utils.ts
759
780
  import ansis from "ansis";
@@ -839,9 +860,17 @@ function severityMarker(severity) {
839
860
  }
840
861
  return "\u2139\uFE0F";
841
862
  }
863
+ var MIN_NON_ZERO_RESULT = 0.1;
864
+ function roundValue(value) {
865
+ const roundedValue = Math.round(value * 10) / 10;
866
+ if (roundedValue === 0 && value !== 0) {
867
+ return MIN_NON_ZERO_RESULT * Math.sign(value);
868
+ }
869
+ return roundedValue;
870
+ }
842
871
  function formatScoreChange(diff) {
843
872
  const marker = getDiffMarker(diff);
844
- const text = formatDiffNumber(Math.round(diff * 1e3) / 10);
873
+ const text = formatDiffNumber(roundValue(diff * 100));
845
874
  return colorByScoreDiff(`${marker} ${text}`, diff);
846
875
  }
847
876
  function formatValueChange({
@@ -849,7 +878,7 @@ function formatValueChange({
849
878
  scores
850
879
  }) {
851
880
  const marker = getDiffMarker(values.diff);
852
- const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : Math.round(100 * values.diff / values.before);
881
+ const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : roundValue(values.diff / values.before * 100);
853
882
  const text = `${formatDiffNumber(percentage)}\u2009%`;
854
883
  return colorByScoreDiff(`${marker} ${text}`, scores.diff);
855
884
  }
@@ -880,12 +909,12 @@ function countCategoryAudits(refs, plugins) {
880
909
  }, 0);
881
910
  }
882
911
  function compareCategoryAuditsAndGroups(a, b) {
883
- if (a.weight !== b.weight) {
884
- return b.weight - a.weight;
885
- }
886
912
  if (a.score !== b.score) {
887
913
  return a.score - b.score;
888
914
  }
915
+ if (a.weight !== b.weight) {
916
+ return b.weight - a.weight;
917
+ }
889
918
  if ("value" in a && "value" in b && a.value !== b.value) {
890
919
  return b.value - a.value;
891
920
  }
@@ -977,25 +1006,29 @@ var ProcessError = class extends Error {
977
1006
  }
978
1007
  };
979
1008
  function executeProcess(cfg) {
980
- const { observer, cwd, command: command2, args, ignoreExitCode = false } = cfg;
981
- const { onStdout, onError, onComplete } = observer ?? {};
1009
+ const { command: command2, args, observer, ignoreExitCode = false, ...options2 } = cfg;
1010
+ const { onStdout, onStderr, onError, onComplete } = observer ?? {};
982
1011
  const date = (/* @__PURE__ */ new Date()).toISOString();
983
1012
  const start = performance.now();
984
1013
  return new Promise((resolve, reject) => {
985
- const process2 = spawn(command2, args, { cwd, shell: true });
1014
+ const spawnedProcess = spawn(command2, args ?? [], {
1015
+ shell: true,
1016
+ ...options2
1017
+ });
986
1018
  let stdout = "";
987
1019
  let stderr = "";
988
- process2.stdout.on("data", (data) => {
1020
+ spawnedProcess.stdout.on("data", (data) => {
989
1021
  stdout += String(data);
990
- onStdout?.(String(data));
1022
+ onStdout?.(String(data), spawnedProcess);
991
1023
  });
992
- process2.stderr.on("data", (data) => {
1024
+ spawnedProcess.stderr.on("data", (data) => {
993
1025
  stderr += String(data);
1026
+ onStderr?.(String(data), spawnedProcess);
994
1027
  });
995
- process2.on("error", (err) => {
1028
+ spawnedProcess.on("error", (err) => {
996
1029
  stderr += err.toString();
997
1030
  });
998
- process2.on("close", (code2) => {
1031
+ spawnedProcess.on("close", (code2) => {
999
1032
  const timings = { date, duration: calcDuration(start) };
1000
1033
  if (code2 === 0 || ignoreExitCode) {
1001
1034
  onComplete?.();
@@ -1569,7 +1602,33 @@ function getColumnAlignments(tableData) {
1569
1602
  }
1570
1603
 
1571
1604
  // packages/utils/src/lib/reports/formatting.ts
1572
- import { MarkdownDocument, md as md2 } from "build-md";
1605
+ import {
1606
+ MarkdownDocument,
1607
+ md as md2
1608
+ } from "build-md";
1609
+ import { posix as pathPosix } from "node:path";
1610
+
1611
+ // packages/utils/src/lib/reports/ide-environment.ts
1612
+ function getEnvironmentType() {
1613
+ if (isVSCode()) {
1614
+ return "vscode";
1615
+ }
1616
+ if (isGitHub()) {
1617
+ return "github";
1618
+ }
1619
+ return "other";
1620
+ }
1621
+ function isVSCode() {
1622
+ return process.env["TERM_PROGRAM"] === "vscode";
1623
+ }
1624
+ function isGitHub() {
1625
+ return process.env["GITHUB_ACTIONS"] === "true";
1626
+ }
1627
+ function getGitHubBaseUrl() {
1628
+ return `${process.env["GITHUB_SERVER_URL"]}/${process.env["GITHUB_REPOSITORY"]}/blob/${process.env["GITHUB_SHA"]}`;
1629
+ }
1630
+
1631
+ // packages/utils/src/lib/reports/formatting.ts
1573
1632
  function tableSection(tableData, options2) {
1574
1633
  if (tableData.rows.length === 0) {
1575
1634
  return null;
@@ -1607,6 +1666,44 @@ function metaDescription(audit) {
1607
1666
  }
1608
1667
  return "";
1609
1668
  }
1669
+ function linkToLocalSourceForIde(source, options2) {
1670
+ const { file, position } = source;
1671
+ const { outputDir } = options2 ?? {};
1672
+ if (!outputDir) {
1673
+ return md2.code(file);
1674
+ }
1675
+ return md2.link(formatFileLink(file, position, outputDir), md2.code(file));
1676
+ }
1677
+ function formatSourceLine(position) {
1678
+ if (!position) {
1679
+ return "";
1680
+ }
1681
+ const { startLine, endLine } = position;
1682
+ return endLine && startLine !== endLine ? `${startLine}-${endLine}` : `${startLine}`;
1683
+ }
1684
+ function formatGitHubLink(file, position) {
1685
+ const baseUrl = getGitHubBaseUrl();
1686
+ if (!position) {
1687
+ return `${baseUrl}/${file}`;
1688
+ }
1689
+ const { startLine, endLine, startColumn, endColumn } = position;
1690
+ const start = startColumn ? `L${startLine}C${startColumn}` : `L${startLine}`;
1691
+ const end = endLine ? endColumn ? `L${endLine}C${endColumn}` : `L${endLine}` : "";
1692
+ const lineRange = end && start !== end ? `${start}-${end}` : start;
1693
+ return `${baseUrl}/${file}#${lineRange}`;
1694
+ }
1695
+ function formatFileLink(file, position, outputDir) {
1696
+ const relativePath = pathPosix.relative(outputDir, file);
1697
+ const env = getEnvironmentType();
1698
+ switch (env) {
1699
+ case "vscode":
1700
+ return position ? `${relativePath}#L${position.startLine}` : relativePath;
1701
+ case "github":
1702
+ return formatGitHubLink(file, position);
1703
+ default:
1704
+ return relativePath;
1705
+ }
1706
+ }
1610
1707
 
1611
1708
  // packages/utils/src/lib/reports/generate-md-report-categoy-section.ts
1612
1709
  import { MarkdownDocument as MarkdownDocument2, md as md3 } from "build-md";
@@ -1808,16 +1905,16 @@ function auditDetailsAuditValue({
1808
1905
  String(displayValue ?? value)
1809
1906
  )} (score: ${formatReportScore(score)})`;
1810
1907
  }
1811
- function generateMdReport(report) {
1908
+ function generateMdReport(report, options2) {
1812
1909
  return new MarkdownDocument3().heading(HIERARCHY.level_1, REPORT_HEADLINE_TEXT).$if(
1813
1910
  report.categories.length > 0,
1814
1911
  (doc) => doc.$concat(
1815
1912
  categoriesOverviewSection(report),
1816
1913
  categoriesDetailsSection(report)
1817
1914
  )
1818
- ).$concat(auditsSection(report), aboutSection(report)).rule().paragraph(md4`${FOOTER_PREFIX} ${md4.link(README_LINK, "Code PushUp")}`).toString();
1915
+ ).$concat(auditsSection(report, options2), aboutSection(report)).rule().paragraph(md4`${FOOTER_PREFIX} ${md4.link(README_LINK, "Code PushUp")}`).toString();
1819
1916
  }
1820
- function auditDetailsIssues(issues = []) {
1917
+ function auditDetailsIssues(issues = [], options2) {
1821
1918
  if (issues.length === 0) {
1822
1919
  return null;
1823
1920
  }
@@ -1833,39 +1930,36 @@ function auditDetailsIssues(issues = []) {
1833
1930
  if (!source) {
1834
1931
  return [severity, message];
1835
1932
  }
1836
- const file = md4.code(source.file);
1933
+ const file = linkToLocalSourceForIde(source, options2);
1837
1934
  if (!source.position) {
1838
1935
  return [severity, message, file];
1839
1936
  }
1840
- const { startLine, endLine } = source.position;
1841
- const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
1937
+ const line = formatSourceLine(source.position);
1842
1938
  return [severity, message, file, line];
1843
1939
  })
1844
1940
  );
1845
1941
  }
1846
- function auditDetails(audit) {
1942
+ function auditDetails(audit, options2) {
1847
1943
  const { table: table2, issues = [] } = audit.details ?? {};
1848
1944
  const detailsValue = auditDetailsAuditValue(audit);
1849
1945
  if (issues.length === 0 && !table2?.rows.length) {
1850
1946
  return new MarkdownDocument3().paragraph(detailsValue);
1851
1947
  }
1852
1948
  const tableSectionContent = table2 && tableSection(table2);
1853
- const issuesSectionContent = issues.length > 0 && auditDetailsIssues(issues);
1949
+ const issuesSectionContent = issues.length > 0 && auditDetailsIssues(issues, options2);
1854
1950
  return new MarkdownDocument3().details(
1855
1951
  detailsValue,
1856
1952
  new MarkdownDocument3().$concat(tableSectionContent, issuesSectionContent)
1857
1953
  );
1858
1954
  }
1859
- function auditsSection({
1860
- plugins
1861
- }) {
1955
+ function auditsSection({ plugins }, options2) {
1862
1956
  return new MarkdownDocument3().heading(HIERARCHY.level_2, "\u{1F6E1}\uFE0F Audits").$foreach(
1863
1957
  plugins.flatMap(
1864
1958
  (plugin) => plugin.audits.map((audit) => ({ ...audit, plugin }))
1865
1959
  ),
1866
1960
  (doc, { plugin, ...audit }) => {
1867
1961
  const auditTitle = `${audit.title} (${plugin.title})`;
1868
- const detailsContent = auditDetails(audit);
1962
+ const detailsContent = auditDetails(audit, options2);
1869
1963
  const descriptionContent = metaDescription(audit);
1870
1964
  return doc.heading(HIERARCHY.level_3, auditTitle).$concat(detailsContent).paragraph(descriptionContent);
1871
1965
  }
@@ -1890,17 +1984,17 @@ function pluginMetaTable({
1890
1984
  { heading: "Version", alignment: "center" },
1891
1985
  { heading: "Duration", alignment: "right" }
1892
1986
  ],
1893
- plugins.map(({ title, audits, version: version2 = "", duration }) => [
1987
+ plugins.map(({ title, audits, version: version3 = "", duration }) => [
1894
1988
  title,
1895
1989
  audits.length.toString(),
1896
- version2 && md4.code(version2),
1990
+ version3 && md4.code(version3),
1897
1991
  formatDuration(duration)
1898
1992
  ])
1899
1993
  ];
1900
1994
  }
1901
1995
  function reportMetaTable({
1902
1996
  commit,
1903
- version: version2,
1997
+ version: version3,
1904
1998
  duration,
1905
1999
  plugins,
1906
2000
  categories
@@ -1917,7 +2011,7 @@ function reportMetaTable({
1917
2011
  [
1918
2012
  [
1919
2013
  commit ? `${commit.message} (${commit.hash})` : "N/A",
1920
- md4.code(version2),
2014
+ md4.code(version3),
1921
2015
  formatDuration(duration),
1922
2016
  plugins.length.toString(),
1923
2017
  categories.length.toString(),
@@ -1929,19 +2023,111 @@ function reportMetaTable({
1929
2023
 
1930
2024
  // packages/utils/src/lib/reports/generate-md-reports-diff.ts
1931
2025
  import {
1932
- MarkdownDocument as MarkdownDocument4,
1933
- md as md5
2026
+ MarkdownDocument as MarkdownDocument5,
2027
+ md as md6
1934
2028
  } from "build-md";
2029
+
2030
+ // packages/utils/src/lib/reports/generate-md-reports-diff-utils.ts
2031
+ import { MarkdownDocument as MarkdownDocument4, md as md5 } from "build-md";
1935
2032
  var MAX_ROWS = 100;
1936
- function generateMdReportsDiff(diff, portalUrl) {
1937
- return new MarkdownDocument4().$concat(
1938
- createDiffHeaderSection(diff, portalUrl),
1939
- createDiffCategoriesSection(diff),
1940
- createDiffGroupsSection(diff),
1941
- createDiffAuditsSection(diff)
1942
- ).toString();
2033
+ function summarizeUnchanged(token, { changed, unchanged }) {
2034
+ const pluralizedCount = changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`;
2035
+ const pluralizedVerb = unchanged.length === 1 ? "is" : "are";
2036
+ return `${pluralizedCount} ${pluralizedVerb} unchanged.`;
2037
+ }
2038
+ function summarizeDiffOutcomes(outcomes, token) {
2039
+ return objectToEntries(countDiffOutcomes(outcomes)).filter(
2040
+ (entry) => entry[0] !== "unchanged" && entry[1] > 0
2041
+ ).map(([outcome, count]) => {
2042
+ const formattedCount = `<strong>${count}</strong> ${pluralize(
2043
+ token,
2044
+ count
2045
+ )}`;
2046
+ switch (outcome) {
2047
+ case "positive":
2048
+ return `\u{1F44D} ${formattedCount} improved`;
2049
+ case "negative":
2050
+ return `\u{1F44E} ${formattedCount} regressed`;
2051
+ case "mixed":
2052
+ return `${formattedCount} changed without impacting score`;
2053
+ }
2054
+ }).join(", ");
2055
+ }
2056
+ function createGroupsOrAuditsDetails(token, { changed, unchanged }, ...[columns, rows]) {
2057
+ if (changed.length === 0) {
2058
+ return new MarkdownDocument4().paragraph(
2059
+ summarizeUnchanged(token, { changed, unchanged })
2060
+ );
2061
+ }
2062
+ return new MarkdownDocument4().table(columns, rows.slice(0, MAX_ROWS)).paragraph(
2063
+ changed.length > MAX_ROWS && md5.italic(
2064
+ `Only the ${MAX_ROWS} most affected ${pluralize(
2065
+ token
2066
+ )} are listed above for brevity.`
2067
+ )
2068
+ ).paragraph(
2069
+ unchanged.length > 0 && summarizeUnchanged(token, { changed, unchanged })
2070
+ );
2071
+ }
2072
+ function formatTitle({
2073
+ title,
2074
+ docsUrl
2075
+ }) {
2076
+ if (docsUrl) {
2077
+ return md5.link(docsUrl, title);
2078
+ }
2079
+ return title;
2080
+ }
2081
+ function formatPortalLink(portalUrl) {
2082
+ return portalUrl && md5.link(portalUrl, "\u{1F575}\uFE0F See full comparison in Code PushUp portal \u{1F50D}");
2083
+ }
2084
+ function sortChanges(changes) {
2085
+ return [...changes].sort(
2086
+ (a, b) => Math.abs(b.scores.diff) - Math.abs(a.scores.diff) || Math.abs(b.values?.diff ?? 0) - Math.abs(a.values?.diff ?? 0)
2087
+ );
2088
+ }
2089
+ function getDiffChanges(diff) {
2090
+ return [
2091
+ ...diff.categories.changed,
2092
+ ...diff.groups.changed,
2093
+ ...diff.audits.changed
2094
+ ];
2095
+ }
2096
+ function changesToDiffOutcomes(changes) {
2097
+ return changes.map((change) => {
2098
+ if (change.scores.diff > 0) {
2099
+ return "positive";
2100
+ }
2101
+ if (change.scores.diff < 0) {
2102
+ return "negative";
2103
+ }
2104
+ if (change.values != null && change.values.diff !== 0) {
2105
+ return "mixed";
2106
+ }
2107
+ return "unchanged";
2108
+ });
1943
2109
  }
1944
- function createDiffHeaderSection(diff, portalUrl) {
2110
+ function mergeDiffOutcomes(outcomes) {
2111
+ if (outcomes.every((outcome) => outcome === "unchanged")) {
2112
+ return "unchanged";
2113
+ }
2114
+ if (outcomes.includes("positive") && !outcomes.includes("negative")) {
2115
+ return "positive";
2116
+ }
2117
+ if (outcomes.includes("negative") && !outcomes.includes("positive")) {
2118
+ return "negative";
2119
+ }
2120
+ return "mixed";
2121
+ }
2122
+ function countDiffOutcomes(outcomes) {
2123
+ return {
2124
+ positive: outcomes.filter((outcome) => outcome === "positive").length,
2125
+ negative: outcomes.filter((outcome) => outcome === "negative").length,
2126
+ mixed: outcomes.filter((outcome) => outcome === "mixed").length,
2127
+ unchanged: outcomes.filter((outcome) => outcome === "unchanged").length
2128
+ };
2129
+ }
2130
+ function formatReportOutcome(outcome, commits) {
1945
2131
  const outcomeTexts = {
1946
2132
  positive: md5`🥳 Code PushUp report has ${md5.bold("improved")}`,
1947
2133
  negative: md5`😟 Code PushUp report has ${md5.bold("regressed")}`,
@@ -1950,36 +2136,91 @@ function createDiffHeaderSection(diff, portalUrl) {
1950
2136
  )}`,
1951
2137
  unchanged: md5`😐 Code PushUp report is ${md5.bold("unchanged")}`
1952
2138
  };
2139
+ if (commits) {
2140
+ const commitsText = `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
2141
+ return md5`${outcomeTexts[outcome]} – ${commitsText}.`;
2142
+ }
2143
+ return md5`${outcomeTexts[outcome]}.`;
2144
+ }
2145
+ function compareDiffsBy(type, a, b) {
2146
+ return sumScoreChanges(b[type].changed) - sumScoreChanges(a[type].changed) || sumConfigChanges(b[type]) - sumConfigChanges(a[type]);
2147
+ }
2148
+ function sumScoreChanges(changes) {
2149
+ return changes.reduce(
2150
+ (acc, { scores }) => acc + Math.abs(scores.diff),
2151
+ 0
2152
+ );
2153
+ }
2154
+ function sumConfigChanges({
2155
+ added,
2156
+ removed
2157
+ }) {
2158
+ return added.length + removed.length;
2159
+ }
2160
+
2161
+ // packages/utils/src/lib/reports/generate-md-reports-diff.ts
2162
+ function generateMdReportsDiff(diff) {
2163
+ return new MarkdownDocument5().$concat(
2164
+ createDiffHeaderSection(diff),
2165
+ createDiffCategoriesSection(diff),
2166
+ createDiffDetailsSection(diff)
2167
+ ).toString();
2168
+ }
2169
+ function generateMdReportsDiffForMonorepo(diffs) {
2170
+ const diffsWithOutcomes = diffs.map((diff) => ({
2171
+ ...diff,
2172
+ outcome: mergeDiffOutcomes(changesToDiffOutcomes(getDiffChanges(diff)))
2173
+ })).sort(
2174
+ (a, b) => compareDiffsBy("categories", a, b) || compareDiffsBy("groups", a, b) || compareDiffsBy("audits", a, b) || a.label.localeCompare(b.label)
2175
+ );
2176
+ const unchanged = diffsWithOutcomes.filter(
2177
+ ({ outcome }) => outcome === "unchanged"
2178
+ );
2179
+ const changed = diffsWithOutcomes.filter((diff) => !unchanged.includes(diff));
2180
+ return new MarkdownDocument5().$concat(
2181
+ createDiffHeaderSection(diffs),
2182
+ ...changed.map(createDiffProjectSection)
2183
+ ).$if(
2184
+ unchanged.length > 0,
2185
+ (doc) => doc.rule().paragraph(summarizeUnchanged("project", { unchanged, changed }))
2186
+ ).toString();
2187
+ }
2188
+ function createDiffHeaderSection(diff) {
1953
2189
  const outcome = mergeDiffOutcomes(
1954
- changesToDiffOutcomes([
1955
- ...diff.categories.changed,
1956
- ...diff.groups.changed,
1957
- ...diff.audits.changed
1958
- ])
2190
+ changesToDiffOutcomes(toArray(diff).flatMap(getDiffChanges))
1959
2191
  );
1960
- const styleCommits = (commits) => `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
1961
- return new MarkdownDocument4().heading(HIERARCHY.level_1, "Code PushUp").paragraph(
1962
- diff.commits ? md5`${outcomeTexts[outcome]} – ${styleCommits(diff.commits)}.` : outcomeTexts[outcome]
1963
- ).paragraph(
1964
- portalUrl && md5.link(portalUrl, "\u{1F575}\uFE0F See full comparison in Code PushUp portal \u{1F50D}")
2192
+ const commits = Array.isArray(diff) ? diff[0]?.commits : diff.commits;
2193
+ const portalUrl = Array.isArray(diff) ? void 0 : diff.portalUrl;
2194
+ return new MarkdownDocument5().heading(HIERARCHY.level_1, "Code PushUp").paragraph(formatReportOutcome(outcome, commits)).paragraph(formatPortalLink(portalUrl));
2195
+ }
2196
+ function createDiffProjectSection(diff) {
2197
+ return new MarkdownDocument5().heading(HIERARCHY.level_2, md6`💼 Project ${md6.code(diff.label)}`).paragraph(formatReportOutcome(diff.outcome)).paragraph(formatPortalLink(diff.portalUrl)).$concat(
2198
+ createDiffCategoriesSection(diff, {
2199
+ skipHeading: true,
2200
+ skipUnchanged: true
2201
+ }),
2202
+ createDiffDetailsSection(diff, HIERARCHY.level_3)
1965
2203
  );
1966
2204
  }
1967
- function createDiffCategoriesSection(diff) {
2205
+ function createDiffCategoriesSection(diff, options2) {
1968
2206
  const { changed, unchanged, added } = diff.categories;
2207
+ const { skipHeading, skipUnchanged } = options2 ?? {};
1969
2208
  const categoriesCount = changed.length + unchanged.length + added.length;
1970
2209
  const hasChanges = unchanged.length < categoriesCount;
1971
2210
  if (categoriesCount === 0) {
1972
2211
  return null;
1973
2212
  }
1974
- const columns = [
1975
- { heading: "\u{1F3F7}\uFE0F Category", alignment: "left" },
1976
- {
1977
- heading: hasChanges ? "\u2B50 Previous score" : "\u2B50 Score",
1978
- alignment: "center"
1979
- },
1980
- { heading: "\u2B50 Current score", alignment: "center" },
1981
- { heading: "\u{1F504} Score change", alignment: "center" }
1982
- ];
2213
+ const [columns, rows] = createCategoriesTable(diff, {
2214
+ hasChanges,
2215
+ skipUnchanged
2216
+ });
2217
+ return new MarkdownDocument5().heading(HIERARCHY.level_2, !skipHeading && "\u{1F3F7}\uFE0F Categories").table(columns, rows).paragraph(added.length > 0 && md6.italic("(\\*) New category.")).paragraph(
2218
+ skipUnchanged && unchanged.length > 0 && summarizeUnchanged("category", { changed, unchanged })
2219
+ );
2220
+ }
2221
+ function createCategoriesTable(diff, options2) {
2222
+ const { changed, unchanged, added } = diff.categories;
2223
+ const { hasChanges, skipUnchanged } = options2;
1983
2224
  const rows = [
1984
2225
  ...sortChanges(changed).map((category) => [
1985
2226
  formatTitle(category),
@@ -1991,27 +2232,55 @@ function createDiffCategoriesSection(diff) {
1991
2232
  ]),
1992
2233
  ...added.map((category) => [
1993
2234
  formatTitle(category),
1994
- md5.italic("n/a (\\*)"),
2235
+ md6.italic("n/a (\\*)"),
1995
2236
  formatScoreWithColor(category.score),
1996
- md5.italic("n/a (\\*)")
2237
+ md6.italic("n/a (\\*)")
1997
2238
  ]),
1998
- ...unchanged.map((category) => [
2239
+ ...skipUnchanged ? [] : unchanged.map((category) => [
1999
2240
  formatTitle(category),
2000
2241
  formatScoreWithColor(category.score, { skipBold: true }),
2001
2242
  formatScoreWithColor(category.score),
2002
2243
  "\u2013"
2003
2244
  ])
2004
2245
  ];
2005
- return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F3F7}\uFE0F Categories").table(
2246
+ if (rows.length === 0) {
2247
+ return [[], []];
2248
+ }
2249
+ const columns = [
2250
+ { heading: "\u{1F3F7}\uFE0F Category", alignment: "left" },
2251
+ {
2252
+ heading: hasChanges ? "\u2B50 Previous score" : "\u2B50 Score",
2253
+ alignment: "center"
2254
+ },
2255
+ { heading: "\u2B50 Current score", alignment: "center" },
2256
+ { heading: "\u{1F504} Score change", alignment: "center" }
2257
+ ];
2258
+ return [
2006
2259
  hasChanges ? columns : columns.slice(0, 2),
2007
2260
  rows.map((row) => hasChanges ? row : row.slice(0, 2))
2008
- ).paragraph(added.length > 0 && md5.italic("(\\*) New category."));
2261
+ ];
2262
+ }
2263
+ function createDiffDetailsSection(diff, level = HIERARCHY.level_2) {
2264
+ if (diff.groups.changed.length + diff.audits.changed.length === 0) {
2265
+ return null;
2266
+ }
2267
+ const summary = ["group", "audit"].map(
2268
+ (token) => summarizeDiffOutcomes(
2269
+ changesToDiffOutcomes(diff[`${token}s`].changed),
2270
+ token
2271
+ )
2272
+ ).filter(Boolean).join(", ");
2273
+ const details2 = new MarkdownDocument5().$concat(
2274
+ createDiffGroupsSection(diff, level),
2275
+ createDiffAuditsSection(diff, level)
2276
+ );
2277
+ return new MarkdownDocument5().details(summary, details2);
2009
2278
  }
2010
- function createDiffGroupsSection(diff) {
2279
+ function createDiffGroupsSection(diff, level) {
2011
2280
  if (diff.groups.changed.length + diff.groups.unchanged.length === 0) {
2012
2281
  return null;
2013
2282
  }
2014
- return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F5C3}\uFE0F Groups").$concat(
2283
+ return new MarkdownDocument5().heading(level, "\u{1F5C3}\uFE0F Groups").$concat(
2015
2284
  createGroupsOrAuditsDetails(
2016
2285
  "group",
2017
2286
  diff.groups,
@@ -2032,8 +2301,8 @@ function createDiffGroupsSection(diff) {
2032
2301
  )
2033
2302
  );
2034
2303
  }
2035
- function createDiffAuditsSection(diff) {
2036
- return new MarkdownDocument4().heading(HIERARCHY.level_2, "\u{1F6E1}\uFE0F Audits").$concat(
2304
+ function createDiffAuditsSection(diff, level) {
2305
+ return new MarkdownDocument5().heading(level, "\u{1F6E1}\uFE0F Audits").$concat(
2037
2306
  createGroupsOrAuditsDetails(
2038
2307
  "audit",
2039
2308
  diff.audits,
@@ -2048,7 +2317,7 @@ function createDiffAuditsSection(diff) {
2048
2317
  formatTitle(audit.plugin),
2049
2318
  formatTitle(audit),
2050
2319
  `${scoreMarker(audit.scores.before, "square")} ${audit.displayValues.before || audit.values.before.toString()}`,
2051
- md5`${scoreMarker(audit.scores.after, "square")} ${md5.bold(
2320
+ md6`${scoreMarker(audit.scores.after, "square")} ${md6.bold(
2052
2321
  audit.displayValues.after || audit.values.after.toString()
2053
2322
  )}`,
2054
2323
  formatValueChange(audit)
@@ -2056,96 +2325,6 @@ function createDiffAuditsSection(diff) {
2056
2325
  )
2057
2326
  );
2058
2327
  }
2059
- function createGroupsOrAuditsDetails(token, { changed, unchanged }, ...[columns, rows]) {
2060
- if (changed.length === 0) {
2061
- return new MarkdownDocument4().paragraph(
2062
- summarizeUnchanged(token, { changed, unchanged })
2063
- );
2064
- }
2065
- return new MarkdownDocument4().details(
2066
- summarizeDiffOutcomes(changesToDiffOutcomes(changed), token),
2067
- md5`${md5.table(columns, rows.slice(0, MAX_ROWS))}${changed.length > MAX_ROWS ? md5.paragraph(
2068
- md5.italic(
2069
- `Only the ${MAX_ROWS} most affected ${pluralize(
2070
- token
2071
- )} are listed above for brevity.`
2072
- )
2073
- ) : ""}${unchanged.length > 0 ? md5.paragraph(summarizeUnchanged(token, { changed, unchanged })) : ""}`
2074
- );
2075
- }
2076
- function summarizeUnchanged(token, { changed, unchanged }) {
2077
- return [
2078
- changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`,
2079
- unchanged.length === 1 ? "is" : "are",
2080
- "unchanged."
2081
- ].join(" ");
2082
- }
2083
- function summarizeDiffOutcomes(outcomes, token) {
2084
- return objectToEntries(countDiffOutcomes(outcomes)).filter(
2085
- (entry) => entry[0] !== "unchanged" && entry[1] > 0
2086
- ).map(([outcome, count]) => {
2087
- const formattedCount = `<strong>${count}</strong> ${pluralize(
2088
- token,
2089
- count
2090
- )}`;
2091
- switch (outcome) {
2092
- case "positive":
2093
- return `\u{1F44D} ${formattedCount} improved`;
2094
- case "negative":
2095
- return `\u{1F44E} ${formattedCount} regressed`;
2096
- case "mixed":
2097
- return `${formattedCount} changed without impacting score`;
2098
- }
2099
- }).join(", ");
2100
- }
2101
- function formatTitle({
2102
- title,
2103
- docsUrl
2104
- }) {
2105
- if (docsUrl) {
2106
- return md5.link(docsUrl, title);
2107
- }
2108
- return title;
2109
- }
2110
- function sortChanges(changes) {
2111
- return [...changes].sort(
2112
- (a, b) => Math.abs(b.scores.diff) - Math.abs(a.scores.diff) || Math.abs(b.values?.diff ?? 0) - Math.abs(a.values?.diff ?? 0)
2113
- );
2114
- }
2115
- function changesToDiffOutcomes(changes) {
2116
- return changes.map((change) => {
2117
- if (change.scores.diff > 0) {
2118
- return "positive";
2119
- }
2120
- if (change.scores.diff < 0) {
2121
- return "negative";
2122
- }
2123
- if (change.values != null && change.values.diff !== 0) {
2124
- return "mixed";
2125
- }
2126
- return "unchanged";
2127
- });
2128
- }
2129
- function mergeDiffOutcomes(outcomes) {
2130
- if (outcomes.every((outcome) => outcome === "unchanged")) {
2131
- return "unchanged";
2132
- }
2133
- if (outcomes.includes("positive") && !outcomes.includes("negative")) {
2134
- return "positive";
2135
- }
2136
- if (outcomes.includes("negative") && !outcomes.includes("positive")) {
2137
- return "negative";
2138
- }
2139
- return "mixed";
2140
- }
2141
- function countDiffOutcomes(outcomes) {
2142
- return {
2143
- positive: outcomes.filter((outcome) => outcome === "positive").length,
2144
- negative: outcomes.filter((outcome) => outcome === "negative").length,
2145
- mixed: outcomes.filter((outcome) => outcome === "mixed").length,
2146
- unchanged: outcomes.filter((outcome) => outcome === "unchanged").length
2147
- };
2148
- }
2149
2328
 
2150
2329
  // packages/utils/src/lib/reports/load-report.ts
2151
2330
  import { join as join2 } from "node:path";
@@ -2178,8 +2357,8 @@ function logStdoutSummary(report) {
2178
2357
  log();
2179
2358
  }
2180
2359
  function reportToHeaderSection(report) {
2181
- const { packageName, version: version2 } = report;
2182
- return `${bold4(REPORT_HEADLINE_TEXT)} - ${packageName}@${version2}`;
2360
+ const { packageName, version: version3 } = report;
2361
+ return `${bold4(REPORT_HEADLINE_TEXT)} - ${packageName}@${version3}`;
2183
2362
  }
2184
2363
  function logPlugins(report) {
2185
2364
  const { plugins } = report;
@@ -2202,7 +2381,8 @@ function logPlugins(report) {
2202
2381
  },
2203
2382
  {
2204
2383
  text: cyanBright(audit.displayValue || `${audit.value}`),
2205
- width: 10,
2384
+ // eslint-disable-next-line no-magic-numbers
2385
+ width: 20,
2206
2386
  padding: [0, 0, 0, 0]
2207
2387
  }
2208
2388
  ]);
@@ -2355,7 +2535,7 @@ var verboseUtils = (verbose = false) => ({
2355
2535
 
2356
2536
  // packages/core/package.json
2357
2537
  var name = "@code-pushup/core";
2358
- var version = "0.49.0";
2538
+ var version = "0.51.0";
2359
2539
 
2360
2540
  // packages/core/src/lib/implementation/execute-plugin.ts
2361
2541
  import { bold as bold5 } from "ansis";
@@ -2564,7 +2744,7 @@ async function persistReport(report, options2) {
2564
2744
  case "md":
2565
2745
  return {
2566
2746
  format: "md",
2567
- content: generateMdReport(sortedScoredReport)
2747
+ content: generateMdReport(sortedScoredReport, { outputDir })
2568
2748
  };
2569
2749
  }
2570
2750
  });
@@ -2751,7 +2931,7 @@ function selectMeta(meta) {
2751
2931
  }
2752
2932
 
2753
2933
  // packages/core/src/lib/compare.ts
2754
- async function compareReportFiles(inputPaths, persistConfig, uploadConfig) {
2934
+ async function compareReportFiles(inputPaths, persistConfig, uploadConfig, label) {
2755
2935
  const { outputDir, filename, format } = persistConfig;
2756
2936
  const [reportBefore, reportAfter] = await Promise.all([
2757
2937
  readJsonFile(inputPaths.before),
@@ -2761,12 +2941,20 @@ async function compareReportFiles(inputPaths, persistConfig, uploadConfig) {
2761
2941
  before: reportSchema.parse(reportBefore),
2762
2942
  after: reportSchema.parse(reportAfter)
2763
2943
  };
2764
- const reportsDiff = compareReports(reports);
2765
- const portalUrl = uploadConfig && reportsDiff.commits && format.includes("md") ? await fetchPortalComparisonLink(uploadConfig, reportsDiff.commits) : void 0;
2944
+ const diff = compareReports(reports);
2945
+ if (label) {
2946
+ diff.label = label;
2947
+ }
2948
+ if (uploadConfig && diff.commits) {
2949
+ diff.portalUrl = await fetchPortalComparisonLink(
2950
+ uploadConfig,
2951
+ diff.commits
2952
+ );
2953
+ }
2766
2954
  return Promise.all(
2767
2955
  format.map(async (fmt) => {
2768
2956
  const outputPath = join5(outputDir, `${filename}-diff.${fmt}`);
2769
- const content = reportsDiffToFileContent(reportsDiff, fmt, portalUrl);
2957
+ const content = reportsDiffToFileContent(diff, fmt);
2770
2958
  await ensureDirectoryExists(outputDir);
2771
2959
  await writeFile2(outputPath, content);
2772
2960
  return outputPath;
@@ -2796,12 +2984,12 @@ function compareReports(reports) {
2796
2984
  duration
2797
2985
  };
2798
2986
  }
2799
- function reportsDiffToFileContent(reportsDiff, format, portalUrl) {
2987
+ function reportsDiffToFileContent(reportsDiff, format) {
2800
2988
  switch (format) {
2801
2989
  case "json":
2802
2990
  return JSON.stringify(reportsDiff, null, 2);
2803
2991
  case "md":
2804
- return generateMdReportsDiff(reportsDiff, portalUrl ?? void 0);
2992
+ return generateMdReportsDiff(reportsDiff);
2805
2993
  }
2806
2994
  }
2807
2995
  async function fetchPortalComparisonLink(uploadConfig, commits) {
@@ -3074,6 +3262,45 @@ async function autoloadRc(tsconfig) {
3074
3262
  );
3075
3263
  }
3076
3264
 
3265
+ // packages/core/src/lib/merge-diffs.ts
3266
+ import { writeFile as writeFile3 } from "node:fs/promises";
3267
+ import { basename, dirname, join as join7 } from "node:path";
3268
+ async function mergeDiffs(files, persistConfig) {
3269
+ const results = await Promise.allSettled(
3270
+ files.map(async (file) => {
3271
+ const json = await readJsonFile(file).catch((error) => {
3272
+ throw new Error(
3273
+ `Failed to read JSON file ${file} - ${stringifyError(error)}`
3274
+ );
3275
+ });
3276
+ const result = await reportsDiffSchema.safeParseAsync(json);
3277
+ if (!result.success) {
3278
+ throw new Error(
3279
+ `Invalid reports diff in ${file} - ${result.error.message}`
3280
+ );
3281
+ }
3282
+ return { ...result.data, file };
3283
+ })
3284
+ );
3285
+ results.filter(isPromiseRejectedResult).forEach(({ reason }) => {
3286
+ ui().logger.warning(
3287
+ `Skipped invalid report diff - ${stringifyError(reason)}`
3288
+ );
3289
+ });
3290
+ const diffs = results.filter(isPromiseFulfilledResult).map(({ value }) => value);
3291
+ const labeledDiffs = diffs.map((diff) => ({
3292
+ ...diff,
3293
+ label: diff.label || basename(dirname(diff.file))
3294
+ // fallback is parent folder name
3295
+ }));
3296
+ const markdown = generateMdReportsDiffForMonorepo(labeledDiffs);
3297
+ const { outputDir, filename } = persistConfig;
3298
+ const outputPath = join7(outputDir, `${filename}-diff.md`);
3299
+ await ensureDirectoryExists(outputDir);
3300
+ await writeFile3(outputPath, markdown);
3301
+ return outputPath;
3302
+ }
3303
+
3077
3304
  // packages/cli/src/lib/constants.ts
3078
3305
  var CLI_NAME = "Code PushUp CLI";
3079
3306
  var CLI_SCRIPT_NAME = "code-pushup";
@@ -3207,6 +3434,10 @@ function yargsCompareOptionsDefinition() {
3207
3434
  describe: "Path to target report.json",
3208
3435
  type: "string",
3209
3436
  demandOption: true
3437
+ },
3438
+ label: {
3439
+ describe: "Label for diff (e.g. project name)",
3440
+ type: "string"
3210
3441
  }
3211
3442
  };
3212
3443
  }
@@ -3222,11 +3453,12 @@ function yargsCompareCommandObject() {
3222
3453
  ui().logger.log(bold9(CLI_NAME));
3223
3454
  ui().logger.info(gray6(`Run ${command2}...`));
3224
3455
  const options2 = args;
3225
- const { before, after, persist, upload: upload2 } = options2;
3456
+ const { before, after, label, persist, upload: upload2 } = options2;
3226
3457
  const outputPaths = await compareReportFiles(
3227
3458
  { before, after },
3228
3459
  persist,
3229
- upload2
3460
+ upload2,
3461
+ label
3230
3462
  );
3231
3463
  ui().logger.info(
3232
3464
  `Reports diff written to ${outputPaths.map((path) => bold9(path)).join(" and ")}`
@@ -3268,7 +3500,8 @@ var onlyPluginsOption = {
3268
3500
  describe: "List of plugins to run. If not set all plugins are run.",
3269
3501
  type: "array",
3270
3502
  default: [],
3271
- coerce: coerceArray
3503
+ coerce: coerceArray,
3504
+ alias: "p"
3272
3505
  };
3273
3506
  function yargsOnlyPluginsOptionsDefinition() {
3274
3507
  return {
@@ -3281,7 +3514,8 @@ var skipPluginsOption = {
3281
3514
  describe: "List of plugins to skip. If not set all plugins are run.",
3282
3515
  type: "array",
3283
3516
  default: [],
3284
- coerce: coerceArray
3517
+ coerce: coerceArray,
3518
+ alias: "P"
3285
3519
  };
3286
3520
  function yargsSkipPluginsOptionsDefinition() {
3287
3521
  return {
@@ -3412,6 +3646,38 @@ function yargsHistoryCommandObject() {
3412
3646
  };
3413
3647
  }
3414
3648
 
3649
+ // packages/cli/src/lib/merge-diffs/merge-diffs-command.ts
3650
+ import { bold as bold11, gray as gray8 } from "ansis";
3651
+
3652
+ // packages/cli/src/lib/implementation/merge-diffs.options.ts
3653
+ function yargsMergeDiffsOptionsDefinition() {
3654
+ return {
3655
+ files: {
3656
+ describe: "List of report-diff.json paths",
3657
+ type: "array",
3658
+ demandOption: true
3659
+ }
3660
+ };
3661
+ }
3662
+
3663
+ // packages/cli/src/lib/merge-diffs/merge-diffs-command.ts
3664
+ function yargsMergeDiffsCommandObject() {
3665
+ const command2 = "merge-diffs";
3666
+ return {
3667
+ command: command2,
3668
+ describe: "Combine many report diffs into a single diff file",
3669
+ builder: yargsMergeDiffsOptionsDefinition(),
3670
+ handler: async (args) => {
3671
+ ui().logger.log(bold11(CLI_NAME));
3672
+ ui().logger.info(gray8(`Run ${command2}...`));
3673
+ const options2 = args;
3674
+ const { files, persist } = options2;
3675
+ const outputPath = await mergeDiffs(files, persist);
3676
+ ui().logger.info(`Reports diff written to ${bold11(outputPath)}`);
3677
+ }
3678
+ };
3679
+ }
3680
+
3415
3681
  // packages/cli/src/lib/print-config/print-config-command.ts
3416
3682
  function yargsConfigCommandObject() {
3417
3683
  const command2 = "print-config";
@@ -3427,15 +3693,15 @@ function yargsConfigCommandObject() {
3427
3693
  }
3428
3694
 
3429
3695
  // packages/cli/src/lib/upload/upload-command.ts
3430
- import { bold as bold11, gray as gray8 } from "ansis";
3696
+ import { bold as bold12, gray as gray9 } from "ansis";
3431
3697
  function yargsUploadCommandObject() {
3432
3698
  const command2 = "upload";
3433
3699
  return {
3434
3700
  command: command2,
3435
3701
  describe: "Upload report results to the portal",
3436
3702
  handler: async (args) => {
3437
- ui().logger.log(bold11(CLI_NAME));
3438
- ui().logger.info(gray8(`Run ${command2}...`));
3703
+ ui().logger.log(bold12(CLI_NAME));
3704
+ ui().logger.info(gray9(`Run ${command2}...`));
3439
3705
  const options2 = args;
3440
3706
  if (options2.upload == null) {
3441
3707
  renderIntegratePortalHint();
@@ -3458,7 +3724,8 @@ var commands = [
3458
3724
  yargsUploadCommandObject(),
3459
3725
  yargsHistoryCommandObject(),
3460
3726
  yargsCompareCommandObject(),
3461
- yargsConfigCommandObject()
3727
+ yargsConfigCommandObject(),
3728
+ yargsMergeDiffsCommandObject()
3462
3729
  ];
3463
3730
 
3464
3731
  // packages/cli/src/lib/implementation/core-config.middleware.ts
@@ -3499,7 +3766,6 @@ async function coreConfigMiddleware(processArgs) {
3499
3766
  var normalizeFormats = (formats) => (formats ?? []).flatMap((format) => format.split(","));
3500
3767
 
3501
3768
  // packages/cli/src/lib/implementation/validate-plugin-filter-options.utils.ts
3502
- import { yellow } from "ansis";
3503
3769
  function validatePluginFilterOption(filterOption, {
3504
3770
  plugins,
3505
3771
  categories
@@ -3513,13 +3779,9 @@ function validatePluginFilterOption(filterOption, {
3513
3779
  );
3514
3780
  const isSkipOption = filterOption === "skipPlugins";
3515
3781
  const filterFunction = (plugin) => isSkipOption ? pluginsToFilterSet.has(plugin) : !pluginsToFilterSet.has(plugin);
3516
- if (missingPlugins.length > 0 && verbose) {
3517
- ui().logger.info(
3518
- `${yellow(
3519
- "\u26A0"
3520
- )} The --${filterOption} argument references plugins with "${missingPlugins.join(
3521
- '", "'
3522
- )}" slugs, but no such plugins are present in the configuration. Expected one of the following plugin slugs: "${plugins.map(({ slug }) => slug).join('", "')}".`
3782
+ if (missingPlugins.length > 0) {
3783
+ ui().logger.warning(
3784
+ `The --${filterOption} argument references ${missingPlugins.length === 1 ? "a plugin that does" : "plugins that do"} not exist: ${missingPlugins.join(", ")}. The valid plugin ${plugins.length === 1 ? "slug is" : "slugs are"} ${plugins.map(({ slug }) => slug).join(", ")}.`
3523
3785
  );
3524
3786
  }
3525
3787
  if (categories.length > 0 && verbose) {
@@ -3528,71 +3790,53 @@ function validatePluginFilterOption(filterOption, {
3528
3790
  ({ plugin }) => filterFunction(plugin)
3529
3791
  ).map(({ slug }) => slug);
3530
3792
  ui().logger.info(
3531
- `The --${filterOption} argument removed categories with "${removedCategorySlugs.join(
3532
- '", "'
3533
- )}" slugs.
3534
- `
3793
+ `The --${filterOption} argument removed the following categories: ${removedCategorySlugs.join(
3794
+ ", "
3795
+ )}.`
3535
3796
  );
3536
3797
  }
3537
3798
  }
3538
3799
 
3539
- // packages/cli/src/lib/implementation/only-plugins.middleware.ts
3540
- function onlyPluginsMiddleware(originalProcessArgs) {
3541
- const { categories = [], onlyPlugins: originalOnlyPlugins } = originalProcessArgs;
3542
- if (originalOnlyPlugins && originalOnlyPlugins.length > 0) {
3543
- const { verbose, plugins, onlyPlugins = [] } = originalProcessArgs;
3544
- validatePluginFilterOption(
3545
- "onlyPlugins",
3546
- { plugins, categories },
3547
- { pluginsToFilter: onlyPlugins, verbose }
3548
- );
3549
- const validOnlyPlugins = onlyPlugins.filter(
3550
- (oP) => plugins.find((p) => p.slug === oP)
3551
- );
3552
- const onlyPluginsSet = new Set(validOnlyPlugins);
3553
- return {
3554
- ...originalProcessArgs,
3555
- plugins: onlyPluginsSet.size > 0 ? plugins.filter(({ slug }) => onlyPluginsSet.has(slug)) : plugins,
3556
- categories: onlyPluginsSet.size > 0 ? filterItemRefsBy(
3557
- categories,
3558
- ({ plugin }) => onlyPluginsSet.has(plugin)
3559
- ) : categories
3560
- };
3561
- }
3562
- return {
3563
- ...originalProcessArgs,
3564
- // if undefined fill categories with empty array
3565
- categories
3566
- };
3567
- }
3568
-
3569
- // packages/cli/src/lib/implementation/skip-plugins.middleware.ts
3570
- function skipPluginsMiddleware(originalProcessArgs) {
3571
- const { categories = [], skipPlugins: originalSkipPlugins } = originalProcessArgs;
3572
- if (originalSkipPlugins && originalSkipPlugins.length > 0) {
3573
- const { verbose, plugins, skipPlugins = [] } = originalProcessArgs;
3574
- validatePluginFilterOption(
3575
- "skipPlugins",
3576
- { plugins, categories },
3577
- { pluginsToFilter: skipPlugins, verbose }
3578
- );
3579
- const validSkipPlugins = skipPlugins.filter(
3580
- (sP) => plugins.find((p) => p.slug === sP)
3581
- );
3582
- const skipPluginsSet = new Set(validSkipPlugins);
3583
- return {
3584
- ...originalProcessArgs,
3585
- plugins: skipPluginsSet.size > 0 ? plugins.filter(({ slug }) => !skipPluginsSet.has(slug)) : plugins,
3586
- categories: skipPluginsSet.size > 0 ? filterItemRefsBy(
3587
- categories,
3588
- ({ plugin }) => !skipPluginsSet.has(plugin)
3589
- ) : categories
3590
- };
3800
+ // packages/cli/src/lib/implementation/filter-plugins.middleware.ts
3801
+ function filterPluginsMiddleware(originalProcessArgs) {
3802
+ const {
3803
+ plugins,
3804
+ categories = [],
3805
+ skipPlugins = [],
3806
+ onlyPlugins = [],
3807
+ verbose
3808
+ } = originalProcessArgs;
3809
+ if (skipPlugins.length === 0 && onlyPlugins.length === 0) {
3810
+ return { ...originalProcessArgs, categories };
3591
3811
  }
3812
+ validatePluginFilterOption(
3813
+ "skipPlugins",
3814
+ { plugins, categories },
3815
+ { pluginsToFilter: skipPlugins, verbose }
3816
+ );
3817
+ validatePluginFilterOption(
3818
+ "onlyPlugins",
3819
+ { plugins, categories },
3820
+ { pluginsToFilter: onlyPlugins, verbose }
3821
+ );
3822
+ const validSkipPlugins = new Set(
3823
+ skipPlugins.filter((sP) => plugins.some((p) => p.slug === sP))
3824
+ );
3825
+ const pluginsAfterSkip = plugins.filter(
3826
+ ({ slug }) => !validSkipPlugins.has(slug)
3827
+ );
3828
+ const validOnlyPlugins = new Set(
3829
+ onlyPlugins.filter((oP) => pluginsAfterSkip.some((p) => p.slug === oP))
3830
+ );
3831
+ const filteredPlugins = validOnlyPlugins.size > 0 ? pluginsAfterSkip.filter(({ slug }) => validOnlyPlugins.has(slug)) : pluginsAfterSkip;
3832
+ const filteredCategories = filteredPlugins.length > 0 ? filterItemRefsBy(
3833
+ categories,
3834
+ ({ plugin }) => filteredPlugins.some(({ slug }) => slug === plugin)
3835
+ ) : categories;
3592
3836
  return {
3593
3837
  ...originalProcessArgs,
3594
- // if undefined fill categories with empty array
3595
- categories
3838
+ plugins: filteredPlugins,
3839
+ categories: filteredCategories
3596
3840
  };
3597
3841
  }
3598
3842
 
@@ -3603,11 +3847,7 @@ var middlewares = [
3603
3847
  applyBeforeValidation: false
3604
3848
  },
3605
3849
  {
3606
- middlewareFunction: onlyPluginsMiddleware,
3607
- applyBeforeValidation: false
3608
- },
3609
- {
3610
- middlewareFunction: skipPluginsMiddleware,
3850
+ middlewareFunction: filterPluginsMiddleware,
3611
3851
  applyBeforeValidation: false
3612
3852
  }
3613
3853
  ];
@@ -3670,7 +3910,7 @@ function yargsGlobalOptionsDefinition() {
3670
3910
  default: false
3671
3911
  },
3672
3912
  config: {
3673
- describe: "Path to config file, e.g. code-pushup.config.ts. By default it loads code-pushup.config.(ts|mjs|js).",
3913
+ describe: "Path to config file. By default it loads code-pushup.config.(ts|mjs|js).",
3674
3914
  type: "string"
3675
3915
  },
3676
3916
  tsconfig: {
@@ -3698,8 +3938,55 @@ var groups = {
3698
3938
  };
3699
3939
 
3700
3940
  // packages/cli/src/lib/yargs-cli.ts
3701
- import { bold as bold12 } from "ansis";
3941
+ import { blue, dim as dim2, green as green4 } from "ansis";
3702
3942
  import yargs from "yargs";
3943
+
3944
+ // packages/cli/package.json
3945
+ var version2 = "0.51.0";
3946
+
3947
+ // packages/cli/src/lib/implementation/formatting.ts
3948
+ import { bold as bold13, dim, green as green3 } from "ansis";
3949
+ function titleStyle(title) {
3950
+ return `${bold13(title)}`;
3951
+ }
3952
+ function headerStyle(title) {
3953
+ return `${green3(title)}`;
3954
+ }
3955
+ function descriptionStyle(title) {
3956
+ return `${dim(title)}`;
3957
+ }
3958
+ function formatObjectValue(opts, propName) {
3959
+ const description = opts[propName];
3960
+ return {
3961
+ ...opts,
3962
+ ...typeof description === "string" && {
3963
+ [propName]: descriptionStyle(description)
3964
+ }
3965
+ };
3966
+ }
3967
+ function formatNestedValues(options2, propName) {
3968
+ return Object.fromEntries(
3969
+ Object.entries(options2).map(([key, opts]) => [
3970
+ key,
3971
+ formatObjectValue(opts, propName)
3972
+ ])
3973
+ );
3974
+ }
3975
+
3976
+ // packages/cli/src/lib/yargs-cli.ts
3977
+ var yargsDecorator = {
3978
+ "Commands:": `${green4("Commands")}:`,
3979
+ "Options:": `${green4("Options")}:`,
3980
+ "Examples:": `${green4("Examples")}:`,
3981
+ boolean: blue("boolean"),
3982
+ count: blue("count"),
3983
+ string: blue("string"),
3984
+ array: blue("array"),
3985
+ required: blue("required"),
3986
+ "default:": `${blue("default")}:`,
3987
+ "choices:": `${blue("choices")}:`,
3988
+ "aliases:": `${blue("aliases")}:`
3989
+ };
3703
3990
  function yargsCli(argv, cfg) {
3704
3991
  const { usageMessage, scriptName, noExitProcess } = cfg;
3705
3992
  const commands2 = cfg.commands ?? [];
@@ -3708,7 +3995,7 @@ function yargsCli(argv, cfg) {
3708
3995
  const groups2 = cfg.groups ?? {};
3709
3996
  const examples = cfg.examples ?? [];
3710
3997
  const cli2 = yargs(argv);
3711
- cli2.help().version(false).alias("h", "help").check((args) => {
3998
+ cli2.updateLocale(yargsDecorator).wrap(Math.max(TERMINAL_WIDTH, cli2.terminalWidth())).help("help", descriptionStyle("Show help")).alias("h", "help").showHelpOnFail(false).version("version", dim2`Show version`, version2).check((args) => {
3712
3999
  const persist = args["persist"];
3713
4000
  return persist == null || validatePersistFormat(persist);
3714
4001
  }).parserConfiguration({
@@ -3716,18 +4003,18 @@ function yargsCli(argv, cfg) {
3716
4003
  }).coerce(
3717
4004
  "config",
3718
4005
  (config) => Array.isArray(config) ? config.at(-1) : config
3719
- ).options(options2).wrap(TERMINAL_WIDTH);
4006
+ ).options(formatNestedValues(options2, "describe"));
3720
4007
  if (usageMessage) {
3721
- cli2.usage(bold12(usageMessage));
4008
+ cli2.usage(titleStyle(usageMessage));
3722
4009
  }
3723
4010
  if (scriptName) {
3724
4011
  cli2.scriptName(scriptName);
3725
4012
  }
3726
4013
  examples.forEach(
3727
- ([exampleName, description]) => cli2.example(exampleName, description)
4014
+ ([exampleName, description]) => cli2.example(exampleName, descriptionStyle(description))
3728
4015
  );
3729
4016
  Object.entries(groups2).forEach(
3730
- ([groupName, optionNames]) => cli2.group(optionNames, groupName)
4017
+ ([groupName, optionNames]) => cli2.group(optionNames, headerStyle(groupName))
3731
4018
  );
3732
4019
  middlewares2.forEach(({ middlewareFunction, applyBeforeValidation }) => {
3733
4020
  cli2.middleware(
@@ -3736,13 +4023,18 @@ function yargsCli(argv, cfg) {
3736
4023
  );
3737
4024
  });
3738
4025
  commands2.forEach((commandObj) => {
3739
- cli2.command({
3740
- ...commandObj,
3741
- handler: logErrorBeforeThrow(commandObj.handler),
3742
- ...typeof commandObj.builder === "function" && {
3743
- builder: logErrorBeforeThrow(commandObj.builder)
3744
- }
3745
- });
4026
+ cli2.command(
4027
+ formatObjectValue(
4028
+ {
4029
+ ...commandObj,
4030
+ handler: logErrorBeforeThrow(commandObj.handler),
4031
+ ...typeof commandObj.builder === "function" && {
4032
+ builder: logErrorBeforeThrow(commandObj.builder)
4033
+ }
4034
+ },
4035
+ "describe"
4036
+ )
4037
+ );
3746
4038
  });
3747
4039
  if (noExitProcess) {
3748
4040
  cli2.exitProcess(false);
@@ -3788,8 +4080,8 @@ var cli = (args) => yargsCli(args, {
3788
4080
  "Run collect skiping the coverage plugin, other plugins from config file will be included."
3789
4081
  ],
3790
4082
  [
3791
- "code-pushup upload --persist.outputDir=dist --persist.filename=cp-report --upload.apiKey=$CP_API_KEY",
3792
- "Upload dist/cp-report.json to portal using API key from environment variable"
4083
+ "code-pushup upload --persist.outputDir=dist --upload.apiKey=$CP_API_KEY",
4084
+ "Upload dist/report.json to portal using API key from environment variable"
3793
4085
  ],
3794
4086
  [
3795
4087
  "code-pushup print-config --config code-pushup.config.test.js",