@adobe/sizewatcher 1.3.0 → 1.4.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.0
4
+
5
+ Major changes:
6
+
7
+ - [#106](https://github.com/adobe/sizewatcher/issues/106) Require at least node 18. Was 12 before. Node 18 is last version we can make work with being a CommonJS module.
8
+
9
+ Improvements:
10
+
11
+ - Upgrade dependencies (many major bumps)
12
+ - Added renovate
13
+ - [#98](https://github.com/adobe/sizewatcher/pull/98) CI: drop `.travis.yml`, was not running on Travis CI for years anyway
14
+ - [#101](https://github.com/adobe/sizewatcher/pull/101) fixed package.json using `npm pkg fix`
15
+ - CI: various CI improvements
16
+
17
+ Fixes:
18
+
19
+ - [#108](https://github.com/adobe/sizewatcher/pull/108) [#113](https://github.com/adobe/sizewatcher/pull/113) Git comparator reporting incorrect deltas. now count files in checkout instead of .git folder for consistent sizes
20
+ - [#112](https://github.com/adobe/sizewatcher/pull/112) Node-modules: replace broken cost-of-modules with howfat for better largest modules reporting
21
+ - [#97](https://github.com/adobe/sizewatcher/pull/97) Fix node version 20+ test failures
22
+
23
+ ## 1.3.0
24
+
25
+ Improvements:
26
+
27
+ - [#87](https://github.com/adobe/sizewatcher/issues/87) Drop node 10 support
28
+ - Various dependency updates
29
+
30
+ ## 1.2.1
31
+
32
+ Important bug fixes.
33
+
34
+ Fixes:
35
+
36
+ - [#63](https://github.com/adobe/sizewatcher/issues/63) fix and improve git checkout logic
37
+ - [#62](https://github.com/adobe/sizewatcher/issues/62) fix failure on fork PRs
38
+ - [#53](https://github.com/adobe/sizewatcher/issues/53) sizewatcher fails if there is no "package.json" file in the main branch, or if a package.json has no dependencies and thus node_modules is missing
39
+ - [#74](https://github.com/adobe/sizewatcher/issues/74) improve delta calculation (showed weird negative values in some cases)
40
+
3
41
  ## 1.2.0
4
42
 
5
43
  Improvements:
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  [![Version](https://img.shields.io/npm/v/@adobe/sizewatcher.svg)](https://npmjs.org/package/@adobe/sizewatcher)
2
2
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0)
3
3
  [![Coverage Status](https://coveralls.io/repos/github/adobe/sizewatcher/badge.svg)](https://coveralls.io/github/adobe/sizewatcher)
4
- [![Travis](https://travis-ci.com/adobe/sizewatcher.svg?branch=main)](https://travis-ci.com/adobe/sizewatcher)
5
4
  [![CircleCI](https://circleci.com/gh/adobe/sizewatcher.svg?style=shield)](https://circleci.com/gh/adobe/sizewatcher)
6
5
  [![Github Actions Node.js CI Status](https://github.com/adobe/sizewatcher/workflows/Node.js%20CI/badge.svg)](https://github.com/adobe/sizewatcher/actions?query=workflow%3A%22Node.js+CI%22)
7
6
  [![CodeQL Status](https://github.com/adobe/sizewatcher/workflows/CodeQL/badge.svg)](https://github.com/adobe/sizewatcher/actions?query=workflow%3ACodeQL)
@@ -14,7 +13,7 @@
14
13
  - [Requirements](#requirements)
15
14
  - [Installation](#installation)
16
15
  - [Usage](#usage)
17
- - [**CI Setup**](#ci-setup)
16
+ - [CI Setup](#ci-setup)
18
17
  - [Configuration](#configuration)
19
18
  - [Comparators reference](#comparators-reference)
20
19
  - [Contribute](#contribute)
@@ -33,7 +32,6 @@ While any custom file or folder path can be measured via configuration, various
33
32
 
34
33
  `sizewatcher` runs as part of your CI and reports results as comment on the pull request or as github commit status (optional), allowing to block PRs if a certain threshold was exceeded.
35
34
 
36
- :warning: **The git size comparison feature (the `git` comparator) is currently not calculating correct values (as of 1.2.1). See [issue #52](https://github.com/adobe/sizewatcher/issues/52) for more details.**
37
35
 
38
36
  This is an example of a `sizewatcher` Github PR comment with a failure (ignore the small numbers):
39
37
 
@@ -127,7 +125,7 @@ By default `sizewatcher` will
127
125
 
128
126
  ## Requirements
129
127
 
130
- - [Nodejs](https://nodejs.org) version 12+ (since 1.3.0)
128
+ - [Nodejs](https://nodejs.org) version 18+ (since 1.4.0)
131
129
  - recommended to use latest stable version "LTS"
132
130
  - Github or Github Enterprise
133
131
  - if you want to run it on pull requests
@@ -278,11 +276,11 @@ jobs:
278
276
  runs-on: ubuntu-latest
279
277
 
280
278
  steps:
281
- - uses: actions/checkout@v2
279
+ - uses: actions/checkout@v4
282
280
  - name: Install Node.js
283
- uses: actions/setup-node@v1
281
+ uses: actions/setup-node@v4
284
282
  with:
285
- node-version: '14'
283
+ node-version: 'lts/*'
286
284
  # ---------- this runs sizewatcher ------------
287
285
  - run: npx @adobe/sizewatcher
288
286
  env:
@@ -436,23 +434,33 @@ comparators:
436
434
 
437
435
  ### git
438
436
 
439
- Compares the size of the git repository by measuring the `.git` folder. Useful to detect if large files are added to the repo.
437
+ Compares the size of the git repository by measuring the total size of the checked out folder (without the `.git` folder or any build artifacts since it runs first on a clean checkout). Useful to detect if large files are added to the repo.
440
438
 
441
439
  Name: `git`
442
440
 
443
441
  Trigger: Runs if a `.git` directory is found.
444
442
 
445
- Details: Shows the largest files (git objects) added in the PR:
443
+ Details: Shows the largest files (git objects) in the entire repository and added in the PR:
446
444
 
447
445
  ---
448
- Largest files in new changes:
446
+ Largest files:
449
447
  ```
450
- 710a7c687b06 2.8KiB lib/render.js
451
- 0a8f1a2ddb4f 2.4KiB test/config.test.js
452
- 6846cf298cd4 2.3KiB test/config.test.js
453
- a643d322cc26 1.5KiB lib/config.js
454
- 933e4432aae5 73B .sizewatcher.yml
455
- 6db7d5c27a66 69B .sizewatcher.yml
448
+ Largest files in repository checkout:
449
+
450
+ 356KiB package-lock.json
451
+ 26KiB README.md
452
+ 11KiB LICENSE
453
+ 5.8KiB lib/compare.js
454
+ 5.8KiB lib/checkout.js
455
+ 5.0KiB test/mocha-capture-console.js
456
+ 4.7KiB test/cli.test.js
457
+ 4.5KiB lib/report.js
458
+ 4.3KiB test/config.test.js
459
+ 3.8KiB lib/render.js
460
+
461
+ Largest files among new changes:
462
+
463
+ 3.0KiB CHANGELOG.md
456
464
  ```
457
465
  ---
458
466
 
@@ -464,34 +472,37 @@ Name: `node_modules`
464
472
 
465
473
  Trigger: Runs if a `package.json` is found.
466
474
 
467
- Details: Prints the largest modules using [cost-of-modules](https://www.npmjs.com/package/cost-of-modules):
475
+ Details: Prints the largest modules using [howfat](https://www.npmjs.com/package/howfat):
468
476
 
469
477
  ---
470
- Largest node modules:
471
- ```
472
- ┌───────────────┬─────────────┬────────┐
473
- │ name │ children │ size │
474
- ├───────────────┼─────────────┼────────┤
475
- │ @octokit/rest │ 33 │ 11.25M │
476
- ├───────────────┼─────────────┼────────┤
477
- │ js-yaml │ 3 │ 0.72M │
478
- ├───────────────┼─────────────┼────────┤
479
- │ simple-git │ 2 │ 0.24M │
480
- ├───────────────┼─────────────┼────────┤
481
- │ tmp │ 13 │ 0.22M │
482
- ├───────────────┼─────────────┼────────┤
483
- │ debug │ 1 │ 0.08M │
484
- ├───────────────┼─────────────┼────────┤
485
- │ deepmerge │ 0 │ 0.03M │
486
- ├───────────────┼─────────────┼────────┤
487
- │ require-dir │ 0 │ 0.02M │
488
- ├───────────────┼─────────────┼────────┤
489
- │ du │ 1 │ 0.01M │
490
- ├───────────────┼─────────────┼────────┤
491
- │ pretty-bytes │ 0 │ 0.01M │
492
- ├───────────────┼─────────────┼────────┤
493
- │ 9 modules │ 34 children │ 4.57M │
494
- └───────────────┴─────────────┴────────┘
478
+ Largest production node modules:
479
+ ```
480
+ @adobe/sizewatcher@1.3.0 (67 deps, 21.64mb, 2583 files, ©undefined)
481
+ ╭───────────────────────┬──────────────┬──────────┬───────┬───────────┬────────────┬───────────╮
482
+ │ Name │ Dependencies │ Size │ Files │ Native │ License │ Deprec │
483
+ ├───────────────────────┼──────────────┼──────────┼───────┼───────────┼────────────┼───────────┤
484
+ │ @octokit/rest@19.0.3 │ 25 │ 11mb │ 380 │ │ MIT │ │
485
+ ├───────────────────────┼──────────────┼──────────┼───────┼───────────┼────────────┼───────────┤
486
+ │ glob@10.3.5 │ 27 │ 3.08mb │ 367 │ │ ISC │ │
487
+ ├───────────────────────┼──────────────┼──────────┼───────┼───────────┼────────────┼───────────┤
488
+ │ simple-git@3.22.0 │ 4 │ 934.57kb │ 142 │ │ MIT │ │
489
+ ├───────────────────────┼──────────────┼──────────┼───────┼───────────┼────────────┼───────────┤
490
+ │ js-yaml@4.1.0 │ 1 │ 562.78kb │ 40 │ │ MIT │ │
491
+ ├───────────────────────┼──────────────┼──────────┼───────┼───────────┼────────────┼───────────┤
492
+ │ xbytes@1.9.1 │ │ 74.45kb │ 11 │ │ Apache-2.0 │ │
493
+ ├───────────────────────┼──────────────┼──────────┼───────┼───────────┼────────────┼───────────┤
494
+ │ tmp@0.2.3 │ │ 53.08kb │ 5 │ │ MIT │ │
495
+ ├───────────────────────┼──────────────┼──────────┼───────┼───────────┼────────────┼───────────┤
496
+ │ debug@4.4.0 │ 1 │ 48.36kb │ 11 │ │ MIT │ │
497
+ ├───────────────────────┼──────────────┼──────────┼───────┼───────────┼────────────┼───────────┤
498
+ │ deepmerge@4.3.1 │ │ 30.43kb │ 11 │ │ MIT │ │
499
+ ├───────────────────────┼──────────────┼──────────┼───────┼───────────┼────────────┼───────────┤
500
+ │ require-dir@1.2.0 │ │ 16.87kb │ 40 │ │ MIT │ │
501
+ ├───────────────────────┼──────────────┼──────────┼───────┼───────────┼────────────┼───────────┤
502
+ │ get-folder-size@5.0.0 │ │ 11.65kb │ 5 │ │ MIT │ │
503
+ ├───────────────────────┼──────────────┼──────────┼───────┼───────────┼────────────┼───────────┤
504
+ │ pretty-bytes@5.6.0 │ │ 11.27kb │ 5 │ │ MIT │ │
505
+ ╰───────────────────────┴──────────────┴──────────┴───────┴───────────┴────────────┴───────────╯
495
506
  ```
496
507
 
497
508
  Configuration: supports `dir`
@@ -500,13 +511,13 @@ Configuration: supports `dir`
500
511
 
501
512
  ### npm_package
502
513
 
503
- Compares the size of an npm package tarball by running `npm publish --dry-run`.
514
+ Compares the size of an npm package tarball by running `npm pack --dry-run`.
504
515
 
505
516
  Name: `npm_package`
506
517
 
507
518
  Trigger: Runs if a `package.json` is found.
508
519
 
509
- Details: Prints the package contents and metadata using the output of `npm publish --dry-run`.
520
+ Details: Prints the package contents and metadata using the output of `npm pack --dry-run`.
510
521
 
511
522
  ---
512
523
  Package contents:
@@ -644,6 +655,23 @@ If you want to add a new automatic comparator for language X or dependency manag
644
655
  6. test and validate
645
656
  7. create PR
646
657
 
658
+ ### How to release
659
+
660
+ How to create a new release of version `1.4` (as example):
661
+
662
+ 1. Ensure all changes are merged to `main`
663
+ 2. On `main` checkout, bump version in `package.json` to `1.4`
664
+ 3. Run `npm publish --dry-run` to ensure all looks good
665
+ 4. Commit straight to `main` like [here](https://github.com/adobe/sizewatcher/commit/4da848b6cc52a38f98203760f8f74e44a0ec77cd)
666
+ 5. Create a new draft [release in Github](https://github.com/adobe/sizewatcher/releases)
667
+ 1. Nmae the release `1.4`
668
+ 2. Add release notes with new features, improvements, fixes
669
+ 3. Ensure github creates a tag `v1.4`
670
+ 6. Save draft and check things look good
671
+ 7. Publish the release
672
+ 8. The [Publish Package to NPM](https://github.com/adobe/sizewatcher/actions/workflows/npm-publish.yml) Github action workflow should automatically release the new version to npmjs.com
673
+
674
+
647
675
  ## Licensing
648
676
 
649
677
  This project is licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information.
package/lib/checkout.js CHANGED
@@ -18,7 +18,9 @@ const debug = require("debug")("sizewatcher");
18
18
  const tmp = require("tmp");
19
19
  tmp.setGracefulCleanup();
20
20
  const path = require("path");
21
+ // TODO: get rid of simple-git, replace with execSync('git ...') (sanitize user input!)
21
22
  const Git = require("simple-git");
23
+ const { execSync } = require("child_process");
22
24
 
23
25
  async function getGitRoot(dir) {
24
26
  // --git-dir will return relative directories
@@ -36,10 +38,8 @@ async function currentBranch(dir) {
36
38
  return branch;
37
39
  }
38
40
 
39
- async function currentSha(dir) {
40
- const git = Git(dir);
41
- const sha = await git.revparse(["HEAD"]);
42
- return sha;
41
+ async function commitSha(dir, branch) {
42
+ return Git(dir).revparse([branch]);
43
43
  }
44
44
 
45
45
  async function getDefaultBranch(dir) {
@@ -50,17 +50,18 @@ async function getDefaultBranch(dir) {
50
50
  return m[1];
51
51
  }
52
52
  } catch (e) {
53
- debug(`ignoring error in getDefaultBranch(): ${e.message}`);
53
+ debug(`- ignoring error in getDefaultBranch(): ${e.message}`);
54
54
  }
55
55
  }
56
56
 
57
- async function hasBranch(dir, branch) {
57
+ // works with branch names and commit hashes
58
+ async function hasRef(dir, branch) {
58
59
  try {
59
- await Git(dir).revparse(["--verify", branch]);
60
- return branch;
60
+ await Git(dir).catFile(["commit", branch]);
61
+ return true;
61
62
  } catch (e) {
62
- debug(`ignoring error in hasBranch(): ${e.message}`);
63
- return undefined;
63
+ debug(`- git returned with non-zero exit code in hasRef(), treating as false: ${e.message}`);
64
+ return false;
64
65
  }
65
66
  }
66
67
 
@@ -73,9 +74,9 @@ async function getBeforeBranch(dir, branch) {
73
74
  return branch
74
75
  || await getPullRequestBaseBranch()
75
76
  || await getDefaultBranch(dir)
76
- || await hasBranch(dir, "main")
77
- || await hasBranch(dir, "trunk")
78
- || await hasBranch(dir, "master"); // eslint-disable-line no-return-await
77
+ || (await hasRef(dir, "main") && "main")
78
+ || (await hasRef(dir, "trunk") && "trunk")
79
+ || (await hasRef(dir, "master") && "master"); // eslint-disable-line no-return-await
79
80
  }
80
81
 
81
82
  async function getAfterBranch(dir, branch) {
@@ -90,46 +91,47 @@ async function getAfterBranch(dir, branch) {
90
91
  async function cloneRemoteAndCheckout(from, to, branch) {
91
92
  // 1. get remote url from existing checkout from CI (in `from` dir)
92
93
  const remoteUrl = (await Git(from).remote(["get-url", "origin"])).trim();
93
- debug(`cloning ${remoteUrl} into ${to}`);
94
+ debug(`- git clone ${remoteUrl} into ${to}`);
94
95
 
95
96
  // 2. git clone remote url into new directory `to`
96
97
  await Git().clone(remoteUrl, to);
97
98
 
98
99
  // 3. checkout desired branch
99
- debug(`checking out '${branch}'...`);
100
+ debug(`- git checkout '${branch}'...`);
100
101
  await Git(to).checkout(branch);
101
102
  }
102
103
 
103
104
  async function cloneLocalAndCheckout(from, to, branch) {
104
105
  const gitRoot = await getGitRoot(from);
105
106
 
106
- debug(`local cloning ${gitRoot} into ${to}`);
107
+ // local clone will preserve the HEAD (aka same branch)
108
+ debug(`- local cloning ${gitRoot} into ${to}`);
107
109
  await Git().clone(gitRoot, to, { '--local' : true });
108
110
 
109
111
  if (branch) {
110
- debug(`checking out '${branch}'...`);
112
+ debug(`- checking out '${branch}'...`);
111
113
  await Git(to).checkout(branch);
112
114
  }
113
115
  }
114
116
 
115
117
  async function gitCheckoutBeforeAndAfter(gitDir, beforeBranch, afterBranch) {
116
- debug(`retrieving before branch: ${beforeBranch}`);
117
- beforeBranch = await getBeforeBranch(gitDir, beforeBranch);
118
- debug(`retrieving after branch: ${afterBranch}`);
119
- afterBranch = await getAfterBranch(gitDir, afterBranch);
120
- if (beforeBranch === afterBranch) {
118
+ const resolvedBeforeBranch = await getBeforeBranch(gitDir, beforeBranch);
119
+ debug(`resolved before branch '${beforeBranch || ""}' => '${resolvedBeforeBranch}'`);
120
+
121
+ const resolvedAfterBranch = await getAfterBranch(gitDir, afterBranch);
122
+ debug(`resolved after branch '${afterBranch || ""}' => '${resolvedAfterBranch}'`);
123
+
124
+ if (resolvedBeforeBranch === resolvedAfterBranch) {
121
125
  return {
122
126
  before: {
123
- branch: beforeBranch
127
+ branch: resolvedBeforeBranch
124
128
  },
125
129
  after: {
126
- branch: afterBranch,
130
+ branch: resolvedAfterBranch,
127
131
  }
128
132
  };
129
133
  }
130
134
 
131
- debug(`cloning branches ${beforeBranch} (before) and ${afterBranch} (after)...`);
132
-
133
135
  const tempDir = tmp.dirSync({unsafeCleanup: true}).name;
134
136
  debug(`temporary directory: ${tempDir}`);
135
137
 
@@ -138,40 +140,53 @@ async function gitCheckoutBeforeAndAfter(gitDir, beforeBranch, afterBranch) {
138
140
  const beforeDir = path.join(tempDir, "before");
139
141
 
140
142
  // we can use the faster local clone only if the "branch" is present locally
141
- if (await hasBranch(gitDir, beforeBranch)) {
142
- debug(`[before] using local cloning because existing checkout already has before branch '${beforeBranch}'`);
143
- await cloneLocalAndCheckout(gitDir, beforeDir, beforeBranch);
143
+ if (await hasRef(gitDir, resolvedBeforeBranch)) {
144
+ debug(`[before] using local cloning (because existing checkout already has '${resolvedBeforeBranch}')`);
145
+ await cloneLocalAndCheckout(gitDir, beforeDir, resolvedBeforeBranch);
144
146
  } else {
145
- debug(`[before] using remote cloning, as existing checkout does not have before branch '${beforeBranch}'`);
147
+ debug(`[before] using remote cloning (because existing checkout does not have '${resolvedBeforeBranch}')`);
146
148
  // ...otherwise need to do a full remote clone
147
- await cloneRemoteAndCheckout(gitDir, beforeDir, beforeBranch);
149
+ await cloneRemoteAndCheckout(gitDir, beforeDir, resolvedBeforeBranch);
148
150
  }
149
151
 
152
+ debug(`[before] input branch : ${beforeBranch || ""}`);
153
+ debug(`[before] resolved branch: ${resolvedBeforeBranch}`);
154
+ const beforeSha = await commitSha(beforeDir, resolvedBeforeBranch);
155
+ debug(`[before] branch sha : ${beforeSha}`);
156
+ debug(`[before] actual ref : ${await currentBranch(beforeDir)}`);
157
+
158
+ // this is needed mostly for Github actions/checkout with its special PR merge commit replacing the actual last sha
159
+ debug(`[before] ensure full history - git fetch origin ${resolvedAfterBranch}:${resolvedAfterBranch}...`);
160
+ execSync(`git fetch origin ${resolvedAfterBranch}:${resolvedAfterBranch}`, { cwd: beforeDir });
161
+
150
162
  // after checkout --------------------------------------------------------------
151
163
 
152
164
  const afterDir = path.join(tempDir, "after");
153
165
 
154
- debug(`[after] using local cloning`);
155
- await cloneLocalAndCheckout(gitDir, afterDir);
156
- try {
157
- // need to ensure before branch is available under local name
158
- await Git(afterDir).branch([beforeBranch, "-t", `origin/${beforeBranch}`]);
159
- } catch (e) {
160
- debug("ignoring error", e);
161
- }
162
- debug(`[after] current branch: ${await currentBranch(afterDir)}`);
166
+ // always local clone after which preserves the HEAD
167
+ // works well for CI checkouts where the checkout is in the after state already (unless afterBranch is specified)
168
+ // if user specified afterBranch on cli, we will checkout that branch after cloning
169
+ debug(`[after] local cloning${afterBranch ? ` and checking out '${afterBranch}'` : ""}`);
170
+ await cloneLocalAndCheckout(gitDir, afterDir, afterBranch);
171
+
172
+ debug(`[after] input branch : ${afterBranch || ""}`);
173
+ debug(`[after] resolved branch: ${resolvedAfterBranch}`);
174
+ const afterSha = await commitSha(beforeDir, resolvedAfterBranch);
175
+ debug(`[after] branch sha : ${afterSha}`);
176
+ debug(`[after] actual ref : ${await currentBranch(afterDir)}`);
163
177
 
164
178
  debug("folders ready");
165
179
 
166
180
  return {
167
181
  before: {
168
182
  dir: beforeDir,
169
- branch: beforeBranch
183
+ branch: resolvedBeforeBranch,
184
+ sha: beforeSha
170
185
  },
171
186
  after: {
172
187
  dir: afterDir,
173
- branch: afterBranch,
174
- sha: await currentSha(afterDir)
188
+ branch: resolvedAfterBranch,
189
+ sha: afterSha
175
190
  }
176
191
  };
177
192
  }
@@ -15,12 +15,12 @@
15
15
  // comparators/custom.js - compares custom path
16
16
 
17
17
  const debug = require("debug")("sizewatcher:custom");
18
- const du = require("du");
19
18
  const path = require("path");
20
19
  const fs = require("fs");
21
20
  const pb = require('pretty-bytes');
22
21
  const { spawn } = require('child_process');
23
22
  const glob = require("glob");
23
+ const { getFileSize } = require("../size");
24
24
 
25
25
  async function spawnProcess(command, args, options) {
26
26
  return new Promise((resolve, reject) => {
@@ -57,7 +57,7 @@ async function measure(baseDir, globPath, fileListing) {
57
57
  const files = glob.sync(path.join(baseDir, globPath));
58
58
  for (const file of files) {
59
59
  if (fs.existsSync(file)) {
60
- const fileSize = await du(file);
60
+ const fileSize = await getFileSize(file);
61
61
  total += fileSize;
62
62
 
63
63
  if (fileListing) {
@@ -12,33 +12,54 @@
12
12
 
13
13
  'use strict';
14
14
 
15
- // comparators/git.js - compares git repo size (.git folder)
15
+ // comparators/git.js - compares git repo size
16
16
 
17
17
  const debug = require("debug")("sizewatcher:git");
18
- const du = require("du");
19
18
  const path = require("path");
20
19
  const fs = require("fs");
21
20
  const { execSync } = require("child_process");
22
- const Git = require("simple-git");
21
+ const { getFileSize, getFolderRegex } = require("../size");
23
22
 
24
23
  async function getSize(dir) {
25
- debug(`git garbage collection of '${dir}'...`);
26
- await Git(dir).raw(["gc"]);
24
+ debug(`calculating folder size of ${dir}...`);
27
25
 
28
- debug(`calculating folder size of ${dir}/.git...`);
29
- return du(path.join(dir, ".git"));
26
+ return getFileSize(dir, {
27
+ // do not include .git folder
28
+ ignore: getFolderRegex(path.join(dir, ".git"))
29
+ });
30
30
  }
31
31
 
32
- async function largestCommits(dir, beforeBranch, afterBranch) {
33
- process.chdir(dir);
34
- debug(`determining object sizes in new commits...`);
35
- // get size of objects in the new commits
32
+ async function getLargestCommits(dir, beforeBranch, afterBranch) {
33
+ debug(`determining largest objects in commits between ${beforeBranch} and ${afterBranch}...`);
34
+ // 1. we then get the list of objects in the range
35
+ // 2. calculate the size of each object
36
+ // 3. sort the objects by size
37
+ // 4. format the output nicely (human readable KiB, MiB, GiB, etc)
36
38
  return execSync(`git rev-list --objects ${beforeBranch}..${afterBranch} \
37
39
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
38
40
  | sed -n 's/^blob //p' \
39
41
  | sort -r --numeric-sort --key=2 \
40
- | cut -c 1-12,41- \
41
- | $(command -v gnumfmt || echo numfmt) --field=2 --to=iec-i --suffix=B --padding=7 --round=nearest`).toString();
42
+ | cut -c 41- \
43
+ | $(command -v gnumfmt || echo numfmt) --field=1 --to=iec-i --suffix=B --padding=7 --round=nearest`, { cwd: dir }).toString();
44
+
45
+ // Note: even if the command will fail this will not throw an error (because of the shell piping)
46
+ // this is ok because this is an informational output only
47
+ }
48
+
49
+ async function getLargestFiles(dir) {
50
+ debug(`determining largest objects in repo...`);
51
+ // 1. get the list of objects in the repo
52
+ // 2. sort the objects by size
53
+ // 3. show the top 10
54
+ // 3. format the output nicely (human readable KiB, MiB, GiB, etc)
55
+ return execSync(`git ls-tree -r --long HEAD \
56
+ | sort --key 4 -n -r \
57
+ | head -n 10 \
58
+ | cut -c 53- \
59
+ | $(command -v gnumfmt || echo numfmt) --field=1 --to=iec-i --suffix=B --padding=7 --round=nearest`, { cwd: dir }).toString();
60
+
61
+ // Note: even if the command will fail this will not throw an error (because of the shell piping)
62
+ // this is ok because this is an informational output only
42
63
  }
43
64
 
44
65
  module.exports = {
@@ -50,12 +71,19 @@ module.exports = {
50
71
  compare: async function(before, after) {
51
72
  const beforeSize = await getSize(before.dir);
52
73
  const afterSize = await getSize(after.dir);
53
- const details = await largestCommits(after.dir, before.branch, after.sha);
74
+
75
+ const largestFiles = await getLargestFiles(after.dir);
76
+ const largestCommits = await getLargestCommits(before.dir, before.branch, after.branch);
77
+
78
+ let details = `Largest files in repository checkout:\n\n${largestFiles}\n`;
79
+ if (largestCommits.length > 0) {
80
+ details += `Largest files among new changes:\n\n${largestCommits}`;
81
+ }
54
82
 
55
83
  return {
56
84
  beforeSize,
57
85
  afterSize,
58
- detailsLabel: "Largest files in new changes",
86
+ detailsLabel: "Largest files",
59
87
  details
60
88
  };
61
89
  }
@@ -16,9 +16,10 @@
16
16
 
17
17
  const debug = require("debug")("sizewatcher:node_modules");
18
18
  const { execSync } = require("child_process");
19
- const du = require("du");
20
19
  const path = require("path");
21
20
  const fs = require("fs");
21
+ const { getFileSize } = require("../size");
22
+
22
23
 
23
24
  function isEmptyObject(obj) {
24
25
  return Object.keys(obj).length === 0 && obj.constructor === Object;
@@ -44,7 +45,7 @@ async function getSize(dir) {
44
45
  const nodeModules = path.join(dir, "node_modules");
45
46
  if (fs.existsSync(nodeModules)) {
46
47
  debug(`calculating folder size of ${nodeModules}...`);
47
- return du(nodeModules);
48
+ return getFileSize(nodeModules);
48
49
  } else {
49
50
  return 0;
50
51
  }
@@ -62,16 +63,16 @@ async function costOfModules(dir) {
62
63
  return "(no production dependencies)";
63
64
  }
64
65
 
65
- debug(`running cost-of-modules...`);
66
+ debug(`running npx howfat...`);
66
67
  try {
67
- const out = execSync("npx cost-of-modules --less --no-install").toString();
68
+ const out = execSync("npx howfat -r table --sort size-").toString();
68
69
  const PREFIX = "\nCalculating...\n\n\n";
69
70
  if (out.startsWith(PREFIX)) {
70
71
  return out.substring(PREFIX.length);
71
72
  }
72
73
  return out;
73
74
  } catch (e) {
74
- throw new Error(`cost-of-modules failed with: ${e.stdout ? e.stdout.toString() : ""} ${e.stdout ? e.stderr.toString() : ""}`);
75
+ throw new Error(`howfat failed with: ${e.stdout ? e.stdout.toString() : ""} ${e.stdout ? e.stderr.toString() : ""}`);
75
76
  }
76
77
 
77
78
  }
@@ -30,8 +30,8 @@ async function getSize(dir) {
30
30
  };
31
31
  }
32
32
 
33
- debug(`running npm publish --dry-run inside ${dir}...`);
34
- let { stderr } = await exec("npm publish --dry-run", { cwd: dir });
33
+ debug(`running npm pack --dry-run inside ${dir}...`);
34
+ let { stderr } = await exec("npm pack --dry-run", { cwd: dir });
35
35
 
36
36
  stderr = stderr.replace(/npm notice /g, "");
37
37
 
package/lib/compare.js CHANGED
@@ -48,10 +48,25 @@ async function compare(before, after) {
48
48
 
49
49
  console.log("Calculating size changes for");
50
50
 
51
- for (const name of Object.keys(COMPARATORS)) {
52
- const comparator = COMPARATORS[name];
51
+ const comparators = [];
52
+ // ensure git comparator comes first as it needs a clean checkout (in after folder)
53
+ if (COMPARATORS.git) {
54
+ const comp = COMPARATORS.git;
55
+ comp.type = "git";
56
+ comparators.push(comp);
57
+ }
58
+ for (const type of Object.keys(COMPARATORS)) {
59
+ if (type !== "git") {
60
+ const comp = COMPARATORS[type];
61
+ comp.type = type;
62
+ comparators.push(comp);
63
+ }
64
+ }
65
+
66
+ for (const comparator of comparators) {
67
+ const type = comparator.type;
53
68
 
54
- if (name === "custom") {
69
+ if (type === "custom") {
55
70
  // custom comparator, multiple could be configured
56
71
  let customConfigs = config.comparators.custom;
57
72
  if (!customConfigs) {
@@ -68,9 +83,9 @@ async function compare(before, after) {
68
83
 
69
84
  } else {
70
85
  // fixed comparators
71
- const comparatorConfig = config.comparators[name];
86
+ const comparatorConfig = config.comparators[type];
72
87
  if (comparatorConfig === false) {
73
- debug(`skipping '${name}' because disabled in config`);
88
+ debug(`skipping '${type}' because disabled in config`);
74
89
  continue;
75
90
  }
76
91
  let dirs = ["."];
@@ -83,9 +98,9 @@ async function compare(before, after) {
83
98
  }
84
99
  for (const dir of dirs) {
85
100
  if (dir !== ".") {
86
- comparator.name = `${name} [${dir}]`;
101
+ comparator.name = `${type} [${dir}]`;
87
102
  } else {
88
- comparator.name = name;
103
+ comparator.name = type;
89
104
  }
90
105
  comparator.config = comparatorConfig || {};
91
106
 
@@ -103,7 +118,7 @@ async function compare(before, after) {
103
118
  deltas.before = before;
104
119
  deltas.after = after;
105
120
 
106
- return process(deltas);
121
+ return processDeltas(deltas);
107
122
  }
108
123
 
109
124
  function exceeds(value, increasePct, limit) {
@@ -126,7 +141,7 @@ function exceeds(value, increasePct, limit) {
126
141
  }
127
142
  }
128
143
 
129
- function process(deltas) {
144
+ function processDeltas(deltas) {
130
145
  const summary = {
131
146
  cheers: 0,
132
147
  ok: 0,
package/lib/config.js CHANGED
@@ -39,7 +39,7 @@ function load() {
39
39
 
40
40
  if (fs.existsSync(CONFIG_FILE)) {
41
41
  const data = fs.readFileSync(CONFIG_FILE, "utf8");
42
- const loadedConfig = yaml.safeLoad(data) || {};
42
+ const loadedConfig = yaml.load(data) || {};
43
43
 
44
44
  config = deepmerge(DEFAULT_CONFIG, loadedConfig);
45
45
 
@@ -65,6 +65,6 @@ module.exports = {
65
65
  },
66
66
 
67
67
  asYaml() {
68
- return yaml.safeDump(this.get());
68
+ return yaml.dump(this.get());
69
69
  }
70
70
  };
package/lib/render.js CHANGED
@@ -42,7 +42,7 @@ function indentEachLine(string, indent=" ") {
42
42
  function renderAsText(deltas) {
43
43
  let text = `Sizewatcher measured the following changes:\n\n`;
44
44
 
45
- text += ` '${deltas.before.branch}' => '${deltas.after.branch}' (sha ${deltas.after.sha})\n\n`;
45
+ text += ` '${deltas.before.branch}' (sha ${deltas.before.sha}) => '${deltas.after.branch}' (sha ${deltas.after.sha})\n\n`;
46
46
 
47
47
  for (const d of deltas) {
48
48
  if (d.error) {
@@ -101,7 +101,7 @@ function renderAsMarkdown(deltas) {
101
101
 
102
102
  markdown += `<details><summary>Notes</summary><br>\n\n`;
103
103
  markdown += `- PR branch: \`${deltas.after.branch}\` @ ${deltas.after.sha}\n`;
104
- markdown += `- Base branch: \`${deltas.before.branch}\`\n`;
104
+ markdown += `- Base branch: \`${deltas.before.branch}\` @ ${deltas.before.sha}\n`;
105
105
  markdown += `- Sizewatcher v${require('../package.json').version}\n`;
106
106
  markdown += `- Effective Configuration:\n`;
107
107
  markdown += `\n\`\`\`yaml\n${config.asYaml()}\`\`\`\n`;
package/lib/size.js ADDED
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Copyright 2020 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const debug = require("debug")("sizewatcher:size");
16
+
17
+ let _getFolderSize;
18
+
19
+ /**
20
+ * Get the size of a folder or file
21
+ *
22
+ * @param {String} dir Path to the folder or file
23
+ * @param {Object} options Supported options:
24
+ * - ignore: Regex pattern. Any file or folder with a path that matches the pattern will not be counted in the total folder size.
25
+ * @returns {Promise<Number>} The size of the folder or file in bytes
26
+ */
27
+ async function getFileSize(dir, options) {
28
+ // lazy load get-folder-size due to ESM vs. CommonJS
29
+ if (!_getFolderSize) {
30
+ _getFolderSize = (await import("get-folder-size")).default;
31
+ }
32
+
33
+ const result = await _getFolderSize(dir, options);
34
+ if (result.errors) {
35
+ debug(`get-folder-size of ${dir} returned some errors:`, result.errors);
36
+ }
37
+ return result.size;
38
+ }
39
+
40
+ function escapeRegex(string) {
41
+ return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
42
+ }
43
+
44
+ /**
45
+ * Get a regex pattern that matches a directory and all its descendants.
46
+ *
47
+ * @param {String} dir Path to the directory
48
+ * @returns {RegExp} The regex pattern
49
+ */
50
+ function getFolderRegex(dir) {
51
+ return new RegExp(`^${escapeRegex(dir)}(\\/.*)?$`);
52
+ }
53
+
54
+ /**
55
+ * Get a regex pattern that matches a file exactly.
56
+ *
57
+ * @param {String} file Path to the file
58
+ * @returns {RegExp} The regex pattern
59
+ */
60
+ function getFileRegex(file) {
61
+ return new RegExp(`^${escapeRegex(file)}$`);
62
+ }
63
+
64
+ module.exports = {
65
+ getFileSize,
66
+ getFolderRegex,
67
+ getFileRegex
68
+ };
@@ -56,7 +56,7 @@ async function sizewatcher(argv) {
56
56
  return process.exit();
57
57
  }
58
58
 
59
- console.log(`Comparing changes from '${before.branch}' to '${after.branch}'\n`);
59
+ console.log(`Comparing changes from '${before.branch}' (sha ${before.sha}) to '${after.branch}' (sha ${after.sha})\n`);
60
60
 
61
61
  const deltas = await compare(before, after);
62
62
 
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "@adobe/sizewatcher",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "Warns if your pull requests introduce large size increases.",
5
5
  "main": "index.js",
6
6
  "bin": {
7
- "sizewatcher": "./index.js"
7
+ "sizewatcher": "index.js"
8
8
  },
9
9
  "author": "Adobe",
10
10
  "license": "Apache-2.0",
11
- "repository": "adobe/sizewatcher",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/adobe/sizewatcher.git"
14
+ },
12
15
  "keywords": [
13
16
  "size",
14
17
  "check",
@@ -19,31 +22,35 @@
19
22
  "pull-request"
20
23
  ],
21
24
  "scripts": {
22
- "test": "nyc -r=text -r=lcov mocha",
25
+ "test": "DEBUG=sizewatcher* nyc -r=text -r=lcov mocha",
23
26
  "posttest": "eslint .",
24
27
  "report-coverage": "nyc report --reporter=text-lcov | coveralls",
25
- "sizewatcher": "DEBUG=sizewatcher* node ./index.js"
28
+ "sizewatcher": "DEBUG=sizewatcher* node ./index.js",
29
+ "clean": "rm -rf test/checkout/*/build"
26
30
  },
27
31
  "devDependencies": {
28
32
  "@adobe/eslint-config-asset-compute": "^1.3.0",
29
33
  "coveralls": "^3.1.0",
30
- "eslint": "^7.1.0",
31
- "mocha": "^10.2.0",
32
- "mock-fs": "^4.13.0",
33
- "nyc": "^15.1.0",
34
+ "eslint": "^8.0.0",
35
+ "mocha": "^11.0.0",
36
+ "mock-fs": "^5.5.0",
37
+ "nyc": "^17.0.0",
34
38
  "supports-color": "^8.1.1"
35
39
  },
36
40
  "dependencies": {
37
- "@octokit/rest": "^18.0.3",
38
- "debug": "^4.1.1",
39
- "deepmerge": "^4.2.2",
40
- "du": "^1.0.0",
41
- "glob": "^7.1.6",
42
- "js-yaml": "^3.14.0",
43
- "pretty-bytes": "^5.3.0",
44
- "require-dir": "^1.2.0",
45
- "simple-git": "^3.7.1",
46
- "tmp": "^0.2.1",
47
- "xbytes": "^1.6.2"
41
+ "@octokit/rest": "19.0.3",
42
+ "debug": "4.4.3",
43
+ "deepmerge": "4.3.1",
44
+ "get-folder-size": "5.0.0",
45
+ "glob": "10.3.5",
46
+ "js-yaml": "4.1.1",
47
+ "pretty-bytes": "5.6.0",
48
+ "require-dir": "1.2.0",
49
+ "simple-git": "3.22.0",
50
+ "tmp": "0.2.5",
51
+ "xbytes": "1.9.1"
52
+ },
53
+ "overrides": {
54
+ "whatwg-url": "14.2.0"
48
55
  }
49
56
  }