@adobe/sizewatcher 1.0.0 → 1.2.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,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.0
4
+
5
+ Improvements:
6
+
7
+ - [#41](https://github.com/adobe/sizewatcher/issues/41) Support project structures where package.json is in subdirectory. Introduces the `dir` config on comparators.
8
+
9
+ Fixes:
10
+
11
+ - [#46](https://github.com/adobe/sizewatcher/issues/46) Support running in a subdirectory of a git checkout.
12
+
13
+ - [#43](https://github.com/adobe/sizewatcher/issues/43) `node_modules` comparator failed if there were no dependencies
14
+
15
+ - [#42](https://github.com/adobe/sizewatcher/issues/42) `npm_package` failed if there was no version field in package.json
16
+
17
+ ## 1.1.0
18
+
19
+ New Comparators:
20
+
21
+ - [#14](https://github.com/adobe/sizewatcher/issues/14) npm package size
22
+
23
+ Improvements:
24
+
25
+ - [#32](https://github.com/adobe/sizewatcher/issues/32) Improved PR comment that can be collapsed as whole, shows the summary result and is collapsed by default if the result is ok.
26
+
27
+ - [#33](https://github.com/adobe/sizewatcher/issues/33) Support `script` option in custom comparator. This allows to run build steps in case the output of those needs to be measured. The `path` option now also supports glob patterns.
28
+
29
+
30
+ - [#31](https://github.com/adobe/sizewatcher/issues/31) Change default limits to make them less aggressive:
31
+ - fail: `100%`
32
+ - warn: `30%`
33
+ - ok: `-10%` (same)
34
+
35
+ - [#39](https://github.com/adobe/sizewatcher/issues/39) Detect if comparing the same branch and exit gracefully.
36
+
37
+
3
38
  ## 1.0.0
4
39
 
5
40
  Initial version with comparators
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  <!-- code_chunk_output -->
12
12
 
13
- - [Standard behavior](#standard-behavior)
13
+ - [How it works](#how-it-works)
14
14
  - [Requirements](#requirements)
15
15
  - [Installation](#installation)
16
16
  - [Usage](#usage)
@@ -28,88 +28,94 @@
28
28
  - addition of large dependencies (and transient dependency trees)
29
29
  - accidental addition of large binary files to the git repository
30
30
  - sudden increase in build artifact size
31
- - etc.
32
31
 
33
- Currently supported are git repository size itself, Node.js/npm modules and measuring any custom files or folders - see [Comparators reference](#comparators-reference). More built-in languages & options are possible in the future.
32
+ While any custom file or folder path can be measured via configuration, various types are automatically measured, including git repository, Node module dependencies and npm package size - see [comparators reference](#comparators-reference). More built-in languages & options are added over time and [contributions are welcome](#new-comparator).
34
33
 
35
- `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. This is an example of a Github PR comment:
34
+ `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
+
36
+ This is an example of a `sizewatcher` Github PR comment with a failure (ignore the small numbers):
36
37
 
37
38
  ---
38
39
 
39
- 📈 [Sizewatcher](https://github.com/adobe/sizewatcher) measured these changes:
40
+ <details open><summary>❌ <a href="https://github.com/adobe/sizewatcher">Sizewatcher</a> detected a problematic size increase 📈:</summary>
41
+ <p><blockquote>
40
42
 
41
- <details>
42
- <summary>✅ <code>git</code> <b>+0.2%</b> (246 kB => 247 kB)</summary>
43
- <br>
43
+ <details><summary>❌ <code>git</code> <b>+97.9%</b> (186 kB => 368 kB)</summary><br>Largest files in new changes:<pre>ecaf42b1d55c 4.5KiB lib/report.js<br>9c1a82fa7efb 2.8KiB lib/render.js<br>710a7c687b06 2.8KiB lib/render.js<br>e7a5a58b23a6 2.7KiB test/config.test.js<br>0a8f1a2ddb4f 2.4KiB test/config.test.js<br>6846cf298cd4 2.3KiB test/config.test.js<br>461b7663fd23 1.6KiB lib/config.js<br>a643d322cc26 1.5KiB lib/config.js<br>6db7d5c27a66 69B .sizewatcher.yml</pre></details>
44
44
 
45
- Largest files in new changes:
46
- ```
47
- 360cccafa87c 974B .npmignore
48
- ```
49
- </details>
45
+ <details><summary>✅ <code>node_modules</code> <b>-6.1%</b> (42.5 MB => 39.9 MB)</summary><br>Largest node modules:<pre>┌───────────────┬─────────────┬────────┐<br>│ name │ children │ size │<br>├───────────────┼─────────────┼────────┤<br>│ @octokit/rest │ 33 │ 11.25M │<br>├───────────────┼─────────────┼────────┤<br>│ js-yaml │ 3 │ 0.72M │<br>├───────────────┼─────────────┼────────┤<br>│ simple-git │ 2 │ 0.24M │<br>├───────────────┼─────────────┼────────┤<br>│ tmp │ 13 │ 0.22M │<br>├───────────────┼─────────────┼────────┤<br>│ debug │ 1 │ 0.08M │<br>├───────────────┼─────────────┼────────┤<br>│ deepmerge │ 0 │ 0.03M │<br>├───────────────┼─────────────┼────────┤<br>│ require-dir │ 0 │ 0.02M │<br>├───────────────┼─────────────┼────────┤<br>│ du │ 1 │ 0.01M │<br>├───────────────┼─────────────┼────────┤<br>│ pretty-bytes │ 0 │ 0.01M │<br>├───────────────┼─────────────┼────────┤<br>│ 9 modules │ 34 children │ 4.57M │<br>└───────────────┴─────────────┴────────┘</pre></details>
50
46
 
51
- <details>
52
- <summary>✅ <code>node_modules</code> has no changes (46.8 MB)</summary>
53
- <br>
47
+ <details><summary>Notes</summary><br>
54
48
 
55
- Largest node modules:
56
- ```
57
- ┌───────────────┬─────────────┬────────┐
58
- name │ children │ size │
59
- ├───────────────┼─────────────┼────────┤
60
- │ @octokit/rest │ 33 │ 11.25M │
61
- ├───────────────┼─────────────┼────────┤
62
- js-yaml │ 3 │ 0.72M │
63
- ├───────────────┼─────────────┼────────┤
64
- simple-git │ 2 │ 0.24M │
65
- ├───────────────┼─────────────┼────────┤
66
- tmp │ 13 │ 0.22M │
67
- ├───────────────┼─────────────┼────────┤
68
- │ debug │ 1 │ 0.08M │
69
- ├───────────────┼─────────────┼────────┤
70
- │ deepmerge │ 0 │ 0.03M │
71
- ├───────────────┼─────────────┼────────┤
72
- require-dir │ 0 │ 0.02M │
73
- ├───────────────┼─────────────┼────────┤
74
- │ du │ 1 │ 0.01M │
75
- ├───────────────┼─────────────┼────────┤
76
- │ pretty-bytes │ 0 │ 0.01M │
77
- ├───────────────┼─────────────┼────────┤
78
- │ 9 modules │ 34 children │ 4.57M │
79
- └───────────────┴─────────────┴────────┘
49
+ - PR branch: `testconfig` @ 21b66dfe6c3f6d09d4929ea2dec1e62cd1a7e7f2
50
+ - Base branch: `main`
51
+ - Sizewatcher v1.0.0
52
+ - Effective Configuration:
53
+
54
+ ```yaml
55
+ limits:
56
+ fail: 100%
57
+ warn: 30%
58
+ ok: '-10%'
59
+ report:
60
+ githubComment: true
61
+ githubStatus: false
62
+ comparators:
63
+ git:
64
+ limits:
65
+ fail: 50%
66
+ custom: null
80
67
  ```
81
68
  </details>
82
69
 
83
- <details>
84
- <summary>Notes</summary>
85
- <br>
70
+ </blockquote></p>
71
+ </details>
72
+
73
+ ---
74
+
75
+ And here if everything looks good:
76
+
77
+ ---
78
+
79
+ <details ><summary>✅ <a href="https://github.com/adobe/sizewatcher">Sizewatcher</a> found no problematic size increases.</summary>
80
+ <p><blockquote>
86
81
 
87
- - PR branch: `docs` @ 15626a050330492da8b745dadb4f5d304b670e83
82
+ <details><summary>✅ <code>git</code> <b>+97.9%</b> (186 kB => 368 kB)</summary><br>Largest files in new changes:<pre>ecaf42b1d55c 4.5KiB lib/report.js<br>9c1a82fa7efb 2.8KiB lib/render.js<br>710a7c687b06 2.8KiB lib/render.js<br>e7a5a58b23a6 2.7KiB test/config.test.js<br>0a8f1a2ddb4f 2.4KiB test/config.test.js<br>6846cf298cd4 2.3KiB test/config.test.js<br>461b7663fd23 1.6KiB lib/config.js<br>a643d322cc26 1.5KiB lib/config.js<br>6db7d5c27a66 69B .sizewatcher.yml</pre></details>
83
+
84
+ <details><summary>✅ <code>node_modules</code> <b>-6.1%</b> (42.5 MB => 39.9 MB)</summary><br>Largest node modules:<pre>┌───────────────┬─────────────┬────────┐<br>│ name │ children │ size │<br>├───────────────┼─────────────┼────────┤<br>│ @octokit/rest │ 33 │ 11.25M │<br>├───────────────┼─────────────┼────────┤<br>│ js-yaml │ 3 │ 0.72M │<br>├───────────────┼─────────────┼────────┤<br>│ simple-git │ 2 │ 0.24M │<br>├───────────────┼─────────────┼────────┤<br>│ tmp │ 13 │ 0.22M │<br>├───────────────┼─────────────┼────────┤<br>│ debug │ 1 │ 0.08M │<br>├───────────────┼─────────────┼────────┤<br>│ deepmerge │ 0 │ 0.03M │<br>├───────────────┼─────────────┼────────┤<br>│ require-dir │ 0 │ 0.02M │<br>├───────────────┼─────────────┼────────┤<br>│ du │ 1 │ 0.01M │<br>├───────────────┼─────────────┼────────┤<br>│ pretty-bytes │ 0 │ 0.01M │<br>├───────────────┼─────────────┼────────┤<br>│ 9 modules │ 34 children │ 4.57M │<br>└───────────────┴─────────────┴────────┘</pre></details>
85
+
86
+ <details><summary>Notes</summary><br>
87
+
88
+ - PR branch: `testconfig` @ 21b66dfe6c3f6d09d4929ea2dec1e62cd1a7e7f2
88
89
  - Base branch: `main`
89
90
  - Sizewatcher v1.0.0
90
- - Configuration
91
- <pre>
91
+ - Effective Configuration:
92
+
93
+ ```yaml
92
94
  limits:
93
- fail: 50%
94
- warn: 10%
95
+ fail: 100%
96
+ warn: 200%
95
97
  ok: '-10%'
96
98
  report:
97
99
  githubComment: true
98
100
  githubStatus: false
99
101
  comparators: {}
100
- </pre></details>
102
+ ```
103
+ </details>
104
+
105
+ </blockquote></p>
106
+ </details>
101
107
 
102
108
  ---
103
109
 
104
- ## Standard behavior
110
+ ## How it works
105
111
 
106
112
  By default `sizewatcher` will
107
113
  - checkout the before and after branch versions in temporary directories
108
114
  - go through all [comparators](#comparators-reference) that apply
109
115
  - measure the sizes, compare and report
110
- - fail ❌ at a 50%+ increase
111
- - warn ⚠️ at a 10%+ increase
112
- - report ok ✅ if the size does not change by +/-10%
116
+ - fail ❌ at a 100%+ increase
117
+ - warn ⚠️ at a 30%+ increase
118
+ - report ok ✅ if the size change is between -10 and +30%
113
119
  - cheer 🎉 if there is a 10% decrease
114
120
  - print result in cli output
115
121
  - report result as PR comment
@@ -121,6 +127,8 @@ By default `sizewatcher` will
121
127
 
122
128
  - [Nodejs](https://nodejs.org) version 10+
123
129
  - recommended to use latest stable version "LTS"
130
+ - Github or Github Enterprise
131
+ - if you want to run it on pull requests
124
132
  - CI system running on Github pull requests
125
133
  - [Github Actions](https://github.com/features/actions) (simplest setup)
126
134
  - [Travis CI](https://travis-ci.org)
@@ -147,7 +155,7 @@ sizewatcher -h
147
155
 
148
156
  Help output:
149
157
  ```
150
- Usage: /usr/local/bin/sizewatcher [<options>] [<before> [<after>]]
158
+ Usage: sizewatcher [<options>] [<before> [<after>]]
151
159
 
152
160
  Arguments:
153
161
  <before> Before branch/commit for comparison. Defaults to default branch or main/master.
@@ -203,18 +211,31 @@ sizewatcher before after
203
211
 
204
212
  ## CI Setup
205
213
 
214
+ - [CI Overview](#ci-overview)
206
215
  - [Github Actions](#github-actions)
207
216
  - [Travis CI](#travis-ci)
208
217
  - [CircleCI](#circleci)
209
218
  - [Other CIs](#other-cis)
210
219
 
211
- To run `sizewatcher` in your CI, which is where it should run, it is best run using [npx](https://nodejs.dev/learn/the-npx-nodejs-package-runner), which comes pre-installed with nodejs and will automatically download and run the latest version in one go:
220
+ ### CI Overview
221
+
222
+ #### Run using npx
223
+
224
+ To run `sizewatcher` in your CI, which is where it needs to run automatically for checking pull requests, it is best run using [npx](https://nodejs.dev/learn/the-npx-nodejs-package-runner), which comes pre-installed with nodejs and will automatically download and run the latest version in one go:
212
225
 
213
226
  ```
214
227
  npx @adobe/sizewatcher
215
228
  ```
216
229
 
217
- Depending on the CI, branches of the pull request are automatically detected. Last but not least, to be able to automatically comment on the PR or report a commit status, a **github token** must be set as environment variable:
230
+ This command will always use the latest published version. In some cases it might be (temporarily) desireable to stick to a certain version, which can be achieved using:
231
+
232
+ ```
233
+ npx @adobe/sizewatcher@1.0.0
234
+ ```
235
+
236
+ #### GITHUB_TOKEN
237
+
238
+ To be able to automatically comment on the PR or report a commit status, a **github token** must be set as environment variable:
218
239
 
219
240
  ```
220
241
  GITHUB_TOKEN=....
@@ -222,6 +243,19 @@ GITHUB_TOKEN=....
222
243
 
223
244
  This token should be a service/bot user that has read/pull permission on the repository (allowing to comment). Note that the comments will be shown under that user's name. For example, you might want to create a user named `sizewatcher-bot` or the like. With Github Actions this is not required, it has a built-in `github-actions` bot user.
224
245
 
246
+ #### GITHUB_API_URL
247
+
248
+ If you use Github Enterprise, set the custom [Github API URL](https://docs.github.com/en/enterprise-server@2.21/rest/reference/enterprise-admin) in the `GITHUB_API_URL` environment variable:
249
+
250
+ ```
251
+ GITHUB_API_URL=https://mygithub.company.com/api/v3/
252
+ ```
253
+
254
+ This is not required for public github.com.
255
+
256
+ #### Order of steps
257
+
258
+ You can run `sizewatcher` before, after or in parallel to your main build step. It does not rely on output of a build, since it will check out the code (before and after PR versions) separately and needs to run any build steps itself, such as using `script` in the [custom comparator](#custom).
225
259
 
226
260
  See below for CI specific setup.
227
261
 
@@ -245,8 +279,6 @@ jobs:
245
279
  uses: actions/setup-node@v1
246
280
  with:
247
281
  node-version: '14'
248
- # run your build/test first in case you want to measure build results
249
- - run: npm install && npm test
250
282
  # ---------- this runs sizewatcher ------------
251
283
  - run: npx @adobe/sizewatcher
252
284
  env:
@@ -258,7 +290,7 @@ jobs:
258
290
  For [Travis CI](https://travis-ci.org) you need to
259
291
  - use `language: node_js`
260
292
  - if you already use a different language, find a way to ensure Nodejs 10+ is installed
261
- - under `script` run `npx @adobe/sizewatcher`, typically after your main build or test
293
+ - under `script` run `npx @adobe/sizewatcher`
262
294
  - set a secret environment variable `GITHUB_TOKEN` in the [Travis repository settings](https://docs.travis-ci.com/user/environment-variables/#defining-variables-in-repository-settings) with a github token with permission to comment on PRs and reporting commit statuses for the repository
263
295
 
264
296
  Example `.travis.yml`:
@@ -270,8 +302,6 @@ install:
270
302
  - npm install
271
303
 
272
304
  script:
273
- # run your build/test first in case you want to measure build results
274
- - npm test
275
305
  # ---------- this runs sizewatcher ------------
276
306
  - npx @adobe/sizewatcher
277
307
  ```
@@ -281,7 +311,7 @@ script:
281
311
  For [CircleCI](https://circleci.com) you need to
282
312
  - use a docker image with Nodejs 10+ installed
283
313
  - alternatively install [using nvm](https://www.google.com/search?q=circleci+use+nvm)
284
- - run `npx @adobe/sizewatcher`, typically after your main build or test
314
+ - run `npx @adobe/sizewatcher`
285
315
  - set a secret environment variable `GITHUB_TOKEN` in the [CircleCI project settings](https://circleci.com/docs/2.0/env-vars/#setting-an-environment-variable-in-a-project) (or in a [Context](https://circleci.com/docs/2.0/env-vars/#setting-an-environment-variable-in-a-context)) with a github token with permission to comment on PRs and reporting commit statuses for the repository
286
316
 
287
317
  Example `.circleci/config.yml`:
@@ -296,10 +326,6 @@ jobs:
296
326
  steps:
297
327
  - checkout
298
328
 
299
- # run your build/test first in case you want to measure build results
300
- - run: npm install
301
- - run: npm test
302
-
303
329
  # ---------- this runs sizewatcher ------------
304
330
  - run: npx @adobe/sizewatcher
305
331
  ```
@@ -316,7 +342,7 @@ Set these environment variables in the CI job:
316
342
  - `GITHUB_HEAD_REF` name of the branch of the pull request
317
343
  - `GITHUB_TOKEN` a github token with permission to comment on PRs and reporting commit statuses for the repository. This is a credential so use the proper CI credential management and never check this into the git repository!
318
344
 
319
- After your build or in parallel, run
345
+ Then simply run
320
346
 
321
347
  ```
322
348
  npx @adobe/sizewatcher
@@ -345,10 +371,10 @@ report:
345
371
  # see https://www.npmjs.com/package/xbytes
346
372
  # - absolute limit, as byte number: 1000000
347
373
  limits:
348
- # when to fail - default: 50%
374
+ # when to fail - default: 100%
349
375
  fail: 50%
350
- # when to warn - default: 10%
351
- warn: 30%
376
+ # when to warn - default: 30%
377
+ warn: 10%
352
378
  # below the ok limit you will get a cheers for making it notably smaller
353
379
  # default: -10%
354
380
  ok: -5%
@@ -369,6 +395,15 @@ comparators:
369
395
  warn: 9 MB
370
396
  ok: 1 MB
371
397
 
398
+ npm_package:
399
+ # `dir` is supported for all comparators that support it and
400
+ # specifies the relative directory inside the project inside which to run the comparator
401
+ dir: "sub/folder"
402
+ # can also be an array with multiple directories to check
403
+ dir:
404
+ - "sub/folder1"
405
+ - "folder2"
406
+
372
407
  # custom comparator (only active if configured)
373
408
  custom:
374
409
  - name: my artifact
@@ -379,7 +414,10 @@ comparators:
379
414
 
380
415
  # there can be multiple custom comparators
381
416
  # name defaults to the path
382
- - path: some_directory/
417
+ # path can include glob patterns
418
+ - path: "artifact-*.tgz"
419
+ # run some custom build command before measuring
420
+ script: npm install
383
421
  # limits can be configured as well
384
422
  limits:
385
423
  fail: 10 MB
@@ -389,6 +427,7 @@ comparators:
389
427
 
390
428
  - [git](#git)
391
429
  - [node_modules](#node_modules)
430
+ - [npm_package](#npm_package)
392
431
  - [custom](#custom)
393
432
 
394
433
  ### git
@@ -451,6 +490,52 @@ Largest node modules:
451
490
  └───────────────┴─────────────┴────────┘
452
491
  ```
453
492
 
493
+ Configuration: supports `dir`
494
+
495
+ ---
496
+
497
+ ### npm_package
498
+
499
+ Compares the size of an npm package tarball by running `npm publish --dry-run`.
500
+
501
+ Name: `npm_package`
502
+
503
+ Trigger: Runs if a `package.json` is found.
504
+
505
+ Details: Prints the package contents and metadata using the output of `npm publish --dry-run`.
506
+
507
+ ---
508
+ Package contents:
509
+
510
+ ```
511
+ 📦 @adobe/sizewatcher@1.0.0
512
+ === Tarball Contents ===
513
+ 11.3kB LICENSE
514
+ 4.8kB lib/checkout.js
515
+ 5.2kB lib/compare.js
516
+ 1.7kB lib/config.js
517
+ 3.9kB lib/comparators/custom.js
518
+ 2.3kB lib/comparators/git.js
519
+ 2.6kB lib/github.js
520
+ 2.1kB index.js
521
+ 2.2kB lib/comparators/node_modules.js
522
+ 3.9kB lib/render.js
523
+ 4.6kB lib/report.js
524
+ 1.1kB package.json
525
+ 695B CHANGELOG.md
526
+ 23.9kB README.md
527
+ === Tarball Details ===
528
+ name: @adobe/sizewatcher
529
+ version: 1.0.0
530
+ package size: 18.6 kB
531
+ unpacked size: 70.3 kB
532
+ shasum: 80846caccca2194f3dd1122e8113206e20c202dc
533
+ integrity: sha512-c3VjMQQvqcqN8[...]ybMS6kg2chjpA==
534
+ total files: 14
535
+ ```
536
+
537
+ Configuration: supports `dir`
538
+
454
539
  ---
455
540
 
456
541
  ### custom
@@ -479,7 +564,16 @@ This comparator requires configuration in the `.sizewatcher.yml`:
479
564
  ```yaml
480
565
  comparators:
481
566
  custom:
482
- path: build/artifact
567
+ path: src/somefile
568
+ ```
569
+
570
+ If a build step is required first, use `script`:
571
+
572
+ ```yaml
573
+ comparators:
574
+ custom:
575
+ path: "build/artifact-*.tgz"
576
+ script: npm run build
483
577
  ```
484
578
 
485
579
  To have multiple paths, with custom names:
@@ -488,14 +582,15 @@ To have multiple paths, with custom names:
488
582
  comparators:
489
583
  custom:
490
584
  - name: my artifact 1
491
- path: build/artifact
585
+ path: "build/artifact"
492
586
  - name: my artifact 2
493
- path: build/artifact2
587
+ path: "build/artifact2"
494
588
  ```
495
589
 
496
590
  Options:
497
- - `path` (required) relative path to file or folder to measure
591
+ - `path` (required) relative path to file or folder to measure; supports [glob patterns](https://www.npmjs.com/package/glob). make sure to use quotes in the yaml if you use glob patterns
498
592
  - `name` (optional) custom label
593
+ - `script` (optional) shell command to run before measuring `path`
499
594
  - `limits` can be set as usual
500
595
 
501
596
  ## Contribute
package/index.js CHANGED
@@ -14,50 +14,5 @@
14
14
 
15
15
  'use strict';
16
16
 
17
- // index.js - main cli entry point
18
-
19
- const gitCheckoutBeforeAndAfter = require("./lib/checkout");
20
- const compare = require("./lib/compare");
21
- const report = require("./lib/report");
22
-
23
- function printUsage() {
24
- console.log(`Usage: ${process.argv[1]} [<options>] [<before> [<after>]]`);
25
- console.log();
26
- console.log("Arguments:");
27
- console.log(" <before> Before branch/commit for comparison. Defaults to default branch or main/master.");
28
- console.log(" <after> After branch/commit for comparison. Defaults to current branch.");
29
- console.log();
30
- console.log("Options:");
31
- console.log(" -h Show help");
32
- }
33
-
34
- async function main(argv) {
35
- try {
36
- // yes, this should probably use a framework like oclif or yargs.
37
- // but cli arguments are very limited now and so manual handling is enough
38
- if (argv[0] === "-h") {
39
- printUsage();
40
- process.exit(1);
41
- }
42
-
43
- // TODO: detect main = main case
44
-
45
- console.log(`Cloning git repository...`);
46
- const { before, after } = await gitCheckoutBeforeAndAfter(process.cwd(), argv[0], argv[1]);
47
-
48
- console.log(`Comparing changes from '${before.branch}' to '${after.branch}'\n`);
49
-
50
- const deltas = await compare(before, after);
51
-
52
- await report(deltas);
53
-
54
- } catch (e) {
55
- console.error(e);
56
- process.exit(1);
57
- } finally {
58
- console.log("Done. Cleaning up...");
59
- }
60
- }
61
-
62
-
63
- main(process.argv.slice(2));
17
+ const sizewatcher = require("./lib/sizewatcher");
18
+ sizewatcher(process.argv.slice(2));
package/lib/checkout.js CHANGED
@@ -18,7 +18,13 @@ const debug = require("debug")("sizewatcher");
18
18
  const tmp = require("tmp");
19
19
  tmp.setGracefulCleanup();
20
20
  const path = require("path");
21
- const Git = require("simple-git/promise");
21
+ const Git = require("simple-git");
22
+
23
+ async function getGitRoot(dir) {
24
+ // --git-dir will return relative directories
25
+ const gitRootDir = await Git(dir).revparse(["--git-dir"]);
26
+ return path.resolve(dir, gitRootDir);
27
+ }
22
28
 
23
29
  async function currentBranch(dir) {
24
30
  const git = Git(dir);
@@ -37,10 +43,14 @@ async function currentSha(dir) {
37
43
  }
38
44
 
39
45
  async function getDefaultBranch(dir) {
40
- const originInfo = await Git(dir).remote(["show", "origin"]);
41
- const m = originInfo.match(/HEAD branch: (.*)$/m);
42
- if (m) {
43
- return m[1];
46
+ try {
47
+ const originInfo = await Git(dir).remote(["show", "origin"]);
48
+ const m = originInfo.match(/HEAD branch: (.*)$/m);
49
+ if (m) {
50
+ return m[1];
51
+ }
52
+ } catch (e) {
53
+ debug(`ignoring error in getDefaultBranch(): ${e.message}`);
44
54
  }
45
55
  }
46
56
 
@@ -48,22 +58,12 @@ async function hasBranch(dir, branch) {
48
58
  try {
49
59
  await Git(dir).revparse(["--verify", branch]);
50
60
  return branch;
51
- } catch (e) { // eslint-disable-line no-unused-vars
61
+ } catch (e) {
62
+ debug(`ignoring error in hasBranch(): ${e.message}`);
52
63
  return undefined;
53
64
  }
54
65
  }
55
66
 
56
- async function hasRemoteTrackingBranch(dir) {
57
- const git = Git(dir);
58
- try {
59
- const remoteBranch = await git.revparse(["--abbrev-ref", "--symbolic-full-name", "@{u}"]);
60
- console.log(remoteBranch);
61
- return true;
62
- } catch (e) { // eslint-disable-line no-unused-vars
63
- return false;
64
- }
65
- }
66
-
67
67
  async function getPullRequestBaseBranch() {
68
68
  return process.env.GITHUB_BASE_REF
69
69
  || (process.env.TRAVIS_PULL_REQUEST !== "false" && process.env.TRAVIS_BRANCH);
@@ -101,36 +101,65 @@ async function cloneRemoteAndCheckout(from, to, branch) {
101
101
  }
102
102
 
103
103
  async function cloneLocalAndCheckout(from, to, branch) {
104
- debug(`local cloning ${from} into ${to}`);
105
- await Git().clone(from, to, { local: true });
104
+ const gitRoot = await getGitRoot(from);
106
105
 
107
- debug(`checking out '${branch}'...`);
108
- await Git(to).checkout(branch);
106
+ debug(`local cloning ${gitRoot} into ${to}`);
107
+ await Git().clone(gitRoot, to, { '--local' : true });
108
+
109
+ if (branch) {
110
+ debug(`checking out '${branch}'...`);
111
+ await Git(to).checkout(branch);
112
+ }
109
113
  }
110
114
 
111
115
  async function gitCheckoutBeforeAndAfter(gitDir, beforeBranch, afterBranch) {
116
+ debug(`retrieving before branch: ${beforeBranch}`);
112
117
  beforeBranch = await getBeforeBranch(gitDir, beforeBranch);
118
+ debug(`retrieving after branch: ${afterBranch}`);
113
119
  afterBranch = await getAfterBranch(gitDir, afterBranch);
120
+ if (beforeBranch === afterBranch) {
121
+ return {
122
+ before: {
123
+ branch: beforeBranch
124
+ },
125
+ after: {
126
+ branch: afterBranch,
127
+ }
128
+ };
129
+ }
114
130
 
115
131
  debug(`cloning branches ${beforeBranch} (before) and ${afterBranch} (after)...`);
116
132
 
117
133
  const tempDir = tmp.dirSync({unsafeCleanup: true}).name;
118
134
  debug(`temporary directory: ${tempDir}`);
119
135
 
120
- const beforeDir = path.join(tempDir, "before");
121
- await cloneRemoteAndCheckout(gitDir, beforeDir, beforeBranch);
136
+ // before checkout --------------------------------------------------------------
122
137
 
123
- const afterDir = path.join(tempDir, "after");
124
- if (process.env.CI || await hasRemoteTrackingBranch(afterDir)) {
125
- await cloneRemoteAndCheckout(gitDir, afterDir, afterBranch);
138
+ const beforeDir = path.join(tempDir, "before");
126
139
 
140
+ // 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);
127
144
  } else {
128
- // if sizewatcher is run locally inside checkout
129
- await cloneLocalAndCheckout(gitDir, afterDir, afterBranch);
145
+ debug(`[before] using remote cloning, as existing checkout does not have before branch '${beforeBranch}'`);
146
+ // ...otherwise need to do a full remote clone
147
+ await cloneRemoteAndCheckout(gitDir, beforeDir, beforeBranch);
148
+ }
149
+
150
+ // after checkout --------------------------------------------------------------
130
151
 
152
+ const afterDir = path.join(tempDir, "after");
153
+
154
+ debug(`[after] using local cloning`);
155
+ await cloneLocalAndCheckout(gitDir, afterDir);
156
+ try {
131
157
  // need to ensure before branch is available under local name
132
158
  await Git(afterDir).branch([beforeBranch, "-t", `origin/${beforeBranch}`]);
159
+ } catch (e) {
160
+ debug("ignoring error", e);
133
161
  }
162
+ debug(`[after] current branch: ${await currentBranch(afterDir)}`);
134
163
 
135
164
  debug("folders ready");
136
165
 
@@ -19,41 +19,97 @@ const du = require("du");
19
19
  const path = require("path");
20
20
  const fs = require("fs");
21
21
  const pb = require('pretty-bytes');
22
+ const { spawn } = require('child_process');
23
+ const glob = require("glob");
22
24
 
23
- module.exports = {
25
+ async function spawnProcess(command, args, options) {
26
+ return new Promise((resolve, reject) => {
27
+ const child = spawn(command, args, options);
24
28
 
25
- getPath: function() {
26
- return this.config && this.config.path;
27
- },
29
+ child.on("error", (err) => reject(err));
30
+ child.on("exit", (code, signal) => {
31
+ if (code !== 0) {
32
+ let cmdString = command;
33
+ if (args.length > 0) {
34
+ cmdString += ` ${args.join(" ")}`;
35
+ }
36
+ const err = new Error(`\`${cmdString}\` failed with exit code ${code}${signal ? `(received signal: ${signal})` : ""}`);
37
+ err.exitCode = code;
38
+ err.signal = signal;
39
+ reject(err);
40
+ }
41
+ resolve();
42
+ });
43
+ });
44
+ }
45
+
46
+ async function runScript(script, path) {
47
+ await spawnProcess(script, [], {
48
+ shell: true,
49
+ cwd: path,
50
+ // log / pass-through all output
51
+ stdio: "inherit"
52
+ });
53
+ }
54
+
55
+ async function measure(baseDir, globPath, fileListing) {
56
+ let total = 0;
57
+ const files = glob.sync(path.join(baseDir, globPath));
58
+ for (const file of files) {
59
+ if (fs.existsSync(file)) {
60
+ const fileSize = await du(file);
61
+ total += fileSize;
62
+
63
+ if (fileListing) {
64
+ fileListing.push(`${pb(fileSize)} ${path.relative(baseDir, file)}`);
65
+ }
66
+ }
67
+ }
68
+ return total;
69
+ }
70
+
71
+ module.exports = {
28
72
 
29
73
  shouldRun: async function(beforeDir, afterDir) {
30
- const p = this.config && this.config.path;
31
- if (!p) {
74
+ if (!this.config.path || typeof this.config.path !== "string") {
32
75
  return false;
33
76
  }
34
- if (path.isAbsolute(p)) {
35
- console.error("Error: custom comparator path must be relative:", p);
77
+ // if it has a script, we have to run (the script might create the files)
78
+ if (typeof this.config.script === "string") {
79
+ return true;
80
+ }
81
+
82
+ if (this.config.path.startsWith("/")) {
83
+ console.error("Error: custom comparator path must be relative:", this.config.path);
36
84
  return false;
37
85
  }
38
- return fs.existsSync(path.join(beforeDir, p)) ||
39
- fs.existsSync(path.join(afterDir, p));
86
+ return glob.sync(path.join(beforeDir, this.config.path)).length > 0 ||
87
+ glob.sync(path.join(afterDir, this.config.path)).length > 0;
40
88
  },
41
89
 
42
90
  compare: async function(before, after) {
43
- debug(`measuring custom ${this.getPath()} in ${before.dir}...`);
44
- const beforePath = path.join(before.dir, this.getPath());
45
- const beforeSize = fs.existsSync(beforePath) ? await du(beforePath) : 0;
91
+ if (this.config.script) {
92
+ debug(`running script in ${before.dir}: ${this.config.script}`);
93
+ await runScript(this.config.script, before.dir);
94
+
95
+ debug(`running script in ${after.dir}: ${this.config.script}`);
96
+ await runScript(this.config.script, after.dir);
97
+ }
98
+
99
+ const fileListing = [];
100
+
101
+ debug(`measuring ${this.config.path} in ${before.dir}...`);
102
+ const beforeSize = await measure(before.dir, this.config.path);
46
103
 
47
- debug(`measuring custom ${this.getPath()} in ${after.dir}...`);
48
- const afterPath = path.join(after.dir, this.getPath());
49
- const afterSize = fs.existsSync(afterPath) ? await du(afterPath) : 0;
104
+ debug(`measuring ${this.config.path} in ${after.dir}...`);
105
+ const afterSize = await measure(after.dir, this.config.path, fileListing);
50
106
 
51
107
  return {
52
108
  name: this.config.name || this.config.path,
53
109
  beforeSize,
54
110
  afterSize,
55
111
  detailsLabel: "New size",
56
- details: `${pb(afterSize)} ${this.config.path}`
112
+ details: fileListing.join("\n")
57
113
  };
58
114
  }
59
115
  };
@@ -19,7 +19,7 @@ const du = require("du");
19
19
  const path = require("path");
20
20
  const fs = require("fs");
21
21
  const { execSync } = require("child_process");
22
- const Git = require("simple-git/promise");
22
+ const Git = require("simple-git");
23
23
 
24
24
  async function getSize(dir) {
25
25
  debug(`git garbage collection of '${dir}'...`);
@@ -50,7 +50,7 @@ module.exports = {
50
50
  compare: async function(before, after) {
51
51
  const beforeSize = await getSize(before.dir);
52
52
  const afterSize = await getSize(after.dir);
53
- const details = await largestCommits(after.dir, before.branch, after.branch);
53
+ const details = await largestCommits(after.dir, before.branch, after.sha);
54
54
 
55
55
  return {
56
56
  beforeSize,
@@ -20,34 +20,66 @@ const du = require("du");
20
20
  const path = require("path");
21
21
  const fs = require("fs");
22
22
 
23
+ function isEmptyObject(obj) {
24
+ return Object.keys(obj).length === 0 && obj.constructor === Object;
25
+ }
26
+
27
+ function hasPackageJson(dir) {
28
+ return fs.existsSync(path.join(dir, "package.json"));
29
+ }
30
+
23
31
  async function getSize(dir) {
32
+ if (!hasPackageJson(dir)) {
33
+ return 0;
34
+ }
24
35
  process.chdir(dir);
36
+
25
37
  debug(`installing node dependencies inside ${dir}...`);
26
38
  if (fs.existsSync("package-lock.json") || fs.existsSync("npm-shrinkwrap.json")) {
27
39
  execSync("npm ci");
28
40
  } else {
29
41
  execSync("npm install");
30
42
  }
31
- debug(`calculating folder size of ${dir}/node_modules...`);
32
- return du(path.join(dir, "node_modules"));
43
+
44
+ const nodeModules = path.join(dir, "node_modules");
45
+ if (fs.existsSync(nodeModules)) {
46
+ debug(`calculating folder size of ${nodeModules}...`);
47
+ return du(nodeModules);
48
+ } else {
49
+ return 0;
50
+ }
33
51
  }
34
52
 
35
53
  async function costOfModules(dir) {
54
+ if (!hasPackageJson(dir)) {
55
+ return "(no package.json)";
56
+ }
36
57
  process.chdir(dir);
37
- debug(`running cost-of-modules...`);
38
- const out = execSync("npx cost-of-modules --less --no-install").toString();
39
58
 
40
- const PREFIX = "\nCalculating...\n\n\n";
41
- if (out.startsWith(PREFIX)) {
42
- return out.substring(PREFIX.length);
59
+ const pkgJsonPath = path.join(dir, "package.json");
60
+ const pkgJson = require(pkgJsonPath);
61
+ if (!pkgJson.dependencies || isEmptyObject(pkgJson.dependencies)) {
62
+ return "(no production dependencies)";
43
63
  }
44
- return out;
64
+
65
+ debug(`running cost-of-modules...`);
66
+ try {
67
+ const out = execSync("npx cost-of-modules --less --no-install").toString();
68
+ const PREFIX = "\nCalculating...\n\n\n";
69
+ if (out.startsWith(PREFIX)) {
70
+ return out.substring(PREFIX.length);
71
+ }
72
+ return out;
73
+ } catch (e) {
74
+ throw new Error(`cost-of-modules failed with: ${e.stdout ? e.stdout.toString() : ""} ${e.stdout ? e.stderr.toString() : ""}`);
75
+ }
76
+
45
77
  }
46
78
 
47
79
  module.exports = {
48
80
 
49
81
  shouldRun: async function(beforeDir, afterDir) {
50
- return fs.existsSync(path.join(afterDir, "package.json"));
82
+ return hasPackageJson(beforeDir) || hasPackageJson(afterDir);
51
83
  },
52
84
 
53
85
  compare: async function(before, after) {
@@ -58,7 +90,7 @@ module.exports = {
58
90
  return {
59
91
  beforeSize,
60
92
  afterSize,
61
- detailsLabel: "Largest node modules",
93
+ detailsLabel: "Largest production node modules",
62
94
  details
63
95
  };
64
96
  }
@@ -0,0 +1,74 @@
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
+ // comparators/npm_package.js - compares npm package (tarball) size
16
+
17
+ const debug = require("debug")("sizewatcher:node_modules");
18
+ const util = require('util');
19
+ const exec = util.promisify(require('child_process').exec);
20
+ const path = require("path");
21
+ const fs = require("fs");
22
+ const xbytes = require("xbytes");
23
+
24
+ const PACKSIZE_REGEX = /package size: (.*)/;
25
+
26
+ async function getSize(dir) {
27
+ if (!fs.existsSync(path.join(dir, "package.json"))) {
28
+ return {
29
+ size: 0
30
+ };
31
+ }
32
+
33
+ debug(`running npm publish --dry-run inside ${dir}...`);
34
+ let { stderr } = await exec("npm publish --dry-run", { cwd: dir });
35
+
36
+ stderr = stderr.replace(/npm notice /g, "");
37
+
38
+ const found = stderr.match(PACKSIZE_REGEX);
39
+ const size = found ? xbytes.parseSize(found[1].trim()) : 0;
40
+
41
+ return {
42
+ size,
43
+ output: stderr
44
+ };
45
+ }
46
+
47
+ module.exports = {
48
+
49
+ shouldRun: async function(beforeDir, afterDir) {
50
+ const pkgJsonPath = path.join(afterDir, "package.json");
51
+ if (!fs.existsSync(pkgJsonPath)) {
52
+ return false;
53
+ }
54
+
55
+ const pkgJson = require(pkgJsonPath);
56
+ if (pkgJson.private || !pkgJson.version) {
57
+ debug("skpping npm_package: private package or no version in package.json");
58
+ return false;
59
+ }
60
+ return true;
61
+ },
62
+
63
+ compare: async function(before, after) {
64
+ const beforeResult = await getSize(before.dir);
65
+ const afterResult = await getSize(after.dir);
66
+
67
+ return {
68
+ beforeSize: beforeResult.size,
69
+ afterSize: afterResult.size,
70
+ detailsLabel: "Package contents",
71
+ details: afterResult.output
72
+ };
73
+ }
74
+ };
package/lib/compare.js CHANGED
@@ -18,18 +18,28 @@ const debug = require("debug")("sizewatcher");
18
18
  const config = require("./config").get();
19
19
  const COMPARATORS = require('require-dir')("./comparators");
20
20
  const xbytes = require('xbytes');
21
+ const path = require("path");
21
22
 
22
23
  async function runComparator(comparator, before, after, deltas) {
23
- if (await comparator.shouldRun(before.dir, after.dir)) {
24
- console.log(`- ${comparator.name}`);
25
- const delta = await comparator.compare(before, after);
26
- // force same name for consistency with config
27
- delta.name = comparator.name;
28
- delta.config = comparator.config;
29
-
30
- deltas.push(delta);
31
- } else {
32
- debug(`skipping '${comparator.name}' because it detected it should not run`);
24
+ try {
25
+ if (await comparator.shouldRun(before.dir, after.dir)) {
26
+ console.log(`- ${comparator.name}`);
27
+ const delta = await comparator.compare(before, after);
28
+ // force same name for consistency with config
29
+ delta.name = comparator.name;
30
+ delta.config = comparator.config;
31
+
32
+ deltas.push(delta);
33
+ } else {
34
+ debug(`skipping '${comparator.name}' because it detected it should not run`);
35
+ }
36
+ } catch (e) {
37
+ console.error(`comparator ${comparator.name} failed:`, e);
38
+
39
+ deltas.push({
40
+ name: comparator.name,
41
+ error: e
42
+ });
33
43
  }
34
44
  }
35
45
 
@@ -63,10 +73,30 @@ async function compare(before, after) {
63
73
  debug(`skipping '${name}' because disabled in config`);
64
74
  continue;
65
75
  }
66
- comparator.name = name;
67
- comparator.config = comparatorConfig || {};
68
-
69
- await runComparator(comparator, before, after, deltas);
76
+ let dirs = ["."];
77
+ if (comparatorConfig && comparatorConfig.dir) {
78
+ if (Array.isArray(comparatorConfig.dir)) {
79
+ dirs = comparatorConfig.dir;
80
+ } else {
81
+ dirs = [comparatorConfig.dir];
82
+ }
83
+ }
84
+ for (const dir of dirs) {
85
+ if (dir !== ".") {
86
+ comparator.name = `${name} [${dir}]`;
87
+ } else {
88
+ comparator.name = name;
89
+ }
90
+ comparator.config = comparatorConfig || {};
91
+
92
+ await runComparator(comparator, {
93
+ ...before,
94
+ dir: path.join(before.dir, dir)
95
+ }, {
96
+ ...after,
97
+ dir: path.join(after.dir, dir)
98
+ }, deltas);
99
+ }
70
100
  }
71
101
  }
72
102
 
@@ -101,17 +131,32 @@ function process(deltas) {
101
131
  cheers: 0,
102
132
  ok: 0,
103
133
  warn: 0,
104
- fail: 0
134
+ fail: 0,
135
+ error: 0
105
136
  };
106
137
 
107
138
  for (const d of deltas) {
139
+ debug("processing delta:", d);
140
+ if (d.error) {
141
+ summary.error++;
142
+ continue;
143
+ }
108
144
  // merge limit config, comparator-specific one wins over global
109
145
  const limits = {
110
146
  ...config.limits,
111
147
  ...d.config.limits
112
148
  };
113
149
 
114
- const increase = (d.afterSize - d.beforeSize) / d.beforeSize * 100;
150
+ let increase;
151
+ if (d.beforeSize <= 0 && d.afterSize <= 0) {
152
+ increase = 0;
153
+ } else if (d.afterSize <= 0) {
154
+ increase = -100;
155
+ } else if (d.beforeSize <= 0) {
156
+ increase = 100;
157
+ } else {
158
+ increase = (d.afterSize - d.beforeSize) / d.beforeSize * 100;
159
+ }
115
160
  d.increase = increase.toFixed(1);
116
161
 
117
162
  if (exceeds(d.afterSize, increase, limits.fail)) {
@@ -134,7 +179,9 @@ function process(deltas) {
134
179
  }
135
180
 
136
181
  deltas.summary = "ok";
137
- if (summary.fail >= 1) {
182
+ if (summary.error >= 1) {
183
+ deltas.summary = "error";
184
+ } else if (summary.fail >= 1) {
138
185
  deltas.summary = "fail";
139
186
  } else if (summary.warn >= 1) {
140
187
  deltas.summary = "warn";
package/lib/config.js CHANGED
@@ -22,8 +22,8 @@ const CONFIG_FILE = ".sizewatcher.yml";
22
22
 
23
23
  const DEFAULT_CONFIG = {
24
24
  limits: {
25
- fail: "50%",
26
- warn: "10%",
25
+ fail: "100%",
26
+ warn: "30%",
27
27
  ok: "-10%"
28
28
  },
29
29
  report: {
@@ -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.safeLoad(data) || {};
43
43
 
44
44
  config = deepmerge(DEFAULT_CONFIG, loadedConfig);
45
45
 
package/lib/render.js CHANGED
@@ -21,9 +21,20 @@ const ICONS = {
21
21
  cheers: "🎉",
22
22
  ok: "✅",
23
23
  warn: "⚠️",
24
- fail: "❌"
24
+ fail: "❌",
25
+ error: "🚨"
25
26
  };
26
27
 
28
+ const SUMMARY = {
29
+ cheers: "congratulates on the size improvement 📉:",
30
+ ok: "found no problematic size increases.",
31
+ warn: "detected a size increase 📈:",
32
+ fail: "detected a problematic size increase 📈:",
33
+ error: "had a measurement error:"
34
+ };
35
+
36
+ const EXPANDED_CASES = ["cheers", "warn", "fail", "error"];
37
+
27
38
  function indentEachLine(string, indent=" ") {
28
39
  return string.replace(/^(?!\s*$)/gm, indent);
29
40
  }
@@ -34,11 +45,15 @@ function renderAsText(deltas) {
34
45
  text += ` '${deltas.before.branch}' => '${deltas.after.branch}' (sha ${deltas.after.sha})\n\n`;
35
46
 
36
47
  for (const d of deltas) {
37
- text += `+ ${ICONS[d.result]} ${d.name}: ${d.increase}% (${pb(d.beforeSize)} => ${pb(d.afterSize)})\n\n`;
48
+ if (d.error) {
49
+ text += `+ ${ICONS.error} ${d.name}: measurement error: ${d.error.message || d.error}\n`;
50
+ } else {
51
+ text += `+ ${ICONS[d.result]} ${d.name}: ${d.increase}% (${pb(d.beforeSize)} => ${pb(d.afterSize)})\n\n`;
38
52
 
39
- if (d.details) {
40
- text += ` ${d.detailsLabel}:\n\n`;
41
- text += indentEachLine(d.details.trim()) + "\n";
53
+ if (d.details) {
54
+ text += ` ${d.detailsLabel}:\n\n`;
55
+ text += indentEachLine(d.details.trim()) + "\n";
56
+ }
42
57
  }
43
58
  text += `\n`;
44
59
  }
@@ -47,12 +62,20 @@ function renderAsText(deltas) {
47
62
  }
48
63
 
49
64
  function renderAsMarkdown(deltas) {
50
- let markdown = `📈 [Sizewatcher](https://github.com/adobe/sizewatcher) measured these changes:\n\n`;
65
+ const summaryState = deltas.summary || "ok";
66
+ const detailState = EXPANDED_CASES.includes(summaryState) ? "open" : "";
67
+
68
+ let markdown = `<details ${detailState}><summary>${ICONS[summaryState]} <a href="https://github.com/adobe/sizewatcher">Sizewatcher</a> ${SUMMARY[summaryState]}</summary>\n`;
69
+ markdown += `<p><blockquote>\n\n`;
51
70
 
52
71
  for (const d of deltas) {
53
72
  let change;
54
73
 
55
- if (d.increase >= 0.1) {
74
+ if (d.error) {
75
+ d.result = "error";
76
+ change = `measurement error: ${d.error.message || d.error}`;
77
+
78
+ } else if (d.increase >= 0.1) {
56
79
  change = `<b>+${d.increase}%</b> (${pb(d.beforeSize)} => ${pb(d.afterSize)})`;
57
80
 
58
81
  } else if (d.increase <= -0.1) {
@@ -63,22 +86,30 @@ function renderAsMarkdown(deltas) {
63
86
 
64
87
  }
65
88
 
66
- markdown += `<details>\n<summary>${ICONS[d.result]} <code>${d.name}</code> ${change}</summary>\n`;
67
89
  if (d.details) {
68
- markdown += `<br>\n\n${d.detailsLabel}:\n\`\`\`\n${d.details.trim()}\n\`\`\`\n`;
90
+ markdown += `<details><summary>`;
91
+ } else {
92
+ markdown += "&nbsp;&nbsp;&nbsp;";
93
+ }
94
+ markdown += `${ICONS[d.result]} <code>${d.name}</code> ${change}`;
95
+ if (d.details) {
96
+ markdown += `</summary><br>${d.detailsLabel}:<pre>${d.details.trim().replace(/\n/g, "<br>")}</pre></details>`;
69
97
  }
70
98
 
71
- markdown += `</details>\n\n`;
99
+ markdown += `\n\n`;
72
100
  }
73
101
 
74
- markdown += `<details>\n<summary>Notes</summary>\n<br>\n\n`;
102
+ markdown += `<details><summary>Notes</summary><br>\n\n`;
75
103
  markdown += `- PR branch: \`${deltas.after.branch}\` @ ${deltas.after.sha}\n`;
76
104
  markdown += `- Base branch: \`${deltas.before.branch}\`\n`;
77
105
  markdown += `- Sizewatcher v${require('../package.json').version}\n`;
78
- markdown += `- Configuration\n`;
79
- markdown += `<pre>\n${config.asYaml()}</pre>`;
106
+ markdown += `- Effective Configuration:\n`;
107
+ markdown += `\n\`\`\`yaml\n${config.asYaml()}\`\`\`\n`;
80
108
  markdown += `</details>\n\n`;
81
109
 
110
+ markdown += `</blockquote></p>\n`;
111
+ markdown += `</details>\n`;
112
+
82
113
  return markdown;
83
114
  }
84
115
 
@@ -0,0 +1,74 @@
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
+ // main cli entry point
16
+
17
+ const debug = require("debug")("sizewatcher");
18
+ const gitCheckoutBeforeAndAfter = require("./checkout");
19
+ const compare = require("./compare");
20
+ const report = require("./report");
21
+ const Git = require("simple-git");
22
+
23
+ function printUsage() {
24
+ console.error(`Usage: sizewatcher [<options>] [<before> [<after>]]`);
25
+ console.error();
26
+ console.error("Arguments:");
27
+ console.error(" <before> Before branch/commit for comparison. Defaults to default branch or main/master.");
28
+ console.error(" <after> After branch/commit for comparison. Defaults to current branch.");
29
+ console.error();
30
+ console.error("Options:");
31
+ console.error(" -h Show help");
32
+ }
33
+
34
+ async function sizewatcher(argv) {
35
+ try {
36
+ if (!argv) argv = [];
37
+
38
+ // yes, this should probably use a framework like oclif or yargs.
39
+ // but cli arguments are very limited now and so manual handling is enough
40
+ if (argv[0] === "-h") {
41
+ printUsage();
42
+ return process.exit(1);
43
+ }
44
+
45
+ debug(`Running inside ${process.cwd()}`);
46
+
47
+ if (!await Git().checkIsRepo()) {
48
+ throw new Error(`Not inside a git checkout: ${process.cwd()}`);
49
+ }
50
+
51
+ console.log(`Checking out git branches...`);
52
+ const { before, after } = await gitCheckoutBeforeAndAfter(process.cwd(), argv[0], argv[1]);
53
+
54
+ if (before.branch === after.branch) {
55
+ console.log(`Branches are identical, nothing to compare (${before.branch}=${after.branch}). Compare selected branches using 'sizewatcher <before> <after>'.`);
56
+ return process.exit();
57
+ }
58
+
59
+ console.log(`Comparing changes from '${before.branch}' to '${after.branch}'\n`);
60
+
61
+ const deltas = await compare(before, after);
62
+
63
+ await report(deltas);
64
+
65
+ console.log("Done. Cleaning up...");
66
+
67
+ } catch (e) {
68
+ debug(e);
69
+ console.error("Error:", e.message || e);
70
+ return process.exit(1);
71
+ }
72
+ }
73
+
74
+ module.exports = sizewatcher;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/sizewatcher",
3
- "version": "1.0.0",
3
+ "version": "1.2.1",
4
4
  "description": "Warns if your pull requests introduce large size increases.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -30,17 +30,19 @@
30
30
  "eslint": "^7.1.0",
31
31
  "mocha": "^7.2.0",
32
32
  "mock-fs": "^4.13.0",
33
- "nyc": "^15.1.0"
33
+ "nyc": "^15.1.0",
34
+ "supports-color": "^8.1.1"
34
35
  },
35
36
  "dependencies": {
36
37
  "@octokit/rest": "^18.0.3",
37
38
  "debug": "^4.1.1",
38
39
  "deepmerge": "^4.2.2",
39
40
  "du": "^1.0.0",
41
+ "glob": "^7.1.6",
40
42
  "js-yaml": "^3.14.0",
41
43
  "pretty-bytes": "^5.3.0",
42
44
  "require-dir": "^1.2.0",
43
- "simple-git": "^2.5.0",
45
+ "simple-git": "^3.7.1",
44
46
  "tmp": "^0.2.1",
45
47
  "xbytes": "^1.6.2"
46
48
  }