@code-pushup/core 0.34.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -578,10 +578,14 @@ function makeArraysComparisonSchema(diffSchema, resultSchema, description) {
578
578
  { description }
579
579
  );
580
580
  }
581
- var scorableMetaSchema = z14.object({ slug: slugSchema, title: titleSchema });
581
+ var scorableMetaSchema = z14.object({
582
+ slug: slugSchema,
583
+ title: titleSchema,
584
+ docsUrl: docsUrlSchema
585
+ });
582
586
  var scorableWithPluginMetaSchema = scorableMetaSchema.merge(
583
587
  z14.object({
584
- plugin: pluginMetaSchema.pick({ slug: true, title: true }).describe("Plugin which defines it")
588
+ plugin: pluginMetaSchema.pick({ slug: true, title: true, docsUrl: true }).describe("Plugin which defines it")
585
589
  })
586
590
  );
587
591
  var scorableDiffSchema = scorableMetaSchema.merge(
@@ -888,7 +892,7 @@ async function ensureDirectoryExists(baseDir) {
888
892
  await mkdir(baseDir, { recursive: true });
889
893
  return;
890
894
  } catch (error) {
891
- ui().logger.error(error.message);
895
+ ui().logger.info(error.message);
892
896
  if (error.code !== "EEXIST") {
893
897
  throw error;
894
898
  }
@@ -1289,11 +1293,9 @@ function toUnixPath(path) {
1289
1293
  async function getLatestCommit(git = simpleGit()) {
1290
1294
  const log2 = await git.log({
1291
1295
  maxCount: 1,
1296
+ // git log -1 --pretty=format:"%H %s %an %aI" - See: https://git-scm.com/docs/pretty-formats
1292
1297
  format: { hash: "%H", message: "%s", author: "%an", date: "%aI" }
1293
1298
  });
1294
- if (!log2.latest) {
1295
- return null;
1296
- }
1297
1299
  return commitSchema.parse(log2.latest);
1298
1300
  }
1299
1301
  function getGitRoot(git = simpleGit()) {
@@ -1304,6 +1306,58 @@ function formatGitPath(path, gitRoot) {
1304
1306
  const relativePath = relative(gitRoot, absolutePath);
1305
1307
  return toUnixPath(relativePath);
1306
1308
  }
1309
+ var GitStatusError = class _GitStatusError extends Error {
1310
+ static ignoredProps = /* @__PURE__ */ new Set(["current", "tracking"]);
1311
+ static getReducedStatus(status) {
1312
+ return Object.fromEntries(
1313
+ Object.entries(status).filter(([key]) => !this.ignoredProps.has(key)).filter(
1314
+ (entry) => {
1315
+ const value = entry[1];
1316
+ if (value == null) {
1317
+ return false;
1318
+ }
1319
+ if (Array.isArray(value) && value.length === 0) {
1320
+ return false;
1321
+ }
1322
+ if (typeof value === "number" && value === 0) {
1323
+ return false;
1324
+ }
1325
+ return !(typeof value === "boolean" && !value);
1326
+ }
1327
+ )
1328
+ );
1329
+ }
1330
+ constructor(status) {
1331
+ super(
1332
+ `Working directory needs to be clean before we you can proceed. Commit your local changes or stash them:
1333
+ ${JSON.stringify(
1334
+ _GitStatusError.getReducedStatus(status),
1335
+ null,
1336
+ 2
1337
+ )}`
1338
+ );
1339
+ }
1340
+ };
1341
+ async function guardAgainstLocalChanges(git = simpleGit()) {
1342
+ const status = await git.status(["-s"]);
1343
+ if (status.files.length > 0) {
1344
+ throw new GitStatusError(status);
1345
+ }
1346
+ }
1347
+ async function getCurrentBranchOrTag(git = simpleGit()) {
1348
+ return await git.branch().then((r) => r.current) || // If no current branch, try to get the tag
1349
+ // @TODO use simple git
1350
+ await git.raw(["describe", "--tags", "--exact-match"]).then((out) => out.trim());
1351
+ }
1352
+ async function safeCheckout(branchOrHash, forceCleanStatus = false, git = simpleGit()) {
1353
+ if (forceCleanStatus) {
1354
+ await git.raw(["reset", "--hard"]);
1355
+ await git.clean(["f", "d"]);
1356
+ ui().logger.info(`git status cleaned`);
1357
+ }
1358
+ await guardAgainstLocalChanges(git);
1359
+ await git.checkout(branchOrHash);
1360
+ }
1307
1361
 
1308
1362
  // packages/utils/src/lib/group-by-status.ts
1309
1363
  function groupByStatus(results) {
@@ -1617,19 +1671,19 @@ function formatDiffCategoriesSection(diff) {
1617
1671
  "\u{1F504} Score change"
1618
1672
  ],
1619
1673
  ...sortChanges(changed).map((category) => [
1620
- category.title,
1674
+ formatTitle(category),
1621
1675
  formatScoreWithColor(category.scores.after),
1622
1676
  formatScoreWithColor(category.scores.before, { skipBold: true }),
1623
1677
  formatScoreChange(category.scores.diff)
1624
1678
  ]),
1625
1679
  ...added.map((category) => [
1626
- category.title,
1680
+ formatTitle(category),
1627
1681
  formatScoreWithColor(category.score),
1628
1682
  style("n/a (\\*)", ["i"]),
1629
1683
  style("n/a (\\*)", ["i"])
1630
1684
  ]),
1631
1685
  ...unchanged.map((category) => [
1632
- category.title,
1686
+ formatTitle(category),
1633
1687
  formatScoreWithColor(category.score),
1634
1688
  formatScoreWithColor(category.score, { skipBold: true }),
1635
1689
  "\u2013"
@@ -1645,7 +1699,7 @@ function formatDiffGroupsSection(diff) {
1645
1699
  return "";
1646
1700
  }
1647
1701
  return paragraphs(
1648
- h2("\u{1F397}\uFE0F Groups"),
1702
+ h2("\u{1F5C3}\uFE0F Groups"),
1649
1703
  formatGroupsOrAuditsDetails("group", diff.groups, {
1650
1704
  headings: [
1651
1705
  "\u{1F50C} Plugin",
@@ -1655,8 +1709,8 @@ function formatDiffGroupsSection(diff) {
1655
1709
  "\u{1F504} Score change"
1656
1710
  ],
1657
1711
  rows: sortChanges(diff.groups.changed).map((group) => [
1658
- group.plugin.title,
1659
- group.title,
1712
+ formatTitle(group.plugin),
1713
+ formatTitle(group),
1660
1714
  formatScoreWithColor(group.scores.after),
1661
1715
  formatScoreWithColor(group.scores.before, { skipBold: true }),
1662
1716
  formatScoreChange(group.scores.diff)
@@ -1677,8 +1731,8 @@ function formatDiffAuditsSection(diff) {
1677
1731
  "\u{1F504} Value change"
1678
1732
  ],
1679
1733
  rows: sortChanges(diff.audits.changed).map((audit) => [
1680
- audit.plugin.title,
1681
- audit.title,
1734
+ formatTitle(audit.plugin),
1735
+ formatTitle(audit),
1682
1736
  `${getSquaredScoreMarker(audit.scores.after)} ${style(
1683
1737
  audit.displayValues.after || audit.values.after.toString()
1684
1738
  )}`,
@@ -1709,7 +1763,7 @@ function formatGroupsOrAuditsDetails(token, { changed, unchanged }, table) {
1709
1763
  }
1710
1764
  function formatScoreChange(diff) {
1711
1765
  const marker = getDiffMarker(diff);
1712
- const text = formatDiffNumber(Math.round(diff * 100));
1766
+ const text = formatDiffNumber(Math.round(diff * 1e3) / 10);
1713
1767
  return colorByScoreDiff(`${marker} ${text}`, diff);
1714
1768
  }
1715
1769
  function formatValueChange({
@@ -1746,6 +1800,15 @@ function summarizeDiffOutcomes(outcomes, token) {
1746
1800
  }
1747
1801
  }).join(", ");
1748
1802
  }
1803
+ function formatTitle({
1804
+ title,
1805
+ docsUrl
1806
+ }) {
1807
+ if (docsUrl) {
1808
+ return link(docsUrl, title);
1809
+ }
1810
+ return title;
1811
+ }
1749
1812
  function sortChanges(changes) {
1750
1813
  return [...changes].sort(
1751
1814
  (a, b) => Math.abs(b.scores.diff) - Math.abs(a.scores.diff) || Math.abs(b.values?.diff ?? 0) - Math.abs(a.values?.diff ?? 0)
@@ -2034,7 +2097,7 @@ var verboseUtils = (verbose = false) => ({
2034
2097
 
2035
2098
  // packages/core/package.json
2036
2099
  var name = "@code-pushup/core";
2037
- var version = "0.34.0";
2100
+ var version = "0.39.0";
2038
2101
 
2039
2102
  // packages/core/src/lib/implementation/execute-plugin.ts
2040
2103
  import chalk5 from "chalk";
@@ -2317,8 +2380,7 @@ function compareAudits2(reports) {
2317
2380
  }
2318
2381
  function categoryToResult(category) {
2319
2382
  return {
2320
- slug: category.slug,
2321
- title: category.title,
2383
+ ...selectMeta(category),
2322
2384
  score: category.score
2323
2385
  };
2324
2386
  }
@@ -2327,8 +2389,7 @@ function categoryPairToDiff({
2327
2389
  after
2328
2390
  }) {
2329
2391
  return {
2330
- slug: after.slug,
2331
- title: after.title,
2392
+ ...selectMeta(after),
2332
2393
  scores: {
2333
2394
  before: before.score,
2334
2395
  after: after.score,
@@ -2338,12 +2399,8 @@ function categoryPairToDiff({
2338
2399
  }
2339
2400
  function pluginGroupToResult({ group, plugin }) {
2340
2401
  return {
2341
- slug: group.slug,
2342
- title: group.title,
2343
- plugin: {
2344
- slug: plugin.slug,
2345
- title: plugin.title
2346
- },
2402
+ ...selectMeta(group),
2403
+ plugin: selectMeta(plugin),
2347
2404
  score: group.score
2348
2405
  };
2349
2406
  }
@@ -2352,12 +2409,8 @@ function pluginGroupPairToDiff({
2352
2409
  after
2353
2410
  }) {
2354
2411
  return {
2355
- slug: after.group.slug,
2356
- title: after.group.title,
2357
- plugin: {
2358
- slug: after.plugin.slug,
2359
- title: after.plugin.title
2360
- },
2412
+ ...selectMeta(after.group),
2413
+ plugin: selectMeta(after.plugin),
2361
2414
  scores: {
2362
2415
  before: before.group.score,
2363
2416
  after: after.group.score,
@@ -2367,12 +2420,8 @@ function pluginGroupPairToDiff({
2367
2420
  }
2368
2421
  function pluginAuditToResult({ audit, plugin }) {
2369
2422
  return {
2370
- slug: audit.slug,
2371
- title: audit.title,
2372
- plugin: {
2373
- slug: plugin.slug,
2374
- title: plugin.title
2375
- },
2423
+ ...selectMeta(audit),
2424
+ plugin: selectMeta(plugin),
2376
2425
  score: audit.score,
2377
2426
  value: audit.value,
2378
2427
  displayValue: audit.displayValue
@@ -2383,12 +2432,8 @@ function pluginAuditPairToDiff({
2383
2432
  after
2384
2433
  }) {
2385
2434
  return {
2386
- slug: after.audit.slug,
2387
- title: after.audit.title,
2388
- plugin: {
2389
- slug: after.plugin.slug,
2390
- title: after.plugin.title
2391
- },
2435
+ ...selectMeta(after.audit),
2436
+ plugin: selectMeta(after.plugin),
2392
2437
  scores: {
2393
2438
  before: before.audit.score,
2394
2439
  after: after.audit.score,
@@ -2405,6 +2450,15 @@ function pluginAuditPairToDiff({
2405
2450
  }
2406
2451
  };
2407
2452
  }
2453
+ function selectMeta(meta) {
2454
+ return {
2455
+ slug: meta.slug,
2456
+ title: meta.title,
2457
+ ...meta.docsUrl && {
2458
+ docsUrl: meta.docsUrl
2459
+ }
2460
+ };
2461
+ }
2408
2462
 
2409
2463
  // packages/core/src/lib/compare.ts
2410
2464
  async function compareReportFiles(inputPaths, persistConfig) {
@@ -2460,45 +2514,8 @@ function reportsDiffToFileContent(reportsDiff, format) {
2460
2514
  }
2461
2515
  }
2462
2516
 
2463
- // packages/core/src/lib/implementation/read-rc-file.ts
2464
- import { join as join6 } from "node:path";
2465
- var ConfigPathError = class extends Error {
2466
- constructor(configPath) {
2467
- super(`Provided path '${configPath}' is not valid.`);
2468
- }
2469
- };
2470
- async function readRcByPath(filepath, tsconfig) {
2471
- if (filepath.length === 0) {
2472
- throw new Error("The path to the configuration file is empty.");
2473
- }
2474
- if (!await fileExists(filepath)) {
2475
- throw new ConfigPathError(filepath);
2476
- }
2477
- const cfg = await importEsmModule({ filepath, tsconfig });
2478
- return coreConfigSchema.parse(cfg);
2479
- }
2480
- async function autoloadRc(tsconfig) {
2481
- let ext = "";
2482
- for (const extension of SUPPORTED_CONFIG_FILE_FORMATS) {
2483
- const path = `${CONFIG_FILE_NAME}.${extension}`;
2484
- const exists2 = await fileExists(path);
2485
- if (exists2) {
2486
- ext = extension;
2487
- break;
2488
- }
2489
- }
2490
- if (!ext) {
2491
- throw new Error(
2492
- `No file ${CONFIG_FILE_NAME}.(${SUPPORTED_CONFIG_FILE_FORMATS.join(
2493
- "|"
2494
- )}) present in ${process.cwd()}`
2495
- );
2496
- }
2497
- return readRcByPath(
2498
- join6(process.cwd(), `${CONFIG_FILE_NAME}.${ext}`),
2499
- tsconfig
2500
- );
2501
- }
2517
+ // packages/core/src/lib/history.ts
2518
+ import { simpleGit as simpleGit2 } from "simple-git";
2502
2519
 
2503
2520
  // packages/core/src/lib/upload.ts
2504
2521
  import {
@@ -2579,6 +2596,7 @@ function categoryToGQL(category) {
2579
2596
  slug: category.slug,
2580
2597
  title: category.title,
2581
2598
  description: category.description,
2599
+ isBinary: category.isBinary,
2582
2600
  refs: category.refs.map((ref) => ({
2583
2601
  plugin: ref.plugin,
2584
2602
  type: categoryRefTypeToGQL(ref.type),
@@ -2627,6 +2645,98 @@ async function upload(options, uploadFn = uploadToPortal) {
2627
2645
  };
2628
2646
  return uploadFn({ apiKey, server, data, timeout });
2629
2647
  }
2648
+
2649
+ // packages/core/src/lib/history.ts
2650
+ async function history(config, commits) {
2651
+ const initialBranch = await getCurrentBranchOrTag();
2652
+ const { skipUploads = false, forceCleanStatus, persist } = config;
2653
+ const reports = [];
2654
+ for (const commit of commits) {
2655
+ ui().logger.info(`Collect ${commit}`);
2656
+ await safeCheckout(commit, forceCleanStatus);
2657
+ const currentConfig = {
2658
+ ...config,
2659
+ persist: {
2660
+ ...persist,
2661
+ format: ["json"],
2662
+ filename: `${commit}-report`
2663
+ }
2664
+ };
2665
+ await collectAndPersistReports(currentConfig);
2666
+ if (skipUploads) {
2667
+ ui().logger.info("Upload is skipped because skipUploads is set to true.");
2668
+ } else {
2669
+ if (currentConfig.upload) {
2670
+ await upload(currentConfig);
2671
+ } else {
2672
+ ui().logger.info(
2673
+ "Upload is skipped because upload config is undefined."
2674
+ );
2675
+ }
2676
+ }
2677
+ reports.push(currentConfig.persist.filename);
2678
+ }
2679
+ await safeCheckout(initialBranch, forceCleanStatus);
2680
+ return reports;
2681
+ }
2682
+ async function getHashes(options, git = simpleGit2()) {
2683
+ const { from, to } = options;
2684
+ if (to && !from) {
2685
+ throw new Error(
2686
+ `git log command needs the "from" option defined to accept the "to" option.
2687
+ `
2688
+ );
2689
+ }
2690
+ const logs = await git.log({
2691
+ ...options,
2692
+ from,
2693
+ to
2694
+ });
2695
+ return prepareHashes(logs);
2696
+ }
2697
+ function prepareHashes(logs) {
2698
+ return logs.all.map(({ hash }) => hash).reverse();
2699
+ }
2700
+
2701
+ // packages/core/src/lib/implementation/read-rc-file.ts
2702
+ import { join as join6 } from "node:path";
2703
+ var ConfigPathError = class extends Error {
2704
+ constructor(configPath) {
2705
+ super(`Provided path '${configPath}' is not valid.`);
2706
+ }
2707
+ };
2708
+ async function readRcByPath(filepath, tsconfig) {
2709
+ if (filepath.length === 0) {
2710
+ throw new Error("The path to the configuration file is empty.");
2711
+ }
2712
+ if (!await fileExists(filepath)) {
2713
+ throw new ConfigPathError(filepath);
2714
+ }
2715
+ const cfg = await importEsmModule({ filepath, tsconfig });
2716
+ return coreConfigSchema.parse(cfg);
2717
+ }
2718
+ async function autoloadRc(tsconfig) {
2719
+ let ext = "";
2720
+ for (const extension of SUPPORTED_CONFIG_FILE_FORMATS) {
2721
+ const path = `${CONFIG_FILE_NAME}.${extension}`;
2722
+ const exists2 = await fileExists(path);
2723
+ if (exists2) {
2724
+ ext = extension;
2725
+ break;
2726
+ }
2727
+ }
2728
+ if (!ext) {
2729
+ throw new Error(
2730
+ `No file ${CONFIG_FILE_NAME}.(${SUPPORTED_CONFIG_FILE_FORMATS.join(
2731
+ "|"
2732
+ )}) present in ${process.cwd()}`
2733
+ );
2734
+ }
2735
+ return readRcByPath(
2736
+ join6(process.cwd(), `${CONFIG_FILE_NAME}.${ext}`),
2737
+ tsconfig
2738
+ );
2739
+ }
2630
2740
  export {
2631
2741
  ConfigPathError,
2632
2742
  PersistDirError,
@@ -2639,6 +2749,8 @@ export {
2639
2749
  compareReports,
2640
2750
  executePlugin,
2641
2751
  executePlugins,
2752
+ getHashes,
2753
+ history,
2642
2754
  persistReport,
2643
2755
  readRcByPath,
2644
2756
  upload
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@code-pushup/core",
3
- "version": "0.34.0",
3
+ "version": "0.39.0",
4
4
  "license": "MIT",
5
5
  "dependencies": {
6
- "@code-pushup/models": "0.34.0",
7
- "@code-pushup/utils": "0.34.0",
6
+ "@code-pushup/models": "0.39.0",
7
+ "@code-pushup/utils": "0.39.0",
8
8
  "@code-pushup/portal-client": "^0.6.1",
9
9
  "chalk": "^5.3.0",
10
10
  "simple-git": "^3.20.0"
package/src/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { CollectOptions, collect } from './lib/implementation/collect';
4
4
  export { ReportsToCompare } from './lib/implementation/compare-scorables';
5
5
  export { PluginOutputMissingAuditError, executePlugin, executePlugins, } from './lib/implementation/execute-plugin';
6
6
  export { PersistDirError, PersistError, persistReport, } from './lib/implementation/persist';
7
+ export { history, HistoryOptions, HistoryOnlyOptions, getHashes, } from './lib/history';
7
8
  export { ConfigPathError, autoloadRc, readRcByPath, } from './lib/implementation/read-rc-file';
8
9
  export { GlobalOptions } from './lib/types';
9
10
  export { UploadOptions, upload } from './lib/upload';
@@ -6,10 +6,49 @@ export type HistoryOnlyOptions = {
6
6
  skipUploads?: boolean;
7
7
  forceCleanStatus?: boolean;
8
8
  };
9
- export type HistoryOptions = Required<Pick<CoreConfig, 'plugins' | 'categories'>> & {
9
+ export type HistoryOptions = Required<Pick<CoreConfig, 'plugins'> & Required<Pick<CoreConfig, 'categories'>>> & {
10
10
  persist: Required<PersistConfig>;
11
11
  upload?: Required<UploadConfig>;
12
12
  } & HistoryOnlyOptions & Partial<GlobalOptions>;
13
13
  export declare function history(config: HistoryOptions, commits: string[]): Promise<string[]>;
14
+ /**
15
+ * `getHashes` returns a list of commit hashes. Internally it uses `git.log()` to determine the commits within a range.
16
+ * The amount can be limited to a maximum number of commits specified by `maxCount`.
17
+ * With `from` and `to`, you can specify a range of commits.
18
+ *
19
+ * **NOTE:**
20
+ * In Git, specifying a range with two dots (`from..to`) selects commits that are reachable from `to` but not from `from`.
21
+ * Essentially, it shows the commits that are in `to` but not in `from`, excluding the commits unique to `from`.
22
+ *
23
+ * Example:
24
+ *
25
+ * Let's consider the following commit history:
26
+ *
27
+ * A---B---C---D---E (main)
28
+ *
29
+ * Using `git log B..D`, you would get the commits C and D:
30
+ *
31
+ * C---D
32
+ *
33
+ * This is because these commits are reachable from D but not from B.
34
+ *
35
+ * ASCII Representation:
36
+ *
37
+ * Main Branch: A---B---C---D---E
38
+ * \ \
39
+ * \ +--- Commits included in `git log B..D`
40
+ * \
41
+ * +--- Excluded by the `from` parameter
42
+ *
43
+ * With `simple-git`, when you specify a `from` and `to` range like this:
44
+ *
45
+ * git.log({ from: 'B', to: 'D' });
46
+ *
47
+ * It interprets it similarly, selecting commits between B and D, inclusive of D but exclusive of B.
48
+ * For `git.log({ from: 'B', to: 'D' })` or `git log B..D`, commits C and D are selected.
49
+ *
50
+ * @param options Object containing `from`, `to`, and optionally `maxCount` to specify the commit range and limit.
51
+ * @param git The `simple-git` instance used to execute Git commands.
52
+ */
14
53
  export declare function getHashes(options: LogOptions, git?: import("simple-git").SimpleGit): Promise<string[]>;
15
54
  export declare function prepareHashes(logs: LogResult): string[];