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