Compare commits
95 commits
Author | SHA1 | Date | |
---|---|---|---|
|
5486566b59 | ||
|
993fc1df07 | ||
|
241b69de25 | ||
77083da638 | |||
f591e1b968 | |||
|
3c8cb50170 | ||
|
add6f86f0a | ||
2d35bfd890 | |||
|
e4e90d4f8d | ||
|
e6263de79e | ||
|
7e2e8b2bd1 | ||
38cd4289e9 | |||
0a2e11a67e | |||
|
0f82bfbdb5 | ||
|
143d281752 | ||
|
827af7d2f2 | ||
|
bebb222c26 | ||
|
58e2c74a4d | ||
|
2ec9c0e533 | ||
|
548213968a | ||
|
86e6d47adf | ||
|
60d7b5775b | ||
|
606cb4d607 | ||
|
6817fe9783 | ||
|
5b34811a79 | ||
|
d14e47274f | ||
|
3e61c3adb8 | ||
|
3ca6748593 | ||
|
a65f0371e2 | ||
|
14be95d0ac | ||
|
fbb4bf5260 | ||
|
19cafbf7f9 | ||
|
5c1ec76feb | ||
|
5e1dec842a | ||
|
72673970c0 | ||
|
5446dc4df2 | ||
|
da74b4d9fb | ||
|
8275f7674e | ||
|
ac1adaa22f | ||
|
401196d62a | ||
|
034f4e4089 | ||
|
0d39f4b681 | ||
|
32308e8f02 | ||
|
d2be716818 | ||
|
4efe5230ca | ||
|
79ee5b8347 | ||
|
3f93c82674 | ||
|
e2e7d4f837 | ||
|
5609d92832 | ||
|
3eca41cb99 | ||
|
d616d38486 | ||
|
24e5404cae | ||
|
b845124433 | ||
|
7ad46696f9 | ||
|
58a6956f92 | ||
|
31d76f17f6 | ||
|
b8eeb40145 | ||
|
94a8011214 | ||
|
dd139e659d | ||
|
d0fd8358be | ||
|
923f9865f1 | ||
|
227bae7321 | ||
|
36c458e744 | ||
|
97eff14aa5 | ||
|
3bbff855b4 | ||
|
f4e457cdc7 | ||
|
23cde8502d | ||
|
c4b5617c56 | ||
|
520728bff5 | ||
|
3dbf478f6d | ||
|
13f198b50d | ||
|
cb69857691 | ||
|
ccfe9e034b | ||
|
71e4d93140 | ||
|
b7afc6fb85 | ||
|
9782699ff0 | ||
|
e4fd9b385e | ||
|
ef9e514fed | ||
|
13b773a0f5 | ||
|
429fbe5e60 | ||
|
3deaa55445 | ||
|
37ca97aa26 | ||
|
0f80d8db06 | ||
|
77c458c20c | ||
|
54ece0f127 | ||
|
a932a81300 | ||
|
f2dafe15ec | ||
|
5e77ce11c8 | ||
|
bb42bdd362 | ||
|
f264079e33 | ||
|
1839184785 | ||
|
278f86e5cd | ||
|
7d120b14c0 | ||
|
98686a319d | ||
|
6b94792932 |
29 changed files with 5427 additions and 8749 deletions
|
@ -25,7 +25,8 @@
|
||||||
"profile": "http://msrose.github.io",
|
"profile": "http://msrose.github.io",
|
||||||
"contributions": [
|
"contributions": [
|
||||||
"infra",
|
"infra",
|
||||||
"doc"
|
"doc",
|
||||||
|
"code"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
extends: [
|
|
||||||
"@tophat/eslint-config/base",
|
|
||||||
"@tophat/eslint-config/jest",
|
|
||||||
]
|
|
||||||
}
|
|
119
.github/workflows/nodejs.yml
vendored
119
.github/workflows/nodejs.yml
vendored
|
@ -1,71 +1,102 @@
|
||||||
name: packwatch CI
|
name: packwatch CI
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
dependencies:
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Use Node.js 12.x
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 12.x
|
|
||||||
- run: yarn
|
|
||||||
- run: yarn lint
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [10.x, 12.x]
|
node-version: [14, 16, 18]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- uses: actions/setup-node@v3
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: yarn
|
- uses: actions/cache@v3
|
||||||
- run: yarn test:coverage
|
id: dependencies-cache
|
||||||
|
env:
|
||||||
|
cache-name: dependencies-cache
|
||||||
|
with:
|
||||||
|
path: .yarn
|
||||||
|
key: ${{ runner.os }}-build-${{env.cache-name}}-${{ hashFiles('**/yarn.lock') }}-node-${{ matrix.node-version }}
|
||||||
|
- if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
|
||||||
|
run: yarn
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: dependencies
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
id: dependencies-cache
|
||||||
|
env:
|
||||||
|
cache-name: dependencies-cache
|
||||||
|
with:
|
||||||
|
path: .yarn
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}-node-18
|
||||||
|
- run: |
|
||||||
|
yarn
|
||||||
|
yarn lint
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: dependencies
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [14, 16, 18]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
id: dependencies-cache
|
||||||
|
env:
|
||||||
|
cache-name: dependencies-cache
|
||||||
|
with:
|
||||||
|
path: .yarn
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}-node-${{ matrix.node-version }}
|
||||||
|
- run: |
|
||||||
|
yarn
|
||||||
|
yarn test:coverage
|
||||||
- name: Coverage
|
- name: Coverage
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: false
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: test
|
needs: dependencies
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [10.x, 12.x]
|
node-version: [14, 16, 18]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: yarn --frozen-lockfile
|
- uses: actions/cache@v3
|
||||||
- run: yarn build
|
id: dependencies-cache
|
||||||
release:
|
|
||||||
name: Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [build, lint, test]
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v1
|
|
||||||
- name: Node setup
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 12
|
|
||||||
- name: Prepare
|
|
||||||
run: yarn && yarn build
|
|
||||||
- name: Release
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
cache-name: dependencies-cache
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
with:
|
||||||
run: yarn semantic-release
|
path: .yarn
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}-node-${{ matrix.node-version }}
|
||||||
|
- run: |
|
||||||
|
yarn
|
||||||
|
yarn build
|
||||||
|
|
30
.github/workflows/release.yml
vendored
Normal file
30
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
name: packwatch release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build, lint, test]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Node setup
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 14
|
||||||
|
- name: Prepare
|
||||||
|
run: yarn && yarn build
|
||||||
|
- name: get tag
|
||||||
|
id: get_tag
|
||||||
|
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/v}
|
||||||
|
- name: Release
|
||||||
|
env:
|
||||||
|
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
run: yarn publish --access public --tag latest --new-version ${{steps.get_tag.outputs.tag}}
|
||||||
|
|
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -6,6 +6,14 @@ yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Yarntifacts
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/versions
|
||||||
|
!.yarn/sdks
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
@ -102,3 +110,5 @@ dist
|
||||||
|
|
||||||
# TernJS port file
|
# TernJS port file
|
||||||
.tern-port
|
.tern-port
|
||||||
|
|
||||||
|
*.sw[a-z]
|
||||||
|
|
2
.nvmrc
2
.nvmrc
|
@ -1 +1 @@
|
||||||
12
|
18.15.0
|
||||||
|
|
873
.yarn/releases/yarn-3.4.1.cjs
vendored
Executable file
873
.yarn/releases/yarn-3.4.1.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
yarnPath: .yarn/releases/yarn-3.4.1.cjs
|
1
.yvmrc
1
.yvmrc
|
@ -1 +0,0 @@
|
||||||
1.22.0
|
|
31
BEST_PRACTICES.md
Normal file
31
BEST_PRACTICES.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Best Practices: tips and tricks to use Packwatch to its fullest
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Monitoring the footprint of the packages you publish can be a powerful first step towards being producing efficient, trim code that doesn't use more resources that it should and that is a no-brainer to adopt. This document compiles some suggestions on how to get the most out of packwatch for your project.
|
||||||
|
|
||||||
|
Got best practices questions or suggestions? Open an [issue](https://github.com/mcataford/packwatch/issues)!
|
||||||
|
|
||||||
|
### First-run
|
||||||
|
|
||||||
|
On the first-run, Packwatch will generate a manifest file that sets the package size and the limit to the same value. Following this, the run will return a non-zero status code. This is perfectly normal! This is meant to ensure that an initial run or a run without a manifest won't pass on CI. The next time you will run Packwatch, it will use the now-present manifest as a comparison point and proceed normally.
|
||||||
|
|
||||||
|
### Understanding `.packwatch.json`
|
||||||
|
|
||||||
|
The `.packwatch.json` file persisted in your project keeps track of the last reported package size that was commited to your version control system. At the moment, it consists of three keys:
|
||||||
|
|
||||||
|
- `packageSize`, representing the size of your package's archive as it is when packed using `npm pack`;
|
||||||
|
- `unpackedSize`, representing the _unpacked_ size of that package (i.e. once installed, what space do the published filed occupy on disk);
|
||||||
|
- `limit`, representing a threshold that will cause Packwatch to throw an error if crossed.
|
||||||
|
|
||||||
|
While the `packageSize` and `unpackedSize` are automatically populated when you update Packwatch's manifest, the `limit` value can be set manually so that you leave yourself some "head room" for growth.
|
||||||
|
|
||||||
|
#### Setting sensible thresholds
|
||||||
|
|
||||||
|
When Packwatch initializes its manifest, it will initialize the `limit` parameter to be equal to `packedSize` so that any increase in size will cause a failure. Once you determine what kind of growth you want to allow for, you can edit the manifest to increase the limit.__ Any automatic updates to the manifest will leave the `limit` value as-is__.
|
||||||
|
|
||||||
|
Setting a sensible limit is essential to avoiding "packwatch fatigue", a scenario in which packwatch fails every time your package size grows because the `limit` parameter is too close to the `packageSize` value. Usually, __having a limit that is more or less 5 kB above the `packageSize` will allow for growth while still preventing bloat to sneak in__. From there, you can readjust the `limit` value as your project grows so that the "head room" space between `packageSize` and `limit` stays adequate.
|
||||||
|
|
||||||
|
### Integrating Packwatch into your workflow
|
||||||
|
|
||||||
|
In order to monitor your project's growth accurately, it's preferrable to have Packwatch run both in your pre-commit hooks and CI pipeline. This way, you can catch bloat before commits are event pushed up and you can get another chance to catch undesired overgrowth before you merge in change bundles. This will also act as a reminder to keep the manifest up to date so that it doesn't go out sync.
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
[![codecov](https://codecov.io/gh/mcataford/packwatch/branch/master/graph/badge.svg)](https://codecov.io/gh/mcataford/packwatch)
|
[![codecov](https://codecov.io/gh/mcataford/packwatch/branch/master/graph/badge.svg)](https://codecov.io/gh/mcataford/packwatch)
|
||||||
![packwatch CI](https://github.com/mcataford/packwatch/workflows/packwatch%20CI/badge.svg)
|
![packwatch CI](https://github.com/mcataford/packwatch/workflows/packwatch%20CI/badge.svg)
|
||||||
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
@ -46,6 +45,8 @@ packwatch --update-manifest
|
||||||
|
|
||||||
Just commit your `.packwatch.json` manifest and you're good to go!
|
Just commit your `.packwatch.json` manifest and you're good to go!
|
||||||
|
|
||||||
|
Check out [Packwatch's best practices tips and tricks](https://github.com/mcataford/packwatch/blob/master/BEST_PRACTICES.md) for some advice on how to make the most of it!
|
||||||
|
|
||||||
## Contributors ✨
|
## Contributors ✨
|
||||||
|
|
||||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||||
|
@ -56,7 +57,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center"><a href="https://mcataford.github.io"><img src="https://avatars2.githubusercontent.com/u/6210361?v=4" width="100px;" alt=""/><br /><sub><b>Marc Cataford</b></sub></a><br /><a href="#ideas-mcataford" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/mcataford/packwatch/commits?author=mcataford" title="Code">💻</a> <a href="#infra-mcataford" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/mcataford/packwatch/commits?author=mcataford" title="Tests">⚠️</a> <a href="https://github.com/mcataford/packwatch/commits?author=mcataford" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://mcataford.github.io"><img src="https://avatars2.githubusercontent.com/u/6210361?v=4" width="100px;" alt=""/><br /><sub><b>Marc Cataford</b></sub></a><br /><a href="#ideas-mcataford" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/mcataford/packwatch/commits?author=mcataford" title="Code">💻</a> <a href="#infra-mcataford" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/mcataford/packwatch/commits?author=mcataford" title="Tests">⚠️</a> <a href="https://github.com/mcataford/packwatch/commits?author=mcataford" title="Documentation">📖</a></td>
|
||||||
<td align="center"><a href="http://msrose.github.io"><img src="https://avatars3.githubusercontent.com/u/3495264?v=4" width="100px;" alt=""/><br /><sub><b>Michael Rose</b></sub></a><br /><a href="#infra-msrose" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/mcataford/packwatch/commits?author=msrose" title="Documentation">📖</a></td>
|
<td align="center"><a href="http://msrose.github.io"><img src="https://avatars3.githubusercontent.com/u/3495264?v=4" width="100px;" alt=""/><br /><sub><b>Michael Rose</b></sub></a><br /><a href="#infra-msrose" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/mcataford/packwatch/commits?author=msrose" title="Documentation">📖</a> <a href="https://github.com/mcataford/packwatch/commits?author=msrose" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -64,4 +65,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"comments": false,
|
|
||||||
"ignore": ["**/*.test.js"],
|
|
||||||
"presets": [["@babel/preset-env", { "targets": { "node": 10 } }]]
|
|
||||||
}
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
transformIgnorePatterns: ['.test.js'],
|
preset: 'ts-jest',
|
||||||
}
|
testEnvironment: 'node',
|
||||||
|
};
|
46
package.json
46
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "packwatch",
|
"name": "packwatch",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"main": "dist/index.js",
|
"main": "dist/cli.js",
|
||||||
"description": "📦👀 Keep an eye on your packages' footprint",
|
"description": "📦👀 Keep an eye on your packages' footprint",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"npm",
|
"npm",
|
||||||
|
@ -19,41 +19,31 @@
|
||||||
"author": "Marc Cataford <c.marcandre@gmail.com>",
|
"author": "Marc Cataford <c.marcandre@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
"files": [
|
||||||
"dist/**/*.js"
|
"dist/*.js"
|
||||||
],
|
],
|
||||||
"bin": "./dist/index.js",
|
"bin": "./dist/cli.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/mcataford/packwatch.git"
|
"url": "https://github.com/mcataford/packwatch.git"
|
||||||
},
|
},
|
||||||
"pre-commit": [
|
|
||||||
"lint",
|
|
||||||
"test"
|
|
||||||
],
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "rimraf dist",
|
"prepack": "yarn build",
|
||||||
"build": "babel src -d dist",
|
"prebuild": "rm -rf dist",
|
||||||
"lint": "eslint src *.js",
|
"build": "tsc --project .",
|
||||||
"lint:fix": "yarn lint --fix",
|
"lint": "yarn rome format src tests && yarn rome check src tests",
|
||||||
"test": "jest src",
|
"lint:fix": "yarn rome format src tests --write && yarn rome check src tests --apply",
|
||||||
|
"test": "jest tests",
|
||||||
"test:watch": "yarn test --watchAll",
|
"test:watch": "yarn test --watchAll",
|
||||||
"test:coverage": "yarn test --coverage"
|
"test:coverage": "yarn test --coverage",
|
||||||
|
"types": "tsc --noEmit src/**/*.ts",
|
||||||
|
"types:watch": "yarn types --watch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.8.4",
|
"@types/jest": "^29.5.0",
|
||||||
"@babel/core": "^7.8.6",
|
"@types/node": "^18.15.5",
|
||||||
"@babel/preset-env": "^7.8.7",
|
"jest": "^29.5.0",
|
||||||
"@tophat/eslint-config": "^0.6.0",
|
"rome": "^12.0.0",
|
||||||
"eslint": "^6.8.0",
|
"ts-jest": "^29.0.5",
|
||||||
"eslint-config-prettier": "^6.10.0",
|
"typescript": "^4.3.0"
|
||||||
"eslint-plugin-import": "^2.20.1",
|
|
||||||
"eslint-plugin-jest": "^23.8.0",
|
|
||||||
"eslint-plugin-prettier": "^3.1.2",
|
|
||||||
"jest": "^25.1.0",
|
|
||||||
"mock-fs": "^4.11.0",
|
|
||||||
"pre-commit": "^1.2.2",
|
|
||||||
"prettier": "^1.19.1",
|
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"semantic-release": "^17.0.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
5
renovate.json
Normal file
5
renovate.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"config:base"
|
||||||
|
]
|
||||||
|
}
|
13
rome.json
Normal file
13
rome.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"lineWidth": 120
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"semicolons": "asNeeded",
|
||||||
|
"quoteStyle": "single"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
9
src/cli.ts
Normal file
9
src/cli.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import packwatch from '.'
|
||||||
|
|
||||||
|
const isUpdatingManifest = process.argv.includes('--update-manifest')
|
||||||
|
const cwd = process.cwd()
|
||||||
|
packwatch({ cwd, isUpdatingManifest })
|
||||||
|
.catch(() => process.exit(1))
|
||||||
|
.then(() => process.exit(0))
|
|
@ -1,73 +0,0 @@
|
||||||
const { spawnSync } = require('child_process')
|
|
||||||
const { readFileSync, writeFileSync } = require('fs')
|
|
||||||
|
|
||||||
const PACKAGE_SIZE_PATT = /package size:\s+([0-9]+\.?[0-9]*\s+[A-Za-z]+)/g
|
|
||||||
const UNPACKED_SIZE_PATT = /unpacked size:\s+([0-9]+\.?[0-9]*\s+[A-Za-z]+)/g
|
|
||||||
const SIZE_SUFFIX_PATT = /([A-Za-z]+)/
|
|
||||||
const SIZE_MAGNITUDE_PATT = /([0-9]+\.?[0-9]*)/
|
|
||||||
const MANIFEST_FILENAME = '.packwatch.json'
|
|
||||||
|
|
||||||
const FS_OPTIONS = { encoding: 'utf-8' }
|
|
||||||
|
|
||||||
function convertSizeToBytes(sizeString) {
|
|
||||||
const sizeSuffix = SIZE_SUFFIX_PATT.exec(sizeString)[1]
|
|
||||||
const sizeMagnitude = SIZE_MAGNITUDE_PATT.exec(sizeString)[1]
|
|
||||||
|
|
||||||
let multiplier = 1
|
|
||||||
|
|
||||||
if (sizeSuffix === 'kB') multiplier = 1000
|
|
||||||
else if (sizeSuffix === 'mB') {
|
|
||||||
multiplier = 1000000
|
|
||||||
}
|
|
||||||
|
|
||||||
return multiplier * parseFloat(sizeMagnitude)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentPackageStats() {
|
|
||||||
const { stderr } = spawnSync('npm', ['pack', '--dry-run'], FS_OPTIONS)
|
|
||||||
const packageSize = PACKAGE_SIZE_PATT.exec(stderr)[1]
|
|
||||||
const unpackedSize = UNPACKED_SIZE_PATT.exec(stderr)[1]
|
|
||||||
|
|
||||||
return {
|
|
||||||
packageSize,
|
|
||||||
unpackedSize,
|
|
||||||
packageSizeBytes: convertSizeToBytes(packageSize),
|
|
||||||
unpackedSizeBytes: convertSizeToBytes(unpackedSize),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPreviousPackageStats() {
|
|
||||||
try {
|
|
||||||
const currentManifest = readFileSync(MANIFEST_FILENAME, FS_OPTIONS)
|
|
||||||
const parsedManifest = JSON.parse(currentManifest)
|
|
||||||
return {
|
|
||||||
...parsedManifest,
|
|
||||||
packageSizeBytes: convertSizeToBytes(parsedManifest.packageSize),
|
|
||||||
unpackedSizeBytes: convertSizeToBytes(parsedManifest.unpackedSize),
|
|
||||||
limitBytes: convertSizeToBytes(parsedManifest.limit),
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createOrUpdateManifest({ previous, current, updateLimit = false }) {
|
|
||||||
const { limit } = previous || {}
|
|
||||||
const { packageSize, unpackedSize } = current
|
|
||||||
|
|
||||||
const newManifest = {
|
|
||||||
limit: updateLimit ? packageSize : limit || packageSize,
|
|
||||||
packageSize: packageSize,
|
|
||||||
unpackedSize: unpackedSize,
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFileSync(MANIFEST_FILENAME, JSON.stringify(newManifest))
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createOrUpdateManifest,
|
|
||||||
getPreviousPackageStats,
|
|
||||||
getCurrentPackageStats,
|
|
||||||
convertSizeToBytes,
|
|
||||||
MANIFEST_FILENAME,
|
|
||||||
}
|
|
100
src/index.js
100
src/index.js
|
@ -1,100 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const { existsSync } = require('fs')
|
|
||||||
|
|
||||||
const {
|
|
||||||
MANIFEST_FILENAME,
|
|
||||||
getCurrentPackageStats,
|
|
||||||
getPreviousPackageStats,
|
|
||||||
createOrUpdateManifest,
|
|
||||||
} = require('./helpers')
|
|
||||||
|
|
||||||
if (!existsSync('package.json')) {
|
|
||||||
console.log(
|
|
||||||
'🤔 There is no package.json file here. Are you in the root directory of your project?',
|
|
||||||
)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUpdatingManifest = process.argv.includes('--update-manifest')
|
|
||||||
|
|
||||||
const currentStats = getCurrentPackageStats()
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If there is no manifest file yet, we can use the current package stats as
|
|
||||||
* a base to build one. The current package size becomes the limit.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!existsSync(MANIFEST_FILENAME)) {
|
|
||||||
createOrUpdateManifest({ current: currentStats })
|
|
||||||
console.log(
|
|
||||||
`📝 No Manifest to compare against! Current package stats written to ${MANIFEST_FILENAME}!`,
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
`Package size (${currentStats.packageSize}) adopted as new limit.`,
|
|
||||||
)
|
|
||||||
// If the update flag wasn't specified, exit with a non-zero code so we
|
|
||||||
// don't "accidentally" pass CI builds if the manifest didn't exist
|
|
||||||
process.exit(isUpdatingManifest ? 0 : 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousStats = getPreviousPackageStats()
|
|
||||||
const { packageSizeBytes, packageSize } = currentStats
|
|
||||||
const {
|
|
||||||
packageSize: previousSize,
|
|
||||||
packageSizeBytes: previousSizeBytes,
|
|
||||||
limit,
|
|
||||||
limitBytes,
|
|
||||||
} = previousStats
|
|
||||||
const hasExceededLimit = packageSizeBytes > limitBytes
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If we are updating the manifest, we can write right away and terminate.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (isUpdatingManifest) {
|
|
||||||
createOrUpdateManifest({
|
|
||||||
previous: previousStats,
|
|
||||||
current: currentStats,
|
|
||||||
updateLimit: true,
|
|
||||||
})
|
|
||||||
console.log(
|
|
||||||
`📝 Updated the manifest! Package size: ${packageSize}, Limit: ${packageSize}`,
|
|
||||||
)
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If there is a manifest file and the current package busts its limit
|
|
||||||
* we signal it and terminate with an error.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (hasExceededLimit) {
|
|
||||||
console.log(
|
|
||||||
`🔥🔥📦🔥🔥 Your package exceeds the limit set in ${MANIFEST_FILENAME}! ${packageSize} > ${limit}`,
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
'Either update the limit by using the --update-manifest flag or trim down your packed files!',
|
|
||||||
)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If there is a manifest file and the limit is not busted, we give
|
|
||||||
* the user some feedback on how the current package compares with
|
|
||||||
* the previous one.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (packageSizeBytes > previousSizeBytes) {
|
|
||||||
console.log(
|
|
||||||
`📦 👀 Your package grew! ${packageSize} > ${previousSize} (Limit: ${limit})`,
|
|
||||||
)
|
|
||||||
} else if (packageSizeBytes < previousSizeBytes) {
|
|
||||||
console.log(
|
|
||||||
`📦 💯 Your package shrank! ${packageSize} < ${previousSize} (Limit: ${limit})`,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`📦 Nothing to report! Your package is the same size as the latest manifest reports! (Limit: ${limit})`,
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,177 +0,0 @@
|
||||||
const childProcess = require('child_process')
|
|
||||||
const { readFileSync } = require('fs')
|
|
||||||
|
|
||||||
const mockFS = require('mock-fs')
|
|
||||||
|
|
||||||
jest.mock('child_process')
|
|
||||||
childProcess.spawnSync = jest.fn(() => ({ stderr: mockPackOutput }))
|
|
||||||
|
|
||||||
const {
|
|
||||||
MANIFEST_FILENAME,
|
|
||||||
convertSizeToBytes,
|
|
||||||
getCurrentPackageStats,
|
|
||||||
getPreviousPackageStats,
|
|
||||||
createOrUpdateManifest,
|
|
||||||
} = require('./helpers')
|
|
||||||
|
|
||||||
const mockPackageSize = '1.1 kB'
|
|
||||||
const mockUnpackedSize = '9000 kB'
|
|
||||||
|
|
||||||
const mockPackOutput = `
|
|
||||||
npm notice
|
|
||||||
npm notice 📦 footprint@0.0.0
|
|
||||||
npm notice === Tarball Contents ===
|
|
||||||
npm notice 732B package.json
|
|
||||||
npm notice 1.8kB dist/helpers.js
|
|
||||||
npm notice 1.9kB dist/index.js
|
|
||||||
npm notice === Tarball Details ===
|
|
||||||
npm notice name: footprint
|
|
||||||
npm notice version: 0.0.0
|
|
||||||
npm notice filename: footprint-0.0.0.tgz
|
|
||||||
npm notice package size: ${mockPackageSize}
|
|
||||||
npm notice unpacked size: ${mockUnpackedSize}
|
|
||||||
npm notice shasum: bdf33d471543cd8126338a82a27b16a9010b8dbd
|
|
||||||
npm notice integrity: sha512-ZZvTg9GVcJw8J[...]bkE0xlqQhlt4Q==
|
|
||||||
npm notice total files: 3
|
|
||||||
npm notice
|
|
||||||
`
|
|
||||||
describe('Helpers', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockFS.restore()
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(mockFS.restore)
|
|
||||||
|
|
||||||
describe('Size string conversion', () => {
|
|
||||||
it.each`
|
|
||||||
sizeString | expectedValue
|
|
||||||
${'1 B'} | ${1}
|
|
||||||
${'1.1 B'} | ${1.1}
|
|
||||||
${'1 kB'} | ${1000}
|
|
||||||
${'1.1kB'} | ${1100}
|
|
||||||
${'1 mB'} | ${1000000}
|
|
||||||
${'1.1 mB'} | ${1100000}
|
|
||||||
`(
|
|
||||||
'converts $sizeString properly to $expectedValue bytes',
|
|
||||||
({ sizeString, expectedValue }) => {
|
|
||||||
expect(convertSizeToBytes(sizeString)).toEqual(expectedValue)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Current package statistics', () => {
|
|
||||||
it('constructs the current package report properly', () => {
|
|
||||||
const packageSizeBytes = 1100
|
|
||||||
const unpackedSizeBytes = 9000000
|
|
||||||
|
|
||||||
expect(getCurrentPackageStats()).toEqual({
|
|
||||||
packageSize: mockPackageSize,
|
|
||||||
packageSizeBytes,
|
|
||||||
unpackedSize: mockUnpackedSize,
|
|
||||||
unpackedSizeBytes,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Previous package statistics', () => {
|
|
||||||
it('constructs the previous package report properly', () => {
|
|
||||||
const packageSize = '0.9 kB'
|
|
||||||
const packageSizeBytes = 900
|
|
||||||
const unpackedSize = '90 kB'
|
|
||||||
const unpackedSizeBytes = 90000
|
|
||||||
const limit = '1 kB'
|
|
||||||
const limitBytes = 1000
|
|
||||||
const mockReport = { packageSize, unpackedSize, limit }
|
|
||||||
mockFS({ [MANIFEST_FILENAME]: JSON.stringify(mockReport) })
|
|
||||||
|
|
||||||
expect(getPreviousPackageStats()).toEqual({
|
|
||||||
packageSize,
|
|
||||||
packageSizeBytes,
|
|
||||||
unpackedSize,
|
|
||||||
unpackedSizeBytes,
|
|
||||||
limit,
|
|
||||||
limitBytes,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns an empty manifest if it fails to reads the manifest file', () => {
|
|
||||||
mockFS({
|
|
||||||
[MANIFEST_FILENAME]: 'not valid JSON',
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(getPreviousPackageStats()).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Creating or updating the manifest', () => {
|
|
||||||
const currentStats = {
|
|
||||||
packageSize: '1 kB',
|
|
||||||
unpackedSize: '10 kB',
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousManifest = {
|
|
||||||
limit: '2 kB',
|
|
||||||
packageSize: '1.5 kB',
|
|
||||||
}
|
|
||||||
it('creates a anifest from the current data if no previous data is provided', () => {
|
|
||||||
mockFS({})
|
|
||||||
|
|
||||||
createOrUpdateManifest({ current: currentStats })
|
|
||||||
|
|
||||||
const writtenManifest = readFileSync(MANIFEST_FILENAME, {
|
|
||||||
encoding: 'utf-8',
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(JSON.parse(writtenManifest)).toEqual({
|
|
||||||
packageSize: currentStats.packageSize,
|
|
||||||
unpackedSize: currentStats.unpackedSize,
|
|
||||||
limit: currentStats.packageSize,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('updates the previous manifest sizes if previous data exists', () => {
|
|
||||||
mockFS({
|
|
||||||
[MANIFEST_FILENAME]: JSON.stringify(previousManifest),
|
|
||||||
})
|
|
||||||
|
|
||||||
createOrUpdateManifest({
|
|
||||||
current: currentStats,
|
|
||||||
previous: previousManifest,
|
|
||||||
updateLimit: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const writtenManifest = readFileSync(MANIFEST_FILENAME, {
|
|
||||||
encoding: 'utf-8',
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(JSON.parse(writtenManifest)).toEqual({
|
|
||||||
packageSize: currentStats.packageSize,
|
|
||||||
unpackedSize: currentStats.unpackedSize,
|
|
||||||
limit: previousManifest.limit,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('updates the previous manifest sizes and limit if previous data exists and updateLimit is set', () => {
|
|
||||||
mockFS({
|
|
||||||
[MANIFEST_FILENAME]: JSON.stringify(previousManifest),
|
|
||||||
})
|
|
||||||
|
|
||||||
createOrUpdateManifest({
|
|
||||||
current: currentStats,
|
|
||||||
previous: previousManifest,
|
|
||||||
updateLimit: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const writtenManifest = readFileSync(MANIFEST_FILENAME, {
|
|
||||||
encoding: 'utf-8',
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(JSON.parse(writtenManifest)).toEqual({
|
|
||||||
packageSize: currentStats.packageSize,
|
|
||||||
unpackedSize: currentStats.unpackedSize,
|
|
||||||
limit: currentStats.packageSize,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
87
src/index.ts
Normal file
87
src/index.ts
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { join, resolve } from 'path'
|
||||||
|
|
||||||
|
import { assertInPackageRoot } from './invariants'
|
||||||
|
import logger from './logger'
|
||||||
|
import { createOrUpdateManifest, getCurrentPackageStats, getPreviousPackageStats, mergeDefaultArguments } from './utils'
|
||||||
|
|
||||||
|
import type { PackwatchArguments } from './types'
|
||||||
|
|
||||||
|
const MANIFEST_FILENAME = '.packwatch.json'
|
||||||
|
|
||||||
|
export default async function packwatch(args: Partial<PackwatchArguments>): Promise<void> {
|
||||||
|
const { cwd, isUpdatingManifest } = mergeDefaultArguments(args)
|
||||||
|
|
||||||
|
const manifestPath = resolve(join(cwd, MANIFEST_FILENAME))
|
||||||
|
|
||||||
|
assertInPackageRoot(cwd)
|
||||||
|
|
||||||
|
const currentStats = getCurrentPackageStats(cwd)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If there is no manifest file yet, we can use the current package stats as
|
||||||
|
* a base to build one. The current package size becomes the limit.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!existsSync(manifestPath)) {
|
||||||
|
createOrUpdateManifest({ manifestPath, current: currentStats })
|
||||||
|
logger.warn(
|
||||||
|
`📝 No Manifest to compare against! Current package stats written to ${MANIFEST_FILENAME}!\nPackage size (${currentStats.packageSize}) adopted as new limit.`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isUpdatingManifest) {
|
||||||
|
logger.error(
|
||||||
|
'❗ It looks like you ran PackWatch without a manifest. To prevent accidental passes in CI or hooks, packwatch will terminate with an error. If you are running packwatch for the first time in your project, this is expected!',
|
||||||
|
)
|
||||||
|
throw new Error('NO_MANIFEST_NO_UPDATE')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousStats = getPreviousPackageStats(cwd)
|
||||||
|
const { packageSizeBytes, packageSize } = currentStats
|
||||||
|
const { packageSize: previousSize, packageSizeBytes: previousSizeBytes, limit, limitBytes } = previousStats
|
||||||
|
const hasExceededLimit = limitBytes && packageSizeBytes > limitBytes
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If we are updating the manifest, we can write right away and terminate.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (isUpdatingManifest) {
|
||||||
|
createOrUpdateManifest({
|
||||||
|
previous: previousStats,
|
||||||
|
current: currentStats,
|
||||||
|
updateLimit: true,
|
||||||
|
manifestPath,
|
||||||
|
})
|
||||||
|
logger.log(`📝 Updated the manifest! Package size: ${packageSize}, Limit: ${packageSize}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If there is a manifest file and the current package busts its limit
|
||||||
|
* we signal it and terminate with an error.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (hasExceededLimit) {
|
||||||
|
logger.error(
|
||||||
|
`🔥🔥📦🔥🔥 Your package exceeds the limit set in ${MANIFEST_FILENAME}! ${packageSize} > ${limit}\nEither update the limit by using the --update-manifest flag or trim down your packed files!`,
|
||||||
|
)
|
||||||
|
throw new Error('PACKAGE_EXCEEDS_LIMIT')
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If there is a manifest file and the limit is not busted, we give
|
||||||
|
* the user some feedback on how the current package compares with
|
||||||
|
* the previous one.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (packageSizeBytes > previousSizeBytes) {
|
||||||
|
logger.log(`📦 👀 Your package grew! ${packageSize} > ${previousSize} (Limit: ${limit})`)
|
||||||
|
} else if (packageSizeBytes < previousSizeBytes) {
|
||||||
|
logger.log(`📦 💯 Your package shrank! ${packageSize} < ${previousSize} (Limit: ${limit})`)
|
||||||
|
} else {
|
||||||
|
logger.log(`📦 Nothing to report! Your package is the same size as the latest manifest reports! (Limit: ${limit})`)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
14
src/invariants.ts
Normal file
14
src/invariants.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { join, resolve } from 'path'
|
||||||
|
|
||||||
|
import logger from './logger'
|
||||||
|
|
||||||
|
export function assertInPackageRoot(cwd: string): void {
|
||||||
|
const packagePath = resolve(join(cwd, 'package.json'))
|
||||||
|
const packageJsonExists = existsSync(packagePath)
|
||||||
|
|
||||||
|
if (!packageJsonExists) {
|
||||||
|
logger.log('🤔 There is no package.json file here. Are you in the root directory of your project?')
|
||||||
|
throw new Error('NOT_IN_PACKAGE_ROOT')
|
||||||
|
}
|
||||||
|
}
|
11
src/logger.ts
Normal file
11
src/logger.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export default {
|
||||||
|
log: (...args: unknown[]): void => {
|
||||||
|
console.log(...args)
|
||||||
|
},
|
||||||
|
warn: (...args: unknown[]): void => {
|
||||||
|
console.warn(...args)
|
||||||
|
},
|
||||||
|
error: (...args: unknown[]): void => {
|
||||||
|
console.error(...args)
|
||||||
|
},
|
||||||
|
}
|
13
src/types.ts
Normal file
13
src/types.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export type PackwatchArguments = {
|
||||||
|
cwd: string
|
||||||
|
isUpdatingManifest: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Report = {
|
||||||
|
packageSize: string
|
||||||
|
unpackedSize: string
|
||||||
|
packageSizeBytes: number
|
||||||
|
unpackedSizeBytes: number
|
||||||
|
limit?: string
|
||||||
|
limitBytes?: number
|
||||||
|
}
|
98
src/utils.ts
Normal file
98
src/utils.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import { spawnSync } from 'child_process'
|
||||||
|
import { readFileSync, writeFileSync } from 'fs'
|
||||||
|
import { join, resolve } from 'path'
|
||||||
|
|
||||||
|
import { type PackwatchArguments, type Report } from './types'
|
||||||
|
|
||||||
|
const PACKAGE_SIZE_PATT = /package size:\s*([0-9]+\.?[0-9]*\s+[A-Za-z]{1,2})/
|
||||||
|
const UNPACKED_SIZE_PATT = /unpacked size:\s*([0-9]+\.?[0-9]*\s+[A-Za-z]{1,2})/
|
||||||
|
const SIZE_SUFFIX_PATT = /([A-Za-z]+)/
|
||||||
|
const SIZE_MAGNITUDE_PATT = /([0-9]+\.?[0-9]*)/
|
||||||
|
|
||||||
|
const MANIFEST_FILENAME = '.packwatch.json'
|
||||||
|
|
||||||
|
export function mergeDefaultArguments(args: Partial<PackwatchArguments>): PackwatchArguments {
|
||||||
|
return {
|
||||||
|
cwd: args.cwd ?? '.',
|
||||||
|
isUpdatingManifest: args.isUpdatingManifest ?? false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertSizeToBytes(sizeString: string): number {
|
||||||
|
const sizeSuffix = SIZE_SUFFIX_PATT.exec(sizeString)?.[1] ?? ''
|
||||||
|
const sizeMagnitude = SIZE_MAGNITUDE_PATT.exec(sizeString)?.[1] ?? '0.0'
|
||||||
|
|
||||||
|
let multiplier = 1
|
||||||
|
|
||||||
|
if (sizeSuffix === 'kB') multiplier = 1000
|
||||||
|
else if (sizeSuffix === 'mB') {
|
||||||
|
multiplier = 1000000
|
||||||
|
}
|
||||||
|
|
||||||
|
return multiplier * parseFloat(sizeMagnitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentPackageStats(cwd: string): Report {
|
||||||
|
const { stderr } = spawnSync('npm', ['pack', '--dry-run'], {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
cwd,
|
||||||
|
})
|
||||||
|
const stderrString = String(stderr)
|
||||||
|
const packageSize = PACKAGE_SIZE_PATT.exec(stderrString)?.[1] ?? '0'
|
||||||
|
const unpackedSize = UNPACKED_SIZE_PATT.exec(stderrString)?.[1] ?? '0'
|
||||||
|
|
||||||
|
return {
|
||||||
|
packageSize,
|
||||||
|
unpackedSize,
|
||||||
|
packageSizeBytes: convertSizeToBytes(packageSize),
|
||||||
|
unpackedSizeBytes: convertSizeToBytes(unpackedSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPreviousPackageStats(cwd: string): Report {
|
||||||
|
const manifestPath = resolve(join(cwd, MANIFEST_FILENAME))
|
||||||
|
try {
|
||||||
|
const currentManifest = readFileSync(manifestPath, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
})
|
||||||
|
const parsedManifest = JSON.parse(currentManifest)
|
||||||
|
return {
|
||||||
|
...parsedManifest,
|
||||||
|
packageSizeBytes: convertSizeToBytes(parsedManifest.packageSize),
|
||||||
|
unpackedSizeBytes: convertSizeToBytes(parsedManifest.unpackedSize),
|
||||||
|
limitBytes: convertSizeToBytes(parsedManifest.limit),
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* No manifest */
|
||||||
|
return {
|
||||||
|
packageSize: '0',
|
||||||
|
packageSizeBytes: 0,
|
||||||
|
unpackedSizeBytes: 0,
|
||||||
|
unpackedSize: '0',
|
||||||
|
limitBytes: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOrUpdateManifest({
|
||||||
|
previous,
|
||||||
|
current,
|
||||||
|
manifestPath,
|
||||||
|
updateLimit = false,
|
||||||
|
}: {
|
||||||
|
previous?: Report
|
||||||
|
current: Report
|
||||||
|
manifestPath: string
|
||||||
|
updateLimit?: boolean
|
||||||
|
}): void {
|
||||||
|
const { limit } = previous || {}
|
||||||
|
const { packageSize, unpackedSize } = current
|
||||||
|
|
||||||
|
const newManifest = {
|
||||||
|
limit: updateLimit ? packageSize : limit || packageSize,
|
||||||
|
packageSize: packageSize,
|
||||||
|
unpackedSize: unpackedSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(manifestPath, JSON.stringify(newManifest))
|
||||||
|
}
|
234
tests/index.test.ts
Normal file
234
tests/index.test.ts
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
import { promises as fs } from 'fs'
|
||||||
|
import { tmpdir } from 'os'
|
||||||
|
import { join, resolve } from 'path'
|
||||||
|
|
||||||
|
import packwatch from '../src'
|
||||||
|
|
||||||
|
import type { Report } from '../src/types'
|
||||||
|
|
||||||
|
let workspace: string | null
|
||||||
|
|
||||||
|
function getActualPackageSizeByNodeVersion(nodeVersion: string): string {
|
||||||
|
if (nodeVersion.startsWith('v14')) return '160'
|
||||||
|
else if (nodeVersion.startsWith('v16')) return '157'
|
||||||
|
else if (nodeVersion.startsWith('v18')) return '157'
|
||||||
|
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareWorkspace(): Promise<string> {
|
||||||
|
const workspacePath = await fs.mkdtemp(`${tmpdir()}/`)
|
||||||
|
workspace = workspacePath
|
||||||
|
return workspacePath
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanUpWorkspace(paths: string[]): Promise<void> {
|
||||||
|
await Promise.all(paths.map(async (path) => fs.rmdir(path, { recursive: true })))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createFile(path: string, content: string): Promise<void> {
|
||||||
|
await fs.writeFile(path, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPackageJson(cwd: string): Promise<void> {
|
||||||
|
const path = resolve(join(cwd, 'package.json'))
|
||||||
|
await createFile(path, '{ "name": "wow", "version": "0.0.0", "files": ["!.packwatch.json"] }')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createManifest(cwd: string, configuration: Report): Promise<void> {
|
||||||
|
const path = resolve(join(cwd, '.packwatch.json'))
|
||||||
|
await createFile(path, JSON.stringify(configuration))
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Packwatch', () => {
|
||||||
|
const actualSize = getActualPackageSizeByNodeVersion(process.version)
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
|
||||||
|
if (workspace) {
|
||||||
|
await cleanUpWorkspace([workspace])
|
||||||
|
workspace = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('warns the user and errors if run away from package.json', async () => {
|
||||||
|
const workspacePath = await prepareWorkspace()
|
||||||
|
const mockLogger = jest.spyOn(console, 'log')
|
||||||
|
|
||||||
|
await expect(async () => packwatch({ cwd: workspacePath })).rejects.toThrow('NOT_IN_PACKAGE_ROOT')
|
||||||
|
|
||||||
|
expect(mockLogger.mock.calls).toHaveLength(1)
|
||||||
|
expect(mockLogger.mock.calls[0][0]).toEqual(
|
||||||
|
expect.stringMatching('There is no package.json file here. Are you in the root directory of your project?'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('without manifest', () => {
|
||||||
|
it('generates the initial manifest properly', async () => {
|
||||||
|
const workspacePath = await prepareWorkspace()
|
||||||
|
await createPackageJson(workspacePath)
|
||||||
|
|
||||||
|
await expect(async () => packwatch({ cwd: workspacePath })).rejects.toThrow('NO_MANIFEST_NO_UPDATE')
|
||||||
|
|
||||||
|
const generatedManifest = await fs.readFile(resolve(join(workspacePath, '.packwatch.json')), { encoding: 'utf8' })
|
||||||
|
|
||||||
|
expect(generatedManifest).toBe(
|
||||||
|
`{"limit":"${actualSize} B","packageSize":"${actualSize} B","unpackedSize":"68 B"}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('outputs expected messaging', async () => {
|
||||||
|
const workspacePath = await prepareWorkspace()
|
||||||
|
const mockWarn = jest.spyOn(console, 'warn')
|
||||||
|
const mockError = jest.spyOn(console, 'error')
|
||||||
|
await createPackageJson(workspacePath)
|
||||||
|
|
||||||
|
await expect(async () => packwatch({ cwd: workspacePath })).rejects.toThrow()
|
||||||
|
|
||||||
|
expect(mockWarn.mock.calls).toHaveLength(1)
|
||||||
|
expect(mockWarn.mock.calls[0][0]).toEqual(
|
||||||
|
expect.stringMatching(
|
||||||
|
/No Manifest to compare against! Current package stats written to \.packwatch\.json!\nPackage size \(\d+ B\) adopted as new limit\./,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
expect(mockError.mock.calls).toHaveLength(1)
|
||||||
|
expect(mockError.mock.calls[0][0]).toEqual(
|
||||||
|
expect.stringMatching(
|
||||||
|
'It looks like you ran PackWatch without a manifest. To prevent accidental passes in CI or hooks, packwatch will terminate with an error. If you are running packwatch for the first time in your project, this is expected!',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('outputs expected messaging when not updating the manifest', async () => {
|
||||||
|
const mockWarn = jest.spyOn(console, 'warn')
|
||||||
|
const workspacePath = await prepareWorkspace()
|
||||||
|
|
||||||
|
await createPackageJson(workspacePath)
|
||||||
|
|
||||||
|
await packwatch({ cwd: workspacePath, isUpdatingManifest: true })
|
||||||
|
|
||||||
|
expect(mockWarn.mock.calls).toHaveLength(1)
|
||||||
|
expect(mockWarn.mock.calls[0][0]).toEqual(
|
||||||
|
expect.stringMatching(
|
||||||
|
/No Manifest to compare against! Current package stats written to \.packwatch\.json!\nPackage size \(\d+ B\) adopted as new limit\./,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with manifest', () => {
|
||||||
|
it('messages when the size is equal to the limit', async () => {
|
||||||
|
const workspacePath = await prepareWorkspace()
|
||||||
|
const mockLogger = jest.spyOn(console, 'log')
|
||||||
|
await createPackageJson(workspacePath)
|
||||||
|
await createManifest(workspacePath, {
|
||||||
|
limit: `${actualSize}B`,
|
||||||
|
packageSize: `${actualSize}B`,
|
||||||
|
packageSizeBytes: Number(actualSize),
|
||||||
|
unpackedSize: '150B',
|
||||||
|
unpackedSizeBytes: 150,
|
||||||
|
})
|
||||||
|
await packwatch({ cwd: workspacePath })
|
||||||
|
expect(mockLogger.mock.calls).toHaveLength(1)
|
||||||
|
expect(mockLogger.mock.calls[0][0]).toEqual(
|
||||||
|
expect.stringMatching(/Nothing to report! Your package is the same size as the latest manifest reports!/),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('messages when the size is lower than the limit (no growth)', async () => {
|
||||||
|
const workspacePath = await prepareWorkspace()
|
||||||
|
const mockLogger = jest.spyOn(console, 'log')
|
||||||
|
await createPackageJson(workspacePath)
|
||||||
|
await createManifest(workspacePath, {
|
||||||
|
limit: '170B',
|
||||||
|
packageSize: `${actualSize}B`,
|
||||||
|
packageSizeBytes: Number(actualSize),
|
||||||
|
unpackedSize: '150B',
|
||||||
|
unpackedSizeBytes: 150,
|
||||||
|
})
|
||||||
|
|
||||||
|
await packwatch({ cwd: workspacePath })
|
||||||
|
expect(mockLogger.mock.calls).toHaveLength(1)
|
||||||
|
expect(mockLogger.mock.calls[0][0]).toEqual(
|
||||||
|
expect.stringMatching(
|
||||||
|
/Nothing to report! Your package is the same size as the latest manifest reports! \(Limit: 170B\)/,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('messages when the size is lower than the limit (growth)', async () => {
|
||||||
|
const workspacePath = await prepareWorkspace()
|
||||||
|
const mockLogger = jest.spyOn(console, 'log')
|
||||||
|
await createPackageJson(workspacePath)
|
||||||
|
await createManifest(workspacePath, {
|
||||||
|
limit: '180B',
|
||||||
|
packageSize: '150B',
|
||||||
|
packageSizeBytes: 150,
|
||||||
|
unpackedSize: '140B',
|
||||||
|
unpackedSizeBytes: 140,
|
||||||
|
})
|
||||||
|
|
||||||
|
await packwatch({ cwd: workspacePath })
|
||||||
|
expect(mockLogger.mock.calls).toHaveLength(1)
|
||||||
|
expect(mockLogger.mock.calls[0][0]).toEqual(
|
||||||
|
expect.stringMatching(/Your package grew! \d+ B > 150B \(Limit: 180B\)/),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('messages when the size is lower than the limit (shrinkage)', async () => {
|
||||||
|
const workspacePath = await prepareWorkspace()
|
||||||
|
const mockLogger = jest.spyOn(console, 'log')
|
||||||
|
await createPackageJson(workspacePath)
|
||||||
|
await createManifest(workspacePath, {
|
||||||
|
limit: '180B',
|
||||||
|
packageSize: '170B',
|
||||||
|
packageSizeBytes: 170,
|
||||||
|
unpackedSize: '140B',
|
||||||
|
unpackedSizeBytes: 140,
|
||||||
|
})
|
||||||
|
|
||||||
|
await packwatch({ cwd: workspacePath })
|
||||||
|
expect(mockLogger.mock.calls).toHaveLength(1)
|
||||||
|
expect(mockLogger.mock.calls[0][0]).toEqual(
|
||||||
|
expect.stringMatching(/Your package shrank! \d+ B < 170B \(Limit: 180B\)/),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('messages when the size exceeds the limit', async () => {
|
||||||
|
const workspacePath = await prepareWorkspace()
|
||||||
|
const mockError = jest.spyOn(console, 'error')
|
||||||
|
await createPackageJson(workspacePath)
|
||||||
|
await createManifest(workspacePath, {
|
||||||
|
limit: '10B',
|
||||||
|
packageSize: '170B',
|
||||||
|
packageSizeBytes: 170,
|
||||||
|
unpackedSize: '140B',
|
||||||
|
unpackedSizeBytes: 140,
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(async () => packwatch({ cwd: workspacePath })).rejects.toThrow('PACKAGE_EXCEEDS_LIMIT')
|
||||||
|
expect(mockError.mock.calls).toHaveLength(1)
|
||||||
|
expect(mockError.mock.calls[0][0]).toEqual(
|
||||||
|
expect.stringMatching(
|
||||||
|
/Your package exceeds the limit set in \.packwatch\.json! \d+ B > 10B\nEither update the limit by using the --update-manifest flag or trim down your packed files!/,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('messages when updating the manifest', async () => {
|
||||||
|
const workspacePath = await prepareWorkspace()
|
||||||
|
const mockLogger = jest.spyOn(console, 'log')
|
||||||
|
await createPackageJson(workspacePath)
|
||||||
|
await createManifest(workspacePath, {
|
||||||
|
limit: '10B',
|
||||||
|
packageSize: '170B',
|
||||||
|
packageSizeBytes: 170,
|
||||||
|
unpackedSize: '140B',
|
||||||
|
unpackedSizeBytes: 140,
|
||||||
|
})
|
||||||
|
|
||||||
|
await packwatch({ cwd: workspacePath, isUpdatingManifest: true })
|
||||||
|
expect(mockLogger.mock.calls).toHaveLength(1)
|
||||||
|
expect(mockLogger.mock.calls[0][0]).toEqual(
|
||||||
|
expect.stringMatching(/Updated the manifest! Package size: \d+ B, Limit: \d+ B/),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
12
tests/utils.test.ts
Normal file
12
tests/utils.test.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { convertSizeToBytes } from '../src/utils'
|
||||||
|
|
||||||
|
describe('utils', () => {
|
||||||
|
it.each`
|
||||||
|
initialSize | expectedSize
|
||||||
|
${'1 B'} | ${1}
|
||||||
|
${'1 kB'} | ${1000}
|
||||||
|
${'1 mB'} | ${1000000}
|
||||||
|
`('converts sizes properly ($initialSize -> $expectedSize)', ({ initialSize, expectedSize }) => {
|
||||||
|
expect(convertSizeToBytes(initialSize)).toEqual(expectedSize)
|
||||||
|
})
|
||||||
|
})
|
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"module": "commonjs",
|
||||||
|
"allowJs": false,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./",
|
||||||
|
"removeComments": true,
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": [
|
||||||
|
"dist/**/*",
|
||||||
|
"tests/**/*"
|
||||||
|
]
|
||||||
|
}
|
Reference in a new issue