├── .gitignore ├── .npmignore ├── assets └── screenshot.png ├── .editorconfig ├── package.json ├── README.md ├── LICENSE ├── .eslintrc.js ├── index.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .editorconfig 4 | .eslintignore 5 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenstensberg/webpack-plugin-extended-network/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-plugin-extended-network", 3 | "version": "1.0.0", 4 | "description": "Chrome Protocol extended info for your bundles", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint ./lib && eslint ./index.js" 8 | }, 9 | "author": "Even Stensberg ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "chalk": "^4.1.1", 13 | "puppeteer": "^9.0.0" 14 | }, 15 | "devDependencies": { 16 | "eslint": "^7.24.0", 17 | "eslint-config-google": "^0.14.0", 18 | "webpack": "^5.35.1" 19 | }, 20 | "peerDependencies": { 21 | "webpack": "^5.35.1" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/intraperf/webpack-plugin-extended-network.git" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webpack-plugin-extended-network 2 | 3 | webpack plugin for analyzing how your bundle performs in a browser. This plugin is experimental and the API might change in the future. The plugin uses Puppeteer and works with code-splitting and regular entries. 4 | 5 | ![Screenshot](https://github.com/ev1stensberg/webpack-plugin-extended-network/blob/master/assets/screenshot.png) 6 | 7 | ## Installation 8 | 9 | Install the plugin through npm: 10 | 11 | `$ npm install --save webpack-plugin-extended-network` 12 | 13 | 14 | ## Usage without devServer 15 | ```js 16 | const NetworkPlugin = require('webpack-plugin-extended-network'); 17 | 18 | module.exports = { 19 | ... 20 | plugins: [ 21 | new NetworkPlugin({ 22 | url: 'http://localhost:3000' 23 | }) 24 | ] 25 | } 26 | ``` 27 | 28 | ## Usage with devServer 29 | 30 | ```js 31 | const NetworkPlugin = require('webpack-plugin-extended-network'); 32 | 33 | module.exports = { 34 | ... 35 | plugins: [ 36 | new NetworkPlugin() 37 | ], 38 | devServer: { 39 | ... 40 | port: 3000 41 | } 42 | } 43 | ``` 44 | 45 | ## Contributing 46 | 47 | Send a PR, post an issue, would love help! 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017 Even Stensberg evenstensberg@gmail.com 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // start with google standard style 3 | // https://github.com/google/eslint-config-google/blob/master/index.js 4 | "extends": ["eslint:recommended", "google"], 5 | "env": { 6 | "node": true, 7 | "es6": true 8 | }, 9 | "rules": { 10 | // 2 == error, 1 == warning, 0 == off 11 | "indent": [2, 2, { 12 | "SwitchCase": 1, 13 | "VariableDeclarator": 2 14 | }], 15 | "max-len": [2, 100, { 16 | "ignoreComments": true, 17 | "ignoreUrls": true, 18 | "tabWidth": 2 19 | }], 20 | "no-empty": [2, { 21 | "allowEmptyCatch": true 22 | }], 23 | "no-implicit-coercion": [2, { 24 | "boolean": false, 25 | "number": true, 26 | "string": true 27 | }], 28 | "no-unused-expressions": [2, { 29 | "allowShortCircuit": true, 30 | "allowTernary": false 31 | }], 32 | "no-unused-vars": [2, { 33 | "vars": "all", 34 | "args": "after-used", 35 | "argsIgnorePattern": "(^reject$|^_$)", 36 | "varsIgnorePattern": "(^_$)" 37 | }], 38 | "strict": [2, "global"], 39 | "prefer-const": 2, 40 | "curly": [2, "multi-line"], 41 | 42 | // Disabled rules 43 | "require-jsdoc": 0, 44 | "valid-jsdoc": 0, 45 | "comma-dangle": 0, 46 | "arrow-parens": 0, 47 | }, 48 | "parserOptions": { 49 | "ecmaVersion": 6, 50 | "ecmaFeatures": { 51 | "globalReturn": true, 52 | "jsx": false, 53 | "experimentalObjectRestSpread": false 54 | }, 55 | "sourceType": "script" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const chalk = require('chalk'); 3 | const path = require('path'); 4 | 5 | const printHeaderBasedOnWidth = (title) => { 6 | const half = Math.round(process.stdout.columns / 2); 7 | for (let k = 0; k < process.stdout.columns; k++) { 8 | if (k === half) { 9 | process.stdout.write(title); 10 | k += title.length; 11 | } 12 | process.stdout.write(chalk.blue.bold('-')); 13 | } 14 | }; 15 | 16 | class NetworkPlugin { 17 | constructor(options) { 18 | this.chunkPaths = []; 19 | this.url = options.url || null; 20 | 21 | } 22 | 23 | apply(compiler) { 24 | if (!this.url) { 25 | if (compiler.options.devServer) { 26 | const port = compiler.options.devServer.port || undefined; 27 | const host = compiler.options.devServer.host || undefined; 28 | const publicPath = compiler.options.devServer.publicPath || undefined; 29 | if ([port, host, publicPath].includes(undefined)) { 30 | throw new SyntaxError('Make sure devServer has the properties port, host and publicPath'); 31 | } 32 | this.url = `http://${host}:${port}`; 33 | } else { 34 | throw new SyntaxError('No URL detected nor any devServer configuration'); 35 | } 36 | } 37 | compiler.hooks.done.tapAsync( { 38 | name: "webpack-network-plugin", 39 | stage: 9999, 40 | }, async () => { 41 | const runner = require('./test.js'); 42 | await runner(this.chunkPaths, this.url); 43 | }) 44 | 45 | compiler.hooks.emit.tap('NetworkPlugin', (compilation) => { 46 | compilation.chunks.forEach(chunk => { 47 | chunk.files.forEach(filename => { 48 | this.chunkPaths.push(filename); 49 | }); 50 | }); 51 | }); 52 | 53 | } 54 | }; 55 | 56 | module.exports = NetworkPlugin; -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | const chalk = require('chalk'); 3 | 4 | 5 | 6 | const printHeaderBasedOnWidth = (title) => { 7 | const half = Math.round(process.stdout.columns / 2); 8 | for (let k = 0; k < process.stdout.columns; k++) { 9 | if (k === half) { 10 | process.stdout.write(title); 11 | k += title.length; 12 | } 13 | process.stdout.write(chalk.blue.bold('-')); 14 | } 15 | }; 16 | 17 | module.exports = async (chunks, url) => { 18 | const browser = await puppeteer.launch(); 19 | const [page] = await browser.pages(); 20 | 21 | const results = []; // collects all results 22 | 23 | let paused = false; 24 | let pausedRequests = []; 25 | 26 | const nextRequest = () => { // continue the next request or "unpause" 27 | if (pausedRequests.length === 0) { 28 | paused = false; 29 | } else { 30 | // continue first request in "queue" 31 | (pausedRequests.shift())(); // calls the request.continue function 32 | } 33 | }; 34 | 35 | await page.setRequestInterception(true); 36 | page.on('request', request => { 37 | if (paused) { 38 | pausedRequests.push(() => request.continue()); 39 | } else { 40 | paused = true; // pause, as we are processing a request now 41 | request.continue(); 42 | } 43 | }); 44 | process.stdout.write('\n' + chalk.bold('webpack-plugin-extended-network:') + '\n\n'); 45 | page.on('requestfinished', async (request) => { 46 | const response = await request.response(); 47 | 48 | const responseHeaders = response.headers(); 49 | let responseBody; 50 | if (request.redirectChain().length === 0) { 51 | // body can only be access for non-redirect responses 52 | responseBody = await response.buffer(); 53 | } 54 | const requestUrl = request.url(); 55 | 56 | chunks.forEach(chunk => { 57 | if(requestUrl.indexOf(chunk) >= 0) { 58 | const information = { 59 | url: request.url(), 60 | responseHeaders: responseHeaders, 61 | responseSize: responseHeaders['content-length'], 62 | responseBody, 63 | }; 64 | process.stdout.write(chalk.red('Asset: ') + chunk + '\n\n'); 65 | printHeaderBasedOnWidth('General'); 66 | process.stdout.write('\n\n' + chalk.bold('Type: ') + 67 | information.responseHeaders['content-type'] + '\n' + 68 | chalk.bold('Resource URL: ') + requestUrl + '\n' 69 | ); 70 | printHeaderBasedOnWidth('Headers'); 71 | process.stdout.write('\n\n'); 72 | Object.keys(information.responseHeaders).forEach(header => { 73 | process.stdout.write( 74 | '\n' + chalk.bold(header + ': ') + information.responseHeaders[header] + '\n' 75 | ); 76 | }); 77 | process.stdout.write('\n'); 78 | process.stdout.write('>\n\n'); 79 | for (let k = 0; k < process.stdout.columns; k++) { 80 | process.stdout.write(chalk.blue.bold('#')); 81 | } 82 | process.stdout.write('\n\n'); 83 | } 84 | }) 85 | 86 | nextRequest(); // continue with next request 87 | }); 88 | page.on('requestfailed', (request) => { 89 | // handle failed request 90 | nextRequest(); 91 | }); 92 | 93 | await page.goto(url, { waitUntil: 'networkidle0' }); 94 | 95 | await browser.close(); 96 | }; 97 | --------------------------------------------------------------------------------