Skip to content
This repository was archived by the owner on Feb 1, 2021. It is now read-only.

Commit 6c4cf84

Browse files
committed
feat: add CLI to upload stats
1 parent 0295c00 commit 6c4cf84

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2841
-149
lines changed

.eslintignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
node_modules/
2-
lib/
2+
lib/
3+
dist.js
4+
__fixtures__

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules/
2-
lib/
2+
lib/
3+
dist.js

.prettierignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules/
22
package.json
3-
CHANGELOG.md
3+
CHANGELOG.md
4+
dist.js

jest.config.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
testEnvironment: 'node',
3+
}

package.json

+11-5
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
],
66
"scripts": {
77
"build": "lerna run build",
8-
"ci": "yarn build && yarn lint",
8+
"ci": "yarn build && yarn lint && yarn test --ci",
9+
"dev": "lerna run build --parallel -- --watch",
910
"format": "prettier --write \"**/*.{js,json,md}\"",
1011
"lint": "eslint .",
11-
"release": "lerna publish --conventional-commits && conventional-github-releaser --preset angular"
12+
"release": "lerna publish --conventional-commits && conventional-github-releaser --preset angular",
13+
"test": "jest"
1214
},
1315
"devDependencies": {
1416
"@babel/cli": "^7.4.4",
@@ -17,15 +19,19 @@
1719
"@babel/preset-env": "^7.4.5",
1820
"babel-eslint": "^10.0.1",
1921
"conventional-github-releaser": "^3.1.3",
20-
"eslint": "^6.0.1",
22+
"eslint": "^6.4.0",
2123
"eslint-config-airbnb": "^18.0.1",
22-
"eslint-config-prettier": "^6.2.0",
24+
"eslint-config-prettier": "^6.3.0",
2325
"eslint-config-smooth": "^2.1.1",
2426
"eslint-plugin-import": "^2.18.2",
2527
"eslint-plugin-jsx-a11y": "^6.2.3",
2628
"eslint-plugin-react": "^7.14.3",
2729
"eslint-plugin-react-hooks": "^2.0.1",
30+
"jest": "^24.9.0",
2831
"lerna": "^3.14.1",
29-
"prettier": "^1.18.2"
32+
"memory-fs": "^0.4.1",
33+
"nock": "^11.3.4",
34+
"prettier": "^1.18.2",
35+
"webpack": "^4.40.2"
3036
}
3137
}

packages/cli/.npmignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/*
2+
!/lib/**/*.js
3+
*.test.js

packages/cli/README.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# @bundle-analyzer/cli
2+
3+
Official CLI compatible with [Bundle Analyzer service](https://www.bundle-analyzer.com).
4+
5+
## Install
6+
7+
```
8+
npm install --save-dev @bundle-analyzer/cli
9+
```
10+
11+
## Usage
12+
13+
```js
14+
webpack --json | bundle-analyzer --token <your-token>
15+
```
16+
17+
## Complete documentation
18+
19+
👉 [See full documentation](https://docs.bundle-analyzer.com/)
20+
21+
## License
22+
23+
MIT

packages/cli/__fixtures__/main.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log('hello world!')

packages/cli/bin/bundle-analyzer

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env node
2+
3+
require('../lib/index')

packages/cli/package.json

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "@bundle-analyzer/cli",
3+
"description": "Bundle Analyzer CLI.",
4+
"version": "0.2.1",
5+
"main": "lib/index.js",
6+
"repository": "https://github.com/smooth-code/bundle-analyzer-javascript/tree/master/packages/cli",
7+
"author": "Greg Bergé <[email protected]>",
8+
"publishConfig": {
9+
"access": "public"
10+
},
11+
"keywords": [
12+
"bundle-analyzer",
13+
"bundlesize"
14+
],
15+
"engines": {
16+
"node": ">=8"
17+
},
18+
"license": "MIT",
19+
"scripts": {
20+
"prebuild": "rm -rf lib/",
21+
"build": "babel --config-file ../../babel.config.js -d lib --ignore \"**/*.test.js\" src",
22+
"prepublishOnly": "yarn run build"
23+
},
24+
"dependencies": {
25+
"@bundle-analyzer/core": "^0.2.1",
26+
"commander": "^3.0.1"
27+
},
28+
"devDependencies": {
29+
"webpack-cli": "^3.3.8"
30+
}
31+
}

packages/cli/src/index.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/* eslint-disable no-console */
2+
import program from 'commander'
3+
import { uploadStats } from '@bundle-analyzer/core'
4+
import pkg from '../package.json'
5+
6+
program
7+
.version(pkg.version)
8+
.usage('[options] <stats>')
9+
.option('--token <repository-token>', 'specify the repository token')
10+
11+
program.on('--help', () => {
12+
console.log(`
13+
Example:
14+
webpack --json | bundle-analyzer --token "your-repository-token"
15+
cat webpack-stats.json | bundle-analyzer --token "your-repository-token"
16+
`)
17+
})
18+
19+
program.parse(process.argv)
20+
21+
async function readStdin() {
22+
return new Promise((resolve, reject) => {
23+
let stdin = ''
24+
25+
process.stdin.setEncoding('utf8')
26+
process.stdin.on('readable', () => {
27+
const chunk = process.stdin.read()
28+
if (chunk !== null) stdin += chunk
29+
})
30+
process.stdin.on('error', reject)
31+
process.stdin.on('end', () => resolve(stdin))
32+
})
33+
}
34+
35+
async function run() {
36+
const rawStats = await readStdin()
37+
const stats = JSON.parse(rawStats)
38+
await uploadStats({ webpackStats: stats, token: program.token })
39+
}
40+
41+
run().catch(error => {
42+
setTimeout(() => {
43+
throw error
44+
})
45+
})

packages/core/README.md

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# @bundle-analyzer/core
2+
3+
Official Node.js package compatible with [Bundle Analyzer service](https://www.bundle-analyzer.com).
4+
5+
## Install
6+
7+
```
8+
npm install --save-dev @bundle-analyzer/core
9+
```
10+
11+
## Usage
12+
13+
```js
14+
import { uploadStats } from '@bundle-analyzer/core'
15+
import webpackStats from './webpack-stats.json'
16+
17+
uploadStats({ webpackStats, token: '<repository-token>' })
18+
.then(() => {
19+
console.log('uploaded)
20+
})
21+
```
22+
23+
## Options
24+
25+
### webpackStats
26+
27+
Stats generated from webpack.
28+
29+
### token
30+
31+
You can specify the token using options or environment variable `BUNDLE_ANALYZER_TOKEN`.
32+
33+
### fileSystem
34+
35+
Custom filesystem.
36+
37+
## Complete documentation
38+
39+
👉 [See full documentation](https://docs.bundle-analyzer.com/)
40+
41+
## License
42+
43+
MIT

packages/core/__fixtures__/main.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log('hello world!')

packages/core/package.json

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@bundle-analyzer/core",
3+
"description": "Bundle Analyzer Node.js uploader.",
4+
"version": "0.2.1",
5+
"main": "lib/index.js",
6+
"repository": "https://github.com/smooth-code/bundle-analyzer-javascript/tree/master/packages/core",
7+
"author": "Greg Bergé <[email protected]>",
8+
"publishConfig": {
9+
"access": "public"
10+
},
11+
"keywords": [
12+
"bundle-analyzer",
13+
"bundlesize"
14+
],
15+
"engines": {
16+
"node": ">=8"
17+
},
18+
"license": "MIT",
19+
"scripts": {
20+
"prebuild": "rm -rf lib/",
21+
"build": "babel --config-file ../../babel.config.js -d lib --ignore \"**/*.test.js\" src",
22+
"prepublishOnly": "yarn run build"
23+
},
24+
"dependencies": {
25+
"axios": "^0.19.0",
26+
"brotli-size": "^4.0.0",
27+
"gzip-size": "^5.1.1",
28+
"omit-deep": "^0.3.0"
29+
}
30+
}
File renamed without changes.

packages/core/src/config.test.js

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { getToken, getApiUrl } from './config'
2+
3+
describe('config', () => {
4+
describe('#getToken', () => {
5+
it('throws an error if token is not specified', () => {
6+
expect(() => getToken()).toThrow(
7+
'Token not found, please specify a token using BUNDLE_ANALYZER_TOKEN env variable',
8+
)
9+
})
10+
11+
describe('with BUNDLE_ANALYZER_TOKEN env variable', () => {
12+
beforeEach(() => {
13+
process.env.BUNDLE_ANALYZER_TOKEN = 'env-token'
14+
})
15+
16+
afterEach(() => {
17+
delete process.env.BUNDLE_ANALYZER_TOKEN
18+
})
19+
20+
it('returns provided token', () => {
21+
expect(getToken('foo')).toBe('foo')
22+
})
23+
24+
it('uses it as default', () => {
25+
expect(getToken()).toBe('env-token')
26+
})
27+
})
28+
})
29+
30+
describe('#getApiUrl', () => {
31+
describe('with BUNDLE_ANALYZER_API_URL env variable', () => {
32+
beforeEach(() => {
33+
process.env.BUNDLE_ANALYZER_API_URL = 'env-api'
34+
})
35+
36+
afterEach(() => {
37+
delete process.env.BUNDLE_ANALYZER_API_URL
38+
})
39+
40+
it('uses it', () => {
41+
expect(getApiUrl()).toBe('env-api')
42+
})
43+
})
44+
45+
it('returns default API url', () => {
46+
expect(getApiUrl()).toBe('https://api.bundle-analyzer.com')
47+
})
48+
})
49+
})
File renamed without changes.

packages/core/src/index.js

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import zlib from 'zlib'
2+
import { promisify } from 'util'
3+
import path from 'path'
4+
import fs from 'fs'
5+
import axios from 'axios'
6+
import gzipSize from 'gzip-size'
7+
import brotliSize from 'brotli-size'
8+
import omitDeep from 'omit-deep'
9+
import { detectProvider } from './provider'
10+
import { getToken, getApiUrl } from './config'
11+
12+
const gzip = promisify(zlib.gzip)
13+
14+
async function sizeAssets(webpackStats, { fileSystem = fs } = {}) {
15+
const readFile = promisify(fileSystem.readFile.bind(fileSystem))
16+
return Promise.all(
17+
webpackStats.assets.map(async asset => {
18+
const fullPath = path.join(webpackStats.outputPath, asset.name)
19+
const buffer = await readFile(fullPath)
20+
return {
21+
...asset,
22+
gzipSize: await gzipSize(buffer),
23+
brotliSize: await brotliSize(buffer),
24+
}
25+
}),
26+
)
27+
}
28+
29+
function getErrorMessage(error) {
30+
if (
31+
error.response &&
32+
error.response.data &&
33+
error.response.data.error &&
34+
error.response.data.error.message
35+
) {
36+
return error.response.data.error.message
37+
}
38+
return error.message
39+
}
40+
41+
export async function uploadStats({
42+
webpackStats,
43+
token: optionToken,
44+
fileSystem,
45+
}) {
46+
try {
47+
const token = getToken(optionToken)
48+
const apiUrl = getApiUrl()
49+
const metadata = detectProvider()
50+
const stats = omitDeep(webpackStats, 'source')
51+
const assets = await sizeAssets(stats, { fileSystem })
52+
53+
const { data: bundle } = await axios.post(`${apiUrl}/bundles`, {
54+
token,
55+
bundler: 'webpack',
56+
stats: {
57+
assets,
58+
chunksNumber: stats.chunks.length,
59+
modulesNumber: stats.modules.length,
60+
assetsNumber: stats.assets.length,
61+
},
62+
})
63+
64+
const data = await gzip(Buffer.from(JSON.stringify(stats)))
65+
66+
await axios.request({
67+
method: 'put',
68+
url: bundle.webpackStatsPutUrl,
69+
data,
70+
headers: {
71+
'content-encoding': 'gzip',
72+
},
73+
maxContentLength: 30 * 1024 * 1024,
74+
})
75+
76+
await axios.post(`${apiUrl}/builds`, {
77+
token,
78+
bundleId: bundle.id,
79+
branch: metadata.branch,
80+
commit: metadata.commit,
81+
providerMetadata: metadata,
82+
})
83+
} catch (error) {
84+
throw new Error(getErrorMessage(error))
85+
}
86+
}

0 commit comments

Comments
 (0)