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