├── .coveralls.yml
├── .eslintrc
├── .gitignore
├── .npmignore
├── .nvmrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bin.js
├── config.js
├── index.js
├── logger.js
├── options_error.js
├── package.json
├── test
├── config.test.js
├── files
│ ├── add.js
│ ├── entry.js
│ └── subtract.js
└── webpack.test.js
├── upload.js
└── yarn.lock
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | repo_token: ENV[COVERALLS_TOKEN]
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "standard"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | test/files/output
3 | coverage
4 | npm-debug.log
5 | yarn-error.log
6 | *.tgz
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | test
3 | coverage
4 | npm-debug.log
5 | yarn.lock
6 | .nvmrc
7 | .travis.yml
8 | .eslintrc
9 | .coveralls.yml
10 | *.tgz
11 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 8
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | script: yarn test:ci
2 | language: node_js
3 | cache:
4 | yarn: true
5 | directories:
6 | - node_modules
7 | sudo: false
8 | node_js:
9 | - "10"
10 | - "8"
11 | - "6"
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## 2.3.0
9 |
10 | ### Added
11 | - Added the ability to set the `exclude_assets` option via environment variable `PT_EXCLUDE_ASSETS`
12 |
13 | ## 2.2.0
14 |
15 | ### Added
16 | - Added the uploader hostname to payload in order to identify errant uploads
17 |
18 | ### Changed
19 | - Updated a few dependencies
20 |
21 | ## 2.1.1
22 |
23 | ### Changed
24 | - Better error handling and visibility for failed states
25 | - More general purpose logging (to help with debugging)
26 |
27 | ## 2.1.0
28 |
29 | ### Added
30 | - Improved error messaging, fixing both [#5](https://github.com/packtracker/webpack-plugin/issues/5) and [#9](https://github.com/packtracker/webpack-plugin/issues/9)
31 |
32 | ### Changed
33 | - Updated dependencies
34 |
35 | ## 2.0.1
36 |
37 | ### Changed
38 | - Fixed a few typos [#8](https://github.com/packtracker/webpack-plugin/pull/8)
39 |
40 | ## 2.0.0
41 |
42 | ### Added
43 | - CLI tool! for use with create react app
44 | - new `exclude_assets` option to filter assets you don't want to track
45 | - Improved js bundle reporting by leveraging webpack-bundle-analyzer directly
46 |
47 | ## [1.1.1]
48 |
49 | ### Changed
50 | - [Update tiny-json-http to handle multi-byte characters in the json payload, for real](https://github.com/packtracker/webpack-plugin/pull/3/files)
51 |
52 | ## [1.1.0]
53 |
54 | ### Added
55 | - [Add an option to fail the webpack build if the stat upload fails](https://github.com/packtracker/webpack-plugin/pull/2/files).
56 |
57 | ### Changed
58 | - [Update tiny-json-http to handle multi-byte characters in the json payload](https://github.com/packtracker/webpack-plugin/pull/3/files)
59 |
60 |
61 | ## [1.0.1]
62 |
63 | Initial Release
64 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Jonathan Johnson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # packtracker.io webpack plugin
6 |
7 | [](https://travis-ci.org/packtracker/webpack-plugin)
8 | [](https://coveralls.io/github/packtracker/webpack-plugin?branch=master)
9 | [](https://app.codacy.com/app/jondavidjohn/webpack-plugin)
10 | [](https://standardjs.com)
11 |
12 | This plugin is designed to upload your webpack build stats to the [packtracker.io](https://packtracker.io) service.
13 |
14 | ## Installation
15 |
16 | Once you have your [project created](https://docs.packtracker.io/creating-your-first-project) on [packtracker.io](https://app.packtracker.io), and a `project_token` in hand, you can get your data flowing by installing and configuring this plugin.
17 |
18 | ```sh
19 | npm install --save-dev @packtracker/webpack-plugin
20 |
21 | ```
22 |
23 | ## Usage
24 |
25 |
26 | ### Webpack Plugin
27 |
28 | In your webpack configuration include the plugin (along with your project token).
29 |
30 | > If the plugin fails to upload your stats, **it will not error out your build** but it will **log output signaling the failure**.
31 |
32 | ```js
33 | const PacktrackerPlugin = require('@packtracker/webpack-plugin')
34 |
35 | module.exports = {
36 | plugins: [
37 | new PacktrackerPlugin({
38 | project_token: '',
39 | upload: true
40 | })
41 | ]
42 | }
43 | ```
44 |
45 | The `upload` option above tells the plugin whether or not to upload your build stats when running webpack. By default, this option is set to `false` to prevent accidental uploading from your local machine. If the upload option is left `false`, the plugin will do nothing.
46 |
47 | Once you see your stats are uploading, it is common to only upload when building your assets in a CI environment or during deployment. You can also omit this option altogether, and set the `PT_UPLOAD` environment variable on a per run basis to control the upload of your stats.
48 |
49 | For example
50 |
51 | ```js
52 | const PacktrackerPlugin = require('@packtracker/webpack-plugin')
53 |
54 | module.exports = {
55 | plugins: [
56 | new PacktrackerPlugin({
57 | project_token: '',
58 | upload: process.env.CI === 'true'
59 | })
60 | ]
61 | }
62 | ```
63 |
64 |
65 | ### CLI
66 |
67 | In addition to the primary use case of uploading as part of your build process via our webpack plugin, we also have a command line uploader that works well with tools like [create-react-app](https://facebook.github.io/create-react-app/) that allow you to export your stats, but don't allow full plugin configuration.
68 |
69 | The only caveat to using the CLI is that you **must** use environment variables to configure your stat reporting (most importantly `PT_PROJECT_TOKEN`).
70 |
71 | #### Example with `create-react-app`
72 |
73 | In your `package.json` you can add a run script like the following
74 |
75 | ```json
76 | {
77 | "scripts": {
78 | "packtracker": "react-scripts build --stats && packtracker-upload --stats=build/bundle-stats.json"
79 | }
80 | }
81 | ```
82 |
83 | The only additional parameter you can pass via the CLI is the `--output-path=`, if it is not passed we assume it is the directory that contains your bundle stats file.
84 |
85 | Then running `npm run packtracker` should upload your stats to our service
86 |
87 |
88 | ### Options
89 |
90 | All of the options, available to the plugin can be set [via argument to the plugin, environment variable, or allowed to query your local git repository.](https://github.com/packtracker/webpack-plugin/blob/master/config.js)
91 |
92 | Here is a listing of the plugin options, environment variable counterparts, and a description.
93 |
94 | | Option | Env Variable | Description
95 | |---------------- |---------------------|------------
96 | |`project_token` | `PT_PROJECT_TOKEN` | The project token for your packtracker.io project (required)
97 | |`fail_build` | `PT_FAIL_BUILD` | Fail the build if the stat upload fails (default: `false`)
98 | |`branch` | `PT_BRANCH` | Branch of the commit
(default: `git rev-parse --abbrev-ref HEAD`)
99 | |`author` | `PT_AUTHOR` | Committer's email (default: `git log --format="%aE" -n 1 HEAD`)
100 | |`message` | `PT_MESSAGE` | The commit message (default: `git log --format="%B" -n 1 HEAD`)
101 | |`commit` | `PT_COMMIT` | The commit sha (default: `git rev-parse HEAD`)
102 | |`committed_at` | `PT_COMMITTED_AT` | Unix timestamp (ms) of the commit
(default: `git log --format="%ct" -n 1 HEAD`)
103 | |`prior_commit` | `PT_PRIOR_COMMIT` | The previous commit sha (default: `git rev-parse HEAD^`)
104 | |`exclude_assets` | `PT_EXCLUDE_ASSETS` | Mirrors the [excludeAssets configuration in the webpack stats config](https://webpack.js.org/configuration/stats/#stats) (only available to webpack version 3.5.0+) (compiled as RegExp when provided via environment variable)
105 |
106 | You can find more documentation about the packtracker.io service in general at [https://docs.packtracker.io](https://docs.packtracker.io)
107 |
--------------------------------------------------------------------------------
/bin.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | const args = require('minimist')(process.argv)
4 | const { resolve, dirname } = require('path')
5 | const fs = require('fs')
6 | const Config = require('./config')
7 | const Upload = require('./upload')
8 | const logger = require('./logger')
9 |
10 | const statsFilePath = resolve(args['stats'])
11 | const outputPath = args['output-path'] || dirname(statsFilePath)
12 |
13 | let stats
14 |
15 | try {
16 | logger(`retrieving stats from ${statsFilePath}`)
17 | stats = JSON.parse(fs.readFileSync(statsFilePath, 'utf8'))
18 | } catch (err) {
19 | logger('there was a problem reading your stats file.')
20 | console.error(err)
21 | process.exit(1)
22 | }
23 |
24 | const config = new Config({ upload: true, fail_build: true })
25 | const upload = new Upload(config)
26 |
27 | upload.process(stats, outputPath)
28 | .then(() => {
29 | logger('stats uploaded successfully')
30 | process.exit(0)
31 | })
32 | .catch((err) => {
33 | logger('there was a problem uploading your stats.')
34 | console.error(err)
35 | process.exit(1)
36 | })
37 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | const { execSync } = require('child_process')
2 | const OptionsError = require('./options_error')
3 | const logger = require('./logger')
4 |
5 | class Config {
6 | constructor (options = {}) {
7 | this.upload = options.upload || process.env.PT_UPLOAD === 'true' || false
8 | if (!this.upload) return
9 |
10 | this.projectToken = options.project_token || process.env.PT_PROJECT_TOKEN
11 | this.excludeAssets = retrieveExcludeAssets(options)
12 | this.statOptions = { source: false, excludeAssets: this.excludeAssets }
13 |
14 | this.host = options.host ||
15 | process.env.PT_HOST ||
16 | 'https://api.packtracker.io'
17 |
18 | this.failBuild = options.fail_build ||
19 | process.env.PT_FAIL_BUILD === 'true' ||
20 | false
21 |
22 | this.branch = options.branch ||
23 | process.env.PT_BRANCH ||
24 | retrieveConfig('git rev-parse --abbrev-ref HEAD', 'branch')
25 |
26 | this.author = options.author ||
27 | process.env.PT_AUTHOR ||
28 | retrieveConfig('git log --format="%aE" -n 1 HEAD', 'author')
29 |
30 | this.message = options.message ||
31 | process.env.PT_MESSAGE ||
32 | retrieveConfig('git log --format="%B" -n 1 HEAD', 'message')
33 |
34 | this.commit = options.commit ||
35 | process.env.PT_COMMIT ||
36 | retrieveConfig('git rev-parse HEAD', 'commit')
37 |
38 | this.committedAt = options.committed_at ||
39 | process.env.PT_COMMITTED_AT ||
40 | retrieveConfig('git log --format="%ct" -n 1 HEAD', 'committed_at')
41 |
42 | this.priorCommit = options.prior_commit ||
43 | process.env.PT_PRIOR_COMMIT ||
44 | retrieveConfig('git rev-parse HEAD^', 'prior_commit')
45 |
46 | if (!this.commit) {
47 | logger('required configuration attribute `commit` was not set.')
48 | }
49 |
50 | if (!this.branch) {
51 | logger('required configuration attribute `branch` was not set.')
52 | }
53 |
54 | if (!this.committedAt) {
55 | logger('required configuration attribute `committed_at` was not set.')
56 | }
57 |
58 | if (!this.commit || !this.branch || !this.committedAt) {
59 | logger('config validation failed, throwing options error')
60 | throw new OptionsError()
61 | }
62 |
63 | if (this.branch === 'HEAD') {
64 | throw new Error('packtracker: Not able to determine branch name with git, please provide it manually via config options: https://docs.packtracker.io/faq#why-cant-the-plugin-determine-my-branch-name')
65 | }
66 |
67 | logger('configured successfully')
68 | }
69 | }
70 |
71 | function retrieveConfig (command, configName) {
72 | try {
73 | logger(`${configName} not explicitly provided, falling back to retrieve it from from git`)
74 | return execSync(command).toString().trim()
75 | } catch (error) {
76 | logger(`ooops, looks like we had trouble trying to retrieve the '${configName}' from git`)
77 | console.error(error.message)
78 | throw new OptionsError()
79 | }
80 | }
81 |
82 | function retrieveExcludeAssets (options) {
83 | let exclusion
84 |
85 | if (process.env.PT_EXCLUDE_ASSETS) {
86 | exclusion = new RegExp(process.env.PT_EXCLUDE_ASSETS)
87 | }
88 |
89 | if (options.exclude_assets) {
90 | exclusion = options.exclude_assets
91 | }
92 |
93 | if (exclusion) {
94 | logger(`excluding assets using ${exclusion}`)
95 | }
96 |
97 | return exclusion
98 | }
99 |
100 | module.exports = Config
101 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const Config = require('./config')
2 | const Upload = require('./upload')
3 | const logger = require('./logger')
4 |
5 | function PacktrackerPlugin (options = {}) {
6 | logger('instantiate plugin')
7 | this.config = new Config(options)
8 | }
9 |
10 | PacktrackerPlugin.prototype.apply = function (compiler) {
11 | if (!this.config.upload) {
12 | logger('not uploading due to `upload` configuration')
13 | return
14 | }
15 |
16 | this.upload = new Upload(this.config)
17 |
18 | if (compiler.hooks) {
19 | logger('using compiler.hooks')
20 | compiler.hooks.done.tapPromise('packtracker', (stats) => {
21 | return this.upload.process(
22 | stats.toJson(this.config.statOptions),
23 | getOutputPath(compiler)
24 | )
25 | })
26 | } else {
27 | logger('using after-emit plugin')
28 | compiler.plugin('after-emit', (compilation, done) => {
29 | this.upload.process(
30 | compilation.getStats().toJson(this.config.statOptions),
31 | getOutputPath(compiler)
32 | ).then(done)
33 | })
34 | }
35 | }
36 |
37 | function getOutputPath (compiler) {
38 | return (compiler.outputFileSystem.constructor.name === 'MemoryFileSystem')
39 | ? null
40 | : compiler.outputPath
41 | }
42 |
43 | module.exports = PacktrackerPlugin
44 |
--------------------------------------------------------------------------------
/logger.js:
--------------------------------------------------------------------------------
1 | module.exports = function (message) {
2 | console.log(`packtracker: ${message}`)
3 | }
4 |
--------------------------------------------------------------------------------
/options_error.js:
--------------------------------------------------------------------------------
1 | class OptionsError extends Error {
2 | constructor () {
3 | super('packtracker: You can review the different ways you can set these options here: https://github.com/packtracker/webpack-plugin#options')
4 | this.name = 'OptionsError'
5 | }
6 | }
7 |
8 | module.exports = OptionsError
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@packtracker/webpack-plugin",
3 | "version": "2.3.0",
4 | "description": "Upload your webpack build stats to the packtracker.io service for greater visiblity into the artifacts you're delivering to your customers.",
5 | "homepage": "https://app.packtracker.io",
6 | "repository": "github:packtracker/webpack-plugin",
7 | "license": "MIT",
8 | "keywords": [
9 | "webpack",
10 | "stats",
11 | "packtracker",
12 | "bundle analyzer",
13 | "bundle"
14 | ],
15 | "engines": {
16 | "node": ">= 6.14.4"
17 | },
18 | "main": "index.js",
19 | "bin": {
20 | "packtracker-upload": "./bin.js"
21 | },
22 | "scripts": {
23 | "lint": "standard",
24 | "test": "jest .test/",
25 | "test:ci": "yarn test --coverage --coverageReporters=text-lcov | coveralls"
26 | },
27 | "jest": {
28 | "testEnvironment": "node"
29 | },
30 | "devDependencies": {
31 | "coveralls": "^3.0.4",
32 | "jest": "^24.8.0",
33 | "standard": "^12.0.1",
34 | "webpack": "^4.35.0",
35 | "webpack-2": "npm:webpack@2",
36 | "webpack-3": "npm:webpack@3"
37 | },
38 | "peerDependencies": {
39 | "webpack": ">= 2.0.0"
40 | },
41 | "dependencies": {
42 | "lodash": "^4.17.15",
43 | "minimist": "^1.2.0",
44 | "omit-deep": "^0.3.0",
45 | "tiny-json-http": "^7.1.2",
46 | "webpack-bundle-analyzer": "^3.4.1"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test/config.test.js:
--------------------------------------------------------------------------------
1 | /* globals jest, describe, test, beforeEach, expect */
2 |
3 | const { execSync } = require('child_process')
4 | const PacktrackerPlugin = require('../')
5 | const OptionsError = require('../options_error')
6 |
7 | jest.mock('child_process')
8 |
9 | describe('PacktrackerPlugin', () => {
10 | describe('config', () => {
11 | beforeEach(() => {
12 | execSync.mockClear()
13 | console.log = jest.fn()
14 | console.error = jest.fn()
15 | })
16 |
17 | test('default', () => {
18 | const plugin = new PacktrackerPlugin({
19 | project_token: 'abc123'
20 | })
21 |
22 | expect(plugin.config.upload).toBe(false)
23 | expect(plugin.config.excludeAssets).toBe(undefined)
24 | expect(plugin.config.host).toBe(undefined)
25 | expect(plugin.config.failBuild).toBe(undefined)
26 | expect(plugin.config.branch).toBe(undefined)
27 | expect(plugin.config.author).toBe(undefined)
28 | expect(plugin.config.message).toBe(undefined)
29 | expect(plugin.config.commit).toBe(undefined)
30 | expect(plugin.config.committedAt).toBe(undefined)
31 | expect(plugin.config.priorCommit).toBe(undefined)
32 | expect(execSync).not.toHaveBeenCalled()
33 | })
34 |
35 | test('uploading with failing shell out to git', () => {
36 | execSync.mockImplementation(() => {
37 | throw new Error('error message')
38 | })
39 |
40 | expect(() => {
41 | const plugin = new PacktrackerPlugin({
42 | upload: true,
43 | project_token: 'abc123'
44 | })
45 |
46 | expect(plugin).toBe(undefined)
47 | }).toThrowError(OptionsError)
48 | })
49 |
50 | test('uploading with missing required options', () => {
51 | execSync.mockReturnValue('')
52 |
53 | expect(() => {
54 | const plugin = new PacktrackerPlugin({
55 | upload: true,
56 | project_token: 'abc123'
57 | })
58 |
59 | expect(plugin).toBe(undefined)
60 | }).toThrowError(OptionsError)
61 | })
62 |
63 | test('default uploading', () => {
64 | execSync.mockReturnValue('default')
65 |
66 | const plugin = new PacktrackerPlugin({
67 | upload: true,
68 | project_token: 'abc123'
69 | })
70 |
71 | expect(plugin.config.upload).toBe(true)
72 | expect(plugin.config.excludeAssets).toBe(undefined)
73 | expect(plugin.config.projectToken).toEqual('abc123')
74 | expect(plugin.config.host).toEqual('https://api.packtracker.io')
75 | expect(plugin.config.failBuild).toEqual(false)
76 | expect(plugin.config.branch).toEqual('default')
77 | expect(plugin.config.author).toEqual('default')
78 | expect(plugin.config.message).toEqual('default')
79 | expect(plugin.config.commit).toEqual('default')
80 | expect(plugin.config.committedAt).toEqual('default')
81 | expect(plugin.config.priorCommit).toEqual('default')
82 | expect(execSync).toHaveBeenCalled()
83 | })
84 |
85 | test('env variables', () => {
86 | process.env.PT_UPLOAD = 'true'
87 | process.env.PT_PROJECT_TOKEN = 'abc123'
88 | process.env.PT_HOST = 'http://custom.host'
89 | process.env.PT_FAIL_BUILD = 'true'
90 | process.env.PT_BRANCH = 'branch'
91 | process.env.PT_AUTHOR = 'email@author.com'
92 | process.env.PT_MESSAGE = 'Some message.'
93 | process.env.PT_COMMIT = '07db3813141ca398ffe8cd07cf71769195abe8a3'
94 | process.env.PT_COMMITTED_AT = '1534978373'
95 | process.env.PT_PRIOR_COMMIT = '4a47653d5fc58fc62757c6b815e715ec77c8ee2e'
96 | process.env.PT_EXCLUDE_ASSETS = 'test'
97 |
98 | const plugin = new PacktrackerPlugin()
99 |
100 | expect(plugin.config.upload).toBe(true)
101 | expect('test').toMatch(plugin.config.excludeAssets)
102 | expect(plugin.config.projectToken).toEqual('abc123')
103 | expect(plugin.config.host).toEqual('http://custom.host')
104 | expect(plugin.config.failBuild).toEqual(true)
105 | expect(plugin.config.branch).toEqual('branch')
106 | expect(plugin.config.author).toEqual('email@author.com')
107 | expect(plugin.config.message).toEqual('Some message.')
108 | expect(plugin.config.commit).toEqual('07db3813141ca398ffe8cd07cf71769195abe8a3')
109 | expect(plugin.config.committedAt).toEqual('1534978373')
110 | expect(plugin.config.priorCommit).toEqual('4a47653d5fc58fc62757c6b815e715ec77c8ee2e')
111 | expect(execSync).not.toHaveBeenCalled()
112 | })
113 |
114 | test('arguments', () => {
115 | const exclude = jest.fn()
116 |
117 | const plugin = new PacktrackerPlugin({
118 | upload: true,
119 | project_token: 'abc123',
120 | host: 'https://fake.host',
121 | fail_build: true,
122 | branch: 'master',
123 | author: 'jane@doe.com',
124 | message: 'This is a commit message',
125 | commit: '07db3813141ca398ffe8cd07cf71769195abe8a3',
126 | committed_at: '1534978373',
127 | prior_commit: '4a47653d5fc58fc62757c6b815e715ec77c8ee2e',
128 | exclude_assets: exclude
129 | })
130 |
131 | expect(plugin.config.upload).toBe(true)
132 | expect(plugin.config.excludeAssets).toBe(exclude)
133 | expect(plugin.config.projectToken).toEqual('abc123')
134 | expect(plugin.config.host).toEqual('https://fake.host')
135 | expect(plugin.config.failBuild).toEqual(true)
136 | expect(plugin.config.branch).toEqual('master')
137 | expect(plugin.config.author).toEqual('jane@doe.com')
138 | expect(plugin.config.message).toEqual('This is a commit message')
139 | expect(plugin.config.commit).toEqual('07db3813141ca398ffe8cd07cf71769195abe8a3')
140 | expect(plugin.config.committedAt).toEqual('1534978373')
141 | expect(plugin.config.priorCommit).toEqual('4a47653d5fc58fc62757c6b815e715ec77c8ee2e')
142 | expect(execSync).not.toHaveBeenCalled()
143 | })
144 |
145 | test('HEAD prevention', () => {
146 | expect(() => {
147 | const plugin = new PacktrackerPlugin({ // eslint-disable-line
148 | upload: true,
149 | project_token: 'abc123',
150 | branch: 'HEAD'
151 | })
152 | }).toThrow()
153 | })
154 | })
155 | })
156 |
--------------------------------------------------------------------------------
/test/files/add.js:
--------------------------------------------------------------------------------
1 | module.exports = function (a, b) {
2 | return a + b
3 | }
4 |
--------------------------------------------------------------------------------
/test/files/entry.js:
--------------------------------------------------------------------------------
1 | var add = require('./add')
2 | var subtract = require('./subtract')
3 |
4 | console.log(add(1, 1))
5 | console.log(subtract(1, 1))
6 |
--------------------------------------------------------------------------------
/test/files/subtract.js:
--------------------------------------------------------------------------------
1 | module.exports = function (a, b) {
2 | return a - b
3 | }
4 |
--------------------------------------------------------------------------------
/test/webpack.test.js:
--------------------------------------------------------------------------------
1 | /* globals jest, describe, test, beforeEach, expect */
2 |
3 | const path = require('path')
4 | const os = require('os')
5 | const webpack2 = require('webpack-2')
6 | const webpack3 = require('webpack-3')
7 | const webpack4 = require('webpack')
8 | const tiny = require('tiny-json-http')
9 |
10 | jest.mock('tiny-json-http')
11 |
12 | const PacktrackerPlugin = require('../')
13 |
14 | tiny.post.mockResolvedValue({
15 | body: {
16 | project_id: 'project-id',
17 | upload_url: 'http://upload.url'
18 | }
19 | })
20 |
21 | tiny.put.mockResolvedValue({
22 | body: {}
23 | })
24 |
25 | function plugin (options = {}) {
26 | return new PacktrackerPlugin(Object.assign({
27 | upload: true,
28 | project_token: 'abc123',
29 | host: 'https://fake.host',
30 | fail_build: false,
31 | branch: 'master',
32 | author: 'jane@doe.com',
33 | message: 'This is a commit message',
34 | commit: '07db3813141ca398ffe8cd07cf71769195abe8a3',
35 | committed_at: '1534978373',
36 | prior_commit: '4a47653d5fc58fc62757c6b815e715ec77c8ee2e'
37 | }, options))
38 | }
39 |
40 | describe('PacktrackerPlugin', () => {
41 | beforeEach(() => {
42 | jest.clearAllMocks()
43 | console.log = jest.fn()
44 | })
45 |
46 | test('webpack@2', (done) => {
47 | webpack2({
48 | entry: path.resolve(__dirname, 'files/entry.js'),
49 | output: {
50 | path: path.resolve(__dirname, 'files/output'),
51 | filename: 'bundle.js'
52 | },
53 | plugins: [ plugin() ]
54 | }, (err, stats) => {
55 | if (err) return done(err)
56 | expectations(stats)
57 | done()
58 | })
59 | })
60 |
61 | test('webpack@3', (done) => {
62 | webpack3({
63 | entry: path.resolve(__dirname, 'files/entry.js'),
64 | output: {
65 | path: path.resolve(__dirname, 'files/output'),
66 | filename: 'bundle.js'
67 | },
68 | plugins: [ plugin() ]
69 | }, (err, stats) => {
70 | if (err) return done(err)
71 | expectations(stats)
72 | done()
73 | })
74 | })
75 |
76 | test('webpack@4', (done) => {
77 | webpack4({
78 | mode: 'production',
79 | entry: path.resolve(__dirname, 'files/entry.js'),
80 | output: {
81 | path: path.resolve(__dirname, 'files/output'),
82 | filename: 'bundle.js'
83 | },
84 | plugins: [ plugin() ]
85 | }, (err, stats) => {
86 | if (err) return done(err)
87 | expectations(stats)
88 | done()
89 | })
90 | })
91 |
92 | test('webpack@4 short circuit uploading', (done) => {
93 | webpack4({
94 | mode: 'production',
95 | entry: path.resolve(__dirname, 'files/entry.js'),
96 | output: {
97 | path: path.resolve(__dirname, 'files/output'),
98 | filename: 'bundle.js'
99 | },
100 | plugins: [
101 | plugin({ upload: false })
102 | ]
103 | }, (err, stats) => {
104 | if (err) return done(err)
105 | expect(tiny.post).not.toHaveBeenCalled()
106 | expect(tiny.put).not.toHaveBeenCalled()
107 | done()
108 | })
109 | })
110 |
111 | test('webpack@4 failed to upload', (done) => {
112 | const error = new Error('Error')
113 | tiny.post.mockRejectedValue(error)
114 |
115 | webpack4({
116 | mode: 'production',
117 | entry: path.resolve(__dirname, 'files/entry.js'),
118 | output: {
119 | path: path.resolve(__dirname, 'files/output'),
120 | filename: 'bundle.js'
121 | },
122 | plugins: [ plugin() ]
123 | }, (err, stats) => {
124 | expect(err).toBe(null)
125 | done()
126 | })
127 | })
128 |
129 | test('webpack@4 failed to upload with fail build option', (done) => {
130 | const error = new Error('Error')
131 | tiny.post.mockRejectedValue(error)
132 |
133 | webpack4({
134 | mode: 'production',
135 | entry: path.resolve(__dirname, 'files/entry.js'),
136 | output: {
137 | path: path.resolve(__dirname, 'files/output'),
138 | filename: 'bundle.js'
139 | },
140 | plugins: [ plugin({ fail_build: true }) ]
141 | }, (err, stats) => {
142 | expect(err).toBeInstanceOf(Error)
143 | expect(err.message).toBe('Error')
144 | done()
145 | })
146 | })
147 | })
148 |
149 | function expectations (stats) {
150 | stats = stats.toJson({ source: false })
151 | expect(tiny.post).toHaveBeenCalledWith({
152 | url: `https://fake.host/generate-upload-url`,
153 | headers: { 'Accept': 'application/json' },
154 | data: {
155 | project_token: 'abc123',
156 | commit_hash: '07db3813141ca398ffe8cd07cf71769195abe8a3'
157 | }
158 | })
159 |
160 | expect(tiny.put).toHaveBeenCalledWith({
161 | url: 'http://upload.url',
162 | data: {
163 | project_id: 'project-id',
164 | packer: 'webpack@' + stats.version,
165 | commit: '07db3813141ca398ffe8cd07cf71769195abe8a3',
166 | committed_at: 1534978373,
167 | branch: 'master',
168 | author: 'jane@doe.com',
169 | message: 'This is a commit message',
170 | prior_commit: '4a47653d5fc58fc62757c6b815e715ec77c8ee2e',
171 | uploader_hostname: os.hostname(),
172 | stats: expect.objectContaining({
173 | assets: [{
174 | chunkNames: ['main'],
175 | chunks: [0],
176 | emitted: true,
177 | isOverSizeLimit: {},
178 | name: 'bundle.js',
179 | size: expect.any(Number)
180 | }]
181 | }),
182 | bundle: [{
183 | groups: [{
184 | groups: expect.arrayContaining([{
185 | gzipSize: expect.any(Number),
186 | id: expect.any(Number),
187 | label: 'entry.js',
188 | parsedSize: expect.any(Number),
189 | path: './test/files/entry.js',
190 | statSize: 116
191 | }, {
192 | gzipSize: expect.any(Number),
193 | id: expect.any(Number),
194 | label: 'add.js',
195 | parsedSize: expect.any(Number),
196 | path: './test/files/add.js',
197 | statSize: 52
198 | }, {
199 | gzipSize: expect.any(Number),
200 | id: expect.any(Number),
201 | label: 'subtract.js',
202 | parsedSize: expect.any(Number),
203 | path: './test/files/subtract.js',
204 | statSize: 52
205 | }]),
206 | gzipSize: expect.any(Number),
207 | label: 'test/files',
208 | parsedSize: expect.any(Number),
209 | path: './test/files',
210 | statSize: 220
211 | }],
212 | gzipSize: expect.any(Number),
213 | label: 'bundle.js',
214 | isAsset: true,
215 | parsedSize: expect.any(Number),
216 | statSize: 220
217 | }]
218 | }
219 | })
220 | }
221 |
--------------------------------------------------------------------------------
/upload.js:
--------------------------------------------------------------------------------
1 | const tiny = require('tiny-json-http')
2 | const { getViewerData } = require('webpack-bundle-analyzer/lib/analyzer')
3 | const { isPlainObject, isEmpty, cloneDeep } = require('lodash')
4 | const omitDeep = require('omit-deep')
5 | const os = require('os')
6 | const logger = require('./logger')
7 |
8 | class Upload {
9 | constructor (config) {
10 | this.config = config
11 | }
12 |
13 | process (statsJson, outputPath) {
14 | logger('processing upload')
15 | if (statsJson.errors.length) {
16 | logger('halting upload due to stats errors')
17 | statsJson.errors.forEach((e) => logger(`stats error: ${e}`))
18 | return Promise.resolve()
19 | }
20 |
21 | // Ensure we're not capturing the source
22 | statsJson = omitDeep(statsJson, ['source'])
23 | logger('filtering out source from stats json')
24 |
25 | const payload = {
26 | packer: 'webpack@' + statsJson.version,
27 | commit: this.config.commit,
28 | committed_at: parseInt(this.config.committedAt),
29 | branch: this.config.branch,
30 | author: this.config.author,
31 | message: this.config.message,
32 | prior_commit: this.config.priorCommit,
33 | stats: statsJson,
34 | uploader_hostname: os.hostname(),
35 | bundle: getBundleData(
36 | cloneDeep(statsJson),
37 | outputPath,
38 | this.config.excludeAssets
39 | )
40 | }
41 |
42 | return generateUploadUrl(
43 | this.config.host,
44 | this.config.projectToken,
45 | this.config.commit
46 | )
47 | .then(response => {
48 | logger(`upload url generated`)
49 | payload.project_id = response.project_id
50 | return uploadToS3(response.upload_url, payload)
51 | })
52 | .then(() => {
53 | logger('stats uploaded')
54 | })
55 | .catch((error) => {
56 | logger(`stats failed to upload: ${error.message}`)
57 | logger(`this could be because your project token is not properly set`)
58 |
59 | if (this.config.failBuild) {
60 | logger('re-throwing failure because `fail_build` set to true')
61 | throw error
62 | }
63 | })
64 | }
65 | }
66 |
67 | function getBundleData (statJson, outputPath, excludeAssets = null) {
68 | let data
69 |
70 | logger('retrieving javascript bundle data')
71 |
72 | try {
73 | data = getViewerData(statJson, outputPath, { excludeAssets })
74 | } catch (err) {
75 | logger(`could not analyze webpack bundle (${err})`)
76 | data = null
77 | }
78 |
79 | if (isPlainObject(data) && isEmpty(data)) {
80 | logger('could not find any javascript bundles')
81 | data = null
82 | }
83 |
84 | return data
85 | }
86 |
87 | function generateUploadUrl (host, projectToken, commitHash) {
88 | logger('generating upload url')
89 | return tiny.post({
90 | url: `${host}/generate-upload-url`,
91 | headers: { 'Accept': 'application/json' },
92 | data: {
93 | project_token: projectToken,
94 | commit_hash: commitHash
95 | }
96 | }).then(response => response.body)
97 | }
98 |
99 | function uploadToS3 (url, data) {
100 | logger('uploading to s3')
101 | return tiny.put({ url, data })
102 | }
103 |
104 | module.exports = Upload
105 |
--------------------------------------------------------------------------------