@code-pushup/cli 0.51.0 → 0.53.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 CHANGED
@@ -118,7 +118,26 @@ _If you're looking for programmatic usage, then refer to the underlying [@code-p
118
118
 
119
119
  ## Portal integration
120
120
 
121
- If you have access to the Code PushUp portal, provide credentials in order to upload reports.
121
+ If you have access to the Code PushUp portal, you can enable report uploads by installing the `@code-pushup/portal-client` package.
122
+
123
+ <details>
124
+ <summary>Installation command for <code>npm</code>, <code>yarn</code> and <code>pnpm</code></summary>
125
+
126
+ ```sh
127
+ npm install --save-dev @code-pushup/portal-client
128
+ ```
129
+
130
+ ```sh
131
+ yarn add --dev @code-pushup/portal-client
132
+ ```
133
+
134
+ ```sh
135
+ pnpm add --save-dev @code-pushup/portal-client
136
+ ```
137
+
138
+ </details>
139
+
140
+ Once the package is installed, update your configuration file to include your portal credentials:
122
141
 
123
142
  ```ts
124
143
  const config: CoreConfig = {
package/index.js CHANGED
@@ -1608,25 +1608,30 @@ import {
1608
1608
  } from "build-md";
1609
1609
  import { posix as pathPosix } from "node:path";
1610
1610
 
1611
- // packages/utils/src/lib/reports/ide-environment.ts
1611
+ // packages/utils/src/lib/reports/types.ts
1612
+ var SUPPORTED_ENVIRONMENTS = [
1613
+ "vscode",
1614
+ "github",
1615
+ "gitlab",
1616
+ "other"
1617
+ ];
1618
+
1619
+ // packages/utils/src/lib/reports/environment-type.ts
1620
+ var environmentChecks = {
1621
+ vscode: () => process.env["TERM_PROGRAM"] === "vscode",
1622
+ github: () => process.env["GITHUB_ACTIONS"] === "true",
1623
+ gitlab: () => process.env["GITLAB_CI"] === "true",
1624
+ other: () => true
1625
+ };
1612
1626
  function getEnvironmentType() {
1613
- if (isVSCode()) {
1614
- return "vscode";
1615
- }
1616
- if (isGitHub()) {
1617
- return "github";
1618
- }
1619
- return "other";
1620
- }
1621
- function isVSCode() {
1622
- return process.env["TERM_PROGRAM"] === "vscode";
1623
- }
1624
- function isGitHub() {
1625
- return process.env["GITHUB_ACTIONS"] === "true";
1627
+ return SUPPORTED_ENVIRONMENTS.find((env) => environmentChecks[env]()) ?? "other";
1626
1628
  }
1627
1629
  function getGitHubBaseUrl() {
1628
1630
  return `${process.env["GITHUB_SERVER_URL"]}/${process.env["GITHUB_REPOSITORY"]}/blob/${process.env["GITHUB_SHA"]}`;
1629
1631
  }
1632
+ function getGitLabBaseUrl() {
1633
+ return `${process.env["CI_SERVER_URL"]}/${process.env["CI_PROJECT_PATH"]}/-/blob/${process.env["CI_COMMIT_SHA"]}`;
1634
+ }
1630
1635
 
1631
1636
  // packages/utils/src/lib/reports/formatting.ts
1632
1637
  function tableSection(tableData, options2) {
@@ -1692,6 +1697,15 @@ function formatGitHubLink(file, position) {
1692
1697
  const lineRange = end && start !== end ? `${start}-${end}` : start;
1693
1698
  return `${baseUrl}/${file}#${lineRange}`;
1694
1699
  }
1700
+ function formatGitLabLink(file, position) {
1701
+ const baseUrl = getGitLabBaseUrl();
1702
+ if (!position) {
1703
+ return `${baseUrl}/${file}`;
1704
+ }
1705
+ const { startLine, endLine } = position;
1706
+ const lineRange = endLine && startLine !== endLine ? `${startLine}-${endLine}` : startLine;
1707
+ return `${baseUrl}/${file}#L${lineRange}`;
1708
+ }
1695
1709
  function formatFileLink(file, position, outputDir) {
1696
1710
  const relativePath = pathPosix.relative(outputDir, file);
1697
1711
  const env = getEnvironmentType();
@@ -1700,6 +1714,8 @@ function formatFileLink(file, position, outputDir) {
1700
1714
  return position ? `${relativePath}#L${position.startLine}` : relativePath;
1701
1715
  case "github":
1702
1716
  return formatGitHubLink(file, position);
1717
+ case "gitlab":
1718
+ return formatGitLabLink(file, position);
1703
1719
  default:
1704
1720
  return relativePath;
1705
1721
  }
@@ -2345,11 +2361,11 @@ import { bold as bold4, cyan, cyanBright, green as green2, red } from "ansis";
2345
2361
  function log(msg = "") {
2346
2362
  ui().logger.log(msg);
2347
2363
  }
2348
- function logStdoutSummary(report) {
2364
+ function logStdoutSummary(report, verbose = false) {
2349
2365
  const printCategories = report.categories.length > 0;
2350
2366
  log(reportToHeaderSection(report));
2351
2367
  log();
2352
- logPlugins(report);
2368
+ logPlugins(report.plugins, verbose);
2353
2369
  if (printCategories) {
2354
2370
  logCategories(report);
2355
2371
  }
@@ -2360,36 +2376,49 @@ function reportToHeaderSection(report) {
2360
2376
  const { packageName, version: version3 } = report;
2361
2377
  return `${bold4(REPORT_HEADLINE_TEXT)} - ${packageName}@${version3}`;
2362
2378
  }
2363
- function logPlugins(report) {
2364
- const { plugins } = report;
2379
+ function logPlugins(plugins, verbose) {
2365
2380
  plugins.forEach((plugin) => {
2366
2381
  const { title, audits } = plugin;
2382
+ const filteredAudits = verbose ? audits : audits.filter(({ score }) => score !== 1);
2383
+ const diff = audits.length - filteredAudits.length;
2384
+ logAudits(title, filteredAudits);
2385
+ if (diff > 0) {
2386
+ const notice = filteredAudits.length === 0 ? `... All ${diff} audits have perfect scores ...` : `... ${diff} audits with perfect scores omitted for brevity ...`;
2387
+ logRow(1, notice);
2388
+ }
2367
2389
  log();
2368
- log(bold4.magentaBright(`${title} audits`));
2369
- log();
2370
- audits.forEach((audit) => {
2371
- ui().row([
2372
- {
2373
- text: applyScoreColor({ score: audit.score, text: "\u25CF" }),
2374
- width: 2,
2375
- padding: [0, 1, 0, 0]
2376
- },
2377
- {
2378
- text: audit.title,
2379
- // eslint-disable-next-line no-magic-numbers
2380
- padding: [0, 3, 0, 0]
2381
- },
2382
- {
2383
- text: cyanBright(audit.displayValue || `${audit.value}`),
2384
- // eslint-disable-next-line no-magic-numbers
2385
- width: 20,
2386
- padding: [0, 0, 0, 0]
2387
- }
2388
- ]);
2389
- });
2390
- log();
2391
2390
  });
2392
2391
  }
2392
+ function logAudits(pluginTitle, audits) {
2393
+ log();
2394
+ log(bold4.magentaBright(`${pluginTitle} audits`));
2395
+ log();
2396
+ audits.forEach(({ score, title, displayValue, value }) => {
2397
+ logRow(score, title, displayValue || `${value}`);
2398
+ });
2399
+ }
2400
+ function logRow(score, title, value) {
2401
+ ui().row([
2402
+ {
2403
+ text: applyScoreColor({ score, text: "\u25CF" }),
2404
+ width: 2,
2405
+ padding: [0, 1, 0, 0]
2406
+ },
2407
+ {
2408
+ text: title,
2409
+ // eslint-disable-next-line no-magic-numbers
2410
+ padding: [0, 3, 0, 0]
2411
+ },
2412
+ ...value ? [
2413
+ {
2414
+ text: cyanBright(value),
2415
+ // eslint-disable-next-line no-magic-numbers
2416
+ width: 20,
2417
+ padding: [0, 0, 0, 0]
2418
+ }
2419
+ ] : []
2420
+ ]);
2421
+ }
2393
2422
  function logCategories({ categories, plugins }) {
2394
2423
  const hAlign = (idx) => idx === 0 ? "left" : "right";
2395
2424
  const rows = categories.map(({ title, score, refs, isBinary }) => [
@@ -2535,7 +2564,7 @@ var verboseUtils = (verbose = false) => ({
2535
2564
 
2536
2565
  // packages/core/package.json
2537
2566
  var name = "@code-pushup/core";
2538
- var version = "0.51.0";
2567
+ var version = "0.53.0";
2539
2568
 
2540
2569
  // packages/core/src/lib/implementation/execute-plugin.ts
2541
2570
  import { bold as bold5 } from "ansis";
@@ -2730,10 +2759,8 @@ var PersistError = class extends Error {
2730
2759
  super(`fileName: ${reportPath} could not be saved.`);
2731
2760
  }
2732
2761
  };
2733
- async function persistReport(report, options2) {
2762
+ async function persistReport(report, sortedScoredReport, options2) {
2734
2763
  const { outputDir, filename, format } = options2;
2735
- const sortedScoredReport = sortReport(scoreReport(report));
2736
- logStdoutSummary(sortedScoredReport);
2737
2764
  const results = format.map((reportType) => {
2738
2765
  switch (reportType) {
2739
2766
  case "json":
@@ -2779,7 +2806,13 @@ function logPersistedResults(persistResults) {
2779
2806
  async function collectAndPersistReports(options2) {
2780
2807
  const { exec } = verboseUtils(options2.verbose);
2781
2808
  const report = await collect(options2);
2782
- const persistResults = await persistReport(report, options2.persist);
2809
+ const sortedScoredReport = sortReport(scoreReport(report));
2810
+ const persistResults = await persistReport(
2811
+ report,
2812
+ sortedScoredReport,
2813
+ options2.persist
2814
+ );
2815
+ logStdoutSummary(sortedScoredReport, options2.verbose);
2783
2816
  exec(() => {
2784
2817
  logPersistedResults(persistResults);
2785
2818
  });
@@ -2791,10 +2824,6 @@ async function collectAndPersistReports(options2) {
2791
2824
  // packages/core/src/lib/compare.ts
2792
2825
  import { writeFile as writeFile2 } from "node:fs/promises";
2793
2826
  import { join as join5 } from "node:path";
2794
- import {
2795
- PortalOperationError,
2796
- getPortalComparisonLink
2797
- } from "@code-pushup/portal-client";
2798
2827
 
2799
2828
  // packages/core/src/lib/implementation/compare-scorables.ts
2800
2829
  function compareCategories(reports) {
@@ -2930,6 +2959,18 @@ function selectMeta(meta) {
2930
2959
  };
2931
2960
  }
2932
2961
 
2962
+ // packages/core/src/lib/load-portal-client.ts
2963
+ async function loadPortalClient() {
2964
+ try {
2965
+ return await import("@code-pushup/portal-client");
2966
+ } catch {
2967
+ ui().logger.error(
2968
+ "Optional peer dependency @code-pushup/portal-client is not available. Make sure it is installed to enable upload functionality."
2969
+ );
2970
+ return null;
2971
+ }
2972
+ }
2973
+
2933
2974
  // packages/core/src/lib/compare.ts
2934
2975
  async function compareReportFiles(inputPaths, persistConfig, uploadConfig, label) {
2935
2976
  const { outputDir, filename, format } = persistConfig;
@@ -2994,6 +3035,11 @@ function reportsDiffToFileContent(reportsDiff, format) {
2994
3035
  }
2995
3036
  async function fetchPortalComparisonLink(uploadConfig, commits) {
2996
3037
  const { server, apiKey, organization, project } = uploadConfig;
3038
+ const portalClient = await loadPortalClient();
3039
+ if (!portalClient) {
3040
+ return;
3041
+ }
3042
+ const { PortalOperationError, getPortalComparisonLink } = portalClient;
2997
3043
  try {
2998
3044
  return await getPortalComparisonLink({
2999
3045
  server,
@@ -3016,18 +3062,7 @@ async function fetchPortalComparisonLink(uploadConfig, commits) {
3016
3062
  }
3017
3063
  }
3018
3064
 
3019
- // packages/core/src/lib/upload.ts
3020
- import {
3021
- uploadToPortal
3022
- } from "@code-pushup/portal-client";
3023
-
3024
3065
  // packages/core/src/lib/implementation/report-to-gql.ts
3025
- import {
3026
- CategoryConfigRefType as PortalCategoryRefType,
3027
- IssueSeverity as PortalIssueSeverity,
3028
- IssueSourceType as PortalIssueSourceType,
3029
- TableAlignment as PortalTableAlignment
3030
- } from "@code-pushup/portal-client";
3031
3066
  function reportToGQL(report) {
3032
3067
  return {
3033
3068
  packageName: report.packageName,
@@ -3094,7 +3129,7 @@ function issueToGQL(issue) {
3094
3129
  message: issue.message,
3095
3130
  severity: issueSeverityToGQL(issue.severity),
3096
3131
  ...issue.source?.file && {
3097
- sourceType: PortalIssueSourceType.SourceCode,
3132
+ sourceType: safeEnum("SourceCode"),
3098
3133
  sourceFilePath: issue.source.file,
3099
3134
  sourceStartLine: issue.source.position?.startLine,
3100
3135
  sourceStartColumn: issue.source.position?.startColumn,
@@ -3140,37 +3175,45 @@ function categoryToGQL(category) {
3140
3175
  function categoryRefTypeToGQL(type) {
3141
3176
  switch (type) {
3142
3177
  case "audit":
3143
- return PortalCategoryRefType.Audit;
3178
+ return safeEnum("Audit");
3144
3179
  case "group":
3145
- return PortalCategoryRefType.Group;
3180
+ return safeEnum("Group");
3146
3181
  }
3147
3182
  }
3148
3183
  function issueSeverityToGQL(severity) {
3149
3184
  switch (severity) {
3150
3185
  case "info":
3151
- return PortalIssueSeverity.Info;
3186
+ return safeEnum("Info");
3152
3187
  case "error":
3153
- return PortalIssueSeverity.Error;
3188
+ return safeEnum("Error");
3154
3189
  case "warning":
3155
- return PortalIssueSeverity.Warning;
3190
+ return safeEnum("Warning");
3156
3191
  }
3157
3192
  }
3158
3193
  function tableAlignmentToGQL(alignment) {
3159
3194
  switch (alignment) {
3160
3195
  case "left":
3161
- return PortalTableAlignment.Left;
3196
+ return safeEnum("Left");
3162
3197
  case "center":
3163
- return PortalTableAlignment.Center;
3198
+ return safeEnum("Center");
3164
3199
  case "right":
3165
- return PortalTableAlignment.Right;
3200
+ return safeEnum("Right");
3166
3201
  }
3167
3202
  }
3203
+ function safeEnum(value) {
3204
+ return value;
3205
+ }
3168
3206
 
3169
3207
  // packages/core/src/lib/upload.ts
3170
- async function upload(options2, uploadFn = uploadToPortal) {
3208
+ async function upload(options2) {
3171
3209
  if (options2.upload == null) {
3172
3210
  throw new Error("Upload configuration is not set.");
3173
3211
  }
3212
+ const portalClient = await loadPortalClient();
3213
+ if (!portalClient) {
3214
+ return;
3215
+ }
3216
+ const { uploadToPortal } = portalClient;
3174
3217
  const { apiKey, server, organization, project, timeout } = options2.upload;
3175
3218
  const report = await loadReport({
3176
3219
  ...options2.persist,
@@ -3185,7 +3228,7 @@ async function upload(options2, uploadFn = uploadToPortal) {
3185
3228
  commit: report.commit.hash,
3186
3229
  ...reportToGQL(report)
3187
3230
  };
3188
- return uploadFn({ apiKey, server, data, timeout });
3231
+ return uploadToPortal({ apiKey, server, data, timeout });
3189
3232
  }
3190
3233
 
3191
3234
  // packages/core/src/lib/history.ts
@@ -3368,8 +3411,10 @@ function yargsAutorunCommandObject() {
3368
3411
  renderConfigureCategoriesHint();
3369
3412
  }
3370
3413
  if (options2.upload) {
3371
- const { url } = await upload(options2);
3372
- uploadSuccessfulLog(url);
3414
+ const report = await upload(options2);
3415
+ if (report?.url) {
3416
+ uploadSuccessfulLog(report.url);
3417
+ }
3373
3418
  } else {
3374
3419
  ui().logger.warning("Upload skipped because configuration is not set.");
3375
3420
  renderIntegratePortalHint();
@@ -3470,6 +3515,94 @@ function yargsCompareCommandObject() {
3470
3515
  // packages/cli/src/lib/history/history-command.ts
3471
3516
  import { bold as bold10, gray as gray7 } from "ansis";
3472
3517
 
3518
+ // packages/cli/src/lib/implementation/global.utils.ts
3519
+ import yargs from "yargs";
3520
+
3521
+ // packages/cli/src/lib/implementation/validate-filter-options.utils.ts
3522
+ var OptionValidationError = class extends Error {
3523
+ };
3524
+ function validateFilterOption(option, { plugins, categories }, { itemsToFilter, verbose }) {
3525
+ const itemsToFilterSet = new Set(itemsToFilter);
3526
+ const validItems = isCategoryOption(option) ? categories : isPluginOption(option) ? plugins : [];
3527
+ const invalidItems = itemsToFilter.filter(
3528
+ (item) => !validItems.some(({ slug }) => slug === item)
3529
+ );
3530
+ const message = createValidationMessage(option, invalidItems, validItems);
3531
+ if (isOnlyOption(option) && itemsToFilterSet.size > 0 && itemsToFilterSet.size === invalidItems.length) {
3532
+ throw new OptionValidationError(message);
3533
+ }
3534
+ if (invalidItems.length > 0) {
3535
+ ui().logger.warning(message);
3536
+ }
3537
+ if (isPluginOption(option) && categories.length > 0 && verbose) {
3538
+ const removedCategorySlugs = filterItemRefsBy(
3539
+ categories,
3540
+ ({ plugin }) => isOnlyOption(option) ? !itemsToFilterSet.has(plugin) : itemsToFilterSet.has(plugin)
3541
+ ).map(({ slug }) => slug);
3542
+ if (removedCategorySlugs.length > 0) {
3543
+ ui().logger.info(
3544
+ `The --${option} argument removed the following categories: ${removedCategorySlugs.join(
3545
+ ", "
3546
+ )}.`
3547
+ );
3548
+ }
3549
+ }
3550
+ }
3551
+ function validateFinalState(filteredItems, originalItems) {
3552
+ const { categories: filteredCategories, plugins: filteredPlugins } = filteredItems;
3553
+ const { categories: originalCategories, plugins: originalPlugins } = originalItems;
3554
+ if (filteredCategories.length === 0 && filteredPlugins.length === 0 && (originalPlugins.length > 0 || originalCategories.length > 0)) {
3555
+ const availablePlugins = originalPlugins.map((p) => p.slug).join(", ") || "none";
3556
+ const availableCategories = originalCategories.map((c) => c.slug).join(", ") || "none";
3557
+ throw new OptionValidationError(
3558
+ `Nothing to report. No plugins or categories are available after filtering. Available plugins: ${availablePlugins}. Available categories: ${availableCategories}.`
3559
+ );
3560
+ }
3561
+ }
3562
+ function isCategoryOption(option) {
3563
+ return option.endsWith("Categories");
3564
+ }
3565
+ function isPluginOption(option) {
3566
+ return option.endsWith("Plugins");
3567
+ }
3568
+ function isOnlyOption(option) {
3569
+ return option.startsWith("only");
3570
+ }
3571
+ function getItemType(option, count) {
3572
+ const itemType = isCategoryOption(option) ? "category" : isPluginOption(option) ? "plugin" : "item";
3573
+ return pluralize(itemType, count);
3574
+ }
3575
+ function createValidationMessage(option, invalidItems, validItems) {
3576
+ const invalidItem = getItemType(option, invalidItems.length);
3577
+ const invalidItemText = invalidItems.length === 1 ? `a ${invalidItem} that does not exist:` : `${invalidItem} that do not exist:`;
3578
+ const invalidSlugs = invalidItems.join(", ");
3579
+ const validItem = getItemType(option, validItems.length);
3580
+ const validItemText = validItems.length === 1 ? `The only valid ${validItem} is` : `Valid ${validItem} are`;
3581
+ const validSlugs = validItems.map(({ slug }) => slug).join(", ");
3582
+ return `The --${option} argument references ${invalidItemText} ${invalidSlugs}. ${validItemText} ${validSlugs}.`;
3583
+ }
3584
+ function handleConflictingOptions(type, onlyItems, skipItems) {
3585
+ const conflictingItems = onlyItems.filter((item) => skipItems.includes(item));
3586
+ if (conflictingItems.length > 0) {
3587
+ const conflictSubject = () => {
3588
+ switch (type) {
3589
+ case "categories":
3590
+ return conflictingItems.length > 1 ? "categories are" : "category is";
3591
+ case "plugins":
3592
+ return conflictingItems.length > 1 ? "plugins are" : "plugin is";
3593
+ }
3594
+ };
3595
+ const conflictingSlugs = conflictingItems.join(", ");
3596
+ throw new OptionValidationError(
3597
+ `The following ${conflictSubject()} specified in both --only${capitalize(
3598
+ type
3599
+ )} and --skip${capitalize(
3600
+ type
3601
+ )}: ${conflictingSlugs}. Please choose one option.`
3602
+ );
3603
+ }
3604
+ }
3605
+
3473
3606
  // packages/cli/src/lib/implementation/global.utils.ts
3474
3607
  function filterKebabCaseKeys(obj) {
3475
3608
  return Object.entries(obj).filter(([key]) => !key.includes("-")).reduce(
@@ -3485,9 +3618,15 @@ function logErrorBeforeThrow(fn) {
3485
3618
  try {
3486
3619
  return await fn(...args);
3487
3620
  } catch (error) {
3488
- console.error(error);
3489
- await new Promise((resolve) => process.stdout.write("", resolve));
3490
- throw error;
3621
+ if (error instanceof OptionValidationError) {
3622
+ ui().logger.error(error.message);
3623
+ await new Promise((resolve) => process.stdout.write("", resolve));
3624
+ yargs().exit(1, error);
3625
+ } else {
3626
+ console.error(error);
3627
+ await new Promise((resolve) => process.stdout.write("", resolve));
3628
+ throw error;
3629
+ }
3491
3630
  }
3492
3631
  };
3493
3632
  }
@@ -3495,21 +3634,19 @@ function coerceArray(param) {
3495
3634
  return [...new Set(toArray(param).flatMap((f) => f.split(",")))];
3496
3635
  }
3497
3636
 
3498
- // packages/cli/src/lib/implementation/only-plugins.options.ts
3499
- var onlyPluginsOption = {
3500
- describe: "List of plugins to run. If not set all plugins are run.",
3637
+ // packages/cli/src/lib/implementation/filter.options.ts
3638
+ var skipCategoriesOption = {
3639
+ describe: "List of categories to skip. If not set all categories are run.",
3501
3640
  type: "array",
3502
3641
  default: [],
3503
- coerce: coerceArray,
3504
- alias: "p"
3642
+ coerce: coerceArray
3643
+ };
3644
+ var onlyCategoriesOption = {
3645
+ describe: "List of categories to run. If not set all categories are run.",
3646
+ type: "array",
3647
+ default: [],
3648
+ coerce: coerceArray
3505
3649
  };
3506
- function yargsOnlyPluginsOptionsDefinition() {
3507
- return {
3508
- onlyPlugins: onlyPluginsOption
3509
- };
3510
- }
3511
-
3512
- // packages/cli/src/lib/implementation/skip-plugins.options.ts
3513
3650
  var skipPluginsOption = {
3514
3651
  describe: "List of plugins to skip. If not set all plugins are run.",
3515
3652
  type: "array",
@@ -3517,9 +3654,19 @@ var skipPluginsOption = {
3517
3654
  coerce: coerceArray,
3518
3655
  alias: "P"
3519
3656
  };
3520
- function yargsSkipPluginsOptionsDefinition() {
3657
+ var onlyPluginsOption = {
3658
+ describe: "List of plugins to run. If not set all plugins are run.",
3659
+ type: "array",
3660
+ default: [],
3661
+ coerce: coerceArray,
3662
+ alias: "p"
3663
+ };
3664
+ function yargsFilterOptionsDefinition() {
3521
3665
  return {
3522
- skipPlugins: skipPluginsOption
3666
+ skipCategories: skipCategoriesOption,
3667
+ onlyCategories: onlyCategoriesOption,
3668
+ skipPlugins: skipPluginsOption,
3669
+ onlyPlugins: onlyPluginsOption
3523
3670
  };
3524
3671
  }
3525
3672
 
@@ -3630,17 +3777,16 @@ function yargsHistoryCommandObject() {
3630
3777
  return {
3631
3778
  command,
3632
3779
  describe: "Collect reports for commit history",
3633
- builder: (yargs2) => {
3634
- yargs2.options({
3780
+ builder: (yargs3) => {
3781
+ yargs3.options({
3635
3782
  ...yargsHistoryOptionsDefinition(),
3636
- ...yargsOnlyPluginsOptionsDefinition(),
3637
- ...yargsSkipPluginsOptionsDefinition()
3783
+ ...yargsFilterOptionsDefinition()
3638
3784
  });
3639
- yargs2.group(
3785
+ yargs3.group(
3640
3786
  Object.keys(yargsHistoryOptionsDefinition()),
3641
3787
  "History Options:"
3642
3788
  );
3643
- return yargs2;
3789
+ return yargs3;
3644
3790
  },
3645
3791
  handler
3646
3792
  };
@@ -3707,8 +3853,10 @@ function yargsUploadCommandObject() {
3707
3853
  renderIntegratePortalHint();
3708
3854
  throw new Error("Upload configuration not set");
3709
3855
  }
3710
- const { url } = await upload(options2);
3711
- uploadSuccessfulLog(url);
3856
+ const report = await upload(options2);
3857
+ if (report?.url) {
3858
+ uploadSuccessfulLog(report.url);
3859
+ }
3712
3860
  }
3713
3861
  };
3714
3862
  }
@@ -3765,80 +3913,99 @@ async function coreConfigMiddleware(processArgs) {
3765
3913
  }
3766
3914
  var normalizeFormats = (formats) => (formats ?? []).flatMap((format) => format.split(","));
3767
3915
 
3768
- // packages/cli/src/lib/implementation/validate-plugin-filter-options.utils.ts
3769
- function validatePluginFilterOption(filterOption, {
3770
- plugins,
3771
- categories
3772
- }, {
3773
- pluginsToFilter = [],
3774
- verbose = false
3775
- } = {}) {
3776
- const pluginsToFilterSet = new Set(pluginsToFilter);
3777
- const missingPlugins = pluginsToFilter.filter(
3778
- (plugin) => !plugins.some(({ slug }) => slug === plugin)
3779
- );
3780
- const isSkipOption = filterOption === "skipPlugins";
3781
- const filterFunction = (plugin) => isSkipOption ? pluginsToFilterSet.has(plugin) : !pluginsToFilterSet.has(plugin);
3782
- if (missingPlugins.length > 0) {
3783
- ui().logger.warning(
3784
- `The --${filterOption} argument references ${missingPlugins.length === 1 ? "a plugin that does" : "plugins that do"} not exist: ${missingPlugins.join(", ")}. The valid plugin ${plugins.length === 1 ? "slug is" : "slugs are"} ${plugins.map(({ slug }) => slug).join(", ")}.`
3785
- );
3786
- }
3787
- if (categories.length > 0 && verbose) {
3788
- const removedCategorySlugs = filterItemRefsBy(
3789
- categories,
3790
- ({ plugin }) => filterFunction(plugin)
3791
- ).map(({ slug }) => slug);
3792
- ui().logger.info(
3793
- `The --${filterOption} argument removed the following categories: ${removedCategorySlugs.join(
3794
- ", "
3795
- )}.`
3796
- );
3797
- }
3798
- }
3799
-
3800
- // packages/cli/src/lib/implementation/filter-plugins.middleware.ts
3801
- function filterPluginsMiddleware(originalProcessArgs) {
3916
+ // packages/cli/src/lib/implementation/filter.middleware.ts
3917
+ function filterMiddleware(originalProcessArgs) {
3802
3918
  const {
3803
3919
  plugins,
3804
3920
  categories = [],
3921
+ skipCategories = [],
3922
+ onlyCategories = [],
3805
3923
  skipPlugins = [],
3806
3924
  onlyPlugins = [],
3807
- verbose
3925
+ verbose = false
3808
3926
  } = originalProcessArgs;
3809
- if (skipPlugins.length === 0 && onlyPlugins.length === 0) {
3927
+ if (skipCategories.length === 0 && onlyCategories.length === 0 && skipPlugins.length === 0 && onlyPlugins.length === 0) {
3810
3928
  return { ...originalProcessArgs, categories };
3811
3929
  }
3812
- validatePluginFilterOption(
3813
- "skipPlugins",
3814
- { plugins, categories },
3815
- { pluginsToFilter: skipPlugins, verbose }
3816
- );
3817
- validatePluginFilterOption(
3818
- "onlyPlugins",
3819
- { plugins, categories },
3820
- { pluginsToFilter: onlyPlugins, verbose }
3930
+ handleConflictingOptions("categories", onlyCategories, skipCategories);
3931
+ handleConflictingOptions("plugins", onlyPlugins, skipPlugins);
3932
+ const filteredCategories = applyCategoryFilters(
3933
+ { categories, plugins },
3934
+ skipCategories,
3935
+ onlyCategories,
3936
+ verbose
3821
3937
  );
3822
- const validSkipPlugins = new Set(
3823
- skipPlugins.filter((sP) => plugins.some((p) => p.slug === sP))
3938
+ const filteredPlugins = applyPluginFilters(
3939
+ { categories: filteredCategories, plugins },
3940
+ skipPlugins,
3941
+ onlyPlugins,
3942
+ verbose
3824
3943
  );
3825
- const pluginsAfterSkip = plugins.filter(
3826
- ({ slug }) => !validSkipPlugins.has(slug)
3944
+ const finalCategories = filterItemRefsBy(
3945
+ filteredCategories,
3946
+ (ref) => filteredPlugins.some((plugin) => plugin.slug === ref.plugin)
3827
3947
  );
3828
- const validOnlyPlugins = new Set(
3829
- onlyPlugins.filter((oP) => pluginsAfterSkip.some((p) => p.slug === oP))
3948
+ validateFinalState(
3949
+ { categories: finalCategories, plugins: filteredPlugins },
3950
+ { categories, plugins }
3830
3951
  );
3831
- const filteredPlugins = validOnlyPlugins.size > 0 ? pluginsAfterSkip.filter(({ slug }) => validOnlyPlugins.has(slug)) : pluginsAfterSkip;
3832
- const filteredCategories = filteredPlugins.length > 0 ? filterItemRefsBy(
3833
- categories,
3834
- ({ plugin }) => filteredPlugins.some(({ slug }) => slug === plugin)
3835
- ) : categories;
3836
3952
  return {
3837
3953
  ...originalProcessArgs,
3838
3954
  plugins: filteredPlugins,
3839
- categories: filteredCategories
3955
+ categories: finalCategories
3840
3956
  };
3841
3957
  }
3958
+ function applyFilters(items, skipItems, onlyItems, key) {
3959
+ return items.filter((item) => {
3960
+ const itemKey = item[key];
3961
+ return !skipItems.includes(itemKey) && (onlyItems.length === 0 || onlyItems.includes(itemKey));
3962
+ });
3963
+ }
3964
+ function applyCategoryFilters({ categories, plugins }, skipCategories, onlyCategories, verbose) {
3965
+ if (skipCategories.length === 0 && onlyCategories.length === 0) {
3966
+ return categories;
3967
+ }
3968
+ validateFilterOption(
3969
+ "skipCategories",
3970
+ { plugins, categories },
3971
+ { itemsToFilter: skipCategories, verbose }
3972
+ );
3973
+ validateFilterOption(
3974
+ "onlyCategories",
3975
+ { plugins, categories },
3976
+ { itemsToFilter: onlyCategories, verbose }
3977
+ );
3978
+ return applyFilters(categories, skipCategories, onlyCategories, "slug");
3979
+ }
3980
+ function applyPluginFilters({ categories, plugins }, skipPlugins, onlyPlugins, verbose) {
3981
+ const filteredPlugins = filterPluginsFromCategories({
3982
+ categories,
3983
+ plugins
3984
+ });
3985
+ if (skipPlugins.length === 0 && onlyPlugins.length === 0) {
3986
+ return filteredPlugins;
3987
+ }
3988
+ validateFilterOption(
3989
+ "skipPlugins",
3990
+ { plugins: filteredPlugins, categories },
3991
+ { itemsToFilter: skipPlugins, verbose }
3992
+ );
3993
+ validateFilterOption(
3994
+ "onlyPlugins",
3995
+ { plugins: filteredPlugins, categories },
3996
+ { itemsToFilter: onlyPlugins, verbose }
3997
+ );
3998
+ return applyFilters(filteredPlugins, skipPlugins, onlyPlugins, "slug");
3999
+ }
4000
+ function filterPluginsFromCategories({
4001
+ categories,
4002
+ plugins
4003
+ }) {
4004
+ const validPluginSlugs = new Set(
4005
+ categories.flatMap((category) => category.refs.map((ref) => ref.plugin))
4006
+ );
4007
+ return categories.length > 0 ? plugins.filter((plugin) => validPluginSlugs.has(plugin.slug)) : plugins;
4008
+ }
3842
4009
 
3843
4010
  // packages/cli/src/lib/middlewares.ts
3844
4011
  var middlewares = [
@@ -3847,7 +4014,7 @@ var middlewares = [
3847
4014
  applyBeforeValidation: false
3848
4015
  },
3849
4016
  {
3850
- middlewareFunction: filterPluginsMiddleware,
4017
+ middlewareFunction: filterMiddleware,
3851
4018
  applyBeforeValidation: false
3852
4019
  }
3853
4020
  ];
@@ -3924,14 +4091,12 @@ function yargsGlobalOptionsDefinition() {
3924
4091
  var options = {
3925
4092
  ...yargsGlobalOptionsDefinition(),
3926
4093
  ...yargsCoreConfigOptionsDefinition(),
3927
- ...yargsOnlyPluginsOptionsDefinition(),
3928
- ...yargsSkipPluginsOptionsDefinition()
4094
+ ...yargsFilterOptionsDefinition()
3929
4095
  };
3930
4096
  var groups = {
3931
4097
  "Global Options:": [
3932
4098
  ...Object.keys(yargsGlobalOptionsDefinition()),
3933
- ...Object.keys(yargsOnlyPluginsOptionsDefinition()),
3934
- ...Object.keys(yargsSkipPluginsOptionsDefinition())
4099
+ ...Object.keys(yargsFilterOptionsDefinition())
3935
4100
  ],
3936
4101
  "Persist Options:": Object.keys(yargsPersistConfigOptionsDefinition()),
3937
4102
  "Upload Options:": Object.keys(yargsUploadConfigOptionsDefinition())
@@ -3939,10 +4104,10 @@ var groups = {
3939
4104
 
3940
4105
  // packages/cli/src/lib/yargs-cli.ts
3941
4106
  import { blue, dim as dim2, green as green4 } from "ansis";
3942
- import yargs from "yargs";
4107
+ import yargs2 from "yargs";
3943
4108
 
3944
4109
  // packages/cli/package.json
3945
- var version2 = "0.51.0";
4110
+ var version2 = "0.53.0";
3946
4111
 
3947
4112
  // packages/cli/src/lib/implementation/formatting.ts
3948
4113
  import { bold as bold13, dim, green as green3 } from "ansis";
@@ -3994,7 +4159,7 @@ function yargsCli(argv, cfg) {
3994
4159
  const options2 = cfg.options ?? {};
3995
4160
  const groups2 = cfg.groups ?? {};
3996
4161
  const examples = cfg.examples ?? [];
3997
- const cli2 = yargs(argv);
4162
+ const cli2 = yargs2(argv);
3998
4163
  cli2.updateLocale(yargsDecorator).wrap(Math.max(TERMINAL_WIDTH, cli2.terminalWidth())).help("help", descriptionStyle("Show help")).alias("h", "help").showHelpOnFail(false).version("version", dim2`Show version`, version2).check((args) => {
3999
4164
  const persist = args["persist"];
4000
4165
  return persist == null || validatePersistFormat(persist);
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@code-pushup/cli",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
4
4
  "license": "MIT",
5
5
  "description": "A CLI to run all kinds of code quality measurements to align your team with company goals",
6
6
  "bin": {
7
7
  "code-pushup": "index.js"
8
8
  },
9
9
  "dependencies": {
10
- "@code-pushup/models": "0.51.0",
11
- "@code-pushup/core": "0.51.0",
12
- "@code-pushup/utils": "0.51.0",
10
+ "@code-pushup/models": "0.53.0",
11
+ "@code-pushup/core": "0.53.0",
12
+ "@code-pushup/utils": "0.53.0",
13
13
  "yargs": "^17.7.2",
14
14
  "ansis": "^3.3.0",
15
15
  "simple-git": "^3.20.0"
@@ -23,6 +23,9 @@
23
23
  "url": "git+https://github.com/code-pushup/cli.git",
24
24
  "directory": "packages/cli"
25
25
  },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
26
29
  "type": "module",
27
30
  "main": "./index.js",
28
31
  "types": "./src/index.d.ts"
@@ -1,8 +1,7 @@
1
1
  import { type CoreConfig, type Format } from '@code-pushup/models';
2
2
  import type { CoreConfigCliOptions } from './core-config.model';
3
+ import type { FilterOptions } from './filter.model';
3
4
  import type { GeneralCliOptions } from './global.model';
4
- import type { OnlyPluginsOptions } from './only-plugins.model';
5
- import type { SkipPluginsOptions } from './skip-plugins.model';
6
- export type CoreConfigMiddlewareOptions = GeneralCliOptions & CoreConfigCliOptions & OnlyPluginsOptions & SkipPluginsOptions;
7
- export declare function coreConfigMiddleware<T extends CoreConfigMiddlewareOptions>(processArgs: T): Promise<GeneralCliOptions & CoreConfig & OnlyPluginsOptions & SkipPluginsOptions>;
5
+ export type CoreConfigMiddlewareOptions = GeneralCliOptions & CoreConfigCliOptions & FilterOptions;
6
+ export declare function coreConfigMiddleware<T extends CoreConfigMiddlewareOptions>(processArgs: T): Promise<GeneralCliOptions & CoreConfig & FilterOptions>;
8
7
  export declare const normalizeFormats: (formats?: string[]) => Format[];
@@ -0,0 +1,2 @@
1
+ import type { FilterOptions } from './filter.model';
2
+ export declare function filterMiddleware<T extends FilterOptions>(originalProcessArgs: T): T;
@@ -0,0 +1,9 @@
1
+ import type { GlobalOptions } from '@code-pushup/core';
2
+ import type { CategoryConfig, CoreConfig, PluginConfig } from '@code-pushup/models';
3
+ export type FilterOptions = Partial<GlobalOptions> & Pick<CoreConfig, 'categories' | 'plugins'> & FilterCliOptions;
4
+ export type FilterCliOptions = Partial<Record<FilterOptionType, string[]>>;
5
+ export type FilterOptionType = 'skipCategories' | 'onlyCategories' | 'skipPlugins' | 'onlyPlugins';
6
+ export type Filterables = {
7
+ categories: CategoryConfig[];
8
+ plugins: PluginConfig[];
9
+ };
@@ -0,0 +1,7 @@
1
+ import type { Options } from 'yargs';
2
+ import type { FilterCliOptions } from './filter.model';
3
+ export declare const skipCategoriesOption: Options;
4
+ export declare const onlyCategoriesOption: Options;
5
+ export declare const skipPluginsOption: Options;
6
+ export declare const onlyPluginsOption: Options;
7
+ export declare function yargsFilterOptionsDefinition(): Record<keyof FilterCliOptions, Options>;
@@ -0,0 +1,13 @@
1
+ import type { CategoryConfig, PluginConfig } from '@code-pushup/models';
2
+ import type { FilterOptionType, Filterables } from './filter.model';
3
+ export declare class OptionValidationError extends Error {
4
+ }
5
+ export declare function validateFilterOption(option: FilterOptionType, { plugins, categories }: Filterables, { itemsToFilter, verbose }: {
6
+ itemsToFilter: string[];
7
+ verbose: boolean;
8
+ }): void;
9
+ export declare function validateFinalState(filteredItems: Filterables, originalItems: Filterables): void;
10
+ export declare function isOnlyOption(option: FilterOptionType): boolean;
11
+ export declare function getItemType(option: FilterOptionType, count: number): string;
12
+ export declare function createValidationMessage(option: FilterOptionType, invalidItems: string[], validItems: Pick<PluginConfig | CategoryConfig, 'slug'>[]): string;
13
+ export declare function handleConflictingOptions(type: 'categories' | 'plugins', onlyItems: string[], skipItems: string[]): void;
@@ -1,4 +1,6 @@
1
1
  export declare const options: {
2
+ skipCategories: import("yargs").Options;
3
+ onlyCategories: import("yargs").Options;
2
4
  skipPlugins: import("yargs").Options;
3
5
  onlyPlugins: import("yargs").Options;
4
6
  "persist.outputDir": import("yargs").Options;
@@ -1,3 +0,0 @@
1
- import type { OnlyPluginsOptions } from './only-plugins.model';
2
- import type { SkipPluginsOptions } from './skip-plugins.model';
3
- export declare function filterPluginsMiddleware<T extends SkipPluginsOptions & OnlyPluginsOptions>(originalProcessArgs: T): T;
@@ -1,6 +0,0 @@
1
- import type { GlobalOptions } from '@code-pushup/core';
2
- import type { CoreConfig } from '@code-pushup/models';
3
- export type OnlyPluginsCliOptions = {
4
- onlyPlugins?: string[];
5
- };
6
- export type OnlyPluginsOptions = Partial<GlobalOptions> & Pick<CoreConfig, 'categories' | 'plugins'> & OnlyPluginsCliOptions;
@@ -1,4 +0,0 @@
1
- import type { Options } from 'yargs';
2
- import type { OnlyPluginsCliOptions } from './only-plugins.model';
3
- export declare const onlyPluginsOption: Options;
4
- export declare function yargsOnlyPluginsOptionsDefinition(): Record<keyof OnlyPluginsCliOptions, Options>;
@@ -1,6 +0,0 @@
1
- import type { GlobalOptions } from '@code-pushup/core';
2
- import type { CoreConfig } from '@code-pushup/models';
3
- export type SkipPluginsCliOptions = {
4
- skipPlugins?: string[];
5
- };
6
- export type SkipPluginsOptions = Partial<GlobalOptions> & Pick<CoreConfig, 'categories' | 'plugins'> & SkipPluginsCliOptions;
@@ -1,4 +0,0 @@
1
- import type { Options } from 'yargs';
2
- import type { SkipPluginsCliOptions } from './skip-plugins.model';
3
- export declare const skipPluginsOption: Options;
4
- export declare function yargsSkipPluginsOptionsDefinition(): Record<keyof SkipPluginsCliOptions, Options>;
@@ -1,8 +0,0 @@
1
- import type { CategoryConfig, PluginConfig } from '@code-pushup/models';
2
- export declare function validatePluginFilterOption(filterOption: 'onlyPlugins' | 'skipPlugins', { plugins, categories, }: {
3
- plugins: PluginConfig[];
4
- categories: CategoryConfig[];
5
- }, { pluginsToFilter, verbose, }?: {
6
- pluginsToFilter?: string[];
7
- verbose?: boolean;
8
- }): void;