├── .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 | [![Build Status](https://travis-ci.org/packtracker/webpack-plugin.svg?branch=master)](https://travis-ci.org/packtracker/webpack-plugin) 8 | [![Coverage Status](https://coveralls.io/repos/github/packtracker/webpack-plugin/badge.svg?branch=master)](https://coveralls.io/github/packtracker/webpack-plugin?branch=master) 9 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/c186c2e767ae4d96a6e900bad30992f8)](https://app.codacy.com/app/jondavidjohn/webpack-plugin) 10 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](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 | --------------------------------------------------------------------------------