@code-pushup/utils 0.1.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 ADDED
@@ -0,0 +1,1529 @@
1
+ // packages/utils/src/lib/execute-process.ts
2
+ import { spawn } from "child_process";
3
+
4
+ // packages/utils/src/lib/report.ts
5
+ import { join as join2 } from "path";
6
+
7
+ // packages/models/src/lib/category-config.ts
8
+ import { z as z2 } from "zod";
9
+
10
+ // packages/models/src/lib/implementation/schemas.ts
11
+ import { z } from "zod";
12
+ import { MATERIAL_ICONS } from "@code-pushup/portal-client";
13
+
14
+ // packages/models/src/lib/implementation/utils.ts
15
+ var slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
16
+ var filenameRegex = /^(?!.*[ \\/:*?"<>|]).+$/;
17
+ function hasDuplicateStrings(strings) {
18
+ const uniqueStrings = Array.from(new Set(strings));
19
+ const duplicatedStrings = strings.filter(
20
+ /* @__PURE__ */ ((i) => (v) => uniqueStrings[i] !== v || !++i)(0)
21
+ );
22
+ return duplicatedStrings.length === 0 ? false : duplicatedStrings;
23
+ }
24
+ function hasMissingStrings(toCheck, existing) {
25
+ const nonExisting = toCheck.filter((s) => !existing.includes(s));
26
+ return nonExisting.length === 0 ? false : nonExisting;
27
+ }
28
+ function errorItems(items, transform = (items2) => items2.join(", ")) {
29
+ const paredItems = items ? items : [];
30
+ return transform(paredItems);
31
+ }
32
+ function exists(value) {
33
+ return value != null;
34
+ }
35
+
36
+ // packages/models/src/lib/implementation/schemas.ts
37
+ function executionMetaSchema(options = {
38
+ descriptionDate: "Execution start date and time",
39
+ descriptionDuration: "Execution duration in ms"
40
+ }) {
41
+ return z.object({
42
+ date: z.string({ description: options.descriptionDate }),
43
+ duration: z.number({ description: options.descriptionDuration })
44
+ });
45
+ }
46
+ function slugSchema(description = "Unique ID (human-readable, URL-safe)") {
47
+ return z.string({ description }).regex(slugRegex, {
48
+ message: "The slug has to follow the pattern [0-9a-z] followed by multiple optional groups of -[0-9a-z]. e.g. my-slug"
49
+ }).max(128, {
50
+ message: "slug can be max 128 characters long"
51
+ });
52
+ }
53
+ function descriptionSchema(description = "Description (markdown)") {
54
+ return z.string({ description }).max(65536).optional();
55
+ }
56
+ function docsUrlSchema(description = "Documentation site") {
57
+ return urlSchema(description).optional().or(z.string().max(0));
58
+ }
59
+ function urlSchema(description) {
60
+ return z.string({ description }).url();
61
+ }
62
+ function titleSchema(description = "Descriptive name") {
63
+ return z.string({ description }).max(256);
64
+ }
65
+ function metaSchema(options) {
66
+ const {
67
+ descriptionDescription,
68
+ titleDescription,
69
+ docsUrlDescription,
70
+ description
71
+ } = options || {};
72
+ return z.object(
73
+ {
74
+ title: titleSchema(titleDescription),
75
+ description: descriptionSchema(descriptionDescription),
76
+ docsUrl: docsUrlSchema(docsUrlDescription)
77
+ },
78
+ { description }
79
+ );
80
+ }
81
+ function filePathSchema(description) {
82
+ return z.string({ description }).trim().min(1, { message: "path is invalid" });
83
+ }
84
+ function fileNameSchema(description) {
85
+ return z.string({ description }).trim().regex(filenameRegex, {
86
+ message: `The filename has to be valid`
87
+ }).min(1, { message: "file name is invalid" });
88
+ }
89
+ function positiveIntSchema(description) {
90
+ return z.number({ description }).int().nonnegative();
91
+ }
92
+ function packageVersionSchema(options) {
93
+ let { versionDescription, optional } = options || {};
94
+ versionDescription = versionDescription || "NPM version of the package";
95
+ optional = !!optional;
96
+ const packageSchema = z.string({ description: "NPM package name" });
97
+ const versionSchema = z.string({ description: versionDescription });
98
+ return z.object(
99
+ {
100
+ packageName: optional ? packageSchema.optional() : packageSchema,
101
+ version: optional ? versionSchema.optional() : versionSchema
102
+ },
103
+ { description: "NPM package name and version of a published package" }
104
+ );
105
+ }
106
+ function weightSchema(description = "Coefficient for the given score (use weight 0 if only for display)") {
107
+ return positiveIntSchema(description);
108
+ }
109
+ function weightedRefSchema(description, slugDescription) {
110
+ return z.object(
111
+ {
112
+ slug: slugSchema(slugDescription),
113
+ weight: weightSchema("Weight used to calculate score")
114
+ },
115
+ { description }
116
+ );
117
+ }
118
+ function scorableSchema(description, refSchema, duplicateCheckFn, duplicateMessageFn) {
119
+ return z.object(
120
+ {
121
+ slug: slugSchema('Human-readable unique ID, e.g. "performance"'),
122
+ refs: z.array(refSchema).refine(
123
+ (refs) => !duplicateCheckFn(refs),
124
+ (refs) => ({
125
+ message: duplicateMessageFn(refs)
126
+ })
127
+ )
128
+ },
129
+ { description }
130
+ );
131
+ }
132
+ var materialIconSchema = z.enum(
133
+ MATERIAL_ICONS,
134
+ { description: "Icon from VSCode Material Icons extension" }
135
+ );
136
+
137
+ // packages/models/src/lib/category-config.ts
138
+ var categoryRefSchema = weightedRefSchema(
139
+ "Weighted references to audits and/or groups for the category",
140
+ "Slug of an audit or group (depending on `type`)"
141
+ ).merge(
142
+ z2.object({
143
+ type: z2.enum(["audit", "group"], {
144
+ description: "Discriminant for reference kind, affects where `slug` is looked up"
145
+ }),
146
+ plugin: slugSchema(
147
+ "Plugin slug (plugin should contain referenced audit or group)"
148
+ )
149
+ })
150
+ );
151
+ var categoryConfigSchema = scorableSchema(
152
+ "Category with a score calculated from audits and groups from various plugins",
153
+ categoryRefSchema,
154
+ getDuplicateRefsInCategoryMetrics,
155
+ duplicateRefsInCategoryMetricsErrorMsg
156
+ ).merge(
157
+ metaSchema({
158
+ titleDescription: "Category Title",
159
+ docsUrlDescription: "Category docs URL",
160
+ descriptionDescription: "Category description",
161
+ description: "Meta info for category"
162
+ })
163
+ ).merge(
164
+ z2.object({
165
+ isBinary: z2.boolean({
166
+ description: 'Is this a binary category (i.e. only a perfect score considered a "pass")?'
167
+ }).optional()
168
+ })
169
+ );
170
+ function duplicateRefsInCategoryMetricsErrorMsg(metrics) {
171
+ const duplicateRefs = getDuplicateRefsInCategoryMetrics(metrics);
172
+ return `In the categories, the following audit or group refs are duplicates: ${errorItems(
173
+ duplicateRefs
174
+ )}`;
175
+ }
176
+ function getDuplicateRefsInCategoryMetrics(metrics) {
177
+ return hasDuplicateStrings(
178
+ metrics.map(({ slug, type, plugin }) => `${type} :: ${plugin} / ${slug}`)
179
+ );
180
+ }
181
+
182
+ // packages/models/src/lib/core-config.ts
183
+ import { z as z11 } from "zod";
184
+
185
+ // packages/models/src/lib/persist-config.ts
186
+ import { z as z3 } from "zod";
187
+ var formatSchema = z3.enum(["json", "md"]);
188
+ var persistConfigSchema = z3.object({
189
+ outputDir: filePathSchema("Artifacts folder"),
190
+ filename: fileNameSchema("Artifacts file name (without extension)").default(
191
+ "report"
192
+ ),
193
+ format: z3.array(formatSchema).default(["json"]).optional()
194
+ // @TODO remove default or optional value and otherwise it will not set defaults.
195
+ });
196
+
197
+ // packages/models/src/lib/plugin-config.ts
198
+ import { z as z9 } from "zod";
199
+
200
+ // packages/models/src/lib/plugin-config-audits.ts
201
+ import { z as z4 } from "zod";
202
+ var auditSchema = z4.object({
203
+ slug: slugSchema("ID (unique within plugin)")
204
+ }).merge(
205
+ metaSchema({
206
+ titleDescription: "Descriptive name",
207
+ descriptionDescription: "Description (markdown)",
208
+ docsUrlDescription: "Link to documentation (rationale)",
209
+ description: "List of scorable metrics for the given plugin"
210
+ })
211
+ );
212
+ var pluginAuditsSchema = z4.array(auditSchema, {
213
+ description: "List of audits maintained in a plugin"
214
+ }).refine(
215
+ (auditMetadata) => !getDuplicateSlugsInAudits(auditMetadata),
216
+ (auditMetadata) => ({
217
+ message: duplicateSlugsInAuditsErrorMsg(auditMetadata)
218
+ })
219
+ );
220
+ function duplicateSlugsInAuditsErrorMsg(audits) {
221
+ const duplicateRefs = getDuplicateSlugsInAudits(audits);
222
+ return `In plugin audits the slugs are not unique: ${errorItems(
223
+ duplicateRefs
224
+ )}`;
225
+ }
226
+ function getDuplicateSlugsInAudits(audits) {
227
+ return hasDuplicateStrings(audits.map(({ slug }) => slug));
228
+ }
229
+
230
+ // packages/models/src/lib/plugin-config-groups.ts
231
+ import { z as z5 } from "zod";
232
+ var auditGroupRefSchema = weightedRefSchema(
233
+ "Weighted references to audits",
234
+ "Reference slug to an audit within this plugin (e.g. 'max-lines')"
235
+ );
236
+ var auditGroupSchema = scorableSchema(
237
+ 'An audit group aggregates a set of audits into a single score which can be referenced from a category. E.g. the group slug "performance" groups audits and can be referenced in a category',
238
+ auditGroupRefSchema,
239
+ getDuplicateRefsInGroups,
240
+ duplicateRefsInGroupsErrorMsg
241
+ ).merge(
242
+ metaSchema({
243
+ titleDescription: "Descriptive name for the group",
244
+ descriptionDescription: "Description of the group (markdown)",
245
+ docsUrlDescription: "Group documentation site",
246
+ description: "Group metadata"
247
+ })
248
+ );
249
+ var auditGroupsSchema = z5.array(auditGroupSchema, {
250
+ description: "List of groups"
251
+ }).optional().refine(
252
+ (groups) => !getDuplicateSlugsInGroups(groups),
253
+ (groups) => ({
254
+ message: duplicateSlugsInGroupsErrorMsg(groups)
255
+ })
256
+ );
257
+ function duplicateRefsInGroupsErrorMsg(groupAudits) {
258
+ const duplicateRefs = getDuplicateRefsInGroups(groupAudits);
259
+ return `In plugin groups the audit refs are not unique: ${errorItems(
260
+ duplicateRefs
261
+ )}`;
262
+ }
263
+ function getDuplicateRefsInGroups(groupAudits) {
264
+ return hasDuplicateStrings(
265
+ groupAudits.map(({ slug: ref }) => ref).filter(exists)
266
+ );
267
+ }
268
+ function duplicateSlugsInGroupsErrorMsg(groups) {
269
+ const duplicateRefs = getDuplicateSlugsInGroups(groups);
270
+ return `In groups the slugs are not unique: ${errorItems(duplicateRefs)}`;
271
+ }
272
+ function getDuplicateSlugsInGroups(groups) {
273
+ return Array.isArray(groups) ? hasDuplicateStrings(groups.map(({ slug }) => slug)) : false;
274
+ }
275
+
276
+ // packages/models/src/lib/plugin-config-runner.ts
277
+ import { z as z8 } from "zod";
278
+
279
+ // packages/models/src/lib/plugin-process-output.ts
280
+ import { z as z7 } from "zod";
281
+
282
+ // packages/models/src/lib/plugin-process-output-audit-issue.ts
283
+ import { z as z6 } from "zod";
284
+ var sourceFileLocationSchema = z6.object(
285
+ {
286
+ file: filePathSchema("Relative path to source file in Git repo"),
287
+ position: z6.object(
288
+ {
289
+ startLine: positiveIntSchema("Start line"),
290
+ startColumn: positiveIntSchema("Start column").optional(),
291
+ endLine: positiveIntSchema("End line").optional(),
292
+ endColumn: positiveIntSchema("End column").optional()
293
+ },
294
+ { description: "Location in file" }
295
+ ).optional()
296
+ },
297
+ { description: "Source file location" }
298
+ );
299
+ var issueSeveritySchema = z6.enum(["info", "warning", "error"], {
300
+ description: "Severity level"
301
+ });
302
+ var issueSchema = z6.object(
303
+ {
304
+ message: z6.string({ description: "Descriptive error message" }).max(512),
305
+ severity: issueSeveritySchema,
306
+ source: sourceFileLocationSchema.optional()
307
+ },
308
+ { description: "Issue information" }
309
+ );
310
+
311
+ // packages/models/src/lib/plugin-process-output.ts
312
+ var auditOutputSchema = z7.object(
313
+ {
314
+ slug: slugSchema("Reference to audit"),
315
+ displayValue: z7.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }).optional(),
316
+ value: positiveIntSchema("Raw numeric value"),
317
+ score: z7.number({
318
+ description: "Value between 0 and 1"
319
+ }).min(0).max(1),
320
+ details: z7.object(
321
+ {
322
+ issues: z7.array(issueSchema, { description: "List of findings" })
323
+ },
324
+ { description: "Detailed information" }
325
+ ).optional()
326
+ },
327
+ { description: "Audit information" }
328
+ );
329
+ var auditOutputsSchema = z7.array(auditOutputSchema, {
330
+ description: "List of JSON formatted audit output emitted by the runner process of a plugin"
331
+ }).refine(
332
+ (audits) => !getDuplicateSlugsInAudits2(audits),
333
+ (audits) => ({ message: duplicateSlugsInAuditsErrorMsg2(audits) })
334
+ );
335
+ function duplicateSlugsInAuditsErrorMsg2(audits) {
336
+ const duplicateRefs = getDuplicateSlugsInAudits2(audits);
337
+ return `In plugin audits the slugs are not unique: ${errorItems(
338
+ duplicateRefs
339
+ )}`;
340
+ }
341
+ function getDuplicateSlugsInAudits2(audits) {
342
+ return hasDuplicateStrings(audits.map(({ slug }) => slug));
343
+ }
344
+
345
+ // packages/models/src/lib/plugin-config-runner.ts
346
+ var outputTransformSchema = z8.function().args(z8.unknown()).returns(z8.union([auditOutputsSchema, z8.promise(auditOutputsSchema)]));
347
+ var runnerConfigSchema = z8.object(
348
+ {
349
+ command: z8.string({
350
+ description: "Shell command to execute"
351
+ }),
352
+ args: z8.array(z8.string({ description: "Command arguments" })).optional(),
353
+ outputFile: filePathSchema("Output path"),
354
+ outputTransform: outputTransformSchema.optional()
355
+ },
356
+ {
357
+ description: "How to execute runner"
358
+ }
359
+ );
360
+ var onProgressSchema = z8.function().args(z8.unknown()).returns(z8.void());
361
+ var runnerFunctionSchema = z8.function().args(onProgressSchema.optional()).returns(z8.union([auditOutputsSchema, z8.promise(auditOutputsSchema)]));
362
+
363
+ // packages/models/src/lib/plugin-config.ts
364
+ var pluginMetaSchema = packageVersionSchema({
365
+ optional: true
366
+ }).merge(
367
+ metaSchema({
368
+ titleDescription: "Descriptive name",
369
+ descriptionDescription: "Description (markdown)",
370
+ docsUrlDescription: "Plugin documentation site",
371
+ description: "Plugin metadata"
372
+ })
373
+ ).merge(
374
+ z9.object({
375
+ slug: slugSchema("References plugin. ID (unique within core config)"),
376
+ icon: materialIconSchema
377
+ })
378
+ );
379
+ var pluginDataSchema = z9.object({
380
+ runner: z9.union([runnerConfigSchema, runnerFunctionSchema]),
381
+ audits: pluginAuditsSchema,
382
+ groups: auditGroupsSchema
383
+ });
384
+ var pluginConfigSchema = pluginMetaSchema.merge(pluginDataSchema).refine(
385
+ (pluginCfg) => !getMissingRefsFromGroups(pluginCfg),
386
+ (pluginCfg) => ({
387
+ message: missingRefsFromGroupsErrorMsg(pluginCfg)
388
+ })
389
+ );
390
+ function missingRefsFromGroupsErrorMsg(pluginCfg) {
391
+ const missingRefs = getMissingRefsFromGroups(pluginCfg);
392
+ return `In the groups, the following audit ref's needs to point to a audit in this plugin config: ${errorItems(
393
+ missingRefs
394
+ )}`;
395
+ }
396
+ function getMissingRefsFromGroups(pluginCfg) {
397
+ if (pluginCfg?.groups?.length && pluginCfg?.audits?.length) {
398
+ const groups = pluginCfg?.groups || [];
399
+ const audits = pluginCfg?.audits || [];
400
+ return hasMissingStrings(
401
+ groups.flatMap(({ refs: audits2 }) => audits2.map(({ slug: ref }) => ref)),
402
+ audits.map(({ slug }) => slug)
403
+ );
404
+ }
405
+ return false;
406
+ }
407
+
408
+ // packages/models/src/lib/upload-config.ts
409
+ import { z as z10 } from "zod";
410
+ var uploadConfigSchema = z10.object({
411
+ server: urlSchema("URL of deployed portal API"),
412
+ apiKey: z10.string({
413
+ description: "API key with write access to portal (use `process.env` for security)"
414
+ }),
415
+ organization: z10.string({
416
+ description: "Organization in code versioning system"
417
+ }),
418
+ project: z10.string({
419
+ description: "Project in code versioning system"
420
+ })
421
+ });
422
+
423
+ // packages/models/src/lib/core-config.ts
424
+ var unrefinedCoreConfigSchema = z11.object({
425
+ plugins: z11.array(pluginConfigSchema, {
426
+ description: "List of plugins to be used (official, community-provided, or custom)"
427
+ }),
428
+ /** portal configuration for persisting results */
429
+ persist: persistConfigSchema,
430
+ /** portal configuration for uploading results */
431
+ upload: uploadConfigSchema.optional(),
432
+ categories: z11.array(categoryConfigSchema, {
433
+ description: "Categorization of individual audits"
434
+ }).refine(
435
+ (categoryCfg) => !getDuplicateSlugCategories(categoryCfg),
436
+ (categoryCfg) => ({
437
+ message: duplicateSlugCategoriesErrorMsg(categoryCfg)
438
+ })
439
+ )
440
+ });
441
+ var coreConfigSchema = refineCoreConfig(unrefinedCoreConfigSchema);
442
+ function refineCoreConfig(schema) {
443
+ return schema.refine(
444
+ (coreCfg) => !getMissingRefsForCategories(coreCfg),
445
+ (coreCfg) => ({
446
+ message: missingRefsForCategoriesErrorMsg(coreCfg)
447
+ })
448
+ );
449
+ }
450
+ function missingRefsForCategoriesErrorMsg(coreCfg) {
451
+ const missingRefs = getMissingRefsForCategories(coreCfg);
452
+ return `In the categories, the following plugin refs do not exist in the provided plugins: ${errorItems(
453
+ missingRefs
454
+ )}`;
455
+ }
456
+ function getMissingRefsForCategories(coreCfg) {
457
+ const missingRefs = [];
458
+ const auditRefsFromCategory = coreCfg.categories.flatMap(
459
+ ({ refs }) => refs.filter(({ type }) => type === "audit").map(({ plugin, slug }) => `${plugin}/${slug}`)
460
+ );
461
+ const auditRefsFromPlugins = coreCfg.plugins.flatMap(
462
+ ({ audits, slug: pluginSlug }) => {
463
+ return audits.map(({ slug }) => `${pluginSlug}/${slug}`);
464
+ }
465
+ );
466
+ const missingAuditRefs = hasMissingStrings(
467
+ auditRefsFromCategory,
468
+ auditRefsFromPlugins
469
+ );
470
+ if (Array.isArray(missingAuditRefs) && missingAuditRefs.length > 0) {
471
+ missingRefs.push(...missingAuditRefs);
472
+ }
473
+ const groupRefsFromCategory = coreCfg.categories.flatMap(
474
+ ({ refs }) => refs.filter(({ type }) => type === "group").map(({ plugin, slug }) => `${plugin}#${slug} (group)`)
475
+ );
476
+ const groupRefsFromPlugins = coreCfg.plugins.flatMap(
477
+ ({ groups, slug: pluginSlug }) => {
478
+ return Array.isArray(groups) ? groups.map(({ slug }) => `${pluginSlug}#${slug} (group)`) : [];
479
+ }
480
+ );
481
+ const missingGroupRefs = hasMissingStrings(
482
+ groupRefsFromCategory,
483
+ groupRefsFromPlugins
484
+ );
485
+ if (Array.isArray(missingGroupRefs) && missingGroupRefs.length > 0) {
486
+ missingRefs.push(...missingGroupRefs);
487
+ }
488
+ return missingRefs.length ? missingRefs : false;
489
+ }
490
+ function duplicateSlugCategoriesErrorMsg(categories) {
491
+ const duplicateStringSlugs = getDuplicateSlugCategories(categories);
492
+ return `In the categories, the following slugs are duplicated: ${errorItems(
493
+ duplicateStringSlugs
494
+ )}`;
495
+ }
496
+ function getDuplicateSlugCategories(categories) {
497
+ return hasDuplicateStrings(categories.map(({ slug }) => slug));
498
+ }
499
+
500
+ // packages/models/src/lib/report.ts
501
+ import { z as z12 } from "zod";
502
+ var auditReportSchema = auditSchema.merge(auditOutputSchema);
503
+ var pluginReportSchema = pluginMetaSchema.merge(
504
+ executionMetaSchema({
505
+ descriptionDate: "Start date and time of plugin run",
506
+ descriptionDuration: "Duration of the plugin run in ms"
507
+ })
508
+ ).merge(
509
+ z12.object({
510
+ audits: z12.array(auditReportSchema),
511
+ groups: z12.array(auditGroupSchema).optional()
512
+ })
513
+ );
514
+ var reportSchema = packageVersionSchema({
515
+ versionDescription: "NPM version of the CLI"
516
+ }).merge(
517
+ executionMetaSchema({
518
+ descriptionDate: "Start date and time of the collect run",
519
+ descriptionDuration: "Duration of the collect run in ms"
520
+ })
521
+ ).merge(
522
+ z12.object(
523
+ {
524
+ categories: z12.array(categoryConfigSchema),
525
+ plugins: z12.array(pluginReportSchema)
526
+ },
527
+ { description: "Collect output data" }
528
+ )
529
+ );
530
+
531
+ // packages/utils/src/lib/file-system.ts
532
+ import { bundleRequire } from "bundle-require";
533
+ import chalk from "chalk";
534
+ import { mkdir, readFile, readdir, stat } from "fs/promises";
535
+ import { join } from "path";
536
+
537
+ // packages/utils/src/lib/formatting.ts
538
+ function slugify(text) {
539
+ return text.trim().toLowerCase().replace(/\s+|\//g, "-").replace(/[^a-z0-9-]/g, "");
540
+ }
541
+ function pluralize(text) {
542
+ if (text.endsWith("y")) {
543
+ return text.slice(0, -1) + "ies";
544
+ }
545
+ if (text.endsWith("s")) {
546
+ return `${text}es`;
547
+ }
548
+ return `${text}s`;
549
+ }
550
+ function formatBytes(bytes, decimals = 2) {
551
+ if (!+bytes)
552
+ return "0 B";
553
+ const k = 1024;
554
+ const dm = decimals < 0 ? 0 : decimals;
555
+ const sizes = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
556
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
557
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
558
+ }
559
+ function pluralizeToken(token, times = 0) {
560
+ return `${times} ${Math.abs(times) === 1 ? token : pluralize(token)}`;
561
+ }
562
+ function formatDuration(duration) {
563
+ if (duration < 1e3) {
564
+ return `${duration} ms`;
565
+ }
566
+ return `${(duration / 1e3).toFixed(2)} s`;
567
+ }
568
+
569
+ // packages/utils/src/lib/guards.ts
570
+ function isPromiseFulfilledResult(result) {
571
+ return result.status === "fulfilled";
572
+ }
573
+ function isPromiseRejectedResult(result) {
574
+ return result.status === "rejected";
575
+ }
576
+
577
+ // packages/utils/src/lib/log-results.ts
578
+ function logMultipleResults(results, messagePrefix, succeededCallback, failedCallback) {
579
+ if (succeededCallback) {
580
+ const succeededResults = results.filter(isPromiseFulfilledResult);
581
+ logPromiseResults(
582
+ succeededResults,
583
+ `${messagePrefix} successfully: `,
584
+ succeededCallback
585
+ );
586
+ }
587
+ if (failedCallback) {
588
+ const failedResults = results.filter(isPromiseRejectedResult);
589
+ logPromiseResults(
590
+ failedResults,
591
+ `${messagePrefix} failed: `,
592
+ failedCallback
593
+ );
594
+ }
595
+ }
596
+ function logPromiseResults(results, logMessage, callback) {
597
+ if (results.length) {
598
+ if (results[0]?.status === "fulfilled") {
599
+ console.info(logMessage);
600
+ } else {
601
+ console.warn(logMessage);
602
+ }
603
+ results.forEach(callback);
604
+ }
605
+ }
606
+
607
+ // packages/utils/src/lib/file-system.ts
608
+ function toUnixPath(path, options) {
609
+ const unixPath = path.replace(/\\/g, "/");
610
+ if (options?.toRelative) {
611
+ return unixPath.replace(process.cwd().replace(/\\/g, "/") + "/", "");
612
+ }
613
+ return unixPath;
614
+ }
615
+ async function ensureDirectoryExists(baseDir) {
616
+ try {
617
+ await mkdir(baseDir, { recursive: true });
618
+ return;
619
+ } catch (error) {
620
+ console.error(error.message);
621
+ if (error.code !== "EEXIST") {
622
+ throw error;
623
+ }
624
+ }
625
+ }
626
+ async function readTextFile(path) {
627
+ const buffer = await readFile(path);
628
+ return buffer.toString();
629
+ }
630
+ async function readJsonFile(path) {
631
+ const text = await readTextFile(path);
632
+ return JSON.parse(text);
633
+ }
634
+ function logMultipleFileResults(fileResults, messagePrefix) {
635
+ const succeededCallback = (result) => {
636
+ const [fileName, size] = result.value;
637
+ console.info(
638
+ `- ${chalk.bold(fileName)}` + (size ? ` (${chalk.gray(formatBytes(size))})` : "")
639
+ );
640
+ };
641
+ const failedCallback = (result) => {
642
+ console.warn(`- ${chalk.bold(result.reason)}`);
643
+ };
644
+ logMultipleResults(
645
+ fileResults,
646
+ messagePrefix,
647
+ succeededCallback,
648
+ failedCallback
649
+ );
650
+ }
651
+ var NoExportError = class extends Error {
652
+ constructor(filepath) {
653
+ super(`No export found in ${filepath}`);
654
+ }
655
+ };
656
+ async function importEsmModule(options, parse) {
657
+ parse = parse || ((v) => v);
658
+ options = {
659
+ format: "esm",
660
+ ...options
661
+ };
662
+ const { mod } = await bundleRequire(options);
663
+ if (mod.default === void 0) {
664
+ throw new NoExportError(options.filepath);
665
+ }
666
+ return parse(mod.default);
667
+ }
668
+ function pluginWorkDir(slug) {
669
+ return join("node_modules", ".code-pushup", slug);
670
+ }
671
+ async function crawlFileSystem(options) {
672
+ const {
673
+ directory,
674
+ pattern,
675
+ fileTransform = (filePath) => filePath
676
+ } = options;
677
+ const files = await readdir(directory);
678
+ const promises = files.map(async (file) => {
679
+ const filePath = join(directory, file);
680
+ const stats = await stat(filePath);
681
+ if (stats.isDirectory()) {
682
+ return crawlFileSystem({ directory: filePath, pattern, fileTransform });
683
+ }
684
+ if (stats.isFile() && (!pattern || new RegExp(pattern).test(file))) {
685
+ return fileTransform(filePath);
686
+ }
687
+ return [];
688
+ });
689
+ const resultsNestedArray = await Promise.all(promises);
690
+ return resultsNestedArray.flat();
691
+ }
692
+ function findLineNumberInText(content, pattern) {
693
+ const lines = content.split(/\r?\n/);
694
+ const lineNumber = lines.findIndex((line) => line.includes(pattern)) + 1;
695
+ return lineNumber === 0 ? null : lineNumber;
696
+ }
697
+
698
+ // packages/utils/src/lib/report.ts
699
+ var FOOTER_PREFIX = "Made with \u2764 by";
700
+ var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
701
+ var README_LINK = "https://github.com/flowup/quality-metrics-cli#readme";
702
+ var reportHeadlineText = "Code PushUp Report";
703
+ var reportOverviewTableHeaders = [
704
+ "\u{1F3F7} Category",
705
+ "\u2B50 Score",
706
+ "\u{1F6E1} Audits"
707
+ ];
708
+ var reportRawOverviewTableHeaders = ["Category", "Score", "Audits"];
709
+ var reportMetaTableHeaders = [
710
+ "Commit",
711
+ "Version",
712
+ "Duration",
713
+ "Plugins",
714
+ "Categories",
715
+ "Audits"
716
+ ];
717
+ var pluginMetaTableHeaders = [
718
+ "Plugin",
719
+ "Audits",
720
+ "Version",
721
+ "Duration"
722
+ ];
723
+ var detailsTableHeaders = [
724
+ "Severity",
725
+ "Message",
726
+ "Source file",
727
+ "Line(s)"
728
+ ];
729
+ function formatReportScore(score) {
730
+ return Math.round(score * 100).toString();
731
+ }
732
+ function getRoundScoreMarker(score) {
733
+ if (score >= 0.9) {
734
+ return "\u{1F7E2}";
735
+ }
736
+ if (score >= 0.5) {
737
+ return "\u{1F7E1}";
738
+ }
739
+ return "\u{1F534}";
740
+ }
741
+ function getSquaredScoreMarker(score) {
742
+ if (score >= 0.9) {
743
+ return "\u{1F7E9}";
744
+ }
745
+ if (score >= 0.5) {
746
+ return "\u{1F7E8}";
747
+ }
748
+ return "\u{1F7E5}";
749
+ }
750
+ function getSeverityIcon(severity) {
751
+ if (severity === "error") {
752
+ return "\u{1F6A8}";
753
+ }
754
+ if (severity === "warning") {
755
+ return "\u26A0\uFE0F";
756
+ }
757
+ return "\u2139\uFE0F";
758
+ }
759
+ function calcDuration(start, stop) {
760
+ stop = stop !== void 0 ? stop : performance.now();
761
+ return Math.floor(stop - start);
762
+ }
763
+ function countCategoryAudits(refs, plugins) {
764
+ const groupLookup = plugins.reduce((lookup, plugin) => {
765
+ if (!plugin.groups.length) {
766
+ return lookup;
767
+ }
768
+ return {
769
+ ...lookup,
770
+ [plugin.slug]: {
771
+ ...plugin.groups.reduce(
772
+ (groupLookup2, group) => {
773
+ return {
774
+ ...groupLookup2,
775
+ [group.slug]: group
776
+ };
777
+ },
778
+ {}
779
+ )
780
+ }
781
+ };
782
+ }, {});
783
+ return refs.reduce((acc, ref) => {
784
+ if (ref.type === "group") {
785
+ const groupRefs = groupLookup[ref.plugin]?.[ref.slug]?.refs;
786
+ return acc + (groupRefs?.length || 0);
787
+ }
788
+ return acc + 1;
789
+ }, 0);
790
+ }
791
+ function getAuditByRef({ slug, weight, plugin }, plugins) {
792
+ const auditPlugin = plugins.find(({ slug: slug2 }) => slug2 === plugin);
793
+ if (!auditPlugin) {
794
+ throwIsNotPresentError(`Plugin ${plugin}`, "report");
795
+ }
796
+ const audit = auditPlugin?.audits.find(
797
+ ({ slug: auditSlug }) => auditSlug === slug
798
+ );
799
+ if (!audit) {
800
+ throwIsNotPresentError(`Audit ${slug}`, auditPlugin?.slug);
801
+ }
802
+ return {
803
+ ...audit,
804
+ weight,
805
+ plugin
806
+ };
807
+ }
808
+ function getGroupWithAudits(refSlug, refPlugin, plugins) {
809
+ const plugin = plugins.find(({ slug }) => slug === refPlugin);
810
+ if (!plugin) {
811
+ throwIsNotPresentError(`Plugin ${refPlugin}`, "report");
812
+ }
813
+ const groupWithAudits = plugin?.groups?.find(({ slug }) => slug === refSlug);
814
+ if (!groupWithAudits) {
815
+ throwIsNotPresentError(`Group ${refSlug}`, plugin?.slug);
816
+ }
817
+ const groupAudits = groupWithAudits.refs.reduce(
818
+ (acc, ref) => {
819
+ const audit = getAuditByRef(
820
+ { ...ref, plugin: refPlugin },
821
+ plugins
822
+ );
823
+ if (audit) {
824
+ return [...acc, audit];
825
+ }
826
+ return [...acc];
827
+ },
828
+ []
829
+ );
830
+ const audits = groupAudits.sort(sortCategoryAudits);
831
+ return {
832
+ ...groupWithAudits,
833
+ audits
834
+ };
835
+ }
836
+ function sortCategoryAudits(a, b) {
837
+ if (a.weight !== b.weight) {
838
+ return b.weight - a.weight;
839
+ }
840
+ if (a.score !== b.score) {
841
+ return a.score - b.score;
842
+ }
843
+ if (a.value !== b.value) {
844
+ return b.value - a.value;
845
+ }
846
+ return a.title.localeCompare(b.title);
847
+ }
848
+ function sortAudits(a, b) {
849
+ if (a.score !== b.score) {
850
+ return a.score - b.score;
851
+ }
852
+ if (a.value !== b.value) {
853
+ return b.value - a.value;
854
+ }
855
+ return a.title.localeCompare(b.title);
856
+ }
857
+ function compareIssueSeverity(severity1, severity2) {
858
+ const levels = {
859
+ info: 0,
860
+ warning: 1,
861
+ error: 2
862
+ };
863
+ return levels[severity1] - levels[severity2];
864
+ }
865
+ async function loadReport(options) {
866
+ const { outputDir, filename, format } = options;
867
+ await ensureDirectoryExists(outputDir);
868
+ const filePath = join2(outputDir, `${filename}.${format}`);
869
+ if (format === "json") {
870
+ const content = await readJsonFile(filePath);
871
+ return reportSchema.parse(content);
872
+ }
873
+ return readTextFile(filePath);
874
+ }
875
+ function throwIsNotPresentError(itemName, presentPlace) {
876
+ throw new Error(`${itemName} is not present in ${presentPlace}`);
877
+ }
878
+ function getPluginNameFromSlug(slug, plugins) {
879
+ return plugins.find(({ slug: pluginSlug }) => pluginSlug === slug)?.title || slug;
880
+ }
881
+
882
+ // packages/utils/src/lib/execute-process.ts
883
+ var ProcessError = class extends Error {
884
+ code;
885
+ stderr;
886
+ stdout;
887
+ constructor(result) {
888
+ super(result.stderr);
889
+ this.code = result.code;
890
+ this.stderr = result.stderr;
891
+ this.stdout = result.stdout;
892
+ }
893
+ };
894
+ function executeProcess(cfg) {
895
+ const { observer, cwd } = cfg;
896
+ const { onStdout, onError, onComplete } = observer || {};
897
+ const date = (/* @__PURE__ */ new Date()).toISOString();
898
+ const start = performance.now();
899
+ return new Promise((resolve, reject) => {
900
+ const process2 = spawn(cfg.command, cfg.args, { cwd, shell: true });
901
+ let stdout = "";
902
+ let stderr = "";
903
+ process2.stdout.on("data", (data) => {
904
+ stdout += data.toString();
905
+ onStdout?.(data);
906
+ });
907
+ process2.stderr.on("data", (data) => {
908
+ stderr += data.toString();
909
+ });
910
+ process2.on("error", (err) => {
911
+ stderr += err.toString();
912
+ });
913
+ process2.on("close", (code) => {
914
+ const timings = { date, duration: calcDuration(start) };
915
+ if (code === 0) {
916
+ onComplete?.();
917
+ resolve({ code, stdout, stderr, ...timings });
918
+ } else {
919
+ const errorMsg = new ProcessError({ code, stdout, stderr, ...timings });
920
+ onError?.(errorMsg);
921
+ reject(errorMsg);
922
+ }
923
+ });
924
+ });
925
+ }
926
+ function objectToCliArgs(params) {
927
+ if (!params) {
928
+ return [];
929
+ }
930
+ return Object.entries(params).flatMap(([key, value]) => {
931
+ if (key === "_") {
932
+ if (Array.isArray(value)) {
933
+ return value;
934
+ } else {
935
+ return [value + ""];
936
+ }
937
+ }
938
+ const prefix = key.length === 1 ? "-" : "--";
939
+ if (Array.isArray(value)) {
940
+ return value.map((v) => `${prefix}${key}="${v}"`);
941
+ }
942
+ if (Array.isArray(value)) {
943
+ return value.map((v) => `${prefix}${key}="${v}"`);
944
+ }
945
+ if (typeof value === "string") {
946
+ return [`${prefix}${key}="${value}"`];
947
+ }
948
+ if (typeof value === "number") {
949
+ return [`${prefix}${key}=${value}`];
950
+ }
951
+ if (typeof value === "boolean") {
952
+ return [`${prefix}${value ? "" : "no-"}${key}`];
953
+ }
954
+ throw new Error(`Unsupported type ${typeof value} for key ${key}`);
955
+ });
956
+ }
957
+ objectToCliArgs({ z: 5 });
958
+
959
+ // packages/utils/src/lib/git.ts
960
+ import simpleGit from "simple-git";
961
+ var git = simpleGit();
962
+ async function getLatestCommit() {
963
+ const log = await git.log({
964
+ maxCount: 1,
965
+ format: { hash: "%H", message: "%s", author: "%an", date: "%ad" }
966
+ });
967
+ return log?.latest;
968
+ }
969
+
970
+ // packages/utils/src/lib/progress.ts
971
+ import chalk2 from "chalk";
972
+ import { MultiProgressBars } from "multi-progress-bars";
973
+ var barStyles = {
974
+ active: (s) => chalk2.green(s),
975
+ done: (s) => chalk2.gray(s),
976
+ idle: (s) => chalk2.gray(s)
977
+ };
978
+ var messageStyles = {
979
+ active: (s) => chalk2.black(s),
980
+ done: (s) => chalk2.green(chalk2.bold(s)),
981
+ idle: (s) => chalk2.gray(s)
982
+ };
983
+ var mpb;
984
+ function getSingletonProgressBars(options) {
985
+ if (!mpb) {
986
+ mpb = new MultiProgressBars({
987
+ initMessage: "",
988
+ border: true,
989
+ ...options
990
+ });
991
+ }
992
+ return mpb;
993
+ }
994
+ function getProgressBar(taskName) {
995
+ const tasks = getSingletonProgressBars();
996
+ tasks.addTask(taskName, {
997
+ type: "percentage",
998
+ percentage: 0,
999
+ message: "",
1000
+ barTransformFn: barStyles.idle
1001
+ });
1002
+ return {
1003
+ incrementInSteps: (numPlugins) => {
1004
+ tasks.incrementTask(taskName, {
1005
+ percentage: 1 / numPlugins,
1006
+ barTransformFn: barStyles.active
1007
+ });
1008
+ },
1009
+ updateTitle: (title) => {
1010
+ tasks.updateTask(taskName, {
1011
+ message: title,
1012
+ barTransformFn: barStyles.active
1013
+ });
1014
+ },
1015
+ endProgress: (message = "") => {
1016
+ tasks.done(taskName, {
1017
+ message: messageStyles.done(message),
1018
+ barTransformFn: barStyles.done
1019
+ });
1020
+ }
1021
+ };
1022
+ }
1023
+
1024
+ // packages/utils/src/lib/md/details.ts
1025
+ function details(title, content, cfg = { open: false }) {
1026
+ return `<details${cfg.open ? " open" : ""}>
1027
+ <summary>${title}</summary>
1028
+ ${content}
1029
+ </details>
1030
+ `;
1031
+ }
1032
+
1033
+ // packages/utils/src/lib/md/headline.ts
1034
+ function headline(text, hierarchy = 1) {
1035
+ return `${new Array(hierarchy).fill("#").join("")} ${text}`;
1036
+ }
1037
+ function h2(text) {
1038
+ return headline(text, 2);
1039
+ }
1040
+ function h3(text) {
1041
+ return headline(text, 3);
1042
+ }
1043
+
1044
+ // packages/utils/src/lib/md/constants.ts
1045
+ var NEW_LINE = "\n";
1046
+
1047
+ // packages/utils/src/lib/md/table.ts
1048
+ var alignString = /* @__PURE__ */ new Map([
1049
+ ["l", ":--"],
1050
+ ["c", ":--:"],
1051
+ ["r", "--:"]
1052
+ ]);
1053
+ function tableMd(data, align) {
1054
+ if (data.length === 0) {
1055
+ throw new Error("Data can't be empty");
1056
+ }
1057
+ align = align || data[0]?.map(() => "c");
1058
+ const _data = data.map((arr) => "|" + arr.join("|") + "|");
1059
+ const secondRow = "|" + align?.map((s) => alignString.get(s)).join("|") + "|";
1060
+ return _data.shift() + NEW_LINE + secondRow + NEW_LINE + _data.join(NEW_LINE);
1061
+ }
1062
+ function tableHtml(data) {
1063
+ if (data.length === 0) {
1064
+ throw new Error("Data can't be empty");
1065
+ }
1066
+ const _data = data.map((arr, index) => {
1067
+ if (index === 0) {
1068
+ return "<tr>" + arr.map((s) => `<th>${s}</th>`).join("") + "</tr>";
1069
+ }
1070
+ return "<tr>" + arr.map((s) => `<td>${s}</td>`).join("") + "</tr>";
1071
+ });
1072
+ return "<table>" + _data.join("") + "</table>";
1073
+ }
1074
+
1075
+ // packages/utils/src/lib/md/font-style.ts
1076
+ var stylesMap = {
1077
+ i: "_",
1078
+ // italic
1079
+ b: "**",
1080
+ // bold
1081
+ s: "~",
1082
+ // strike through
1083
+ c: "`"
1084
+ // code
1085
+ };
1086
+ function style(text, styles = ["b"]) {
1087
+ return styles.reduce((t, s) => `${stylesMap[s]}${t}${stylesMap[s]}`, text);
1088
+ }
1089
+
1090
+ // packages/utils/src/lib/md/link.ts
1091
+ function link(href, text) {
1092
+ return `[${text || href}](${href})`;
1093
+ }
1094
+
1095
+ // packages/utils/src/lib/md/list.ts
1096
+ function li(text, order = "unordered") {
1097
+ const style2 = order === "unordered" ? "-" : "- [ ]";
1098
+ return `${style2} ${text}`;
1099
+ }
1100
+
1101
+ // packages/utils/src/lib/report-to-md.ts
1102
+ function reportToMd(report, commitData) {
1103
+ let md = reportToHeaderSection() + NEW_LINE;
1104
+ md += reportToOverviewSection(report) + NEW_LINE + NEW_LINE;
1105
+ md += reportToCategoriesSection(report) + NEW_LINE + NEW_LINE;
1106
+ md += reportToAuditsSection(report) + NEW_LINE + NEW_LINE;
1107
+ md += reportToAboutSection(report, commitData) + NEW_LINE + NEW_LINE;
1108
+ md += `${FOOTER_PREFIX} ${link(README_LINK, "Code PushUp")}`;
1109
+ return md;
1110
+ }
1111
+ function reportToHeaderSection() {
1112
+ return headline(reportHeadlineText) + NEW_LINE;
1113
+ }
1114
+ function reportToOverviewSection(report) {
1115
+ const { categories } = report;
1116
+ const tableContent = [
1117
+ reportOverviewTableHeaders,
1118
+ ...categories.map(({ title, refs, score }) => [
1119
+ link(`#${slugify(title)}`, title),
1120
+ `${getRoundScoreMarker(score)} ${style(formatReportScore(score))}`,
1121
+ countCategoryAudits(refs, report.plugins).toString()
1122
+ ])
1123
+ ];
1124
+ return tableMd(tableContent, ["l", "c", "c"]);
1125
+ }
1126
+ function reportToCategoriesSection(report) {
1127
+ const { categories, plugins } = report;
1128
+ const categoryDetails = categories.reduce((acc, category) => {
1129
+ const categoryTitle = h3(category.title);
1130
+ const categoryScore = `${getRoundScoreMarker(
1131
+ category.score
1132
+ )} Score: ${style(formatReportScore(category.score))}`;
1133
+ const categoryDocs = getDocsAndDescription(category);
1134
+ const auditsAndGroups = category.refs.reduce(
1135
+ (acc2, ref) => ({
1136
+ ...acc2,
1137
+ ...ref.type === "group" ? {
1138
+ groups: [
1139
+ ...acc2.groups,
1140
+ getGroupWithAudits(ref.slug, ref.plugin, plugins)
1141
+ ]
1142
+ } : {
1143
+ audits: [...acc2.audits, getAuditByRef(ref, plugins)]
1144
+ }
1145
+ }),
1146
+ { groups: [], audits: [] }
1147
+ );
1148
+ const audits = auditsAndGroups.audits.sort(sortCategoryAudits).map((audit) => auditItemToCategorySection(audit, plugins)).join(NEW_LINE);
1149
+ const groups = auditsAndGroups.groups.map((group) => groupItemToCategorySection(group, plugins)).join("");
1150
+ return acc + NEW_LINE + categoryTitle + NEW_LINE + NEW_LINE + categoryDocs + categoryScore + NEW_LINE + groups + NEW_LINE + audits;
1151
+ }, "");
1152
+ return h2("\u{1F3F7} Categories") + NEW_LINE + categoryDetails;
1153
+ }
1154
+ function auditItemToCategorySection(audit, plugins) {
1155
+ const pluginTitle = getPluginNameFromSlug(audit.plugin, plugins);
1156
+ const auditTitle = link(
1157
+ `#${slugify(audit.title)}-${slugify(pluginTitle)}`,
1158
+ audit?.title
1159
+ );
1160
+ return li(
1161
+ `${getSquaredScoreMarker(
1162
+ audit.score
1163
+ )} ${auditTitle} (_${pluginTitle}_) - ${getAuditResult(audit)}`
1164
+ );
1165
+ }
1166
+ function groupItemToCategorySection(group, plugins) {
1167
+ const pluginTitle = getPluginNameFromSlug(group.plugin, plugins);
1168
+ const groupScore = Number(formatReportScore(group?.score || 0));
1169
+ const groupTitle = li(
1170
+ `${getRoundScoreMarker(groupScore)} ${group.title} (_${pluginTitle}_)`
1171
+ );
1172
+ const groupAudits = group.audits.reduce((acc, audit) => {
1173
+ const auditTitle = link(
1174
+ `#${slugify(audit.title)}-${slugify(pluginTitle)}`,
1175
+ audit?.title
1176
+ );
1177
+ acc += ` ${li(
1178
+ `${getSquaredScoreMarker(audit.score)} ${auditTitle} - ${getAuditResult(
1179
+ audit
1180
+ )}`
1181
+ )}`;
1182
+ acc += NEW_LINE;
1183
+ return acc;
1184
+ }, "");
1185
+ return groupTitle + NEW_LINE + groupAudits;
1186
+ }
1187
+ function reportToAuditsSection(report) {
1188
+ const auditsSection = report.plugins.reduce((acc, plugin) => {
1189
+ const auditsData = plugin.audits.sort(sortAudits).reduce((acc2, audit) => {
1190
+ const auditTitle = `${audit.title} (${getPluginNameFromSlug(
1191
+ audit.plugin,
1192
+ report.plugins
1193
+ )})`;
1194
+ const detailsTitle = `${getSquaredScoreMarker(
1195
+ audit.score
1196
+ )} ${getAuditResult(audit, true)} (score: ${formatReportScore(
1197
+ audit.score
1198
+ )})`;
1199
+ const docsItem = getDocsAndDescription(audit);
1200
+ acc2 += h3(auditTitle);
1201
+ acc2 += NEW_LINE;
1202
+ acc2 += NEW_LINE;
1203
+ if (!audit.details?.issues?.length) {
1204
+ acc2 += detailsTitle;
1205
+ acc2 += NEW_LINE;
1206
+ acc2 += NEW_LINE;
1207
+ acc2 += docsItem;
1208
+ return acc2;
1209
+ }
1210
+ const detailsTableData = [
1211
+ detailsTableHeaders,
1212
+ ...audit.details.issues.map((issue) => {
1213
+ const severity = `${getSeverityIcon(issue.severity)} <i>${issue.severity}</i>`;
1214
+ const message = issue.message;
1215
+ if (!issue.source) {
1216
+ return [severity, message, "", ""];
1217
+ }
1218
+ const file = `<code>${issue.source?.file}</code>`;
1219
+ if (!issue.source.position) {
1220
+ return [severity, message, file, ""];
1221
+ }
1222
+ const { startLine, endLine } = issue.source.position;
1223
+ const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
1224
+ return [severity, message, file, line];
1225
+ })
1226
+ ];
1227
+ const detailsTable = `<h4>Issues</h4>${tableHtml(detailsTableData)}`;
1228
+ acc2 += details(detailsTitle, detailsTable);
1229
+ acc2 += NEW_LINE;
1230
+ acc2 += NEW_LINE;
1231
+ acc2 += docsItem;
1232
+ return acc2;
1233
+ }, "");
1234
+ return acc + auditsData;
1235
+ }, "");
1236
+ return h2("\u{1F6E1}\uFE0F Audits") + NEW_LINE + NEW_LINE + auditsSection;
1237
+ }
1238
+ function reportToAboutSection(report, commitData) {
1239
+ const date = (/* @__PURE__ */ new Date()).toString();
1240
+ const { duration, version, plugins, categories } = report;
1241
+ const commitInfo = commitData ? `${commitData.message} (${commitData.hash.slice(0, 7)})` : "N/A";
1242
+ const reportMetaTable = [
1243
+ reportMetaTableHeaders,
1244
+ [
1245
+ commitInfo,
1246
+ style(version || "", ["c"]),
1247
+ formatDuration(duration),
1248
+ plugins.length.toString(),
1249
+ categories.length.toString(),
1250
+ plugins.reduce((acc, { audits }) => acc + audits.length, 0).toString()
1251
+ ]
1252
+ ];
1253
+ const pluginMetaTable = [
1254
+ pluginMetaTableHeaders,
1255
+ ...plugins.map(({ title, version: version2, duration: duration2, audits }) => [
1256
+ title,
1257
+ audits.length.toString(),
1258
+ style(version2 || "", ["c"]),
1259
+ formatDuration(duration2)
1260
+ ])
1261
+ ];
1262
+ return h2("About") + NEW_LINE + NEW_LINE + `Report was created by [Code PushUp](${README_LINK}) on ${date}.` + NEW_LINE + NEW_LINE + tableMd(reportMetaTable, ["l", "c", "c", "c", "c", "c"]) + NEW_LINE + NEW_LINE + "The following plugins were run:" + NEW_LINE + NEW_LINE + tableMd(pluginMetaTable, ["l", "c", "c", "c"]);
1263
+ }
1264
+ function getDocsAndDescription({
1265
+ docsUrl,
1266
+ description
1267
+ }) {
1268
+ if (docsUrl) {
1269
+ const docsLink = link(docsUrl, "\u{1F4D6} Docs");
1270
+ if (!description) {
1271
+ return docsLink + NEW_LINE + NEW_LINE;
1272
+ }
1273
+ if (description.endsWith("```")) {
1274
+ return description + NEW_LINE + NEW_LINE + docsLink + NEW_LINE + NEW_LINE;
1275
+ }
1276
+ return `${description} ${docsLink}${NEW_LINE}${NEW_LINE}`;
1277
+ }
1278
+ if (description) {
1279
+ return description + NEW_LINE + NEW_LINE;
1280
+ }
1281
+ return "";
1282
+ }
1283
+ function getAuditResult(audit, isHtml = false) {
1284
+ const { displayValue, value } = audit;
1285
+ return isHtml ? `<b>${displayValue || value}</b>` : style(String(displayValue || value));
1286
+ }
1287
+
1288
+ // packages/utils/src/lib/report-to-stdout.ts
1289
+ import cliui from "@isaacs/cliui";
1290
+ import chalk3 from "chalk";
1291
+ import Table from "cli-table3";
1292
+ function addLine(line = "") {
1293
+ return line + NEW_LINE;
1294
+ }
1295
+ function reportToStdout(report) {
1296
+ let output = "";
1297
+ output += addLine(reportToHeaderSection2(report));
1298
+ output += addLine();
1299
+ output += addLine(reportToDetailSection(report));
1300
+ output += addLine(reportToOverviewSection2(report));
1301
+ output += addLine(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`);
1302
+ return output;
1303
+ }
1304
+ function reportToHeaderSection2(report) {
1305
+ const { packageName, version } = report;
1306
+ return `${chalk3.bold(reportHeadlineText)} - ${packageName}@${version}`;
1307
+ }
1308
+ function reportToOverviewSection2({
1309
+ categories,
1310
+ plugins
1311
+ }) {
1312
+ let output = addLine(chalk3.magentaBright.bold("Categories"));
1313
+ output += addLine();
1314
+ const table = new Table({
1315
+ head: reportRawOverviewTableHeaders,
1316
+ colAligns: ["left", "right", "right"],
1317
+ style: {
1318
+ head: ["cyan"]
1319
+ }
1320
+ });
1321
+ table.push(
1322
+ ...categories.map(({ title, score, refs }) => [
1323
+ title,
1324
+ withColor({ score }),
1325
+ countCategoryAudits(refs, plugins)
1326
+ ])
1327
+ );
1328
+ output += addLine(table.toString());
1329
+ return output;
1330
+ }
1331
+ function reportToDetailSection(report) {
1332
+ const { plugins } = report;
1333
+ let output = "";
1334
+ plugins.forEach(({ title, audits }) => {
1335
+ output += addLine();
1336
+ output += addLine(chalk3.magentaBright.bold(`${title} audits`));
1337
+ output += addLine();
1338
+ const ui = cliui({ width: 80 });
1339
+ audits.sort(sortAudits).forEach(({ score, title: title2, displayValue, value }) => {
1340
+ ui.div(
1341
+ {
1342
+ text: withColor({ score, text: "\u25CF" }),
1343
+ width: 2,
1344
+ padding: [0, 1, 0, 0]
1345
+ },
1346
+ {
1347
+ text: title2,
1348
+ padding: [0, 3, 0, 0]
1349
+ },
1350
+ {
1351
+ text: chalk3.cyanBright(displayValue || `${value}`),
1352
+ width: 10,
1353
+ padding: [0, 0, 0, 0]
1354
+ }
1355
+ );
1356
+ });
1357
+ output += addLine(ui.toString());
1358
+ output += addLine();
1359
+ });
1360
+ return output;
1361
+ }
1362
+ function withColor({ score, text }) {
1363
+ let str = text ?? formatReportScore(score);
1364
+ const style2 = text ? chalk3 : chalk3.bold;
1365
+ if (score < 0.5) {
1366
+ str = style2.red(str);
1367
+ } else if (score < 0.9) {
1368
+ str = style2.yellow(str);
1369
+ } else {
1370
+ str = style2.green(str);
1371
+ }
1372
+ return str;
1373
+ }
1374
+
1375
+ // packages/utils/src/lib/transformation.ts
1376
+ function toArray(val) {
1377
+ return Array.isArray(val) ? val : [val];
1378
+ }
1379
+ function objectToKeys(obj) {
1380
+ return Object.keys(obj);
1381
+ }
1382
+ function objectToEntries(obj) {
1383
+ return Object.entries(obj);
1384
+ }
1385
+ function countOccurrences(values) {
1386
+ return values.reduce(
1387
+ (acc, value) => ({ ...acc, [value]: (acc[value] ?? 0) + 1 }),
1388
+ {}
1389
+ );
1390
+ }
1391
+ function distinct(array) {
1392
+ return Array.from(new Set(array));
1393
+ }
1394
+ function deepClone(obj) {
1395
+ if (obj == null || typeof obj !== "object") {
1396
+ return obj;
1397
+ }
1398
+ const cloned = Array.isArray(obj) ? [] : {};
1399
+ for (const key in obj) {
1400
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
1401
+ cloned[key] = deepClone(obj[key]);
1402
+ }
1403
+ }
1404
+ return cloned;
1405
+ }
1406
+
1407
+ // packages/utils/src/lib/scoring.ts
1408
+ function calculateScore(refs, scoreFn) {
1409
+ const { numerator, denominator } = refs.reduce(
1410
+ (acc, ref) => {
1411
+ const score = scoreFn(ref);
1412
+ return {
1413
+ numerator: acc.numerator + score * ref.weight,
1414
+ denominator: acc.denominator + ref.weight
1415
+ };
1416
+ },
1417
+ { numerator: 0, denominator: 0 }
1418
+ );
1419
+ return numerator / denominator;
1420
+ }
1421
+ function scoreReport(report) {
1422
+ const scoredReport = deepClone(report);
1423
+ const allScoredAuditsAndGroups = /* @__PURE__ */ new Map();
1424
+ scoredReport.plugins?.forEach((plugin) => {
1425
+ const { audits } = plugin;
1426
+ const groups = plugin.groups || [];
1427
+ audits.forEach((audit) => {
1428
+ const key = `${plugin.slug}-${audit.slug}-audit`;
1429
+ audit.plugin = plugin.slug;
1430
+ allScoredAuditsAndGroups.set(key, audit);
1431
+ });
1432
+ function groupScoreFn(ref) {
1433
+ const score = allScoredAuditsAndGroups.get(
1434
+ `${plugin.slug}-${ref.slug}-audit`
1435
+ )?.score;
1436
+ if (score == null) {
1437
+ throw new Error(
1438
+ `Group has invalid ref - audit with slug ${plugin.slug}-${ref.slug}-audit not found`
1439
+ );
1440
+ }
1441
+ return score;
1442
+ }
1443
+ groups.forEach((group) => {
1444
+ const key = `${plugin.slug}-${group.slug}-group`;
1445
+ group.score = calculateScore(group.refs, groupScoreFn);
1446
+ group.plugin = plugin.slug;
1447
+ allScoredAuditsAndGroups.set(key, group);
1448
+ });
1449
+ plugin.groups = groups;
1450
+ });
1451
+ function catScoreFn(ref) {
1452
+ const key = `${ref.plugin}-${ref.slug}-${ref.type}`;
1453
+ const item = allScoredAuditsAndGroups.get(key);
1454
+ if (!item) {
1455
+ throw new Error(
1456
+ `Category has invalid ref - ${ref.type} with slug ${key} not found in ${ref.plugin} plugin`
1457
+ );
1458
+ }
1459
+ return item.score;
1460
+ }
1461
+ const scoredCategoriesMap = /* @__PURE__ */ new Map();
1462
+ for (const category of scoredReport.categories) {
1463
+ category.score = calculateScore(category.refs, catScoreFn);
1464
+ scoredCategoriesMap.set(category.slug, category);
1465
+ }
1466
+ scoredReport.categories = Array.from(scoredCategoriesMap.values());
1467
+ return scoredReport;
1468
+ }
1469
+
1470
+ // packages/utils/src/lib/verbose-utils.ts
1471
+ function getLogVerbose(verbose) {
1472
+ return (...args) => {
1473
+ if (verbose) {
1474
+ console.info(...args);
1475
+ }
1476
+ };
1477
+ }
1478
+ function getExecVerbose(verbose) {
1479
+ return (fn) => {
1480
+ if (verbose) {
1481
+ fn();
1482
+ }
1483
+ };
1484
+ }
1485
+ var verboseUtils = (verbose) => ({
1486
+ log: getLogVerbose(verbose),
1487
+ exec: getExecVerbose(verbose)
1488
+ });
1489
+ export {
1490
+ CODE_PUSHUP_DOMAIN,
1491
+ FOOTER_PREFIX,
1492
+ NEW_LINE,
1493
+ ProcessError,
1494
+ README_LINK,
1495
+ calcDuration,
1496
+ compareIssueSeverity,
1497
+ countOccurrences,
1498
+ crawlFileSystem,
1499
+ distinct,
1500
+ ensureDirectoryExists,
1501
+ executeProcess,
1502
+ findLineNumberInText,
1503
+ formatBytes,
1504
+ formatDuration,
1505
+ getLatestCommit,
1506
+ getProgressBar,
1507
+ git,
1508
+ importEsmModule,
1509
+ isPromiseFulfilledResult,
1510
+ isPromiseRejectedResult,
1511
+ loadReport,
1512
+ logMultipleFileResults,
1513
+ logMultipleResults,
1514
+ objectToCliArgs,
1515
+ objectToEntries,
1516
+ objectToKeys,
1517
+ pluginWorkDir,
1518
+ pluralize,
1519
+ pluralizeToken,
1520
+ readJsonFile,
1521
+ readTextFile,
1522
+ reportToMd,
1523
+ reportToStdout,
1524
+ scoreReport,
1525
+ slugify,
1526
+ toArray,
1527
+ toUnixPath,
1528
+ verboseUtils
1529
+ };