├── .changeset └── config.json ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── HISTORY.md ├── LICENSE.txt ├── README.md ├── index.js ├── lib └── stats-writer-plugin.js ├── package.json ├── test ├── README.md ├── expected │ ├── build │ │ ├── stats-custom-stats-fields.json │ │ ├── stats-custom-stats.json │ │ ├── stats-custom.json │ │ ├── stats-dynamic.json │ │ ├── stats-from-write-stream.json │ │ ├── stats-override-tostring-opt.json │ │ ├── stats-transform-custom-obj.json │ │ ├── stats-transform-promise.json │ │ ├── stats-transform.json │ │ ├── stats-transform.md │ │ ├── stats.js │ │ └── stats.json │ └── build2 │ │ └── stats-custom2.json ├── func.spec.js ├── mocha.opts ├── packages │ ├── webpack-cli │ │ └── package.json │ ├── webpack │ │ └── package.json │ ├── webpack1 │ │ ├── index.js │ │ └── package.json │ ├── webpack2 │ │ ├── index.js │ │ └── package.json │ ├── webpack3 │ │ ├── index.js │ │ └── package.json │ ├── webpack4 │ │ ├── index.js │ │ └── package.json │ └── webpack5 │ │ ├── index.js │ │ └── package.json ├── scenarios │ ├── webpack1 │ │ ├── webpack.config.fail-promise.js │ │ ├── webpack.config.fail-sync.js │ │ └── webpack.config.js │ ├── webpack2 │ │ ├── webpack.config.fail-promise.js │ │ ├── webpack.config.fail-sync.js │ │ └── webpack.config.js │ ├── webpack3 │ │ ├── webpack.config.fail-promise.js │ │ ├── webpack.config.fail-sync.js │ │ └── webpack.config.js │ ├── webpack4 │ │ ├── webpack.config.fail-promise.js │ │ ├── webpack.config.fail-sync.js │ │ └── webpack.config.js │ └── webpack5 │ │ ├── webpack.config.contenthash.js │ │ ├── webpack.config.fail-promise.js │ │ ├── webpack.config.fail-sync.js │ │ └── webpack.config.js └── src │ └── main.js ├── webpack-stats-plugin-Hero.png └── yarn.lock /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": [ 4 | "@svitejs/changesets-changelog-github-compact", 5 | { 6 | "repo": "FormidableLabs/webpack-stats-plugin" 7 | } 8 | ], 9 | "access": "public", 10 | "baseBranch": "main" 11 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.js,*.json,*.yml] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/scenarios/*/build* 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - formidable/configurations/es6-node 4 | - plugin:import/errors 5 | 6 | plugins: 7 | - import 8 | - promise 9 | 10 | rules: 11 | consistent-return: [error, { treatUndefinedAsUnspecified: true }] 12 | import/no-unresolved: [error, { commonjs: true }] 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: "ubuntu-latest" 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Use Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 18 21 | cache: "yarn" 22 | 23 | - name: Use node_modules cache 24 | id: node-modules-cache 25 | uses: actions/cache@v4 26 | with: 27 | path: node_modules 28 | key: node-modules-${{ runner.os }}-${{ hashFiles('./yarn.lock') }} 29 | restore-keys: | 30 | node-modules-${{ runner.os }}- 31 | 32 | - name: Project installation 33 | if: steps.node-modules-cache.outputs.cache-hit != 'true' 34 | run: yarn install --prefer-offline --frozen-lockfile --non-interactive 35 | env: 36 | CI: true 37 | 38 | - name: Basic script runs 39 | run: node index.js 40 | 41 | - name: Checks 42 | run: yarn run check 43 | env: 44 | # Webpack fails due to crypto enforcements in Node 17+ 45 | # See, e.g., https://github.com/webpack/webpack/issues/14532 46 | NODE_OPTIONS: "--openssl-legacy-provider" 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | id-token: write 13 | issues: write 14 | repository-projects: write 15 | deployments: write 16 | packages: write 17 | pull-requests: write 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 18 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | 27 | - name: Unit Tests 28 | run: yarn run check 29 | env: 30 | # Webpack fails due to crypto enforcements in Node 17+ 31 | # See, e.g., https://github.com/webpack/webpack/issues/14532 32 | NODE_OPTIONS: "--openssl-legacy-provider" 33 | 34 | - name: PR or Publish 35 | id: changesets 36 | uses: changesets/action@v1 37 | with: 38 | version: yarn changeset version 39 | publish: yarn changeset publish 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \.git 2 | \.hg 3 | 4 | \.DS_Store 5 | \.project 6 | bower_components 7 | node_modules 8 | npm-debug.log* 9 | yarn-error.log* 10 | 11 | # Build 12 | coverage 13 | test/scenarios/*/build* 14 | !test/expected/build* 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # webpack-stats-plugin 2 | 3 | ## 1.1.3 4 | 5 | ### Patch Changes 6 | 7 | - Provenance Badge to NPM Addition ([#101](https://github.com/FormidableLabs/webpack-stats-plugin/pull/101)) 8 | 9 | ## 1.1.2 10 | 11 | ### Patch Changes 12 | 13 | - Adding GitHub Action release workflow ([#98](https://github.com/FormidableLabs/webpack-stats-plugin/pull/98)) 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributions 2 | 3 | Contributions welcome! 4 | 5 | We test against all versions of webpack. For a full explanation of our functional tests, see [test/README.md](test/README.md) 6 | 7 | To get started, first install: 8 | 9 | ```sh 10 | $ yarn 11 | ``` 12 | 13 | Our tests first do various webpack builds and then run mocha asserts on the real outputted stats files. Inefficient, but for our small sample size efficient enough. 14 | 15 | ```sh 16 | # Lint and tests 17 | $ yarn run lint 18 | $ yarn run test 19 | 20 | # All together 21 | $ yarn run check 22 | ``` 23 | 24 | ### Using changesets 25 | 26 | Our official release path is to use automation to perform the actual publishing of our packages. The steps are to: 27 | 28 | 1. A human developer adds a changeset. Ideally this is as a part of a PR that will have a version impact on a package. 29 | 2. On merge of a PR our automation system opens a "Version Packages" PR. 30 | 3. On merging the "Version Packages" PR, the automation system publishes the packages. 31 | 32 | Here are more details: 33 | 34 | ### Add a changeset 35 | 36 | When you would like to add a changeset (which creates a file indicating the type of change), in your branch/PR issue this command: 37 | 38 | ```sh 39 | $ yarn changeset 40 | ``` 41 | 42 | to produce an interactive menu. Navigate the packages with arrow keys and hit `` to select 1+ packages. Hit `` when done. Select semver versions for packages and add appropriate messages. From there, you'll be prompted to enter a summary of the change. Some tips for this summary: 43 | 44 | 1. Aim for a single line, 1+ sentences as appropriate. 45 | 2. Include issue links in GH format (e.g. `#123`). 46 | 3. You don't need to reference the current pull request or whatnot, as that will be added later automatically. 47 | 48 | After this, you'll see a new uncommitted file in `.changesets` like: 49 | 50 | ```sh 51 | $ git status 52 | # .... 53 | Untracked files: 54 | (use "git add ..." to include in what will be committed) 55 | .changeset/flimsy-pandas-marry.md 56 | ``` 57 | 58 | Review the file, make any necessary adjustments, and commit it to source. When we eventually do a package release, the changeset notes and version will be incorporated! 59 | 60 | ### Creating versions 61 | 62 | On a merge of a feature PR, the changesets GitHub action will open a new PR titled `"Version Packages"`. This PR is automatically kept up to date with additional PRs with changesets. So, if you're not ready to publish yet, just keep merging feature PRs and then merge the version packages PR later. 63 | 64 | ### Publishing packages 65 | 66 | On the merge of a version packages PR, the changesets GitHub action will publish the packages to npm. 67 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | History 2 | ======= 3 | 4 | ## 1.1.1 5 | 6 | * *Bug*: Better inference of `RawSource` for use with Terser plugin. 7 | [#91](https://github.com/FormidableLabs/webpack-stats-plugin/issues/91) 8 | 9 | ## 1.1.0 10 | 11 | * *Feature*: Add `opts.emit` option. 12 | [#89](https://github.com/FormidableLabs/webpack-stats-plugin/pull/89) 13 | [#84](https://github.com/FormidableLabs/webpack-stats-plugin/issues/84) 14 | 15 | ## 1.0.3 16 | 17 | * *Bug*: Use `RawSource` from current compiler where available. 18 | [#65](https://github.com/FormidableLabs/webpack-stats-plugin/pull/65) 19 | (*[@alexander-akait][]*) 20 | 21 | ## 1.0.2 22 | 23 | * *Bug*: Fix multiple stats output with `webpack --watch`. 24 | [#59](https://github.com/FormidableLabs/webpack-stats-plugin/issues/59) 25 | 26 | ## 1.0.1 27 | 28 | * *Bug*: Fix multiple stats output issue. 29 | [#55](https://github.com/FormidableLabs/webpack-stats-plugin/issues/55) 30 | [#57](https://github.com/FormidableLabs/webpack-stats-plugin/issues/57) 31 | * *Bug*: Change `processAssets` hook stage to `PROCESS_ASSETS_STAGE_REPORT` to correctly get hashed asset names. 32 | [#56](https://github.com/FormidableLabs/webpack-stats-plugin/issues/56) 33 | 34 | ## 1.0.0 35 | 36 | * *Feature*: Support `webpack@5`. 37 | * *Package*: Remove `package.json:engines` and test tweaks for `node4`. 38 | * *Test*: Refactor and separate webpack versions from configs. 39 | 40 | ## 0.3.2 41 | 42 | * *Feature*: Allow `opts.filename` to take `Function` argument. 43 | [#47](https://github.com/FormidableLabs/webpack-stats-plugin/issues/47) 44 | [#48](https://github.com/FormidableLabs/webpack-stats-plugin/pull/48) 45 | (*[@dominics][]*) 46 | 47 | ## 0.3.1 48 | 49 | * *Bug*: Fix options issue with `stats` default fields included in output. 50 | [#44](https://github.com/FormidableLabs/webpack-stats-plugin/issues/44) 51 | 52 | ## 0.3.0 53 | 54 | * *Feature*: Add `opts.stats` to pass custom webpack-native [stats](https://webpack.js.org/configuration/stats/#stats) config. 55 | [#18](https://github.com/FormidableLabs/webpack-stats-plugin/issues/18) 56 | [#31](https://github.com/FormidableLabs/webpack-stats-plugin/pull/31) 57 | (*[@evocateur][]*) 58 | 59 | ## 0.2.1 60 | 61 | * *Feature*: Allow `opts.transform` to be a `Promise` as well as a `Function`. 62 | [#27](https://github.com/FormidableLabs/webpack-stats-plugin/issues/27) 63 | * *Bug*: Correctly fail plugin if `opts.transform` throws in webpack4. 64 | * *Test*: Test errors in all versions of webpack. 65 | 66 | ## 0.2.0 67 | 68 | * **Breaking**: Update to node4+. 69 | * Webpack4 compatibility. 70 | (*[@jdelStrother][]*) 71 | 72 | ## 0.1.5 73 | 74 | * Slim down published npm package. 75 | (*[@evilebottnawi][]*) 76 | 77 | ## 0.1.4 78 | 79 | * Add constructor definition. 80 | (*[@vlkosinov][]*) 81 | 82 | ## 0.1.3 83 | 84 | * Add `opts.compiler` to transform function. 85 | [#15](https://github.com/FormidableLabs/webpack-stats-plugin/issues/15) 86 | (*[@lostrouter][]*) 87 | 88 | ## 0.1.2 89 | 90 | * _Bad release_ 91 | 92 | ## 0.1.1 93 | 94 | * Allow `opts.transform` to output arbitrary formats. 95 | (*[@tanem][]*) 96 | 97 | ## 0.1.0 98 | 99 | * Emit stat file in compilation assets, allowing use in webpack-dev-server / webpack-stream. 100 | Fixes [#4](https://github.com/FormidableLabs/webpack-stats-plugin/issues/4) 101 | (*[@seanchas116][]*) 102 | 103 | ## 0.0.3 104 | 105 | * Add `mkdir -p` functionality for `opts.path` directories. 106 | 107 | ## 0.0.2 108 | 109 | * Actually works. 110 | 111 | ## 0.0.1 112 | 113 | * Is embarrassing and shall be forgotten. 114 | 115 | [@alexander-akait]: https://github.com/alexander-akait 116 | [@evocateur]: https://github.com/evocateur 117 | [@evilebottnawi]: https://github.com/evilebottnawi 118 | [@dominics]: https://github.com/dominics 119 | [@lostrouter]: https://github.com/lostrouter 120 | [@ryan-roemer]: https://github.com/ryan-roemer 121 | [@seanchas116]: https://github.com/seanchas116 122 | [@tanem]: https://github.com/tanem 123 | [@vlkosinov]: https://github.com/vlkosinov 124 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015-2018 Formidable Labs, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Webpack Stats Plugin 3 | 4 | 5 |
6 |
7 | 8 |

9 | 10 | weekly downloads 11 | 12 | 13 | current version 14 | 15 | 16 | build status 17 | 18 | 19 | Maintenance Status 20 | 21 | 22 | license 23 | 24 |

25 | 26 | This plugin will ingest the webpack [stats](https://webpack.js.org/configuration/stats/#stats) object, process / transform the object and write out to a file for further consumption. 27 | 28 | The most common use case is building a hashed bundle and wanting to programmatically refer to the correct bundle path in your Node.js server. 29 | 30 | ## Installation 31 | 32 | The plugin is available via [npm](https://www.npmjs.com/package/webpack-stats-plugin): 33 | 34 | ```sh 35 | $ npm install --save-dev webpack-stats-plugin 36 | $ yarn add --dev webpack-stats-plugin 37 | ``` 38 | 39 | ## Examples 40 | 41 | We have example webpack configurations for all versions of webpack. See., e.g. [`test/scenarios/webpack5/webpack.config.js`](test/scenarios/webpack5/webpack.config.js). 42 | 43 | ### CLI 44 | 45 | If you are using `webpack-cli`, you can enable with: 46 | 47 | ```sh 48 | $ webpack-cli --plugin webpack-stats-plugin/lib/stats-writer-plugin 49 | ``` 50 | 51 | ### Basic 52 | 53 | A basic `webpack.config.js`-based integration: 54 | 55 | ```js 56 | const { StatsWriterPlugin } = require("webpack-stats-plugin"); 57 | 58 | module.exports = { 59 | plugins: [ 60 | // Everything else **first**. 61 | 62 | // Write out stats file to build directory. 63 | new StatsWriterPlugin({ 64 | filename: "stats.json", // Default 65 | }), 66 | ], 67 | }; 68 | ``` 69 | 70 | ### Custom `stats` Configuration 71 | 72 | This option is passed to the webpack compiler's [`getStats().toJson()`](https://webpack.js.org/api/node/#statstojsonoptions) method. 73 | 74 | ```js 75 | const { StatsWriterPlugin } = require("webpack-stats-plugin"); 76 | 77 | module.exports = { 78 | plugins: [ 79 | new StatsWriterPlugin({ 80 | stats: { 81 | all: false, 82 | assets: true, 83 | }, 84 | }), 85 | ], 86 | }; 87 | ``` 88 | 89 | ### Custom Transform Function 90 | 91 | The transform function has a signature of: 92 | 93 | ```js 94 | /** 95 | * Transform skeleton. 96 | * 97 | * @param {Object} data Stats object 98 | * @param {Object} opts Options 99 | * @param {Object} opts.compiler Current compiler instance 100 | * @returns {String} String to emit to file 101 | */ 102 | function (data, opts) {} 103 | ``` 104 | 105 | which you can use like: 106 | 107 | ```js 108 | const { StatsWriterPlugin } = require("webpack-stats-plugin"); 109 | 110 | module.exports = { 111 | plugins: [ 112 | new StatsWriterPlugin({ 113 | transform(data, opts) { 114 | return JSON.stringify( 115 | { 116 | main: data.assetsByChunkName.main[0], 117 | css: data.assetsByChunkName.main[1], 118 | }, 119 | null, 120 | 2 121 | ); 122 | }, 123 | }), 124 | ], 125 | }; 126 | ``` 127 | 128 | ### Promise transform 129 | 130 | You can use an asynchronous promise to transform as well: 131 | 132 | ```js 133 | const { StatsWriterPlugin } = require("webpack-stats-plugin"); 134 | 135 | module.exports = { 136 | plugins: [ 137 | new StatsWriterPlugin({ 138 | filename: "stats-transform-promise.json", 139 | transform(data) { 140 | return Promise.resolve().then(() => 141 | JSON.stringify( 142 | { 143 | main: data.assetsByChunkName.main, 144 | }, 145 | null, 146 | INDENT 147 | ) 148 | ); 149 | }, 150 | }), 151 | ], 152 | }; 153 | ``` 154 | 155 | ## Plugins 156 | 157 | - [`StatsWriterPlugin(opts)`](#statswriterplugin-opts-) 158 | 159 | ### `StatsWriterPlugin(opts)` 160 | 161 | - **opts** (`Object`) options 162 | - **opts.filename** (`String|Function`) output file name (Default: `"stats.json"`) 163 | - **opts.fields** (`Array`) fields of stats obj to keep (Default: `["assetsByChunkName"]`) 164 | - **opts.stats** (`Object|String`) stats config object or string preset (Default: `{}`) 165 | - **opts.transform** (`Function|Promise`) transform stats obj (Default: `JSON.stringify()`) 166 | - **opts.emit** (`Boolean`) add stats file to webpack output? (Default: `true`) 167 | 168 | Stats writer module. 169 | 170 | Stats can be a string or array (we'll have an array due to source maps): 171 | 172 | ```js 173 | "assetsByChunkName": { 174 | "main": [ 175 | "cd6371d4131fbfbefaa7.bundle.js", 176 | "../js-map/cd6371d4131fbfbefaa7.bundle.js.map" 177 | ] 178 | }, 179 | ``` 180 | 181 | **`fields`, `stats`** 182 | 183 | The stats object is **big**. It includes the entire source included in a bundle. Thus, we default `opts.fields` to `["assetsByChunkName"]` to only include those. However, if you want the _whole thing_ (maybe doing an `opts.transform` function), then you can set `fields: null` in options to get **all** of the stats object. 184 | 185 | You may also pass a custom stats config object (or string preset) via `opts.stats` in order to select exactly what you want added to the data passed to the transform. When `opts.stats` is passed, `opts.fields` will default to `null`. 186 | 187 | See: 188 | 189 | - https://webpack.js.org/configuration/stats/#stats 190 | - https://webpack.js.org/api/node/#stats-object 191 | 192 | **`filename`** 193 | 194 | The `opts.filename` option can be a file name or path relative to `output.path` in webpack configuration. It should not be absolute. It may also be a function, in which case it will be passed the current compiler instance and expected to return a filename to use. 195 | 196 | **`transform`** 197 | 198 | By default, the retrieved stats object is `JSON.stringify`'ed: 199 | 200 | ```javascript 201 | new StatsWriterPlugin({ 202 | transform(data) { 203 | return JSON.stringify(data, null, 2); 204 | }, 205 | }); 206 | ``` 207 | 208 | By supplying an alternate transform you can target _any_ output format. See [`test/scenarios/webpack5/webpack.config.js`](test/scenarios/webpack5/webpack.config.js) for various examples including Markdown output. 209 | 210 | - **Warning**: The output of `transform` should be a `String`, not an object. On Node `v4.x` if you return a real object in `transform`, then webpack will break with a `TypeError` (See [#8](https://github.com/FormidableLabs/webpack-stats-plugin/issues/8)). Just adding a simple `JSON.stringify()` around your object is usually what you need to solve any problems. 211 | 212 | **Internal notes** 213 | 214 | In modern webpack, the plugin uses the [`processAssets`](https://webpack.js.org/api/compilation-hooks/#processassets) compilation hook if available when adding the stats object file to the overall compilation to write out along with all the other webpack-built assets. This is the [last possible place](https://github.com/webpack/webpack/blob/f2f998b58362d5edc9945a48f8245a3347ad007c/lib/Compilation.js#L2000-L2007) to hook in before the compilation is frozen in future webpack releases. 215 | 216 | In earlier webpack, the plugin uses the much later [`emit`](https://webpack.js.org/api/compiler-hooks/#emit) compiler hook. There are technically some assets/stats data that could be added after `processAssets` and before `emit`, but for most practical uses of this plugin users shouldn't see any differences in the usable data produced by different versions of webpack. 217 | 218 | ## Maintenance Status 219 | 220 | **Active:** NearForm is actively working on this project, and we expect to continue for work for the foreseeable future. Bug reports, feature requests and pull requests are welcome. 221 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const StatsWriterPlugin = require("./lib/stats-writer-plugin"); 4 | 5 | module.exports = { 6 | StatsWriterPlugin 7 | }; 8 | -------------------------------------------------------------------------------- /lib/stats-writer-plugin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const INDENT = 2; 4 | const DEFAULT_TRANSFORM = (data) => JSON.stringify(data, null, INDENT); 5 | 6 | /** 7 | * Stats writer module. 8 | * 9 | * Stats can be a string or array (we'll have array due to source maps): 10 | * 11 | * ```js 12 | * "assetsByChunkName": { 13 | * "main": [ 14 | * "cd6371d4131fbfbefaa7.bundle.js", 15 | * "../js-map/cd6371d4131fbfbefaa7.bundle.js.map" 16 | * ] 17 | * }, 18 | * ``` 19 | * 20 | * **Note**: The stats object is **big**. It includes the entire source included 21 | * in a bundle. Thus, we default `opts.fields` to `["assetsByChunkName"]` to 22 | * only include those. However, if you want the _whole thing_ (maybe doing an 23 | * `opts.transform` function), then you can set `fields: null` in options to 24 | * get **all** of the stats object. 25 | * 26 | * You may also pass a custom stats config object (or string preset) via `opts.stats` 27 | * in order to select exactly what you want added to the data passed to the transform. 28 | * When `opts.stats` is passed, `opts.fields` will default to `null`. 29 | * 30 | * See: 31 | * - https://webpack.js.org/configuration/stats/#stats 32 | * - https://webpack.js.org/api/node/#stats-object 33 | * 34 | * **`filename`**: The `opts.filename` option can be a file name or path relative to 35 | * `output.path` in webpack configuration. It should not be absolute. It may also 36 | * be a function, in which case it will be passed the current compiler instance 37 | * and expected to return a filename to use. 38 | * 39 | * **`transform`**: By default, the retrieved stats object is `JSON.stringify`'ed 40 | * but by supplying an alternate transform you can target _any_ output format. 41 | * See [`test/scenarios/webpack5/webpack.config.js`](test/scenarios/webpack5/webpack.config.js) for 42 | * various examples including Markdown output. 43 | * 44 | * - **Warning**: The output of `transform` should be a `String`, not an object. 45 | * On Node `v4.x` if you return a real object in `transform`, then webpack 46 | * will break with a `TypeError` (See #8). Just adding a simple 47 | * `JSON.stringify()` around your object is usually what you need to solve 48 | * any problems. 49 | * 50 | * @param {Object} opts options 51 | * @param {String|Function} opts.filename output file name (Default: `"stats.json`") 52 | * @param {Array} opts.fields stats obj fields (Default: `["assetsByChunkName"]`) 53 | * @param {Object|String} opts.stats stats config object or string preset (Default: `{}`) 54 | * @param {Function|Promise} opts.transform transform stats obj (Default: `JSON.stringify()`) 55 | * @param {Boolean} opts.emit add stats file to webpack output? (Default: `true`) 56 | * 57 | * @api public 58 | */ 59 | class StatsWriterPlugin { 60 | constructor(opts) { 61 | opts = opts || {}; 62 | this.opts = {}; 63 | this.opts.filename = opts.filename || "stats.json"; 64 | this.opts.fields = typeof opts.fields !== "undefined" ? opts.fields : ["assetsByChunkName"]; 65 | this.opts.stats = opts.stats || {}; 66 | this.opts.transform = opts.transform || DEFAULT_TRANSFORM; 67 | this.opts.emit = opts.emit !== false; 68 | 69 | if (typeof opts.stats !== "undefined" && typeof opts.fields === "undefined") { 70 | // if custom stats config provided, do not filter fields unless explicitly configured 71 | this.opts.fields = null; 72 | } 73 | } 74 | 75 | apply(compiler) { 76 | if (compiler.hooks) { 77 | let emitHookSet = false; 78 | 79 | // Capture the compilation and then set up further hooks. 80 | compiler.hooks.thisCompilation.tap("stats-writer-plugin", (compilation) => { 81 | if (compilation.hooks.processAssets) { 82 | // Modern: `processAssets` is one of the last hooks before frozen assets. 83 | // We choose `PROCESS_ASSETS_STAGE_REPORT` which is the last possible 84 | // stage after which to emit. 85 | // 86 | // See: 87 | // - https://webpack.js.org/api/compilation-hooks/#processassets 88 | // - https://github.com/FormidableLabs/webpack-stats-plugin/issues/56 89 | compilation.hooks.processAssets.tapPromise( 90 | { 91 | name: "stats-writer-plugin", 92 | stage: compilation.constructor.PROCESS_ASSETS_STAGE_REPORT 93 | }, 94 | () => this.emitStats(compilation) 95 | ); 96 | } else if (!emitHookSet) { 97 | // Legacy. 98 | // 99 | // Set up the `compiler` level hook only once to avoid multiple 100 | // calls during `webpack --watch`. (We have to do this here because 101 | // we can't otherwise detect if `compilation.hooks.processAssets` is 102 | // available for modern mode.) 103 | emitHookSet = true; 104 | compiler.hooks.emit.tapPromise("stats-writer-plugin", this.emitStats.bind(this)); 105 | } 106 | }); 107 | } else { 108 | // Super-legacy. 109 | compiler.plugin("emit", this.emitStats.bind(this)); 110 | } 111 | } 112 | 113 | emitStats(curCompiler, callback) { 114 | // Get stats. 115 | // The second argument automatically skips heavy options (reasons, source, etc) 116 | // if they are otherwise unspecified. 117 | let stats = curCompiler.getStats().toJson(this.opts.stats); 118 | 119 | // Filter to fields. 120 | if (this.opts.fields) { 121 | stats = this.opts.fields.reduce((memo, key) => { 122 | memo[key] = stats[key]; 123 | return memo; 124 | }, {}); 125 | } 126 | 127 | // Transform to string. 128 | let err; 129 | return Promise.resolve() 130 | 131 | // Transform. 132 | .then(() => this.opts.transform(stats, { 133 | compiler: curCompiler 134 | })) 135 | .catch((e) => { err = e; }) 136 | 137 | // Finish up. 138 | // eslint-disable-next-line max-statements,complexity 139 | .then((statsStr) => { 140 | // Handle errors. 141 | if (err) { 142 | curCompiler.errors.push(err); 143 | // eslint-disable-next-line promise/no-callback-in-promise 144 | if (callback) { return void callback(err); } 145 | throw err; 146 | } 147 | 148 | // Short-circuit if not emitting. 149 | if (!this.opts.emit) { 150 | // eslint-disable-next-line promise/no-callback-in-promise,promise/always-return 151 | if (callback) { return void callback(); } 152 | return; 153 | } 154 | 155 | // Create simple equivalent of RawSource from webpack-sources. 156 | const statsBuf = Buffer.from(statsStr || "", "utf-8"); 157 | const webpack = curCompiler.webpack || (curCompiler.compiler || {}).webpack; 158 | const source = webpack 159 | // webpack5+ abstraction 160 | ? new webpack.sources.RawSource(statsBuf) 161 | // webpack4- manual class 162 | : { 163 | source() { 164 | return statsBuf; 165 | }, 166 | size() { 167 | return statsBuf.length; 168 | } 169 | }; 170 | 171 | const filename = typeof this.opts.filename === "function" 172 | ? this.opts.filename(curCompiler) 173 | : this.opts.filename; 174 | 175 | // Add to assets. 176 | if (curCompiler.emitAsset) { 177 | // Modern method. 178 | const asset = curCompiler.getAsset(filename); 179 | if (asset) { 180 | curCompiler.updateAsset(filename, source); 181 | } else { 182 | curCompiler.emitAsset(filename, source); 183 | } 184 | } else { 185 | // Fallback to deprecated method. 186 | curCompiler.assets[filename] = source; 187 | } 188 | 189 | // eslint-disable-next-line promise/no-callback-in-promise,promise/always-return 190 | if (callback) { return void callback(); } 191 | }); 192 | } 193 | } 194 | 195 | module.exports = StatsWriterPlugin; 196 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-stats-plugin", 3 | "version": "1.1.3", 4 | "description": "Webpack stats plugin", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/FormidableLabs/webpack-stats-plugin" 9 | }, 10 | "keywords": [ 11 | "webpack" 12 | ], 13 | "author": "Ryan Roemer ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/FormidableLabs/webpack-stats-plugin/issues" 17 | }, 18 | "files": [ 19 | "lib", 20 | "index.js" 21 | ], 22 | "scripts": { 23 | "lint": "eslint .", 24 | "test": "yarn run test:clean && yarn run test:build && yarn run test:run", 25 | "test:run": "mocha \"test/**/*.spec.js\"", 26 | "test:clean": "rm -rf test/scenarios/webpack*/build*", 27 | "test:build:single": "node node_modules/webpack${VERS}/index.js --config test/scenarios/webpack${VERS}/webpack.config${WP_EXTRA}.js", 28 | "test:build": "builder envs test:build:single \"[{\\\"VERS\\\":1},{\\\"VERS\\\":2},{\\\"VERS\\\":3},{\\\"VERS\\\":4},{\\\"VERS\\\":5},{\\\"VERS\\\":5,\\\"WP_EXTRA\\\":\\\".contenthash\\\"}]\" --buffer", 29 | "check": "yarn run lint && yarn run test" 30 | }, 31 | "dependencies": {}, 32 | "devDependencies": { 33 | "@changesets/cli": "^2.26.1", 34 | "@svitejs/changesets-changelog-github-compact": "^0.1.1", 35 | "babel-eslint": "^10.1.0", 36 | "builder": "^5.0.0", 37 | "chai": "^4.2.0", 38 | "eslint": "^7.6.0", 39 | "eslint-config-formidable": "^4.0.0", 40 | "eslint-plugin-filenames": "^1.3.2", 41 | "eslint-plugin-import": "^2.22.0", 42 | "eslint-plugin-promise": "^4.2.1", 43 | "json-stream-stringify": "^3.0.0", 44 | "mocha": "^8.1.1", 45 | "pify": "^5.0.0", 46 | "webpack": "file:test/packages/webpack", 47 | "webpack-cli": "file:test/packages/webpack-cli", 48 | "webpack1": "file:test/packages/webpack1", 49 | "webpack2": "file:test/packages/webpack2", 50 | "webpack3": "file:test/packages/webpack3", 51 | "webpack4": "file:test/packages/webpack4", 52 | "webpack5": "file:test/packages/webpack5" 53 | }, 54 | "publishConfig": { 55 | "provenance": true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | Tests 2 | ===== 3 | 4 | We test the `webpack-stats-plugin` in all webpack browser version. 5 | 6 | ## Setup 7 | 8 | To allow a multiple install scenario, we have multiple pseudo-packages and 9 | configuration directories as follows: 10 | 11 | ``` 12 | test/{packages,scenarios} 13 | ├── webpack # Fake package to prevent node_modules flattening 14 | ├── webpack-cli # Fake package to prevent node_modules flattening 15 | ├── webpack1 16 | ├── webpack2 17 | ├── webpack3 18 | ├── webpack4 19 | └── webpack5 20 | ``` 21 | 22 | where each `webpack` directory has a structure of: 23 | 24 | ``` 25 | test/packages/webpack1 26 | ├── index.js 27 | └── package.json 28 | 29 | test/scenarios/webpack1 30 | └── webpack.config.js 31 | ``` 32 | 33 | where an independent `package.json` that has the desired `webpack` 34 | (and `webpack-cli` for modern webpacks) version, e.g.: 35 | 36 | ```js 37 | "dependencies": { 38 | "webpack": "^1.15.0" 39 | } 40 | ``` 41 | 42 | The `index.js` file in these directories is simply a re-export of the applicable 43 | `webpack` CLI tool. We are then able to take a root `package.json` dependency on 44 | each of these different versions like: 45 | 46 | ```js 47 | "devDependencies": { 48 | "webpack1": "file:test/packages/webpack1", 49 | "webpack2": "file:test/packages/webpack2", 50 | "webpack3": "file:test/packages/webpack3", 51 | "webpack4": "file:test/packages/webpack4", 52 | "webpack4": "file:test/packages/webpack5" 53 | } 54 | ``` 55 | 56 | and we end up with **all** versions that we want! 57 | 58 | **Side Note**: The `index.js` file is crafted carefully to account for 59 | `node_modules` flattening performed by `yarn`. There are some complexities of 60 | that omitted from this guide. 61 | 62 | ## Build 63 | 64 | We then build files outside of git source to, e.g.: 65 | 66 | ```js 67 | test/scenarios/webpack1 68 | ├── build 69 | └── build2 70 | ``` 71 | 72 | We do this with a command that looks something like: 73 | 74 | ```sh 75 | $ node node_modules/webpack5/index.js --config test/scenarios/webpack5/webpack.config.js 76 | ``` 77 | 78 | which importantly **must** change directory to our re-export file so that the 79 | "detect a local webpack in `CWD/node_modules/.bin/webpack` scheme" doesn't take 80 | over. The package in that location is only correct for **one** scenario and is 81 | there because of webpack flattening. 82 | 83 | ## Tests 84 | 85 | Once we have a build, we can do tests. We have a set of "expected" files that 86 | are committed to source in: 87 | 88 | ``` 89 | test/expected/ 90 | ├── build 91 | │   ├── stats-custom.json 92 | │   ├── stats-transform-custom-obj.json 93 | │   ├── stats-transform.json 94 | │   ├── stats-transform.md 95 | │   └── stats.json 96 | └── build2 97 | └── stats-custom2.json 98 | ``` 99 | 100 | Our mocha tests in [func.spec.js](test/func.spec.js) first read in all of these expected files, then all of the build files from each of the `webpack` directories. We then have dynamic suites and tests to wrap this up and assert similarities. 101 | 102 | We take a slight helping tool to regex replace any file hashes with the token `HASH` to get clean asserts. 103 | 104 | Putting this all together, our steps are: 105 | 106 | ```sh 107 | $ yarn run test:clean 108 | $ yarn run test:build 109 | $ yarn run test:run 110 | ``` 111 | 112 | or as a single command: 113 | 114 | ```sh 115 | $ yarn run test 116 | ``` 117 | -------------------------------------------------------------------------------- /test/expected/build/stats-custom-stats-fields.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [], 3 | "warnings": [], 4 | "assets": [ 5 | { 6 | "name": "HASH.main.js", 7 | "size": 3947, 8 | "chunks": [ 9 | "main" 10 | ], 11 | "chunkNames": [ 12 | "main" 13 | ] 14 | }, 15 | { 16 | "name": "stats.json", 17 | "size": 75, 18 | "chunks": [], 19 | "chunkNames": [] 20 | }, 21 | { 22 | "name": "stats-transform.json", 23 | "size": 44, 24 | "chunks": [], 25 | "chunkNames": [] 26 | }, 27 | { 28 | "name": "stats-transform.md", 29 | "size": 62, 30 | "chunks": [], 31 | "chunkNames": [] 32 | }, 33 | { 34 | "name": "stats-transform-custom-obj.json", 35 | "size": 44, 36 | "chunks": [], 37 | "chunkNames": [] 38 | }, 39 | { 40 | "name": "stats-custom.json", 41 | "size": 75, 42 | "chunks": [], 43 | "chunkNames": [] 44 | }, 45 | { 46 | "name": "../build2/stats-custom2.json", 47 | "size": 75, 48 | "chunks": [], 49 | "chunkNames": [] 50 | }, 51 | { 52 | "name": "stats-dynamic.json", 53 | "size": 75, 54 | "chunks": [], 55 | "chunkNames": [] 56 | }, 57 | { 58 | "name": "stats-transform-promise.json", 59 | "size": 44, 60 | "chunks": [], 61 | "chunkNames": [] 62 | }, 63 | { 64 | "name": "stats-custom-stats.json", 65 | "size": 125, 66 | "chunks": [], 67 | "chunkNames": [] 68 | } 69 | ], 70 | "hash": "HASH", 71 | "publicPath": "/website-o-doom/", 72 | "namedChunkGroups": { 73 | "main": { 74 | "chunks": [ 75 | "main" 76 | ], 77 | "assets": [ 78 | "HASH.main.js" 79 | ], 80 | "children": {}, 81 | "childAssets": {} 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /test/expected/build/stats-custom-stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets": [], 3 | "assetsByChunkName": { 4 | "main": "0fbcc60c464127ab3381.main.js" 5 | }, 6 | "errors": [], 7 | "warnings": [] 8 | } -------------------------------------------------------------------------------- /test/expected/build/stats-custom.json: -------------------------------------------------------------------------------- 1 | { 2 | "assetsByChunkName": { 3 | "main": "0fbcc60c464127ab3381.main.js" 4 | } 5 | } -------------------------------------------------------------------------------- /test/expected/build/stats-dynamic.json: -------------------------------------------------------------------------------- 1 | { 2 | "assetsByChunkName": { 3 | "main": [ 4 | "HASH.main.js" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /test/expected/build/stats-from-write-stream.json: -------------------------------------------------------------------------------- 1 | { 2 | "assetsByChunkName": { 3 | "main": [ 4 | "d22a9c95386c8eee182c.main.js" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /test/expected/build/stats-override-tostring-opt.json: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | { 4 | "rendered": true, 5 | "initial": true, 6 | "entry": true, 7 | "size": 102, 8 | "names": [ 9 | "main" 10 | ] 11 | } 12 | ], 13 | "errors": [], 14 | "warnings": [] 15 | } -------------------------------------------------------------------------------- /test/expected/build/stats-transform-custom-obj.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": [ 3 | "HASH.main.js" 4 | ] 5 | } -------------------------------------------------------------------------------- /test/expected/build/stats-transform-promise.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": [ 3 | "HASH.main.js" 4 | ] 5 | } -------------------------------------------------------------------------------- /test/expected/build/stats-transform.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": [ 3 | "HASH.main.js" 4 | ] 5 | } -------------------------------------------------------------------------------- /test/expected/build/stats-transform.md: -------------------------------------------------------------------------------- 1 | Name | Asset 2 | :--- | :---- 3 | main | HASH.main.js 4 | -------------------------------------------------------------------------------- /test/expected/build/stats.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | console.log("hello world"); 3 | -------------------------------------------------------------------------------- /test/expected/build/stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "assetsByChunkName": { 3 | "main": [ 4 | "HASH.main.js" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /test/expected/build2/stats-custom2.json: -------------------------------------------------------------------------------- 1 | { 2 | "assetsByChunkName": { 3 | "main": [ 4 | "HASH.main.js" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /test/func.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* eslint-env mocha*/ 4 | /* eslint-disable max-nested-callbacks*/ 5 | 6 | /** 7 | * Functional tests. 8 | * 9 | * ## Builds 10 | * 11 | * Build webpack1-4 to actual files, read those in, and then assert against 12 | * "expected" fixtures in `test/expected`. 13 | * 14 | * ## Failures 15 | * 16 | * Use `builder` to do multi-process executions and assert against the logged 17 | * error output. 18 | */ 19 | const path = require("path"); 20 | const pify = require("pify"); 21 | const fs = pify(require("fs")); 22 | const cp = require("child_process"); 23 | const { expect } = require("chai"); 24 | const builderCli = require.resolve("builder/bin/builder"); 25 | 26 | const BUILD_DIRS = ["build", "build2"]; 27 | const WEBPACKS = [1, 2, 3, 4, 5].map((n) => `webpack${n}`); // eslint-disable-line no-magic-numbers 28 | const VERSIONS = [{ VERS: 1 }, { VERS: 2 }, { VERS: 3 }, { VERS: 4 }, { VERS: 5 }]; 29 | const NUM_ERRS = VERSIONS.length; 30 | 31 | // Specific hash regex to abstract. 32 | const HASH_RE = /[0-9a-f]{20}/gm; 33 | 34 | const webpackVers = (webpack) => parseInt(webpack.replace("webpack", ""), 10); 35 | 36 | // Permissively allow empty files. 37 | const allowEmpty = (err) => { 38 | if (err.code === "ENOENT") { 39 | return []; 40 | } 41 | 42 | throw err; 43 | }; 44 | 45 | // Normalize expected files if earlier than webpack4. 46 | const EXPECTED_NORMS = { 47 | "build/stats-custom-stats-fields.json": ({ data }) => JSON.stringify({ 48 | ...JSON.parse(data), 49 | // Remove fields not found in early webpack. 50 | namedChunkGroups: undefined 51 | }, null, 2) // eslint-disable-line no-magic-numbers 52 | }; 53 | 54 | const normalizeExpected = ({ data, name, webpack }) => { 55 | // Normalize expecteds and previous to webpack4+ 56 | const norm = EXPECTED_NORMS[name]; 57 | if (norm && webpackVers(webpack) < 4) { // eslint-disable-line no-magic-numbers 58 | return norm({ data }); 59 | } 60 | 61 | // Modern (webpack4) data. 62 | return data; 63 | }; 64 | 65 | const normalizeEntryPoints = (obj) => { 66 | // webpack5+ style. 67 | if (obj.main === "HASH.main.js") { 68 | obj.main = ["HASH.main.js"]; 69 | } 70 | }; 71 | 72 | // Normalize / smooth over webpack version differences in data files. 73 | const normalizeFile = ({ data, name }) => { 74 | // First, do string-based normalizations and short-circuit if not JSON. 75 | const dataStr = data.replace(HASH_RE, "HASH"); 76 | if (!name.endsWith(".json")) { return dataStr; } 77 | 78 | // Then, as an object if JSON file. 79 | const dataObj = JSON.parse(dataStr); 80 | 81 | // Custom output of just entry points. 82 | normalizeEntryPoints(dataObj); 83 | 84 | if (dataObj.assets) { 85 | // Sort for determinism 86 | dataObj.assets = dataObj.assets.sort((a, b) => a.name.localeCompare(b.name)); 87 | 88 | // Normalize ephemeral build stuff. 89 | // eslint-disable-next-line max-statements 90 | dataObj.assets = dataObj.assets.map((asset) => { 91 | // Sort keys. 92 | asset = Object.keys(asset) 93 | .sort() 94 | .reduce((m, k) => Object.assign(m, { [k]: asset[k] }), {}); 95 | 96 | // Mutate size and naming fields. 97 | if (asset.name === "HASH.main.js") { 98 | asset.size = 1234; 99 | asset.chunks = ["main"]; // webpack4+ style. 100 | } else if ((/stats(-.*|)\.json/).test(path.basename(asset.name))) { 101 | // Stats objects themselves are different sizes in webpack5+ bc of array. 102 | asset.size = 1234; 103 | } 104 | 105 | // Remove webpack4+ fields 106 | delete asset.auxiliaryChunkIdHints; 107 | delete asset.auxiliaryChunkNames; 108 | delete asset.auxiliaryChunks; 109 | delete asset.cached; 110 | delete asset.chunkIdHints; 111 | delete asset.comparedForEmit; 112 | delete asset.emitted; 113 | delete asset.info; 114 | delete asset.isOverSizeLimit; 115 | delete asset.related; 116 | delete asset.type; 117 | 118 | return asset; 119 | }); 120 | } 121 | 122 | if (dataObj.assetsByChunkName) { 123 | normalizeEntryPoints(dataObj.assetsByChunkName); 124 | } 125 | 126 | if (dataObj.namedChunkGroups) { 127 | Object.values(dataObj.namedChunkGroups).forEach((val) => { 128 | // webpack5+ normalization 129 | if (val.assets) { 130 | val.assets.forEach((assetName, i) => { 131 | if (typeof assetName === "string") { 132 | val.assets[i] = { name: assetName }; 133 | } 134 | }); 135 | } 136 | 137 | // Remove webpack5+ fields 138 | delete val.name; 139 | delete val.filteredAssets; 140 | delete val.assetsSize; 141 | delete val.auxiliaryAssets; 142 | delete val.filteredAuxiliaryAssets; 143 | delete val.auxiliaryAssetsSize; 144 | delete val.isOverSizeLimit; 145 | }); 146 | } 147 | 148 | return JSON.stringify(dataObj, null, 2); // eslint-disable-line no-magic-numbers 149 | }; 150 | 151 | // Read files to an object. 152 | const readBuild = async (buildDir) => { 153 | const files = await Promise.all(BUILD_DIRS.map( 154 | (dir) => fs.readdir(path.join(__dirname, buildDir, dir)).catch(allowEmpty) 155 | )) 156 | // Create a flat list of files in an array. 157 | .then((dirs) => dirs 158 | // Add directory. 159 | .map((list, idx) => list.map((file) => path.join(BUILD_DIRS[idx], file))) 160 | // Flatten. 161 | .reduce((memo, list) => memo.concat(list), []) 162 | // Remove "main.js" 163 | .filter((file) => !(/\.main\.js$/).test(file)) 164 | ); 165 | 166 | // Read all objects to a string. 167 | const data = await Promise.all( 168 | files.map((file) => fs.readFile(path.join(__dirname, buildDir, file))) 169 | ); 170 | 171 | return data.reduce((m, v, i) => Object.assign( 172 | m, 173 | { [files[i]]: normalizeFile({ data: v.toString(), 174 | name: files[i] }) } 175 | ), 176 | {}); 177 | }; 178 | 179 | // Promise-friendly spawn. 180 | const spawn = (...args) => { 181 | const stdout = []; 182 | const stderr = []; 183 | 184 | // eslint-disable-next-line promise/avoid-new 185 | return new Promise((resolve) => { 186 | const proc = cp.spawn(...args); 187 | proc.stdout.on("data", (data) => { 188 | stdout.push(data.toString()); 189 | }); 190 | proc.stderr.on("data", (data) => { 191 | stderr.push(data.toString()); 192 | }); 193 | proc.on("close", (code, signal) => resolve({ 194 | code, 195 | signal, 196 | stdout: stdout.length ? stdout.join("") : null, 197 | stderr: stderr.length ? stderr.join("") : null 198 | })); 199 | }); 200 | }; 201 | 202 | describe("failures", function () { 203 | // Set higher timeout for exec'ed tests. 204 | this.timeout(5000); // eslint-disable-line no-invalid-this,no-magic-numbers 205 | 206 | it("fails with synchronous error", async () => { 207 | // Use builder to concurrently run: 208 | // `webpack --config test/scenarios/webpack/webpack.config.fail-sync.js` 209 | const obj = await spawn(builderCli, 210 | [ 211 | "envs", "test:build:single", 212 | JSON.stringify(VERSIONS), 213 | "--env", JSON.stringify({ WP_EXTRA: ".fail-sync" }), 214 | "--buffer", "--bail=false" 215 | ] 216 | ); 217 | 218 | expect(obj.code).to.equal(1); 219 | expect(obj.stderr).to.contain(`Hit ${NUM_ERRS} errors`); 220 | 221 | // Webpack5 has "inner" repeat traces of error. 222 | const WEBPACK_5_EXTRAS = 1; 223 | const exps = Array(NUM_ERRS + WEBPACK_5_EXTRAS).fill("Error: SYNC"); 224 | const errs = obj.stderr.match(/(^Error\: SYNC)/gm); 225 | expect(errs).to.eql(exps); 226 | }); 227 | 228 | it("fails with promise rejection", async () => { 229 | // Use builder to concurrently run: 230 | // `webpack --config test/scenarios/webpack/webpack.config.fail-promise.js` 231 | const obj = await spawn(builderCli, 232 | [ 233 | "envs", "test:build:single", 234 | JSON.stringify(VERSIONS), 235 | "--env", JSON.stringify({ WP_EXTRA: ".fail-promise" }), 236 | "--buffer", "--bail=false" 237 | ] 238 | ); 239 | expect(obj.code).to.equal(1); 240 | expect(obj.stderr).to.contain(`Hit ${NUM_ERRS} errors`); 241 | 242 | // Webpack5 has "inner" repeat traces of error. 243 | const WEBPACK_5_EXTRAS = 1; 244 | const exps = Array(NUM_ERRS + WEBPACK_5_EXTRAS).fill("Error: PROMISE"); 245 | const errs = obj.stderr.match(/(^Error\: PROMISE)/gm); 246 | expect(errs).to.eql(exps); 247 | }); 248 | }); 249 | 250 | describe("production", function () { 251 | // Set higher timeout for exec'ed tests. 252 | this.timeout(5000); // eslint-disable-line no-invalid-this,no-magic-numbers 253 | 254 | // Regression test: https://github.com/FormidableLabs/webpack-stats-plugin/issues/91 255 | it("works in production mode", async () => { 256 | // Use builder to concurrently run: 257 | // `webpack 258 | const obj = await spawn(builderCli, 259 | [ 260 | "envs", "test:build:single", 261 | JSON.stringify([{ VERS: 4 }, { VERS: 5 }]), 262 | "--env", JSON.stringify({ MODE: "production", 263 | OUTPUT_PATH: "build-prod" }), 264 | "--buffer" 265 | ] 266 | ); 267 | 268 | expect(obj.stderr).to.equal(null); 269 | expect(obj.code).to.equal(0); 270 | }); 271 | }); 272 | 273 | // Specific tests. 274 | describe("fixtures", () => { 275 | describe("webpack5", () => { 276 | const webpack = "webpack5"; 277 | let buildFiles; 278 | const actuals = {}; 279 | 280 | before(async () => { 281 | const buildDir = path.join(__dirname, "scenarios", webpack, "build"); 282 | buildFiles = await fs.readdir(buildDir); 283 | 284 | await Promise.all(buildFiles 285 | .filter((name) => (/\.json$/).test(name)) 286 | .map((name) => fs.readFile(path.join(buildDir, name)) 287 | // eslint-disable-next-line promise/always-return 288 | .then((buf) => { 289 | actuals[name] = JSON.parse(buf.toString()); 290 | }) 291 | ) 292 | ); 293 | }); 294 | 295 | // https://github.com/FormidableLabs/webpack-stats-plugin/issues/56 296 | it("matches the correct hashed file name in stats object", () => { 297 | const assetNames = buildFiles.filter((name) => (/^contenthash\./).test(name)); 298 | expect(assetNames).to.have.length(1); 299 | 300 | const assetName = assetNames[0]; 301 | expect(actuals["stats-contenthash.json"]) 302 | .to.have.nested.property("entrypoints.main.assets[0].name") 303 | .that.equals(assetName); 304 | }); 305 | }); 306 | }); 307 | 308 | (async () => { 309 | const expecteds = await readBuild("expected"); 310 | const actuals = {}; 311 | await Promise.all(WEBPACKS.map(async (webpack) => { 312 | actuals[webpack] = await readBuild(path.join("scenarios", webpack)); 313 | })); 314 | 315 | // Dynamically and lazily create suites and tests. 316 | describe("builds", () => { 317 | WEBPACKS.forEach((webpack) => { 318 | describe(webpack, () => { 319 | it("creates expected files list", () => { 320 | const expectedFiles = Object.keys(expecteds).sort(); 321 | let actualFiles = Object.keys(actuals[webpack]).sort(); 322 | // Remove webpack5-only files. 323 | if (webpack === "webpack5") { 324 | actualFiles = actualFiles.filter((file) => file !== "build/stats-contenthash.json"); 325 | } 326 | 327 | expect(actualFiles).to.eql(expectedFiles); 328 | }); 329 | 330 | Object.keys(expecteds).forEach((name) => { 331 | it(`matches expected file: ${name}`, () => { 332 | const actual = actuals[webpack][name]; 333 | const expected = normalizeExpected({ 334 | data: expecteds[name], 335 | name, 336 | webpack 337 | }); 338 | 339 | expect(actual, name).to.equal(expected); 340 | }); 341 | }); 342 | }); 343 | }); 344 | }); 345 | })(); 346 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --timeout 10000 3 | -------------------------------------------------------------------------------- /test/packages/webpack-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-cli", 3 | "version": "0.0.0", 4 | "description": "Empty package to prevent flattening of real package", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /test/packages/webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack", 3 | "version": "0.0.0", 4 | "description": "Empty package to prevent flattening of real package", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /test/packages/webpack1/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // eslint-disable-next-line import/no-unresolved 4 | module.exports = require("webpack/bin/webpack"); 5 | -------------------------------------------------------------------------------- /test/packages/webpack1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack1", 3 | "version": "0.0.1", 4 | "description": "Webpack1 installs", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "dependencies": { 8 | "webpack": "^1.15.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/packages/webpack2/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // eslint-disable-next-line import/no-unresolved 4 | module.exports = require("webpack/bin/webpack"); 5 | -------------------------------------------------------------------------------- /test/packages/webpack2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack2", 3 | "version": "0.0.1", 4 | "description": "Webpack2 installs", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "dependencies": { 8 | "webpack": "^2.7.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/packages/webpack3/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // eslint-disable-next-line import/no-unresolved 4 | module.exports = require("webpack/bin/webpack"); 5 | -------------------------------------------------------------------------------- /test/packages/webpack3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack3", 3 | "version": "0.0.1", 4 | "description": "Webpack3 installs", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "dependencies": { 8 | "webpack": "^3.11.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/packages/webpack4/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // eslint-disable-next-line import/no-unresolved,global-require 4 | module.exports = require("webpack-cli/bin/cli.js"); 5 | -------------------------------------------------------------------------------- /test/packages/webpack4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack4", 3 | "version": "0.0.1", 4 | "description": "Webpack4 installs", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "dependencies": { 8 | "webpack": "^4.44.1", 9 | "webpack-cli": "^3.3.11" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/packages/webpack5/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // eslint-disable-next-line import/no-unresolved,global-require 4 | module.exports = require("webpack-cli/bin/cli.js"); 5 | -------------------------------------------------------------------------------- /test/packages/webpack5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack5", 3 | "version": "0.0.1", 4 | "description": "Webpack5 installs", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "dependencies": { 8 | "webpack": "5.74.0", 9 | "webpack-cli": "^4.10.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/scenarios/webpack1/webpack.config.fail-promise.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Fail promise. 5 | */ 6 | const base = require("./webpack.config"); 7 | const fail = require("../webpack5/webpack.config.fail-promise"); 8 | 9 | module.exports = { 10 | ...base, 11 | plugins: fail.plugins 12 | }; 13 | -------------------------------------------------------------------------------- /test/scenarios/webpack1/webpack.config.fail-sync.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Fail synchronously. 5 | */ 6 | const base = require("./webpack.config"); 7 | const fail = require("../webpack5/webpack.config.fail-sync"); 8 | 9 | module.exports = { 10 | ...base, 11 | plugins: fail.plugins 12 | }; 13 | -------------------------------------------------------------------------------- /test/scenarios/webpack1/webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Webpack configuration 5 | */ 6 | const path = require("path"); 7 | const base = require("../webpack5/webpack.config"); 8 | delete base.mode; 9 | 10 | module.exports = { 11 | ...base, 12 | cache: true, 13 | context: __dirname, 14 | output: { 15 | ...base.output, 16 | path: path.join(__dirname, "build") 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /test/scenarios/webpack2/webpack.config.fail-promise.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Fail promise. 5 | */ 6 | const base = require("./webpack.config"); 7 | const fail = require("../webpack5/webpack.config.fail-promise"); 8 | 9 | module.exports = { 10 | ...base, 11 | plugins: fail.plugins 12 | }; 13 | -------------------------------------------------------------------------------- /test/scenarios/webpack2/webpack.config.fail-sync.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Fail synchronously. 5 | */ 6 | const base = require("./webpack.config"); 7 | const fail = require("../webpack5/webpack.config.fail-sync"); 8 | 9 | module.exports = { 10 | ...base, 11 | plugins: fail.plugins 12 | }; 13 | -------------------------------------------------------------------------------- /test/scenarios/webpack2/webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const base = require("../webpack1/webpack.config"); 5 | 6 | // Reuse webpack1 7 | module.exports = { 8 | ...base, 9 | context: __dirname, 10 | output: { 11 | ...base.output, 12 | path: path.join(__dirname, "build") 13 | } 14 | }; 15 | 16 | -------------------------------------------------------------------------------- /test/scenarios/webpack3/webpack.config.fail-promise.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Fail promise. 5 | */ 6 | const base = require("./webpack.config"); 7 | const fail = require("../webpack5/webpack.config.fail-promise"); 8 | 9 | module.exports = { 10 | ...base, 11 | plugins: fail.plugins 12 | }; 13 | -------------------------------------------------------------------------------- /test/scenarios/webpack3/webpack.config.fail-sync.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Fail synchronously. 5 | */ 6 | const base = require("./webpack.config"); 7 | const fail = require("../webpack5/webpack.config.fail-sync"); 8 | 9 | module.exports = { 10 | ...base, 11 | plugins: fail.plugins 12 | }; 13 | -------------------------------------------------------------------------------- /test/scenarios/webpack3/webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const base = require("../webpack1/webpack.config"); 5 | 6 | // Reuse webpack1 7 | module.exports = { 8 | ...base, 9 | context: __dirname, 10 | output: { 11 | ...base.output, 12 | path: path.join(__dirname, "build") 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /test/scenarios/webpack4/webpack.config.fail-promise.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Fail promise. 5 | */ 6 | const base = require("./webpack.config"); 7 | const fail = require("../webpack5/webpack.config.fail-promise"); 8 | 9 | module.exports = { 10 | ...base, 11 | plugins: fail.plugins 12 | }; 13 | -------------------------------------------------------------------------------- /test/scenarios/webpack4/webpack.config.fail-sync.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Fail synchronously. 5 | */ 6 | const base = require("./webpack.config"); 7 | const fail = require("../webpack5/webpack.config.fail-sync"); 8 | 9 | module.exports = { 10 | ...base, 11 | plugins: fail.plugins 12 | }; 13 | -------------------------------------------------------------------------------- /test/scenarios/webpack4/webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const base = require("../webpack5/webpack.config"); 5 | 6 | // Reuse webpack5 7 | module.exports = { 8 | ...base, 9 | context: __dirname, 10 | output: { 11 | ...base.output, 12 | path: path.join(__dirname, "build") 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /test/scenarios/webpack5/webpack.config.contenthash.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | "use strict"; 4 | 5 | /** 6 | * Regression test for: https://github.com/FormidableLabs/webpack-stats-plugin/issues/56 7 | */ 8 | const path = require("path"); 9 | const { StatsWriterPlugin } = require("../../../index"); 10 | 11 | module.exports = { 12 | mode: "production", 13 | context: __dirname, 14 | entry: { 15 | main: "../../src/main.js" 16 | }, 17 | output: { 18 | path: path.join(__dirname, "build"), 19 | publicPath: "/website-o-doom/", 20 | filename: "contenthash.[contenthash].main.js" 21 | }, 22 | devtool: false, 23 | plugins: [ 24 | new StatsWriterPlugin({ 25 | filename: "stats-contenthash.json", 26 | fields: ["entrypoints"] 27 | }) 28 | ], 29 | optimization: { 30 | splitChunks: { 31 | cacheGroups: { 32 | vendors: { 33 | priority: -10, 34 | test: /[\\/]node_modules[\\/]/ 35 | } 36 | }, 37 | chunks: "async", 38 | minChunks: 1, 39 | minSize: 30000, 40 | name: false 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /test/scenarios/webpack5/webpack.config.fail-promise.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Fail promise. 5 | */ 6 | const base = require("./webpack.config"); 7 | const { StatsWriterPlugin } = require("../../../index"); 8 | 9 | module.exports = { 10 | ...base, 11 | plugins: [ 12 | new StatsWriterPlugin({ 13 | filename: "stats-transform-fail-promise.json", 14 | transform() { 15 | return Promise.reject(new Error("PROMISE")); 16 | } 17 | }) 18 | ] 19 | }; 20 | -------------------------------------------------------------------------------- /test/scenarios/webpack5/webpack.config.fail-sync.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Fail synchronously. 5 | */ 6 | const base = require("./webpack.config"); 7 | const { StatsWriterPlugin } = require("../../../index"); 8 | 9 | module.exports = { 10 | ...base, 11 | plugins: [ 12 | new StatsWriterPlugin({ 13 | filename: "stats-transform-fail-sync.json", 14 | transform() { 15 | throw new Error("SYNC"); 16 | } 17 | }) 18 | ] 19 | }; 20 | -------------------------------------------------------------------------------- /test/scenarios/webpack5/webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Webpack configuration 5 | */ 6 | const fs = require("fs"); 7 | const path = require("path"); 8 | const { StatsWriterPlugin } = require("../../../index"); 9 | const INDENT = 2; 10 | const STAT_RESET = Object.freeze({ 11 | // webpack5+ needs explicit declaration. 12 | errors: true, 13 | warnings: true, 14 | // fallback for new stuff added after v3 15 | all: false, 16 | // explicitly turn off older fields 17 | // (webpack <= v2.7.0 does not support "all") 18 | // See: https://webpack.js.org/configuration/stats/ 19 | performance: false, 20 | hash: false, 21 | version: false, 22 | timings: false, 23 | entrypoints: false, 24 | chunks: false, 25 | chunkModules: false, 26 | cached: false, 27 | cachedAssets: false, 28 | children: false, 29 | moduleTrace: false, 30 | assets: false, 31 | modules: false, 32 | publicPath: false 33 | }); 34 | 35 | const MODE = process.env.MODE || "development"; 36 | const OUTPUT_PATH = path.join(__dirname, process.env.OUTPUT_PATH || "build"); 37 | 38 | // webpack5 has deprecated `hash`. 39 | const VERS = parseInt(process.env.VERS || "", 10); 40 | const HASH_KEY = VERS >= 5 ? "fullhash" : "hash"; // eslint-disable-line no-magic-numbers 41 | 42 | // webpack5 returns array even if single item 43 | const normAssets = ({ assetsByChunkName }) => { 44 | Object.entries(assetsByChunkName).forEach(([key, val]) => { 45 | assetsByChunkName[key] = Array.isArray(val) && val.length === 1 ? val[0] : val; 46 | }); 47 | return assetsByChunkName; 48 | }; 49 | 50 | const normData = (data) => Object.keys(data) 51 | .sort() 52 | .reduce((m, k) => Object.assign(m, { [k]: data[k] }), {}); 53 | 54 | module.exports = { 55 | mode: MODE, 56 | context: __dirname, 57 | entry: { 58 | main: "../../src/main.js" 59 | }, 60 | output: { 61 | path: OUTPUT_PATH, 62 | publicPath: "/website-o-doom/", 63 | filename: `[${HASH_KEY}].[name].js` 64 | }, 65 | plugins: [ 66 | // Try various defaults and options. 67 | new StatsWriterPlugin(), 68 | new StatsWriterPlugin({}), 69 | new StatsWriterPlugin({ 70 | filename: "stats-transform.json", 71 | fields: null, 72 | transform({ assetsByChunkName }) { 73 | return JSON.stringify(normAssets({ assetsByChunkName }), null, INDENT); 74 | } 75 | }), 76 | new StatsWriterPlugin({ 77 | filename: "stats-transform.md", 78 | fields: null, 79 | transform({ assetsByChunkName }) { 80 | return Object.entries(normAssets({ assetsByChunkName })).reduce( 81 | (memo, [key, val]) => `${memo}${key} | ${val}\n`, 82 | "Name | Asset\n:--- | :----\n" 83 | ); 84 | } 85 | }), 86 | new StatsWriterPlugin({ 87 | filename: "stats-transform-custom-obj.json", 88 | transform({ assetsByChunkName }) { 89 | return JSON.stringify({ 90 | main: normAssets({ assetsByChunkName }).main 91 | }, null, INDENT); 92 | } 93 | }), 94 | new StatsWriterPlugin({ 95 | filename: "stats-custom.json" 96 | }), 97 | // Relative paths work, but absolute paths do not currently. 98 | new StatsWriterPlugin({ 99 | filename: "../build2/stats-custom2.json" 100 | }), 101 | // Dynamic filename. 102 | new StatsWriterPlugin({ 103 | filename: () => "stats-dynamic.json" 104 | }), 105 | // Promise transform 106 | new StatsWriterPlugin({ 107 | filename: "stats-transform-promise.json", 108 | transform({ assetsByChunkName }) { 109 | return Promise.resolve() 110 | // Force async. 111 | // eslint-disable-next-line promise/avoid-new 112 | .then(() => new Promise((resolve) => { process.nextTick(resolve); })) 113 | .then(() => JSON.stringify({ 114 | main: normAssets({ assetsByChunkName }).main 115 | }, null, INDENT)); 116 | } 117 | }), 118 | // Custom stats 119 | new StatsWriterPlugin({ 120 | filename: "stats-custom-stats.json", 121 | stats: Object.assign({}, STAT_RESET, { 122 | assets: true 123 | }), 124 | transform(data) { 125 | data = normData(data); 126 | 127 | // webpack >= v3 adds this field unconditionally, so remove it 128 | delete data.filteredAssets; 129 | 130 | // webpack >= 5 normalization (remove new extra entries without name). 131 | if (data.assets) { 132 | data.assets = data.assets.filter(({ name }) => name); 133 | } 134 | 135 | return JSON.stringify(data, null, INDENT); 136 | } 137 | }), 138 | // Regression test: Missing `stats` option fields that should be default enabled. 139 | // https://github.com/FormidableLabs/webpack-stats-plugin/issues/44 140 | new StatsWriterPlugin({ 141 | filename: "stats-custom-stats-fields.json", 142 | fields: ["errors", "warnings", "assets", "hash", "publicPath", "namedChunkGroups"] 143 | }), 144 | new StatsWriterPlugin({ 145 | filename: "stats-override-tostring-opt.json", 146 | stats: Object.assign({}, STAT_RESET, { 147 | // chunks are normally omitted due to second argument of .toJson() 148 | chunks: true 149 | }), 150 | transform(data) { 151 | data = normData(data); 152 | 153 | // normalize subset of chunk metadata across all versions of webpack 154 | data.chunks = data.chunks.map((chunk) => [ 155 | "rendered", 156 | "initial", 157 | "entry", 158 | "size", 159 | "names" 160 | ].reduce((obj, key) => { 161 | obj[key] = chunk[key]; 162 | return obj; 163 | }, {})); 164 | 165 | return JSON.stringify(data, null, INDENT); 166 | } 167 | }), 168 | new StatsWriterPlugin({ 169 | filename: "stats-should-not-exist.json", 170 | emit: false 171 | }), 172 | new StatsWriterPlugin({ 173 | emit: false, 174 | async transform(data, context) { 175 | // eslint-disable-next-line global-require 176 | const { JsonStreamStringify } = require("json-stream-stringify"); 177 | 178 | // Use same build directory as webpack. 179 | const outputPath = context.compiler.options.output.path; 180 | const filePath = path.join(outputPath, "stats-from-write-stream.json"); 181 | 182 | // Webpack is going to emit / create intermediate directories _after_ this plugin runs, 183 | // so do a a mkdir -p before starting our streams. 184 | await fs.promises.mkdir(outputPath, { recursive: true }); 185 | 186 | // Create and kick off the streams. 187 | const jsonStream = new JsonStreamStringify(data, undefined, INDENT); 188 | const writeStream = fs.createWriteStream(filePath); 189 | 190 | return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new 191 | jsonStream.pipe(writeStream); 192 | jsonStream.on("end", () => resolve()); 193 | jsonStream.on("error", (err) => reject(new Error(`error converting stream - ${err}`))); 194 | }); 195 | } 196 | }), 197 | new StatsWriterPlugin({ 198 | filename: "stats.js", 199 | transform() { 200 | return "/*eslint-disable*/\nconsole.log(\"hello world\");\n"; 201 | } 202 | }) 203 | ] 204 | }; 205 | -------------------------------------------------------------------------------- /test/src/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * A demo file. 5 | */ 6 | console.log("Hello World!"); // eslint-disable-line no-console 7 | -------------------------------------------------------------------------------- /webpack-stats-plugin-Hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/webpack-stats-plugin/4eeef58c50b8cbea41800234e007156d7f7f287d/webpack-stats-plugin-Hero.png --------------------------------------------------------------------------------