@code-pushup/coverage-plugin 0.16.7 → 0.17.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/bin.js ADDED
@@ -0,0 +1,875 @@
1
+ // packages/plugin-coverage/src/lib/runner/index.ts
2
+ import chalk5 from "chalk";
3
+ import { writeFile } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+
6
+ // packages/models/src/lib/audit.ts
7
+ import { z as z2 } from "zod";
8
+
9
+ // packages/models/src/lib/implementation/schemas.ts
10
+ import { MATERIAL_ICONS } from "vscode-material-icons";
11
+ import { z } from "zod";
12
+
13
+ // packages/models/src/lib/implementation/limits.ts
14
+ var MAX_SLUG_LENGTH = 128;
15
+ var MAX_TITLE_LENGTH = 256;
16
+ var MAX_DESCRIPTION_LENGTH = 65536;
17
+ var MAX_ISSUE_MESSAGE_LENGTH = 1024;
18
+
19
+ // packages/models/src/lib/implementation/utils.ts
20
+ var slugRegex = /^[a-z\d]+(?:-[a-z\d]+)*$/;
21
+ var filenameRegex = /^(?!.*[ \\/:*?"<>|]).+$/;
22
+ function hasDuplicateStrings(strings) {
23
+ const sortedStrings = [...strings].sort();
24
+ const duplStrings = sortedStrings.filter(
25
+ (item, index) => index !== 0 && item === sortedStrings[index - 1]
26
+ );
27
+ return duplStrings.length === 0 ? false : [...new Set(duplStrings)];
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 = (itemArr) => itemArr.join(", ")) {
34
+ return transform(items || []);
35
+ }
36
+ function exists(value) {
37
+ return value != null;
38
+ }
39
+ function getMissingRefsForCategories(categories, plugins) {
40
+ if (categories.length === 0) {
41
+ return false;
42
+ }
43
+ const auditRefsFromCategory = categories.flatMap(
44
+ ({ refs }) => refs.filter(({ type }) => type === "audit").map(({ plugin, slug }) => `${plugin}/${slug}`)
45
+ );
46
+ const auditRefsFromPlugins = plugins.flatMap(
47
+ ({ audits, slug: pluginSlug }) => audits.map(({ slug }) => `${pluginSlug}/${slug}`)
48
+ );
49
+ const missingAuditRefs = hasMissingStrings(
50
+ auditRefsFromCategory,
51
+ auditRefsFromPlugins
52
+ );
53
+ const groupRefsFromCategory = categories.flatMap(
54
+ ({ refs }) => refs.filter(({ type }) => type === "group").map(({ plugin, slug }) => `${plugin}#${slug} (group)`)
55
+ );
56
+ const groupRefsFromPlugins = plugins.flatMap(
57
+ ({ groups, slug: pluginSlug }) => Array.isArray(groups) ? groups.map(({ slug }) => `${pluginSlug}#${slug} (group)`) : []
58
+ );
59
+ const missingGroupRefs = hasMissingStrings(
60
+ groupRefsFromCategory,
61
+ groupRefsFromPlugins
62
+ );
63
+ const missingRefs = [missingAuditRefs, missingGroupRefs].filter((refs) => Array.isArray(refs) && refs.length > 0).flat();
64
+ return missingRefs.length > 0 ? missingRefs : false;
65
+ }
66
+ function missingRefsForCategoriesErrorMsg(categories, plugins) {
67
+ const missingRefs = getMissingRefsForCategories(categories, plugins);
68
+ return `The following category references need to point to an audit or group: ${errorItems(
69
+ missingRefs
70
+ )}`;
71
+ }
72
+
73
+ // packages/models/src/lib/implementation/schemas.ts
74
+ function executionMetaSchema(options = {
75
+ descriptionDate: "Execution start date and time",
76
+ descriptionDuration: "Execution duration in ms"
77
+ }) {
78
+ return z.object({
79
+ date: z.string({ description: options.descriptionDate }),
80
+ duration: z.number({ description: options.descriptionDuration })
81
+ });
82
+ }
83
+ function slugSchema(description = "Unique ID (human-readable, URL-safe)") {
84
+ return z.string({ description }).regex(slugRegex, {
85
+ message: "The slug has to follow the pattern [0-9a-z] followed by multiple optional groups of -[0-9a-z]. e.g. my-slug"
86
+ }).max(MAX_SLUG_LENGTH, {
87
+ message: `slug can be max ${MAX_SLUG_LENGTH} characters long`
88
+ });
89
+ }
90
+ function descriptionSchema(description = "Description (markdown)") {
91
+ return z.string({ description }).max(MAX_DESCRIPTION_LENGTH).optional();
92
+ }
93
+ function docsUrlSchema(description = "Documentation site") {
94
+ return urlSchema(description).optional().or(z.string().max(0));
95
+ }
96
+ function urlSchema(description) {
97
+ return z.string({ description }).url();
98
+ }
99
+ function titleSchema(description = "Descriptive name") {
100
+ return z.string({ description }).max(MAX_TITLE_LENGTH);
101
+ }
102
+ function metaSchema(options) {
103
+ const {
104
+ descriptionDescription,
105
+ titleDescription,
106
+ docsUrlDescription,
107
+ description
108
+ } = options ?? {};
109
+ return z.object(
110
+ {
111
+ title: titleSchema(titleDescription),
112
+ description: descriptionSchema(descriptionDescription),
113
+ docsUrl: docsUrlSchema(docsUrlDescription)
114
+ },
115
+ { description }
116
+ );
117
+ }
118
+ function filePathSchema(description) {
119
+ return z.string({ description }).trim().min(1, { message: "path is invalid" });
120
+ }
121
+ function fileNameSchema(description) {
122
+ return z.string({ description }).trim().regex(filenameRegex, {
123
+ message: `The filename has to be valid`
124
+ }).min(1, { message: "file name is invalid" });
125
+ }
126
+ function positiveIntSchema(description) {
127
+ return z.number({ description }).int().nonnegative();
128
+ }
129
+ function packageVersionSchema(options) {
130
+ const { versionDescription = "NPM version of the package", required } = options ?? {};
131
+ const packageSchema = z.string({ description: "NPM package name" });
132
+ const versionSchema = z.string({ description: versionDescription });
133
+ return z.object(
134
+ {
135
+ packageName: required ? packageSchema : packageSchema.optional(),
136
+ version: required ? versionSchema : versionSchema.optional()
137
+ },
138
+ { description: "NPM package name and version of a published package" }
139
+ );
140
+ }
141
+ function weightSchema(description = "Coefficient for the given score (use weight 0 if only for display)") {
142
+ return positiveIntSchema(description);
143
+ }
144
+ function weightedRefSchema(description, slugDescription) {
145
+ return z.object(
146
+ {
147
+ slug: slugSchema(slugDescription),
148
+ weight: weightSchema("Weight used to calculate score")
149
+ },
150
+ { description }
151
+ );
152
+ }
153
+ function scorableSchema(description, refSchema, duplicateCheckFn, duplicateMessageFn) {
154
+ return z.object(
155
+ {
156
+ slug: slugSchema('Human-readable unique ID, e.g. "performance"'),
157
+ refs: z.array(refSchema).min(1).refine(
158
+ (refs) => !duplicateCheckFn(refs),
159
+ (refs) => ({
160
+ message: duplicateMessageFn(refs)
161
+ })
162
+ ).refine(hasWeightedRefsInCategories, () => ({
163
+ message: `In a category there has to be at least one ref with weight > 0`
164
+ }))
165
+ },
166
+ { description }
167
+ );
168
+ }
169
+ var materialIconSchema = z.enum(MATERIAL_ICONS, {
170
+ description: "Icon from VSCode Material Icons extension"
171
+ });
172
+ function hasWeightedRefsInCategories(categoryRefs) {
173
+ return categoryRefs.reduce((acc, { weight }) => weight + acc, 0) !== 0;
174
+ }
175
+
176
+ // packages/models/src/lib/audit.ts
177
+ var auditSchema = z2.object({
178
+ slug: slugSchema("ID (unique within plugin)")
179
+ }).merge(
180
+ metaSchema({
181
+ titleDescription: "Descriptive name",
182
+ descriptionDescription: "Description (markdown)",
183
+ docsUrlDescription: "Link to documentation (rationale)",
184
+ description: "List of scorable metrics for the given plugin"
185
+ })
186
+ );
187
+ var pluginAuditsSchema = z2.array(auditSchema, {
188
+ description: "List of audits maintained in a plugin"
189
+ }).min(1).refine(
190
+ (auditMetadata) => !getDuplicateSlugsInAudits(auditMetadata),
191
+ (auditMetadata) => ({
192
+ message: duplicateSlugsInAuditsErrorMsg(auditMetadata)
193
+ })
194
+ );
195
+ function duplicateSlugsInAuditsErrorMsg(audits) {
196
+ const duplicateRefs = getDuplicateSlugsInAudits(audits);
197
+ return `In plugin audits the following slugs are not unique: ${errorItems(
198
+ duplicateRefs
199
+ )}`;
200
+ }
201
+ function getDuplicateSlugsInAudits(audits) {
202
+ return hasDuplicateStrings(audits.map(({ slug }) => slug));
203
+ }
204
+
205
+ // packages/models/src/lib/audit-issue.ts
206
+ import { z as z3 } from "zod";
207
+ var sourceFileLocationSchema = z3.object(
208
+ {
209
+ file: filePathSchema("Relative path to source file in Git repo"),
210
+ position: z3.object(
211
+ {
212
+ startLine: positiveIntSchema("Start line"),
213
+ startColumn: positiveIntSchema("Start column").optional(),
214
+ endLine: positiveIntSchema("End line").optional(),
215
+ endColumn: positiveIntSchema("End column").optional()
216
+ },
217
+ { description: "Location in file" }
218
+ ).optional()
219
+ },
220
+ { description: "Source file location" }
221
+ );
222
+ var issueSeveritySchema = z3.enum(["info", "warning", "error"], {
223
+ description: "Severity level"
224
+ });
225
+ var issueSchema = z3.object(
226
+ {
227
+ message: z3.string({ description: "Descriptive error message" }).max(MAX_ISSUE_MESSAGE_LENGTH),
228
+ severity: issueSeveritySchema,
229
+ source: sourceFileLocationSchema.optional()
230
+ },
231
+ { description: "Issue information" }
232
+ );
233
+
234
+ // packages/models/src/lib/audit-output.ts
235
+ import { z as z4 } from "zod";
236
+ var auditOutputSchema = z4.object(
237
+ {
238
+ slug: slugSchema("Reference to audit"),
239
+ displayValue: z4.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }).optional(),
240
+ value: positiveIntSchema("Raw numeric value"),
241
+ score: z4.number({
242
+ description: "Value between 0 and 1"
243
+ }).min(0).max(1),
244
+ details: z4.object(
245
+ {
246
+ issues: z4.array(issueSchema, { description: "List of findings" })
247
+ },
248
+ { description: "Detailed information" }
249
+ ).optional()
250
+ },
251
+ { description: "Audit information" }
252
+ );
253
+ var auditOutputsSchema = z4.array(auditOutputSchema, {
254
+ description: "List of JSON formatted audit output emitted by the runner process of a plugin"
255
+ }).refine(
256
+ (audits) => !getDuplicateSlugsInAudits2(audits),
257
+ (audits) => ({ message: duplicateSlugsInAuditsErrorMsg2(audits) })
258
+ );
259
+ function duplicateSlugsInAuditsErrorMsg2(audits) {
260
+ const duplicateRefs = getDuplicateSlugsInAudits2(audits);
261
+ return `In plugin audits the slugs are not unique: ${errorItems(
262
+ duplicateRefs
263
+ )}`;
264
+ }
265
+ function getDuplicateSlugsInAudits2(audits) {
266
+ return hasDuplicateStrings(audits.map(({ slug }) => slug));
267
+ }
268
+
269
+ // packages/models/src/lib/category-config.ts
270
+ import { z as z5 } from "zod";
271
+ var categoryRefSchema = weightedRefSchema(
272
+ "Weighted references to audits and/or groups for the category",
273
+ "Slug of an audit or group (depending on `type`)"
274
+ ).merge(
275
+ z5.object({
276
+ type: z5.enum(["audit", "group"], {
277
+ description: "Discriminant for reference kind, affects where `slug` is looked up"
278
+ }),
279
+ plugin: slugSchema(
280
+ "Plugin slug (plugin should contain referenced audit or group)"
281
+ )
282
+ })
283
+ );
284
+ var categoryConfigSchema = scorableSchema(
285
+ "Category with a score calculated from audits and groups from various plugins",
286
+ categoryRefSchema,
287
+ getDuplicateRefsInCategoryMetrics,
288
+ duplicateRefsInCategoryMetricsErrorMsg
289
+ ).merge(
290
+ metaSchema({
291
+ titleDescription: "Category Title",
292
+ docsUrlDescription: "Category docs URL",
293
+ descriptionDescription: "Category description",
294
+ description: "Meta info for category"
295
+ })
296
+ ).merge(
297
+ z5.object({
298
+ isBinary: z5.boolean({
299
+ description: 'Is this a binary category (i.e. only a perfect score considered a "pass")?'
300
+ }).optional()
301
+ })
302
+ );
303
+ function duplicateRefsInCategoryMetricsErrorMsg(metrics) {
304
+ const duplicateRefs = getDuplicateRefsInCategoryMetrics(metrics);
305
+ return `In the categories, the following audit or group refs are duplicates: ${errorItems(
306
+ duplicateRefs
307
+ )}`;
308
+ }
309
+ function getDuplicateRefsInCategoryMetrics(metrics) {
310
+ return hasDuplicateStrings(
311
+ metrics.map(({ slug, type, plugin }) => `${type} :: ${plugin} / ${slug}`)
312
+ );
313
+ }
314
+ var categoriesSchema = z5.array(categoryConfigSchema, {
315
+ description: "Categorization of individual audits"
316
+ }).refine(
317
+ (categoryCfg) => !getDuplicateSlugCategories(categoryCfg),
318
+ (categoryCfg) => ({
319
+ message: duplicateSlugCategoriesErrorMsg(categoryCfg)
320
+ })
321
+ );
322
+ function duplicateSlugCategoriesErrorMsg(categories) {
323
+ const duplicateStringSlugs = getDuplicateSlugCategories(categories);
324
+ return `In the categories, the following slugs are duplicated: ${errorItems(
325
+ duplicateStringSlugs
326
+ )}`;
327
+ }
328
+ function getDuplicateSlugCategories(categories) {
329
+ return hasDuplicateStrings(categories.map(({ slug }) => slug));
330
+ }
331
+
332
+ // packages/models/src/lib/core-config.ts
333
+ import { z as z11 } from "zod";
334
+
335
+ // packages/models/src/lib/persist-config.ts
336
+ import { z as z6 } from "zod";
337
+ var formatSchema = z6.enum(["json", "md"]);
338
+ var persistConfigSchema = z6.object({
339
+ outputDir: filePathSchema("Artifacts folder").optional(),
340
+ filename: fileNameSchema(
341
+ "Artifacts file name (without extension)"
342
+ ).optional(),
343
+ format: z6.array(formatSchema).optional()
344
+ });
345
+
346
+ // packages/models/src/lib/plugin-config.ts
347
+ import { z as z9 } from "zod";
348
+
349
+ // packages/models/src/lib/group.ts
350
+ import { z as z7 } from "zod";
351
+ var groupRefSchema = weightedRefSchema(
352
+ "Weighted reference to a group",
353
+ "Reference slug to a group within this plugin (e.g. 'max-lines')"
354
+ );
355
+ var groupMetaSchema = metaSchema({
356
+ titleDescription: "Descriptive name for the group",
357
+ descriptionDescription: "Description of the group (markdown)",
358
+ docsUrlDescription: "Group documentation site",
359
+ description: "Group metadata"
360
+ });
361
+ var groupSchema = scorableSchema(
362
+ 'A 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',
363
+ groupRefSchema,
364
+ getDuplicateRefsInGroups,
365
+ duplicateRefsInGroupsErrorMsg
366
+ ).merge(groupMetaSchema);
367
+ var groupsSchema = z7.array(groupSchema, {
368
+ description: "List of groups"
369
+ }).optional().refine(
370
+ (groups) => !getDuplicateSlugsInGroups(groups),
371
+ (groups) => ({
372
+ message: duplicateSlugsInGroupsErrorMsg(groups)
373
+ })
374
+ );
375
+ function duplicateRefsInGroupsErrorMsg(groups) {
376
+ const duplicateRefs = getDuplicateRefsInGroups(groups);
377
+ return `In plugin groups the following references are not unique: ${errorItems(
378
+ duplicateRefs
379
+ )}`;
380
+ }
381
+ function getDuplicateRefsInGroups(groups) {
382
+ return hasDuplicateStrings(groups.map(({ slug: ref }) => ref).filter(exists));
383
+ }
384
+ function duplicateSlugsInGroupsErrorMsg(groups) {
385
+ const duplicateRefs = getDuplicateSlugsInGroups(groups);
386
+ return `In groups the following slugs are not unique: ${errorItems(
387
+ duplicateRefs
388
+ )}`;
389
+ }
390
+ function getDuplicateSlugsInGroups(groups) {
391
+ return Array.isArray(groups) ? hasDuplicateStrings(groups.map(({ slug }) => slug)) : false;
392
+ }
393
+
394
+ // packages/models/src/lib/runner-config.ts
395
+ import { z as z8 } from "zod";
396
+ var outputTransformSchema = z8.function().args(z8.unknown()).returns(z8.union([auditOutputsSchema, z8.promise(auditOutputsSchema)]));
397
+ var runnerConfigSchema = z8.object(
398
+ {
399
+ command: z8.string({
400
+ description: "Shell command to execute"
401
+ }),
402
+ args: z8.array(z8.string({ description: "Command arguments" })).optional(),
403
+ outputFile: filePathSchema("Output path"),
404
+ outputTransform: outputTransformSchema.optional()
405
+ },
406
+ {
407
+ description: "How to execute runner"
408
+ }
409
+ );
410
+ var onProgressSchema = z8.function().args(z8.unknown()).returns(z8.void());
411
+ var runnerFunctionSchema = z8.function().args(onProgressSchema.optional()).returns(z8.union([auditOutputsSchema, z8.promise(auditOutputsSchema)]));
412
+
413
+ // packages/models/src/lib/plugin-config.ts
414
+ var pluginMetaSchema = packageVersionSchema().merge(
415
+ metaSchema({
416
+ titleDescription: "Descriptive name",
417
+ descriptionDescription: "Description (markdown)",
418
+ docsUrlDescription: "Plugin documentation site",
419
+ description: "Plugin metadata"
420
+ })
421
+ ).merge(
422
+ z9.object({
423
+ slug: slugSchema("Unique plugin slug within core config"),
424
+ icon: materialIconSchema
425
+ })
426
+ );
427
+ var pluginDataSchema = z9.object({
428
+ runner: z9.union([runnerConfigSchema, runnerFunctionSchema]),
429
+ audits: pluginAuditsSchema,
430
+ groups: groupsSchema
431
+ });
432
+ var pluginConfigSchema = pluginMetaSchema.merge(pluginDataSchema).refine(
433
+ (pluginCfg) => !getMissingRefsFromGroups(pluginCfg),
434
+ (pluginCfg) => ({
435
+ message: missingRefsFromGroupsErrorMsg(pluginCfg)
436
+ })
437
+ );
438
+ function missingRefsFromGroupsErrorMsg(pluginCfg) {
439
+ const missingRefs = getMissingRefsFromGroups(pluginCfg);
440
+ return `The following group references need to point to an existing audit in this plugin config: ${errorItems(
441
+ missingRefs
442
+ )}`;
443
+ }
444
+ function getMissingRefsFromGroups(pluginCfg) {
445
+ return hasMissingStrings(
446
+ pluginCfg.groups?.flatMap(
447
+ ({ refs: audits }) => audits.map(({ slug: ref }) => ref)
448
+ ) ?? [],
449
+ pluginCfg.audits.map(({ slug }) => slug)
450
+ );
451
+ }
452
+
453
+ // packages/models/src/lib/upload-config.ts
454
+ import { z as z10 } from "zod";
455
+ var uploadConfigSchema = z10.object({
456
+ server: urlSchema("URL of deployed portal API"),
457
+ apiKey: z10.string({
458
+ description: "API key with write access to portal (use `process.env` for security)"
459
+ }),
460
+ organization: slugSchema("Organization slug from Code PushUp portal"),
461
+ project: slugSchema("Project slug from Code PushUp portal"),
462
+ timeout: z10.number({ description: "Request timeout in minutes (default is 5)" }).positive().int().optional()
463
+ });
464
+
465
+ // packages/models/src/lib/core-config.ts
466
+ var unrefinedCoreConfigSchema = z11.object({
467
+ plugins: z11.array(pluginConfigSchema, {
468
+ description: "List of plugins to be used (official, community-provided, or custom)"
469
+ }).min(1),
470
+ /** portal configuration for persisting results */
471
+ persist: persistConfigSchema.optional(),
472
+ /** portal configuration for uploading results */
473
+ upload: uploadConfigSchema.optional(),
474
+ categories: categoriesSchema.optional()
475
+ });
476
+ var coreConfigSchema = refineCoreConfig(unrefinedCoreConfigSchema);
477
+ function refineCoreConfig(schema) {
478
+ return schema.refine(
479
+ (coreCfg) => !getMissingRefsForCategories(coreCfg.categories ?? [], coreCfg.plugins),
480
+ (coreCfg) => ({
481
+ message: missingRefsForCategoriesErrorMsg(
482
+ coreCfg.categories ?? [],
483
+ coreCfg.plugins
484
+ )
485
+ })
486
+ );
487
+ }
488
+
489
+ // packages/models/src/lib/report.ts
490
+ import { z as z12 } from "zod";
491
+ var auditReportSchema = auditSchema.merge(auditOutputSchema);
492
+ var pluginReportSchema = pluginMetaSchema.merge(
493
+ executionMetaSchema({
494
+ descriptionDate: "Start date and time of plugin run",
495
+ descriptionDuration: "Duration of the plugin run in ms"
496
+ })
497
+ ).merge(
498
+ z12.object({
499
+ audits: z12.array(auditReportSchema).min(1),
500
+ groups: z12.array(groupSchema).optional()
501
+ })
502
+ ).refine(
503
+ (pluginReport) => !getMissingRefsFromGroups2(pluginReport.audits, pluginReport.groups ?? []),
504
+ (pluginReport) => ({
505
+ message: missingRefsFromGroupsErrorMsg2(
506
+ pluginReport.audits,
507
+ pluginReport.groups ?? []
508
+ )
509
+ })
510
+ );
511
+ function missingRefsFromGroupsErrorMsg2(audits, groups) {
512
+ const missingRefs = getMissingRefsFromGroups2(audits, groups);
513
+ return `group references need to point to an existing audit in this plugin report: ${errorItems(
514
+ missingRefs
515
+ )}`;
516
+ }
517
+ function getMissingRefsFromGroups2(audits, groups) {
518
+ return hasMissingStrings(
519
+ groups.flatMap(
520
+ ({ refs: auditRefs }) => auditRefs.map(({ slug: ref }) => ref)
521
+ ),
522
+ audits.map(({ slug }) => slug)
523
+ );
524
+ }
525
+ var reportSchema = packageVersionSchema({
526
+ versionDescription: "NPM version of the CLI",
527
+ required: true
528
+ }).merge(
529
+ executionMetaSchema({
530
+ descriptionDate: "Start date and time of the collect run",
531
+ descriptionDuration: "Duration of the collect run in ms"
532
+ })
533
+ ).merge(
534
+ z12.object(
535
+ {
536
+ categories: z12.array(categoryConfigSchema),
537
+ plugins: z12.array(pluginReportSchema).min(1)
538
+ },
539
+ { description: "Collect output data" }
540
+ )
541
+ ).refine(
542
+ (report) => !getMissingRefsForCategories(report.categories, report.plugins),
543
+ (report) => ({
544
+ message: missingRefsForCategoriesErrorMsg(
545
+ report.categories,
546
+ report.plugins
547
+ )
548
+ })
549
+ );
550
+
551
+ // packages/utils/src/lib/execute-process.ts
552
+ import { spawn } from "node:child_process";
553
+
554
+ // packages/utils/src/lib/file-system.ts
555
+ import { bundleRequire } from "bundle-require";
556
+ import chalk from "chalk";
557
+ import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
558
+ import { join } from "node:path";
559
+ async function readTextFile(path) {
560
+ const buffer = await readFile(path);
561
+ return buffer.toString();
562
+ }
563
+ async function readJsonFile(path) {
564
+ const text = await readTextFile(path);
565
+ return JSON.parse(text);
566
+ }
567
+ async function ensureDirectoryExists(baseDir) {
568
+ try {
569
+ await mkdir(baseDir, { recursive: true });
570
+ return;
571
+ } catch (error) {
572
+ console.error(error.message);
573
+ if (error.code !== "EEXIST") {
574
+ throw error;
575
+ }
576
+ }
577
+ }
578
+ function pluginWorkDir(slug) {
579
+ return join("node_modules", ".code-pushup", slug);
580
+ }
581
+
582
+ // packages/utils/src/lib/reports/utils.ts
583
+ function calcDuration(start, stop) {
584
+ return Math.floor((stop ?? performance.now()) - start);
585
+ }
586
+
587
+ // packages/utils/src/lib/execute-process.ts
588
+ var ProcessError = class extends Error {
589
+ code;
590
+ stderr;
591
+ stdout;
592
+ constructor(result) {
593
+ super(result.stderr);
594
+ this.code = result.code;
595
+ this.stderr = result.stderr;
596
+ this.stdout = result.stdout;
597
+ }
598
+ };
599
+ function executeProcess(cfg) {
600
+ const { observer, cwd, command, args } = cfg;
601
+ const { onStdout, onError, onComplete } = observer ?? {};
602
+ const date = (/* @__PURE__ */ new Date()).toISOString();
603
+ const start = performance.now();
604
+ return new Promise((resolve, reject) => {
605
+ const process2 = spawn(command, args, { cwd, shell: true });
606
+ let stdout = "";
607
+ let stderr = "";
608
+ process2.stdout.on("data", (data) => {
609
+ stdout += String(data);
610
+ onStdout?.(String(data));
611
+ });
612
+ process2.stderr.on("data", (data) => {
613
+ stderr += String(data);
614
+ });
615
+ process2.on("error", (err) => {
616
+ stderr += err.toString();
617
+ });
618
+ process2.on("close", (code) => {
619
+ const timings = { date, duration: calcDuration(start) };
620
+ if (code === 0) {
621
+ onComplete?.();
622
+ resolve({ code, stdout, stderr, ...timings });
623
+ } else {
624
+ const errorMsg = new ProcessError({ code, stdout, stderr, ...timings });
625
+ onError?.(errorMsg);
626
+ reject(errorMsg);
627
+ }
628
+ });
629
+ });
630
+ }
631
+
632
+ // packages/utils/src/lib/git.ts
633
+ import { simpleGit } from "simple-git";
634
+ var git = simpleGit();
635
+
636
+ // packages/utils/src/lib/progress.ts
637
+ import chalk2 from "chalk";
638
+ import { MultiProgressBars } from "multi-progress-bars";
639
+
640
+ // packages/utils/src/lib/reports/generate-stdout-summary.ts
641
+ import cliui from "@isaacs/cliui";
642
+ import chalk3 from "chalk";
643
+ import CliTable3 from "cli-table3";
644
+
645
+ // packages/utils/src/lib/transform.ts
646
+ import { platform } from "node:os";
647
+ function toUnixPath(path, options) {
648
+ const unixPath = path.replace(/\\/g, "/");
649
+ if (options?.toRelative) {
650
+ return unixPath.replace(`${process.cwd().replace(/\\/g, "/")}/`, "");
651
+ }
652
+ return unixPath;
653
+ }
654
+ function toUnixNewlines(text) {
655
+ return platform() === "win32" ? text.replace(/\r\n/g, "\n") : text;
656
+ }
657
+ function toNumberPrecision(value, decimalPlaces) {
658
+ return Number(
659
+ `${Math.round(
660
+ Number.parseFloat(`${value}e${decimalPlaces}`)
661
+ )}e-${decimalPlaces}`
662
+ );
663
+ }
664
+ function toOrdinal(value) {
665
+ if (value % 10 === 1 && value % 100 !== 11) {
666
+ return `${value}st`;
667
+ }
668
+ if (value % 10 === 2 && value % 100 !== 12) {
669
+ return `${value}nd`;
670
+ }
671
+ if (value % 10 === 3 && value % 100 !== 13) {
672
+ return `${value}rd`;
673
+ }
674
+ return `${value}th`;
675
+ }
676
+
677
+ // packages/utils/src/lib/logging.ts
678
+ import chalk4 from "chalk";
679
+
680
+ // packages/plugin-coverage/src/lib/runner/constants.ts
681
+ import { join as join2 } from "node:path";
682
+ var WORKDIR = pluginWorkDir("coverage");
683
+ var RUNNER_OUTPUT_PATH = join2(WORKDIR, "runner-output.json");
684
+ var PLUGIN_CONFIG_PATH = join2(
685
+ process.cwd(),
686
+ WORKDIR,
687
+ "plugin-config.json"
688
+ );
689
+
690
+ // packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts
691
+ import { join as join3 } from "node:path";
692
+
693
+ // packages/plugin-coverage/src/lib/runner/lcov/parse-lcov.ts
694
+ import parseLcovExport from "parse-lcov";
695
+ var godKnows = parseLcovExport;
696
+ var parseLcov = "default" in godKnows ? godKnows.default : godKnows;
697
+
698
+ // packages/plugin-coverage/src/lib/runner/lcov/utils.ts
699
+ function calculateCoverage(hit, found) {
700
+ return found > 0 ? hit / found : 1;
701
+ }
702
+ function mergeConsecutiveNumbers(numberArr) {
703
+ return [...numberArr].sort().reduce((acc, currValue) => {
704
+ const prevValue = acc.at(-1);
705
+ if (prevValue != null && (prevValue.start === currValue - 1 || prevValue.end === currValue - 1)) {
706
+ return [...acc.slice(0, -1), { start: prevValue.start, end: currValue }];
707
+ }
708
+ return [...acc, { start: currValue }];
709
+ }, []);
710
+ }
711
+
712
+ // packages/plugin-coverage/src/lib/runner/lcov/transform.ts
713
+ function lcovReportToFunctionStat(record) {
714
+ return {
715
+ totalFound: record.functions.found,
716
+ totalHit: record.functions.hit,
717
+ issues: record.functions.hit < record.functions.found ? record.functions.details.filter((detail) => !detail.hit).map(
718
+ (detail) => ({
719
+ message: `Function ${detail.name} is not called in any test case.`,
720
+ severity: "error",
721
+ source: {
722
+ file: toUnixPath(record.file),
723
+ position: { startLine: detail.line }
724
+ }
725
+ })
726
+ ) : []
727
+ };
728
+ }
729
+ function lcovReportToLineStat(record) {
730
+ const missingCoverage = record.lines.hit < record.lines.found;
731
+ const lines = missingCoverage ? record.lines.details.filter((detail) => !detail.hit).map((detail) => detail.line) : [];
732
+ const linePositions = mergeConsecutiveNumbers(lines);
733
+ return {
734
+ totalFound: record.lines.found,
735
+ totalHit: record.lines.hit,
736
+ issues: missingCoverage ? linePositions.map((linePosition) => {
737
+ const lineReference = linePosition.end == null ? `Line ${linePosition.start} is` : `Lines ${linePosition.start}-${linePosition.end} are`;
738
+ return {
739
+ message: `${lineReference} not covered in any test case.`,
740
+ severity: "warning",
741
+ source: {
742
+ file: toUnixPath(record.file),
743
+ position: {
744
+ startLine: linePosition.start,
745
+ endLine: linePosition.end
746
+ }
747
+ }
748
+ };
749
+ }) : []
750
+ };
751
+ }
752
+ function lcovReportToBranchStat(record) {
753
+ return {
754
+ totalFound: record.branches.found,
755
+ totalHit: record.branches.hit,
756
+ issues: record.branches.hit < record.branches.found ? record.branches.details.filter((detail) => !detail.taken).map(
757
+ (detail) => ({
758
+ message: `${toOrdinal(
759
+ detail.branch + 1
760
+ )} branch is not taken in any test case.`,
761
+ severity: "error",
762
+ source: {
763
+ file: toUnixPath(record.file),
764
+ position: { startLine: detail.line }
765
+ }
766
+ })
767
+ ) : []
768
+ };
769
+ }
770
+ var recordToStatFunctionMapper = {
771
+ branch: lcovReportToBranchStat,
772
+ line: lcovReportToLineStat,
773
+ function: lcovReportToFunctionStat
774
+ };
775
+ function lcovCoverageToAuditOutput(stat2, coverageType) {
776
+ const coverage = calculateCoverage(stat2.totalHit, stat2.totalFound);
777
+ const MAX_DECIMAL_PLACES = 4;
778
+ const roundedIntValue = toNumberPrecision(coverage * 100, 0);
779
+ return {
780
+ slug: `${coverageType}-coverage`,
781
+ score: toNumberPrecision(coverage, MAX_DECIMAL_PLACES),
782
+ value: roundedIntValue,
783
+ displayValue: `${roundedIntValue} %`,
784
+ ...stat2.issues.length > 0 && { details: { issues: stat2.issues } }
785
+ };
786
+ }
787
+
788
+ // packages/plugin-coverage/src/lib/runner/lcov/lcov-runner.ts
789
+ async function lcovResultsToAuditOutputs(results, coverageTypes) {
790
+ const lcovResults = await parseLcovFiles(results);
791
+ const totalCoverageStats = getTotalCoverageFromLcovRecords(
792
+ lcovResults,
793
+ coverageTypes
794
+ );
795
+ return coverageTypes.map((coverageType) => {
796
+ const stats = totalCoverageStats[coverageType];
797
+ if (!stats) {
798
+ return null;
799
+ }
800
+ return lcovCoverageToAuditOutput(stats, coverageType);
801
+ }).filter(exists);
802
+ }
803
+ async function parseLcovFiles(results) {
804
+ const parsedResults = await Promise.all(
805
+ results.map(async (result) => {
806
+ const lcovFileContent = await readTextFile(result.resultsPath);
807
+ const parsedRecords = parseLcov(toUnixNewlines(lcovFileContent));
808
+ return parsedRecords.map((record) => ({
809
+ ...record,
810
+ file: result.pathToProject == null ? record.file : join3(result.pathToProject, record.file)
811
+ }));
812
+ })
813
+ );
814
+ if (parsedResults.length !== results.length) {
815
+ throw new Error("Some provided LCOV results were not valid.");
816
+ }
817
+ const flatResults = parsedResults.flat();
818
+ if (flatResults.length === 0) {
819
+ throw new Error("All provided results are empty.");
820
+ }
821
+ return flatResults;
822
+ }
823
+ function getTotalCoverageFromLcovRecords(records, coverageTypes) {
824
+ return records.reduce(
825
+ (acc, report) => Object.fromEntries([
826
+ ...Object.entries(acc),
827
+ ...Object.entries(
828
+ getCoverageStatsFromLcovRecord(report, coverageTypes)
829
+ ).map(([type, stats]) => [
830
+ type,
831
+ {
832
+ totalFound: (acc[type]?.totalFound ?? 0) + stats.totalFound,
833
+ totalHit: (acc[type]?.totalHit ?? 0) + stats.totalHit,
834
+ issues: [...acc[type]?.issues ?? [], ...stats.issues]
835
+ }
836
+ ])
837
+ ]),
838
+ {}
839
+ );
840
+ }
841
+ function getCoverageStatsFromLcovRecord(record, coverageTypes) {
842
+ return Object.fromEntries(
843
+ coverageTypes.map((coverageType) => [
844
+ coverageType,
845
+ recordToStatFunctionMapper[coverageType](record)
846
+ ])
847
+ );
848
+ }
849
+
850
+ // packages/plugin-coverage/src/lib/runner/index.ts
851
+ async function executeRunner() {
852
+ const { reports, coverageToolCommand, coverageTypes } = await readJsonFile(PLUGIN_CONFIG_PATH);
853
+ if (coverageToolCommand != null) {
854
+ const { command, args } = coverageToolCommand;
855
+ try {
856
+ await executeProcess({ command, args });
857
+ } catch (error) {
858
+ if (error instanceof ProcessError) {
859
+ console.info(chalk5.bold("stdout from failed process:"));
860
+ console.info(error.stdout);
861
+ console.error(chalk5.bold("stderr from failed process:"));
862
+ console.error(error.stderr);
863
+ }
864
+ throw new Error(
865
+ "Coverage plugin: Running coverage tool failed. Make sure all your provided tests are passing."
866
+ );
867
+ }
868
+ }
869
+ const auditOutputs = await lcovResultsToAuditOutputs(reports, coverageTypes);
870
+ await ensureDirectoryExists(dirname(RUNNER_OUTPUT_PATH));
871
+ await writeFile(RUNNER_OUTPUT_PATH, JSON.stringify(auditOutputs));
872
+ }
873
+
874
+ // packages/plugin-coverage/src/bin.ts
875
+ await executeRunner().catch(console.error);