├── .nvmrc ├── __tests__ ├── common │ ├── constants.js │ ├── styles.css │ ├── jest.config.js │ ├── app.js │ └── smp.test.js └── setups │ ├── .prettierrc │ ├── .gitignore │ ├── v4-multi-conf │ ├── customTests.js │ ├── package.json │ └── webpack.config.js │ ├── v3-plain │ ├── webpack.config.js │ └── package.json │ ├── v4-plain │ ├── webpack.config.js │ └── package.json │ ├── v1-plain │ ├── webpack.config.js │ └── package.json │ ├── v2-plain │ ├── webpack.config.js │ └── package.json │ ├── v4-html-webpack-plugin │ ├── webpack.config.js │ └── package.json │ ├── v3-hard-source-webpack-plugin │ ├── webpack.config.js │ └── package.json │ ├── v4-hard-source-webpack-plugin │ ├── webpack.config.js │ └── package.json │ ├── v3-stats │ ├── webpack.config.js │ └── package.json │ ├── v4-stats │ ├── webpack.config.js │ └── package.json │ ├── v5-deprecated-hooks │ ├── package.json │ └── webpack.config.js │ ├── v3-circular-plugin │ ├── package.json │ └── webpack.config.js │ ├── v4-circular-plugin │ ├── package.json │ └── webpack.config.js │ └── v5-func-proxy │ ├── package.json │ └── webpack.config.js ├── .prettierrc ├── .npmignore ├── .gitignore ├── preview.png ├── lerna.json ├── jest.config.js ├── .travis.yml ├── examples ├── neutrinorc.js ├── basic.webpack.config.js └── webpack-merge.config.js ├── colours.js ├── neutrino.js ├── LICENSE ├── CONTRIBUTING.md ├── package.json ├── migration.md ├── loader.js ├── utils.test.js ├── .all-contributorsrc ├── utils.js ├── output.js ├── WrappedPlugin └── index.js ├── index.js ├── README.md └── logo.svg /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.20.0 -------------------------------------------------------------------------------- /__tests__/common/constants.js: -------------------------------------------------------------------------------- 1 | const a = 1; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5" 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | .prettierrc 3 | __tests__/ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lerna-debug.log 3 | npm-debug.log -------------------------------------------------------------------------------- /__tests__/common/styles.css: -------------------------------------------------------------------------------- 1 | .a { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/setups/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5" 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/common/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "node" 3 | }; 4 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephencookdev/speed-measure-webpack-plugin/HEAD/preview.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.9.0", 3 | "packages": ["__tests__/setups/*"], 4 | "version": "0.0.0" 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/common/app.js: -------------------------------------------------------------------------------- 1 | require("./constants"); 2 | require("./styles.css"); 3 | 4 | console.log("Some javascript", FOO); 5 | -------------------------------------------------------------------------------- /__tests__/setups/.gitignore: -------------------------------------------------------------------------------- 1 | app.js 2 | constants.js 3 | styles.css 4 | smp.test.js 5 | jest.config.js 6 | dist 7 | dist2 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testPathIgnorePatterns: ["__tests__"], 3 | testURL: "http://localhost", 4 | }; 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | script: 3 | - npm run test 4 | - npm run functional-test 5 | cache: 6 | directories: 7 | - "node_modules" 8 | -------------------------------------------------------------------------------- /examples/neutrinorc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | use: [ 3 | "@neutrinojs/airbnb", 4 | "@neutrinojs/jest", 5 | 6 | // Make sure this is last! This strongly depends on being placed last 7 | "speed-measure-webpack-plugin/neutrino" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /colours.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | 3 | module.exports.fg = (text, time) => { 4 | let textModifier = chalk.bold; 5 | if (time > 10000) textModifier = textModifier.red; 6 | else if (time > 2000) textModifier = textModifier.yellow; 7 | else textModifier = textModifier.green; 8 | 9 | return textModifier(text); 10 | }; 11 | 12 | module.exports.bg = (text) => chalk.bgBlack.green.bold(text); 13 | -------------------------------------------------------------------------------- /__tests__/setups/v4-multi-conf/customTests.js: -------------------------------------------------------------------------------- 1 | module.exports = testRef => { 2 | it("should state the time taken by the plugin in both configs", () => { 3 | expect(testRef.smpOutput).toMatch( 4 | /DefinePlugin.* took .*([0-9]+ mins? )?[0-9]+(\.[0-9]+)? secs/ 5 | ); 6 | expect(testRef.smpOutput).toMatch( 7 | /IgnorePlugin.* took .*([0-9]+ mins? )?[0-9]+(\.[0-9]+)? secs/ 8 | ); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /examples/basic.webpack.config.js: -------------------------------------------------------------------------------- 1 | const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); 2 | 3 | const smp = new SpeedMeasurePlugin(); 4 | 5 | module.exports = smp.wrap({ 6 | entry: { 7 | app: ["./app.js"] 8 | }, 9 | output: "./public", 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.js$/, 14 | use: [{ loader: "babel-loader" }] 15 | } 16 | ] 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /neutrino.js: -------------------------------------------------------------------------------- 1 | const SpeedMeasurePlugin = require("."); 2 | const smp = new SpeedMeasurePlugin(); 3 | 4 | module.exports = (neutrino) => { 5 | const origConfig = neutrino.config; 6 | const wrappedConfig = smp.wrap(origConfig.toConfig()); 7 | neutrino.config = new Proxy(origConfig, { 8 | get(target, property) { 9 | if (property === "toConfig") { 10 | return () => wrappedConfig; 11 | } 12 | return target[property]; 13 | }, 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /__tests__/setups/v3-plain/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: ["./app.js"], 6 | }, 7 | output: { 8 | path: __dirname + "/dist" 9 | }, 10 | plugins: [ 11 | new webpack.DefinePlugin({ FOO: "'BAR'" }) 12 | ], 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js?$/, 17 | use: "babel-loader" 18 | }, 19 | { 20 | test: /\.css$/, 21 | use: ["style-loader", "css-loader"] 22 | } 23 | ] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /__tests__/setups/v4-plain/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: ["./app.js"], 6 | }, 7 | output: { 8 | path: __dirname + "/dist" 9 | }, 10 | plugins: [ 11 | new webpack.DefinePlugin({ FOO: "'BAR'" }) 12 | ], 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js?$/, 17 | use: "babel-loader" 18 | }, 19 | { 20 | test: /\.css$/, 21 | use: ["style-loader", "css-loader"] 22 | } 23 | ] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /__tests__/setups/v1-plain/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: ["./app.js"], 6 | }, 7 | output: { 8 | path: __dirname + "/dist" 9 | }, 10 | plugins: [ 11 | new webpack.DefinePlugin({ FOO: "'BAR'" }) 12 | ], 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.js?$/, 17 | loader: "babel-loader" 18 | }, 19 | { 20 | test: /\.css$/, 21 | loaders: ["style-loader", "css-loader"] 22 | } 23 | ] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /__tests__/setups/v2-plain/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: ["./app.js"], 6 | }, 7 | output: { 8 | path: __dirname + "/dist" 9 | }, 10 | plugins: [ 11 | new webpack.DefinePlugin({ FOO: "'BAR'" }) 12 | ], 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.js?$/, 17 | loader: "babel-loader" 18 | }, 19 | { 20 | test: /\.css$/, 21 | loaders: ["style-loader", "css-loader"] 22 | } 23 | ] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /__tests__/setups/v4-html-webpack-plugin/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | module.exports = { 5 | entry: { 6 | bundle: ["./app.js"], 7 | }, 8 | output: { 9 | path: __dirname + "/dist", 10 | }, 11 | plugins: [ 12 | new webpack.DefinePlugin({ FOO: "'BAR'" }), 13 | new HtmlWebpackPlugin(), 14 | ], 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js?$/, 19 | use: "babel-loader", 20 | }, 21 | { 22 | test: /\.css$/, 23 | use: ["style-loader", "css-loader"], 24 | }, 25 | ], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /__tests__/setups/v3-hard-source-webpack-plugin/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const HardSourceWebpackPlugin = require("hard-source-webpack-plugin"); 3 | 4 | module.exports = { 5 | entry: { 6 | bundle: ["./app.js"], 7 | }, 8 | output: { 9 | path: __dirname + "/dist" 10 | }, 11 | plugins: [ 12 | new webpack.DefinePlugin({ FOO: "'BAR'" }), 13 | new HardSourceWebpackPlugin() 14 | ], 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js?$/, 19 | use: "babel-loader" 20 | }, 21 | { 22 | test: /\.css$/, 23 | use: ["style-loader", "css-loader"] 24 | } 25 | ] 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /__tests__/setups/v4-hard-source-webpack-plugin/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const HardSourceWebpackPlugin = require("hard-source-webpack-plugin"); 3 | 4 | module.exports = { 5 | entry: { 6 | bundle: ["./app.js"], 7 | }, 8 | output: { 9 | path: __dirname + "/dist" 10 | }, 11 | plugins: [ 12 | new webpack.DefinePlugin({ FOO: "'BAR'" }), 13 | new HardSourceWebpackPlugin() 14 | ], 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js?$/, 19 | use: "babel-loader" 20 | }, 21 | { 22 | test: /\.css$/, 23 | use: ["style-loader", "css-loader"] 24 | } 25 | ] 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /__tests__/setups/v1-plain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v1-plain", 3 | "version": "0.0.1", 4 | "description": "Test webpack v1 with SMP", 5 | "scripts": { 6 | "full-test": "npm run prepare && npm run test && npm run cleanup", 7 | "prepare": "cp -r ../../common/* .", 8 | "test": "jest ./smp.test.js", 9 | "cleanup": "rm -rf dist", 10 | "audit-fix": "npm audit fix" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=6.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "7.0.x", 19 | "css-loader": "^0.28.10", 20 | "jest": "^26.6.3", 21 | "style-loader": "^0.20.2", 22 | "webpack": "1.x.x" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /__tests__/setups/v2-plain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v2-plain", 3 | "version": "0.0.1", 4 | "description": "Test webpack v2 with SMP", 5 | "scripts": { 6 | "full-test": "npm run prepare && npm run test && npm run cleanup", 7 | "prepare": "cp -r ../../common/* .", 8 | "test": "jest ./smp.test.js", 9 | "cleanup": "rm -rf dist", 10 | "audit-fix": "npm audit fix" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=6.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^7.1.1", 19 | "css-loader": "^0.28.10", 20 | "jest": "^26.6.3", 21 | "style-loader": "^0.20.2", 22 | "webpack": "2.x.x" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /__tests__/setups/v3-plain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v3-plain", 3 | "version": "0.0.1", 4 | "description": "Test webpack v3 with SMP", 5 | "scripts": { 6 | "full-test": "npm run prepare && npm run test && npm run cleanup", 7 | "prepare": "cp -r ../../common/* .", 8 | "test": "jest ./smp.test.js", 9 | "cleanup": "rm -rf dist", 10 | "audit-fix": "npm audit fix" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=6.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^7.1.1", 19 | "css-loader": "^0.28.10", 20 | "jest": "^26.6.3", 21 | "style-loader": "^0.20.2", 22 | "webpack": "3.x.x" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /__tests__/setups/v4-plain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v4-plain", 3 | "version": "0.0.1", 4 | "description": "Test webpack v4 with SMP", 5 | "scripts": { 6 | "full-test": "npm run prepare && npm run test && npm run cleanup", 7 | "prepare": "cp -r ../../common/* .", 8 | "test": "jest ./smp.test.js", 9 | "cleanup": "rm -rf dist", 10 | "audit-fix": "npm audit fix" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=6.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^7.1.4", 19 | "css-loader": "^0.28.11", 20 | "jest": "^26.6.3", 21 | "style-loader": "^0.20.3", 22 | "webpack": "^4.42.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /__tests__/setups/v3-stats/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const StatsPlugin = require("stats-webpack-plugin"); 3 | 4 | module.exports = { 5 | entry: { 6 | bundle: ["./app.js"], 7 | }, 8 | output: { 9 | path: __dirname + "/dist" 10 | }, 11 | plugins: [ 12 | new StatsPlugin("stats.json"), 13 | // StatsPlugin needs to be placed _before_ DefinePlugin to repro the issue 14 | new webpack.DefinePlugin({ FOO: "'BAR'" }), 15 | ], 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js?$/, 20 | use: "babel-loader" 21 | }, 22 | { 23 | test: /\.css$/, 24 | use: ["style-loader", "css-loader"] 25 | } 26 | ] 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /__tests__/setups/v4-stats/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const StatsPlugin = require("stats-webpack-plugin"); 3 | 4 | module.exports = { 5 | entry: { 6 | bundle: ["./app.js"], 7 | }, 8 | output: { 9 | path: __dirname + "/dist" 10 | }, 11 | plugins: [ 12 | new StatsPlugin("stats.json"), 13 | // StatsPlugin needs to be placed _before_ DefinePlugin to repro the issue 14 | new webpack.DefinePlugin({ FOO: "'BAR'" }), 15 | ], 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js?$/, 20 | use: "babel-loader" 21 | }, 22 | { 23 | test: /\.css$/, 24 | use: ["style-loader", "css-loader"] 25 | } 26 | ] 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /__tests__/setups/v5-deprecated-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v5-deprecated-hooks", 3 | "version": "0.0.1", 4 | "description": "Test webpack v5 with a StatsPlugin set-up with SMP", 5 | "scripts": { 6 | "full-test": "npm run prepare && npm run test && npm run cleanup", 7 | "prepare": "cp -r ../../common/* .", 8 | "test": "jest ./smp.test.js", 9 | "cleanup": "rm -rf dist", 10 | "audit-fix": "npm audit fix" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=6.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^7.1.4", 19 | "css-loader": "^0.28.11", 20 | "jest": "^26.6.3", 21 | "style-loader": "^0.20.3", 22 | "webpack": "^5.7.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /__tests__/setups/v3-circular-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v3-circular-plugin", 3 | "version": "0.0.1", 4 | "description": "Test webpack v3 and a plugin with circular references, with SMP", 5 | "scripts": { 6 | "full-test": "npm run prepare && npm run test && npm run cleanup", 7 | "prepare": "cp -r ../../common/* .", 8 | "test": "jest ./smp.test.js", 9 | "cleanup": "rm -rf dist", 10 | "audit-fix": "npm audit fix" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=6.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^7.1.1", 19 | "css-loader": "^0.28.10", 20 | "jest": "^26.6.3", 21 | "style-loader": "^0.20.2", 22 | "webpack": "3.x.x" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /__tests__/setups/v4-circular-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v4-circular-plugin", 3 | "version": "0.0.1", 4 | "description": "Test webpack v4 and a plugin with circular references, with SMP", 5 | "scripts": { 6 | "full-test": "npm run prepare && npm run test && npm run cleanup", 7 | "prepare": "cp -r ../../common/* .", 8 | "test": "jest ./smp.test.js", 9 | "cleanup": "rm -rf dist", 10 | "audit-fix": "npm audit fix" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=6.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^7.1.4", 19 | "css-loader": "^0.28.11", 20 | "jest": "^26.6.3", 21 | "style-loader": "^0.20.3", 22 | "webpack": "^4.42.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /__tests__/setups/v3-stats/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v3-stats", 3 | "version": "0.0.1", 4 | "description": "Test webpack v3 with a StatsPlugin set-up with SMP", 5 | "scripts": { 6 | "full-test": "npm run prepare && npm run test && npm run cleanup", 7 | "prepare": "cp -r ../../common/* .", 8 | "test": "jest ./smp.test.js", 9 | "cleanup": "rm -rf dist", 10 | "audit-fix": "npm audit fix" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=6.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^7.1.1", 19 | "css-loader": "^0.28.10", 20 | "jest": "^26.6.3", 21 | "stats-webpack-plugin": "^0.6.2", 22 | "style-loader": "^0.20.2", 23 | "webpack": "3.x.x" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /__tests__/setups/v4-stats/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v4-stats", 3 | "version": "0.0.1", 4 | "description": "Test webpack v4 with a StatsPlugin set-up with SMP", 5 | "scripts": { 6 | "full-test": "npm run prepare && npm run test && npm run cleanup", 7 | "prepare": "cp -r ../../common/* .", 8 | "test": "jest ./smp.test.js", 9 | "cleanup": "rm -rf dist", 10 | "audit-fix": "npm audit fix" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=6.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^7.1.4", 19 | "css-loader": "^0.28.11", 20 | "jest": "^26.6.3", 21 | "stats-webpack-plugin": "^0.6.2", 22 | "style-loader": "^0.20.3", 23 | "webpack": "^4.42.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /__tests__/setups/v5-func-proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v5-func-proxy", 3 | "version": "0.0.1", 4 | "description": "Test webpack v5 with a function proxy set-up with SMP", 5 | "scripts": { 6 | "full-test": "npm run prepare && npm run test && npm run cleanup", 7 | "prepare": "cp -r ../../common/* .", 8 | "test": "jest ./smp.test.js", 9 | "cleanup": "rm -rf dist", 10 | "audit-fix": "npm audit fix" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=6.0.0" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.0.0", 18 | "babel-loader": "^8.1.0", 19 | "css-loader": "^5.0.2", 20 | "jest": "^26.6.3", 21 | "style-loader": "^2.0.0", 22 | "webpack": "^5.24.0", 23 | "terser-webpack-plugin": "^5.1.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /__tests__/setups/v5-func-proxy/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const TerserPlugin = require("terser-webpack-plugin"); 3 | 4 | module.exports = { 5 | entry: { 6 | bundle: ["./app.js"], 7 | }, 8 | output: { 9 | path: __dirname + "/dist", 10 | }, 11 | plugins: [new webpack.DefinePlugin({ FOO: "'BAR'" })], 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.js?$/, 16 | use: "babel-loader", 17 | }, 18 | { 19 | test: /\.css$/, 20 | use: ["style-loader", "css-loader"], 21 | }, 22 | ], 23 | }, 24 | optimization: { 25 | minimizer: [ 26 | // TerserPlugin need some properties of compiler.webpack function 27 | new TerserPlugin({ parallel: true }), 28 | ], 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /__tests__/setups/v4-multi-conf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v4-multi-conf", 3 | "version": "0.0.1", 4 | "description": "Test webpack v4 with multiple configs with SMP", 5 | "scripts": { 6 | "full-test": "npm run prepare && npm run test && npm run cleanup", 7 | "prepare": "cp -r ../../common/* .", 8 | "test": "jest ./smp.test.js", 9 | "cleanup": "rm -rf dist", 10 | "audit-fix": "npm audit fix" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=6.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^7.1.4", 19 | "css-loader": "^0.28.11", 20 | "jest": "^26.6.3", 21 | "style-loader": "^0.20.3", 22 | "webpack": "^4.42.0" 23 | }, 24 | "dependencies": { 25 | "diff": "^3.5.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /__tests__/setups/v4-html-webpack-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v4-html-webpack-plugin", 3 | "version": "0.0.1", 4 | "description": "Test webpack v4, and HTML Webpack Plugin with SMP", 5 | "scripts": { 6 | "full-test": "npm run prepare && npm run test && npm run cleanup", 7 | "prepare": "cp -r ../../common/* .", 8 | "test": "jest ./smp.test.js", 9 | "cleanup": "rm -rf dist && rm -rf node_modules/.cache", 10 | "audit-fix": "npm audit fix" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=6.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^7.1.4", 19 | "css-loader": "^0.28.11", 20 | "html-webpack-plugin": "^4.5.1", 21 | "jest": "^26.6.3", 22 | "style-loader": "^0.20.3", 23 | "webpack": "^4.46.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /__tests__/setups/v4-multi-conf/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | 3 | const rules = [ 4 | { 5 | test: /\.js?$/, 6 | use: "babel-loader" 7 | }, 8 | { 9 | test: /\.css$/, 10 | use: ["style-loader", "css-loader"] 11 | } 12 | ]; 13 | 14 | const conf1 = { 15 | entry: { 16 | bundle: ["./app.js"], 17 | }, 18 | plugins: [ 19 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), 20 | ], 21 | output: { 22 | path: __dirname + "/dist" 23 | }, 24 | module: {}, 25 | }; 26 | 27 | const conf2 = JSON.parse(JSON.stringify(conf1)); 28 | conf2.plugins = [ 29 | new webpack.DefinePlugin({ FOO: "'BAR'" }) 30 | ]; 31 | conf2.output.path = __dirname + "/dist2"; 32 | 33 | conf1.module.rules = rules; 34 | conf2.module.rules = rules; 35 | 36 | module.exports = env => [conf1, conf2]; 37 | -------------------------------------------------------------------------------- /__tests__/setups/v4-hard-source-webpack-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v4-hard-source-webpack-plugin", 3 | "version": "0.0.1", 4 | "description": "Test webpack v4, and HardSourceWebpackPlugin with SMP", 5 | "scripts": { 6 | "full-test": "npm run prepare && npm run test && npm run cleanup", 7 | "prepare": "cp -r ../../common/* .", 8 | "test": "jest ./smp.test.js", 9 | "cleanup": "rm -rf dist && rm -rf node_modules/.cache", 10 | "audit-fix": "npm audit fix" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=6.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^7.1.4", 19 | "css-loader": "^0.28.11", 20 | "hard-source-webpack-plugin": "^0.6.4", 21 | "jest": "^26.6.3", 22 | "style-loader": "^0.20.3", 23 | "webpack": "^4.46.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /__tests__/setups/v3-hard-source-webpack-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v3-hard-source-webpack-plugin", 3 | "version": "0.0.1", 4 | "description": "Test webpack v3, and HardSourceWebpackPlugin with SMP", 5 | "scripts": { 6 | "full-test": "npm run prepare && npm run test && npm run cleanup", 7 | "prepare": "cp -r ../../common/* .", 8 | "test": "jest ./smp.test.js", 9 | "cleanup": "rm -rf dist && rm -rf node_modules/.cache", 10 | "audit-fix": "npm audit fix" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=6.0.0" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.26.3", 18 | "babel-loader": "^7.1.1", 19 | "css-loader": "^0.28.10", 20 | "hard-source-webpack-plugin": "^0.13.1", 21 | "jest": "^26.6.3", 22 | "style-loader": "^0.20.2", 23 | "webpack": "^3.12.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/webpack-merge.config.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); 3 | 4 | const smp = new SpeedMeasurePlugin(); 5 | const TARGET = process.env.npm_lifecycle_event; 6 | 7 | const commonConfig = { 8 | entry: { 9 | app: ["./app.js"] 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: /\.css$/, 15 | loaders: ["style", "css"], 16 | }, 17 | ], 18 | }, 19 | }; 20 | 21 | let mergedConfig = commonConfig; 22 | 23 | if(TARGET === "start") { 24 | mergedConfig = merge(common, { 25 | module: { 26 | loaders: [ 27 | { 28 | test: /\.jsx?$/, 29 | loader: "babel?stage=1" 30 | } 31 | ] 32 | } 33 | }); 34 | } 35 | 36 | // The only difference to how you normally use webpack-merge is that you need 37 | // to `smp.wrap` whatever your final config is 38 | module.exports = smp.wrap(mergedConfig); 39 | -------------------------------------------------------------------------------- /__tests__/setups/v5-deprecated-hooks/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | 3 | class Plugin { 4 | apply(compiler) { 5 | compiler.hooks.thisCompilation.tap("Plugin", (compilation) => { 6 | // additionalAssets hook is deprecated 7 | // it will be frozen, and returnning a Proxied `tapAsync` will cause issue 8 | compilation.hooks.additionalAssets.tapAsync( 9 | "Plugin", 10 | async (callback) => { 11 | // do nothing 12 | callback(); 13 | } 14 | ); 15 | }); 16 | } 17 | } 18 | 19 | module.exports = { 20 | entry: { 21 | bundle: ["./app.js"], 22 | }, 23 | output: { 24 | path: __dirname + "/dist", 25 | }, 26 | plugins: [ 27 | new webpack.DefinePlugin({ FOO: "'BAR'" }), 28 | new Plugin(), 29 | ], 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.js?$/, 34 | use: "babel-loader", 35 | }, 36 | { 37 | test: /\.css$/, 38 | use: ["style-loader", "css-loader"], 39 | }, 40 | ], 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Stephen Cook 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 | -------------------------------------------------------------------------------- /__tests__/setups/v3-circular-plugin/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | 3 | class CircularPlugin { 4 | constructor() { 5 | this.apply = this.apply.bind(this); 6 | } 7 | 8 | apply(compiler) { 9 | compiler.compilation = new (function Compilation(){})(); 10 | compiler.parser = new (function Parser(){})(); 11 | compiler.compilation.parser = compiler.parser; 12 | compiler.parser.compilation = compiler.compilation; 13 | 14 | compiler.plugin("compile", () => { 15 | // do some random calc with the looped compilation object, to force its 16 | // evaluation 17 | if(compiler.compilation.parser.compilation === false) { 18 | console.log("foo"); 19 | } 20 | }); 21 | } 22 | } 23 | 24 | module.exports = { 25 | entry: { 26 | bundle: ["./app.js"], 27 | }, 28 | output: { 29 | path: __dirname + "/dist" 30 | }, 31 | plugins: [ 32 | new webpack.DefinePlugin({ FOO: "'BAR'" }), 33 | new CircularPlugin(), 34 | ], 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.js?$/, 39 | use: "babel-loader" 40 | }, 41 | { 42 | test: /\.css$/, 43 | use: ["style-loader", "css-loader"] 44 | } 45 | ] 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /__tests__/setups/v4-circular-plugin/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | 3 | class CircularPlugin { 4 | constructor() { 5 | this.apply = this.apply.bind(this); 6 | } 7 | 8 | apply(compiler) { 9 | compiler.compilation = new (function Compilation(){})(); 10 | compiler.parser = new (function Parser(){})(); 11 | compiler.compilation.parser = compiler.parser; 12 | compiler.parser.compilation = compiler.compilation; 13 | 14 | compiler.plugin("compile", () => { 15 | // do some random calc with the looped compilation object, to force its 16 | // evaluation 17 | if(compiler.compilation.parser.compilation === false) { 18 | console.log("foo"); 19 | } 20 | }); 21 | } 22 | } 23 | 24 | module.exports = { 25 | entry: { 26 | bundle: ["./app.js"], 27 | }, 28 | output: { 29 | path: __dirname + "/dist" 30 | }, 31 | plugins: [ 32 | new webpack.DefinePlugin({ FOO: "'BAR'" }), 33 | new CircularPlugin(), 34 | ], 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.js?$/, 39 | use: "babel-loader" 40 | }, 41 | { 42 | test: /\.css$/, 43 | use: ["style-loader", "css-loader"] 44 | } 45 | ] 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributors are welcome to help out with this repository! 😊 4 | 5 | Please follow this guide when raising issues, and contributing to the SMP repository. 6 | 7 | ## Raising an Issue 8 | 9 | If you're raising an issue with SMP being incompatible with a particular webpack config, plugin, or loader - then please include reproduction steps. 10 | 11 | The ideal reproduction steps would be forking this repository, and adding a new [\_\_tests\_\_/setups](./__tests__/setups) test case. 12 | 13 | ## Raising a Pull Request 14 | 15 | SMP uses [Prettier](https://github.com/prettier/prettier) for its code formatting. 16 | 17 | If possible, please also include a new unit test (e.g. [utils.test.js](./utils.test.js)), or integration test (i.e. [\_\_tests\_\_/setups](./__tests__/setups)). 18 | 19 | ## Code Structure 20 | 21 | SMP has 2 primary parts: 22 | 23 | 1. The [SMP class](./index.js) contains the `smp.wrap` instance method that bootstraps the whole wrapping sequence. This class also listens for basic timing events, and orchestrates the main timings. 24 | 2. The [`WrappedPlugin`](./WrappedPlugin) proxy that wraps each webpack plugin. This uses a `Proxy` to wrap everything to do with a plugin, feeding the timing information back to the SMP class. 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "speed-measure-webpack-plugin", 3 | "version": "1.5.0", 4 | "description": "Measure + analyse the speed of your webpack loaders and plugins", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "functional-test": "lerna bootstrap && lerna run full-test", 9 | "audit-fix": "npm audit fix && lerna run audit-fix --parallel", 10 | "documentation-test": "alex ./*.md && write-good ./*.md", 11 | "lint": "prettier --check \"*.{js,json,css,md}\"", 12 | "fixlint": "prettier --write \"*.{js,json,css,md}\"", 13 | "ac": "all-contributors" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/stephencookdev/speed-measure-webpack-plugin.git" 18 | }, 19 | "author": { 20 | "name": "Stephen Cook", 21 | "email": "stephen@stephencookdev.co.uk" 22 | }, 23 | "husky": { 24 | "hooks": { 25 | "pre-commit": "lint-staged" 26 | } 27 | }, 28 | "lint-staged": { 29 | "*.{js,json,css,md}": [ 30 | "prettier --write", 31 | "git add" 32 | ] 33 | }, 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/stephencookdev/speed-measure-webpack-plugin/issues" 37 | }, 38 | "homepage": "https://github.com/stephencookdev/speed-measure-webpack-plugin#readme", 39 | "engines": { 40 | "node": ">=6.0.0" 41 | }, 42 | "peerDependencies": { 43 | "webpack": "^1 || ^2 || ^3 || ^4 || ^5" 44 | }, 45 | "devDependencies": { 46 | "alex": "^9.1.0", 47 | "all-contributors-cli": "^6.19.0", 48 | "husky": "^4.2.3", 49 | "jest": "^26.6.3", 50 | "lerna": "^3.22.1", 51 | "lint-staged": "^10.0.8", 52 | "prettier": "^2.2.1", 53 | "write-good": "^1.0.2" 54 | }, 55 | "dependencies": { 56 | "chalk": "^4.1.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /migration.md: -------------------------------------------------------------------------------- 1 | # Migration Guide 2 | 3 | SMP follows [semver](https://semver.org/). This guide should help with upgrading major versions. 4 | 5 | ## v0 → v1 6 | 7 | ### If Using Static Constructor 8 | 9 | If you're using the `SpeedMeasurePlugin.wrapPlugins(plugins, options)` static method, then 10 | 11 | - remove all `.wrapPlugins` calls 12 | - instantiate an `smp` 13 | - call `smp.wrap` on your entire config 14 | 15 | e.g. 16 | 17 | ```javascript 18 | // v0 19 | const webpackConfig = { 20 | plugins: SpeedMeasurePlugin.wrapPlugins( 21 | { 22 | FooPlugin: new FooPlugin(), 23 | }, 24 | smpOptions 25 | ), 26 | }; 27 | 28 | // v1 29 | const smp = new SpeedMeasurePlugin(smpOptions); 30 | const webpackConfig = smp.wrap({ 31 | plugins: [new FooPlugin()], 32 | }); 33 | ``` 34 | 35 | ### If Using `smp` Instance 36 | 37 | If you're using the `smp.wrapPlugins(plugins)` method, then 38 | 39 | - remove all `.wrapPlugins` calls 40 | - call `smp.wrap` on your entire config 41 | 42 | e.g. 43 | 44 | ```javascript 45 | // v0 46 | const smp = new SpeedMeasurePlugin(smpOptions); 47 | const webpackConfig = { 48 | plugins: smp.wrapPlugins({ 49 | FooPlugin: new FooPlugin(), 50 | }), 51 | }; 52 | 53 | // v1 54 | const smp = new SpeedMeasurePlugin(smpOptions); 55 | const webpackConfig = smp.wrap({ 56 | plugins: [new FooPlugin()], 57 | }); 58 | ``` 59 | 60 | ### If Using Custom Names 61 | 62 | v1 no longer requires you to manually enter each plugin name. If you want to keep any of your custom plugin names, then you can use the new `options.pluginNames` option: 63 | 64 | ```javascript 65 | const fooPlugin = new FooPlugin(); 66 | const smp = new SpeedMeasurePlugin({ 67 | pluginNames: { 68 | customFooPluginName: fooPlugin, 69 | }, 70 | }); 71 | const webpackConfig = smp.wrap({ 72 | plugins: [fooPlugin], 73 | }); 74 | ``` 75 | -------------------------------------------------------------------------------- /loader.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const { hackWrapLoaders } = require("./utils"); 4 | 5 | let id = 0; 6 | 7 | const NS = path.dirname(fs.realpathSync(__filename)); 8 | 9 | const getLoaderName = (path) => { 10 | const standardPath = path.replace(/\\/g, "/"); 11 | const nodeModuleName = /\/node_modules\/([^\/]+)/.exec(standardPath); 12 | return (nodeModuleName && nodeModuleName[1]) || ""; 13 | }; 14 | 15 | module.exports.pitch = function () { 16 | const callback = this[NS]; 17 | const module = this.resourcePath; 18 | const loaderPaths = this.loaders 19 | .map((l) => l.path) 20 | .filter((l) => !l.includes("speed-measure-webpack-plugin")); 21 | 22 | // Hack ourselves to overwrite the `require` method so we can override the 23 | // loadLoaders 24 | hackWrapLoaders(loaderPaths, (loader, path) => { 25 | const loaderName = getLoaderName(path); 26 | const wrapFunc = (func) => 27 | function () { 28 | const loaderId = id++; 29 | const almostThis = Object.assign({}, this, { 30 | async: function () { 31 | const asyncCallback = this.async.apply(this, arguments); 32 | 33 | return function () { 34 | callback({ 35 | id: loaderId, 36 | type: "end", 37 | }); 38 | return asyncCallback.apply(this, arguments); 39 | }; 40 | }.bind(this), 41 | }); 42 | 43 | callback({ 44 | module, 45 | loaderName, 46 | id: loaderId, 47 | type: "start", 48 | }); 49 | const ret = func.apply(almostThis, arguments); 50 | callback({ 51 | id: loaderId, 52 | type: "end", 53 | }); 54 | return ret; 55 | }; 56 | 57 | if (loader.normal) loader.normal = wrapFunc(loader.normal); 58 | if (loader.default) loader.default = wrapFunc(loader.default); 59 | if (loader.pitch) loader.pitch = wrapFunc(loader.pitch); 60 | if (typeof loader === "function") return wrapFunc(loader); 61 | return loader; 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /utils.test.js: -------------------------------------------------------------------------------- 1 | const { prependLoader } = require("./utils"); 2 | 3 | describe("prependLoader", () => { 4 | const expectedMappings = [ 5 | { 6 | name: "single loader", 7 | from: { 8 | test: /\.jsx?$/, 9 | loader: "babel-loader", 10 | }, 11 | to: { 12 | test: /\.jsx?$/, 13 | use: ["speed-measure-webpack-plugin/loader", "babel-loader"], 14 | }, 15 | }, 16 | 17 | { 18 | name: "single use", 19 | from: { 20 | test: /\.jsx?$/, 21 | use: ["babel-loader"], 22 | }, 23 | to: { 24 | test: /\.jsx?$/, 25 | use: ["speed-measure-webpack-plugin/loader", "babel-loader"], 26 | }, 27 | }, 28 | 29 | { 30 | name: "single loader with options", 31 | 32 | from: { 33 | test: /\.jsx?$/, 34 | loader: "babel-loader", 35 | options: {}, 36 | }, 37 | to: { 38 | test: /\.jsx?$/, 39 | use: [ 40 | "speed-measure-webpack-plugin/loader", 41 | { loader: "babel-loader", options: {} }, 42 | ], 43 | }, 44 | }, 45 | 46 | { 47 | name: "single complex use", 48 | 49 | from: { 50 | test: /\.jsx?$/, 51 | use: [{ loader: "babel-loader", options: {} }], 52 | }, 53 | to: { 54 | test: /\.jsx?$/, 55 | use: [ 56 | "speed-measure-webpack-plugin/loader", 57 | { loader: "babel-loader", options: {} }, 58 | ], 59 | }, 60 | }, 61 | 62 | { 63 | name: "multiple uses", 64 | 65 | from: { 66 | test: /\.jsx?$/, 67 | use: [{ loader: "babel-loader", options: {} }, "thread-loader"], 68 | }, 69 | to: { 70 | test: /\.jsx?$/, 71 | use: [ 72 | "speed-measure-webpack-plugin/loader", 73 | { loader: "babel-loader", options: {} }, 74 | "thread-loader", 75 | ], 76 | }, 77 | }, 78 | 79 | { 80 | name: "oneOf", 81 | 82 | from: { 83 | test: /\.jsx?$/, 84 | oneOf: [{ use: ["babel-loader"] }, { use: ["thread-loader"] }], 85 | }, 86 | to: { 87 | test: /\.jsx?$/, 88 | oneOf: [ 89 | { 90 | use: ["speed-measure-webpack-plugin/loader", "babel-loader"], 91 | }, 92 | { 93 | use: ["speed-measure-webpack-plugin/loader", "thread-loader"], 94 | }, 95 | ], 96 | }, 97 | }, 98 | 99 | { 100 | name: "array", 101 | from: [ 102 | { 103 | test: /\.jsx?$/, 104 | loader: "babel-loader", 105 | }, 106 | { 107 | test: /\.css$/, 108 | loader: "css-loader", 109 | }, 110 | ], 111 | to: [ 112 | { 113 | test: /\.jsx?$/, 114 | use: ["speed-measure-webpack-plugin/loader", "babel-loader"], 115 | }, 116 | { 117 | test: /\.css$/, 118 | use: ["speed-measure-webpack-plugin/loader", "css-loader"], 119 | }, 120 | ], 121 | }, 122 | ]; 123 | 124 | expectedMappings.forEach((mapping) => { 125 | it('should create the expected mapping for "' + mapping.name + '"', () => { 126 | expect(prependLoader(mapping.from)).toEqual(mapping.to); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /__tests__/common/smp.test.js: -------------------------------------------------------------------------------- 1 | const SpeedMeasurePlugin = require("../../.."); 2 | const webpack = require("webpack"); 3 | const { readFileSync } = require("fs"); 4 | const webpackConfig = require("./webpack.config"); 5 | 6 | const getStandardConf = conf => { 7 | if (typeof conf === "function") conf = conf(); 8 | const arr = Array.isArray(conf) ? conf : [conf]; 9 | 10 | return arr.map( 11 | subConf => (typeof subConf === "function" ? subConf() : subConf) 12 | ); 13 | }; 14 | 15 | let i = 0; 16 | const prepareSmpWebpackConfig = conf => { 17 | if (Array.isArray(conf)) return conf.map(prepareSmpWebpackConfig); 18 | if (typeof conf === "function") 19 | return (...args) => prepareSmpWebpackConfig(conf(...args)); 20 | 21 | return Object.assign({}, conf, { 22 | output: { 23 | path: conf.output.path + "/_smp_" + i++ + "_" + new Date().getTime(), 24 | }, 25 | }); 26 | }; 27 | 28 | const genSmpWebpackConfig = smp => 29 | smp.wrap(prepareSmpWebpackConfig(webpackConfig)); 30 | 31 | const runWebpack = config => 32 | new Promise((resolve, reject) => { 33 | const standardConf = getStandardConf(config); 34 | webpack(standardConf, (err, stats) => { 35 | if (err || stats.hasErrors()) return reject(err || stats); 36 | const fileContent = standardConf.map(conf => 37 | readFileSync(conf.output.path + "/bundle.js").toString() 38 | ); 39 | resolve(fileContent.join("\n///////// new file /////////\n")); 40 | }); 41 | }); 42 | 43 | jest.setTimeout(20000); 44 | 45 | const testRef = {}; 46 | 47 | describe("smp", () => { 48 | beforeAll(() => 49 | runWebpack(webpackConfig).then(file => (testRef.distApp = file)) 50 | ); 51 | 52 | describe(__dirname.split("/").pop(), () => { 53 | const smp = new SpeedMeasurePlugin({ 54 | outputTarget: output => (testRef.smpOutput = output), 55 | }); 56 | const smpWebpackConfig = genSmpWebpackConfig(smp); 57 | 58 | beforeAll(() => 59 | runWebpack(smpWebpackConfig).then(file => (testRef.smpDistApp = file)) 60 | ); 61 | 62 | it("should generate the same app.js content", () => { 63 | expect(testRef.smpDistApp).toEqual(testRef.distApp); 64 | }); 65 | 66 | it("should generate the same app.js content after 2 runs", () => { 67 | const dupSmpWebpackConfig = genSmpWebpackConfig(smp); 68 | 69 | return runWebpack(dupSmpWebpackConfig).then(dupSmpDistApp => { 70 | expect(dupSmpDistApp).toEqual(testRef.smpDistApp); 71 | expect(dupSmpDistApp).toEqual(testRef.distApp); 72 | }); 73 | }); 74 | 75 | it("should state the time taken overall", () => { 76 | expect(testRef.smpOutput).toMatch( 77 | /General output time took .*([0-9]+ mins? )?[0-9]+(\.[0-9]+)? secs/ 78 | ); 79 | }); 80 | 81 | it("should state the time taken by the plugins", () => { 82 | expect(testRef.smpOutput).toMatch( 83 | /DefinePlugin.* took .*([0-9]+ mins? )?[0-9]+(\.[0-9]+)? secs/ 84 | ); 85 | }); 86 | 87 | it("should state the time taken by the loaders", () => { 88 | expect(testRef.smpOutput).toMatch( 89 | /babel-loader.* took .*([0-9]+ mins? )?[0-9]+(\.[0-9]+)? secs.*\n\s+module count\s+= [0-9]+/ 90 | ); 91 | }); 92 | 93 | let customTests; 94 | try { 95 | customTests = require("./customTests"); 96 | } catch (_) { 97 | // do nothing, we expect `require` to fail if no tests exist 98 | } 99 | if (customTests) { 100 | describe("custom tests", () => customTests(testRef)); 101 | } 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "speed-measure-webpack-plugin", 3 | "projectOwner": "stephencookdev", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "none", 12 | "badgeTemplate": " -orange.svg\" />", 13 | "contributors": [ 14 | { 15 | "login": "stephencookdev", 16 | "name": "Stephen Cook", 17 | "avatar_url": "https://avatars.githubusercontent.com/u/8496655?v=4", 18 | "profile": "https://stephencookdev.co.uk/", 19 | "contributions": [ 20 | "code", 21 | "doc", 22 | "blog", 23 | "design", 24 | "question", 25 | "review" 26 | ] 27 | }, 28 | { 29 | "login": "scarletsky", 30 | "name": "scarletsky", 31 | "avatar_url": "https://avatars.githubusercontent.com/u/2386165?v=4", 32 | "profile": "https://scarletsky.github.io/", 33 | "contributions": [ 34 | "code" 35 | ] 36 | }, 37 | { 38 | "login": "wayou", 39 | "name": "牛さん", 40 | "avatar_url": "https://avatars.githubusercontent.com/u/3783096?v=4", 41 | "profile": "https://github.com/wayou", 42 | "contributions": [ 43 | "code", 44 | "bug" 45 | ] 46 | }, 47 | { 48 | "login": "ThomasHarper", 49 | "name": "Thomas Bentkowski", 50 | "avatar_url": "https://avatars.githubusercontent.com/u/3199791?v=4", 51 | "profile": "https://github.com/ThomasHarper", 52 | "contributions": [ 53 | "doc" 54 | ] 55 | }, 56 | { 57 | "login": "alan-agius4", 58 | "name": "Alan Agius", 59 | "avatar_url": "https://avatars.githubusercontent.com/u/17563226?v=4", 60 | "profile": "https://github.com/alan-agius4", 61 | "contributions": [ 62 | "code", 63 | "bug" 64 | ] 65 | }, 66 | { 67 | "login": "NdYAG", 68 | "name": "Ximing", 69 | "avatar_url": "https://avatars.githubusercontent.com/u/1396511?v=4", 70 | "profile": "https://daix.me/", 71 | "contributions": [ 72 | "code", 73 | "bug" 74 | ] 75 | }, 76 | { 77 | "login": "tanhauhau", 78 | "name": "Tan Li Hau", 79 | "avatar_url": "https://avatars.githubusercontent.com/u/2338632?v=4", 80 | "profile": "https://twitter.com/lihautan", 81 | "contributions": [ 82 | "code", 83 | "bug", 84 | "test" 85 | ] 86 | }, 87 | { 88 | "login": "ZauberNerd", 89 | "name": "Björn Brauer", 90 | "avatar_url": "https://avatars.githubusercontent.com/u/249542?v=4", 91 | "profile": "https://github.com/ZauberNerd", 92 | "contributions": [ 93 | "code", 94 | "bug" 95 | ] 96 | }, 97 | { 98 | "login": "The-Only-Matrix", 99 | "name": "Suraj Patel", 100 | "avatar_url": "https://avatars.githubusercontent.com/u/61681157?v=4", 101 | "profile": "https://github.com/The-Only-Matrix", 102 | "contributions": [ 103 | "code" 104 | ] 105 | }, 106 | { 107 | "login": "hanzooo", 108 | "name": "Jm", 109 | "avatar_url": "https://avatars.githubusercontent.com/u/16368939?v=4", 110 | "profile": "https://github.com/hanzooo", 111 | "contributions": [ 112 | "code", 113 | "bug", 114 | "test" 115 | ] 116 | } 117 | ], 118 | "contributorsPerLine": 7 119 | } 120 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | const isEqual = (x, y) => 2 | Array.isArray(x) 3 | ? Array.isArray(y) && 4 | x.every((xi) => y.includes(xi)) && 5 | y.every((yi) => x.includes(yi)) 6 | : x === y; 7 | 8 | const mergeRanges = (rangeList) => { 9 | const mergedQueue = []; 10 | const inputQueue = [...rangeList]; 11 | while (inputQueue.length) { 12 | const cur = inputQueue.pop(); 13 | const overlapIndex = mergedQueue.findIndex( 14 | (item) => 15 | (item.start >= cur.start && item.start <= cur.end) || 16 | (item.end >= cur.start && item.end <= cur.end) 17 | ); 18 | 19 | if (overlapIndex === -1) { 20 | mergedQueue.push(cur); 21 | } else { 22 | const toMerge = mergedQueue.splice(overlapIndex, 1)[0]; 23 | inputQueue.push({ 24 | start: Math.min(cur.start, cur.end, toMerge.start, toMerge.end), 25 | end: Math.max(cur.start, cur.end, toMerge.start, toMerge.end), 26 | }); 27 | } 28 | } 29 | 30 | return mergedQueue; 31 | }; 32 | 33 | const sqr = (x) => x * x; 34 | const mean = (xs) => xs.reduce((acc, x) => acc + x, 0) / xs.length; 35 | const median = (xs) => xs.sort()[Math.floor(xs.length / 2)]; 36 | const variance = (xs, mean) => 37 | xs.reduce((acc, x) => acc + sqr(x - mean), 0) / (xs.length - 1); 38 | const range = (xs) => 39 | xs.reduce( 40 | (acc, x) => ({ 41 | start: Math.min(x, acc.start), 42 | end: Math.max(x, acc.end), 43 | }), 44 | { start: Number.POSITIVE_INFINITY, end: Number.NEGATIVE_INFINITY } 45 | ); 46 | 47 | module.exports.getModuleName = (module) => module.userRequest; 48 | 49 | module.exports.getLoaderNames = (loaders) => 50 | loaders && loaders.length 51 | ? loaders 52 | .map((l) => l.loader || l) 53 | .map((l) => 54 | l 55 | .replace(/\\/g, "/") 56 | .replace( 57 | /^.*\/node_modules\/(@[a-z0-9][\w-.]+\/[a-z0-9][\w-.]*|[^\/]+).*$/, 58 | (_, m) => m 59 | ) 60 | ) 61 | .filter((l) => !l.includes("speed-measure-webpack-plugin")) 62 | : ["modules with no loaders"]; 63 | 64 | module.exports.groupBy = (key, arr) => { 65 | const groups = []; 66 | (arr || []).forEach((arrItem) => { 67 | const groupItem = groups.find((poss) => 68 | isEqual(poss[0][key], arrItem[key]) 69 | ); 70 | if (groupItem) groupItem.push(arrItem); 71 | else groups.push([arrItem]); 72 | }); 73 | 74 | return groups; 75 | }; 76 | 77 | module.exports.getAverages = (group) => { 78 | const durationList = group.map((cur) => cur.end - cur.start); 79 | 80 | const averages = {}; 81 | averages.dataPoints = group.length; 82 | averages.median = median(durationList); 83 | averages.mean = Math.round(mean(durationList)); 84 | averages.range = range(durationList); 85 | if (group.length > 1) 86 | averages.variance = Math.round(variance(durationList, averages.mean)); 87 | 88 | return averages; 89 | }; 90 | 91 | module.exports.getTotalActiveTime = (group) => { 92 | const mergedRanges = mergeRanges(group); 93 | return mergedRanges.reduce((acc, range) => acc + range.end - range.start, 0); 94 | }; 95 | 96 | const prependLoader = (rules) => { 97 | if (!rules) return rules; 98 | if (Array.isArray(rules)) return rules.map(prependLoader); 99 | 100 | if (rules.loader) { 101 | rules.use = [rules.loader]; 102 | if (rules.options) { 103 | rules.use[0] = { loader: rules.loader, options: rules.options }; 104 | delete rules.options; 105 | } 106 | delete rules.loader; 107 | } 108 | 109 | if (rules.use) { 110 | if (!Array.isArray(rules.use)) rules.use = [rules.use]; 111 | rules.use.unshift("speed-measure-webpack-plugin/loader"); 112 | } 113 | 114 | if (rules.oneOf) { 115 | rules.oneOf = prependLoader(rules.oneOf); 116 | } 117 | if (rules.rules) { 118 | rules.rules = prependLoader(rules.rules); 119 | } 120 | if (Array.isArray(rules.resource)) { 121 | rules.resource = prependLoader(rules.resource); 122 | } 123 | if (rules.resource && rules.resource.and) { 124 | rules.resource.and = prependLoader(rules.resource.and); 125 | } 126 | if (rules.resource && rules.resource.or) { 127 | rules.resource.or = prependLoader(rules.resource.or); 128 | } 129 | 130 | return rules; 131 | }; 132 | module.exports.prependLoader = prependLoader; 133 | 134 | module.exports.hackWrapLoaders = (loaderPaths, callback) => { 135 | const wrapReq = (reqMethod) => { 136 | return function () { 137 | const ret = reqMethod.apply(this, arguments); 138 | if (loaderPaths.includes(arguments[0])) { 139 | if (ret.__smpHacked) return ret; 140 | ret.__smpHacked = true; 141 | return callback(ret, arguments[0]); 142 | } 143 | return ret; 144 | }; 145 | }; 146 | 147 | if (typeof System === "object" && typeof System.import === "function") { 148 | System.import = wrapReq(System.import); 149 | } 150 | const Module = require("module"); 151 | Module.prototype.require = wrapReq(Module.prototype.require); 152 | }; 153 | 154 | const toCamelCase = (s) => s.replace(/(\-\w)/g, (m) => m[1].toUpperCase()); 155 | module.exports.tap = (obj, hookName, func) => { 156 | if (obj.hooks) { 157 | return obj.hooks[toCamelCase(hookName)].tap("smp", func); 158 | } 159 | return obj.plugin(hookName, func); 160 | }; 161 | -------------------------------------------------------------------------------- /output.js: -------------------------------------------------------------------------------- 1 | const MS_IN_MINUTE = 60000; 2 | const MS_IN_SECOND = 1000; 3 | 4 | const chalk = require("chalk"); 5 | const { fg, bg } = require("./colours"); 6 | const { groupBy, getAverages, getTotalActiveTime } = require("./utils"); 7 | 8 | const humanTime = (ms, options = {}) => { 9 | if (options.verbose) { 10 | return ms.toLocaleString() + " ms"; 11 | } 12 | 13 | const minutes = Math.floor(ms / MS_IN_MINUTE); 14 | const secondsRaw = (ms - minutes * MS_IN_MINUTE) / MS_IN_SECOND; 15 | const secondsWhole = Math.floor(secondsRaw); 16 | const remainderPrecision = secondsWhole > 0 ? 2 : 3; 17 | const secondsRemainder = Math.min(secondsRaw - secondsWhole, 0.99); 18 | const seconds = 19 | secondsWhole + 20 | secondsRemainder 21 | .toPrecision(remainderPrecision) 22 | .replace(/^0/, "") 23 | .replace(/0+$/, "") 24 | .replace(/^\.$/, ""); 25 | 26 | let time = ""; 27 | 28 | if (minutes > 0) time += minutes + " min" + (minutes > 1 ? "s" : "") + ", "; 29 | time += seconds + " secs"; 30 | 31 | return time; 32 | }; 33 | 34 | const smpTag = () => bg(" SMP ") + " ⏱ "; 35 | module.exports.smpTag = smpTag; 36 | 37 | module.exports.getHumanOutput = (outputObj, options = {}) => { 38 | const hT = (x) => humanTime(x, options); 39 | let output = "\n\n" + smpTag() + "\n"; 40 | 41 | if (outputObj.misc) { 42 | output += 43 | "General output time took " + 44 | fg(hT(outputObj.misc.compileTime, options), outputObj.misc.compileTime); 45 | output += "\n\n"; 46 | } 47 | if (outputObj.plugins) { 48 | output += smpTag() + "Plugins\n"; 49 | Object.keys(outputObj.plugins) 50 | .sort( 51 | (name1, name2) => outputObj.plugins[name2] - outputObj.plugins[name1] 52 | ) 53 | .forEach((pluginName) => { 54 | output += 55 | chalk.bold(pluginName) + 56 | " took " + 57 | fg(hT(outputObj.plugins[pluginName]), outputObj.plugins[pluginName]); 58 | output += "\n"; 59 | }); 60 | output += "\n"; 61 | } 62 | if (outputObj.loaders) { 63 | output += smpTag() + "Loaders\n"; 64 | outputObj.loaders.build 65 | .sort((obj1, obj2) => obj2.activeTime - obj1.activeTime) 66 | .forEach((loaderObj) => { 67 | output += 68 | loaderObj.loaders.map(fg).join(", and \n") + 69 | " took " + 70 | fg(hT(loaderObj.activeTime), loaderObj.activeTime) + 71 | "\n"; 72 | 73 | let xEqualsY = []; 74 | if (options.verbose) { 75 | xEqualsY.push(["median", hT(loaderObj.averages.median)]); 76 | xEqualsY.push(["mean", hT(loaderObj.averages.mean)]); 77 | if (typeof loaderObj.averages.variance === "number") 78 | xEqualsY.push(["s.d.", hT(Math.sqrt(loaderObj.averages.variance))]); 79 | xEqualsY.push([ 80 | "range", 81 | "(" + 82 | hT(loaderObj.averages.range.start) + 83 | " --> " + 84 | hT(loaderObj.averages.range.end) + 85 | ")", 86 | ]); 87 | } 88 | 89 | if (loaderObj.loaders.length > 1) { 90 | Object.keys(loaderObj.subLoadersTime).forEach((subLoader) => { 91 | xEqualsY.push([subLoader, hT(loaderObj.subLoadersTime[subLoader])]); 92 | }); 93 | } 94 | 95 | xEqualsY.push(["module count", loaderObj.averages.dataPoints]); 96 | 97 | if (options.loaderTopFiles) { 98 | const loopLen = Math.min( 99 | loaderObj.rawStartEnds.length, 100 | options.loaderTopFiles 101 | ); 102 | for (let i = 0; i < loopLen; i++) { 103 | const rawItem = loaderObj.rawStartEnds[i]; 104 | xEqualsY.push([rawItem.name, hT(rawItem.end - rawItem.start)]); 105 | } 106 | } 107 | 108 | const maxXLength = xEqualsY.reduce( 109 | (acc, cur) => Math.max(acc, cur[0].length), 110 | 0 111 | ); 112 | xEqualsY.forEach((xY) => { 113 | const padEnd = maxXLength - xY[0].length; 114 | output += " " + xY[0] + " ".repeat(padEnd) + " = " + xY[1] + "\n"; 115 | }); 116 | }); 117 | } 118 | 119 | output += "\n\n"; 120 | 121 | return output; 122 | }; 123 | 124 | module.exports.getMiscOutput = (data) => ({ 125 | compileTime: data.compile[0].end - data.compile[0].start, 126 | }); 127 | 128 | module.exports.getPluginsOutput = (data) => 129 | Object.keys(data).reduce((acc, key) => { 130 | const inData = data[key]; 131 | 132 | const startEndsByName = groupBy("name", inData); 133 | 134 | return startEndsByName.reduce((innerAcc, startEnds) => { 135 | innerAcc[startEnds[0].name] = 136 | (innerAcc[startEnds[0].name] || 0) + getTotalActiveTime(startEnds); 137 | return innerAcc; 138 | }, acc); 139 | }, {}); 140 | 141 | module.exports.getLoadersOutput = (data) => { 142 | const startEndsByLoader = groupBy("loaders", data.build); 143 | const allSubLoaders = data["build-specific"] || []; 144 | 145 | const buildData = startEndsByLoader.map((startEnds) => { 146 | const averages = getAverages(startEnds); 147 | const activeTime = getTotalActiveTime(startEnds); 148 | const subLoaders = groupBy( 149 | "loader", 150 | allSubLoaders.filter((l) => startEnds.find((x) => x.name === l.name)) 151 | ); 152 | const subLoadersActiveTime = subLoaders.reduce((acc, loaders) => { 153 | acc[loaders[0].loader] = getTotalActiveTime(loaders); 154 | return acc; 155 | }, {}); 156 | 157 | return { 158 | averages, 159 | activeTime, 160 | loaders: startEnds[0].loaders, 161 | subLoadersTime: subLoadersActiveTime, 162 | rawStartEnds: startEnds.sort( 163 | (a, b) => b.end - b.start - (a.end - a.start) 164 | ), 165 | }; 166 | }); 167 | 168 | return { build: buildData }; 169 | }; 170 | -------------------------------------------------------------------------------- /WrappedPlugin/index.js: -------------------------------------------------------------------------------- 1 | let idInc = 0; 2 | 3 | const genWrappedFunc = ({ 4 | func, 5 | smp, 6 | context, 7 | timeEventName, 8 | pluginName, 9 | endType, 10 | }) => (...args) => { 11 | const id = idInc++; 12 | // we don't know if there's going to be a callback applied to a particular 13 | // call, so we just set it multiple times, letting each one override the last 14 | const addEndEvent = () => 15 | smp.addTimeEvent("plugins", timeEventName, "end", { 16 | id, 17 | // we need to allow failure, since webpack can finish compilation and 18 | // cause our callbacks to fall on deaf ears 19 | allowFailure: true, 20 | }); 21 | 22 | smp.addTimeEvent("plugins", timeEventName, "start", { 23 | id, 24 | name: pluginName, 25 | }); 26 | // invoke an end event immediately in case the callback here causes webpack 27 | // to complete compilation. If this gets invoked and not the subsequent 28 | // call, then our data will be inaccurate, sadly 29 | addEndEvent(); 30 | const normalArgMap = a => wrap(a, pluginName, smp); 31 | let ret; 32 | if (endType === "wrapDone") 33 | ret = func.apply( 34 | context, 35 | args.map(a => wrap(a, pluginName, smp, addEndEvent)) 36 | ); 37 | else if (endType === "async") { 38 | const argsButLast = args.slice(0, args.length - 1); 39 | const callback = args[args.length - 1]; 40 | ret = func.apply( 41 | context, 42 | argsButLast.map(normalArgMap).concat((...callbackArgs) => { 43 | addEndEvent(); 44 | callback(...callbackArgs); 45 | }) 46 | ); 47 | } else if (endType === "promise") 48 | ret = func.apply(context, args.map(normalArgMap)).then(promiseArg => { 49 | addEndEvent(); 50 | return promiseArg; 51 | }); 52 | else ret = func.apply(context, args.map(normalArgMap)); 53 | addEndEvent(); 54 | 55 | return ret; 56 | }; 57 | 58 | const genPluginMethod = (orig, pluginName, smp, type) => 59 | function(method, func) { 60 | const timeEventName = pluginName + "/" + type + "/" + method; 61 | const wrappedFunc = genWrappedFunc({ 62 | func, 63 | smp, 64 | context: this, 65 | timeEventName, 66 | pluginName, 67 | endType: "wrapDone", 68 | }); 69 | return orig.plugin(method, wrappedFunc); 70 | }; 71 | 72 | const wrapTap = (tap, pluginName, smp, type, method) => 73 | function(id, func) { 74 | const timeEventName = pluginName + "/" + type + "/" + method; 75 | const wrappedFunc = genWrappedFunc({ 76 | func, 77 | smp, 78 | context: this, 79 | timeEventName, 80 | pluginName, 81 | }); 82 | return tap.call(this, id, wrappedFunc); 83 | }; 84 | 85 | const wrapTapAsync = (tapAsync, pluginName, smp, type, method) => 86 | function(id, func) { 87 | const timeEventName = pluginName + "/" + type + "/" + method; 88 | const wrappedFunc = genWrappedFunc({ 89 | func, 90 | smp, 91 | context: this, 92 | timeEventName, 93 | pluginName, 94 | endType: "async", 95 | }); 96 | return tapAsync.call(this, id, wrappedFunc); 97 | }; 98 | 99 | const wrapTapPromise = (tapPromise, pluginName, smp, type, method) => 100 | function(id, func) { 101 | const timeEventName = pluginName + "/" + type + "/" + method; 102 | const wrappedFunc = genWrappedFunc({ 103 | func, 104 | smp, 105 | context: this, 106 | timeEventName, 107 | pluginName, 108 | endType: "promise", 109 | }); 110 | return tapPromise.call(this, id, wrappedFunc); 111 | }; 112 | 113 | const wrappedHooks = []; 114 | const wrapHooks = (orig, pluginName, smp, type) => { 115 | const hooks = orig.hooks; 116 | if (!hooks) return hooks; 117 | const prevWrapped = wrappedHooks.find( 118 | w => 119 | w.pluginName === pluginName && (w.orig === hooks || w.wrapped === hooks) 120 | ); 121 | if (prevWrapped) return prevWrapped.wrapped; 122 | 123 | const genProxy = method => { 124 | const proxy = new Proxy(hooks[method], { 125 | get: (target, property) => { 126 | const raw = Reflect.get(target, property); 127 | 128 | if (Object.isFrozen(target)) { 129 | return raw; 130 | } 131 | 132 | if (property === "tap" && typeof raw === "function") 133 | return wrapTap(raw, pluginName, smp, type, method).bind(proxy); 134 | if (property === "tapAsync" && typeof raw === "function") 135 | return wrapTapAsync(raw, pluginName, smp, type, method).bind(proxy); 136 | if (property === "tapPromise" && typeof raw === "function") 137 | return wrapTapPromise(raw, pluginName, smp, type, method).bind(proxy); 138 | 139 | return raw; 140 | }, 141 | set: (target, property, value) => { 142 | return Reflect.set(target, property, value); 143 | }, 144 | deleteProperty: (target, property) => { 145 | return Reflect.deleteProperty(target, property); 146 | }, 147 | }); 148 | return proxy; 149 | }; 150 | 151 | const wrapped = Object.keys(hooks).reduce((acc, method) => { 152 | acc[method] = genProxy(method); 153 | return acc; 154 | }, {}); 155 | 156 | wrappedHooks.push({ orig: hooks, wrapped, pluginName }); 157 | 158 | return wrapped; 159 | }; 160 | 161 | const construcNamesToWrap = [ 162 | "Compiler", 163 | "Compilation", 164 | "MainTemplate", 165 | "Parser", 166 | "NormalModuleFactory", 167 | "ContextModuleFactory", 168 | ]; 169 | 170 | const wrappedObjs = []; 171 | const findWrappedObj = (orig, pluginName) => { 172 | const prevWrapped = wrappedObjs.find( 173 | w => w.pluginName === pluginName && (w.orig === orig || w.wrapped === orig) 174 | ); 175 | if (prevWrapped) return prevWrapped.wrapped; 176 | }; 177 | const wrap = (orig, pluginName, smp, addEndEvent) => { 178 | if (!orig) return orig; 179 | const prevWrapped = findWrappedObj(orig, pluginName); 180 | if (prevWrapped) return prevWrapped; 181 | 182 | const getOrigConstrucName = target => 183 | target && target.constructor && target.constructor.name; 184 | const getShouldWrap = target => { 185 | const origConstrucName = getOrigConstrucName(target); 186 | return construcNamesToWrap.includes(origConstrucName); 187 | }; 188 | const shouldWrap = getShouldWrap(orig); 189 | const shouldSoftWrap = Object.keys(orig) 190 | .map(k => orig[k]) 191 | .some(getShouldWrap); 192 | 193 | let wrappedReturn; 194 | 195 | if (!shouldWrap && !shouldSoftWrap) { 196 | const vanillaFunc = orig.name === "next"; 197 | wrappedReturn = 198 | vanillaFunc && addEndEvent 199 | ? function() { 200 | // do this before calling the callback, since the callback can start 201 | // the next plugin step 202 | addEndEvent(); 203 | 204 | return orig.apply(this, arguments); 205 | } 206 | : orig; 207 | } else { 208 | const proxy = new Proxy(orig, { 209 | get: (target, property) => { 210 | const raw = Reflect.get(target, property); 211 | 212 | if (shouldWrap && property === "plugin") 213 | return genPluginMethod( 214 | target, 215 | pluginName, 216 | smp, 217 | getOrigConstrucName(target) 218 | ).bind(proxy); 219 | 220 | if (shouldWrap && property === "hooks") 221 | return wrapHooks( 222 | target, 223 | pluginName, 224 | smp, 225 | getOrigConstrucName(target) 226 | ); 227 | 228 | if (shouldWrap && property === "compiler") { 229 | const prevWrapped = findWrappedObj(raw, pluginName); 230 | if (prevWrapped) { 231 | return prevWrapped; 232 | } 233 | } 234 | 235 | if (typeof raw === "function") { 236 | const ret = raw.bind(proxy); 237 | if (property === "constructor") 238 | Object.defineProperty(ret, "name", { 239 | value: raw.name, 240 | }); 241 | const funcProxy = new Proxy(ret, { 242 | get: (target, property) => { 243 | return raw[property]; 244 | }, 245 | }); 246 | return funcProxy; 247 | } 248 | 249 | return raw; 250 | }, 251 | set: (target, property, value) => { 252 | return Reflect.set(target, property, value); 253 | }, 254 | deleteProperty: (target, property) => { 255 | return Reflect.deleteProperty(target, property); 256 | }, 257 | }); 258 | 259 | wrappedReturn = proxy; 260 | } 261 | 262 | wrappedObjs.push({ pluginName, orig, wrapped: wrappedReturn }); 263 | return wrappedReturn; 264 | }; 265 | 266 | module.exports.clear = () => { 267 | wrappedObjs.length = 0; 268 | wrappedHooks.length = 0; 269 | }; 270 | 271 | module.exports.WrappedPlugin = class WrappedPlugin { 272 | constructor(plugin, pluginName, smp) { 273 | this._smp_plugin = plugin; 274 | this._smp_pluginName = pluginName; 275 | this._smp = smp; 276 | 277 | this.apply = this.apply.bind(this); 278 | 279 | const wp = this; 280 | return new Proxy(plugin, { 281 | get(target, property) { 282 | if (property === "apply") { 283 | return wp.apply; 284 | } 285 | return target[property]; 286 | }, 287 | set: (target, property, value) => { 288 | return Reflect.set(target, property, value); 289 | }, 290 | deleteProperty: (target, property) => { 291 | return Reflect.deleteProperty(target, property); 292 | }, 293 | }); 294 | } 295 | 296 | apply(compiler) { 297 | return this._smp_plugin.apply( 298 | wrap(compiler, this._smp_pluginName, this._smp) 299 | ); 300 | } 301 | }; 302 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const chalk = require("chalk"); 4 | const { WrappedPlugin, clear } = require("./WrappedPlugin"); 5 | const { 6 | getModuleName, 7 | getLoaderNames, 8 | prependLoader, 9 | tap, 10 | } = require("./utils"); 11 | const { 12 | getHumanOutput, 13 | getMiscOutput, 14 | getPluginsOutput, 15 | getLoadersOutput, 16 | smpTag, 17 | } = require("./output"); 18 | 19 | const NS = path.dirname(fs.realpathSync(__filename)); 20 | 21 | module.exports = class SpeedMeasurePlugin { 22 | constructor(options) { 23 | this.options = options || {}; 24 | 25 | this.timeEventData = {}; 26 | this.smpPluginAdded = false; 27 | 28 | this.wrap = this.wrap.bind(this); 29 | this.getOutput = this.getOutput.bind(this); 30 | this.addTimeEvent = this.addTimeEvent.bind(this); 31 | this.apply = this.apply.bind(this); 32 | this.provideLoaderTiming = this.provideLoaderTiming.bind(this); 33 | this.generateLoadersBuildComparison = this.generateLoadersBuildComparison.bind( 34 | this 35 | ); 36 | } 37 | 38 | wrap(config) { 39 | if (this.options.disable) return config; 40 | if (Array.isArray(config)) return config.map(this.wrap); 41 | if (typeof config === "function") 42 | return (...args) => this.wrap(config(...args)); 43 | 44 | config.plugins = (config.plugins || []).map((plugin) => { 45 | const pluginName = 46 | Object.keys(this.options.pluginNames || {}).find( 47 | (pluginName) => plugin === this.options.pluginNames[pluginName] 48 | ) || 49 | (plugin.constructor && plugin.constructor.name) || 50 | "(unable to deduce plugin name)"; 51 | return new WrappedPlugin(plugin, pluginName, this); 52 | }); 53 | 54 | if (config.optimization && config.optimization.minimizer) { 55 | config.optimization.minimizer = config.optimization.minimizer.map( 56 | (plugin) => { 57 | return new WrappedPlugin(plugin, plugin.constructor.name, this); 58 | } 59 | ); 60 | } 61 | 62 | if (config.module && this.options.granularLoaderData) { 63 | config.module = prependLoader(config.module); 64 | } 65 | 66 | if (!this.smpPluginAdded) { 67 | config.plugins = config.plugins.concat(this); 68 | this.smpPluginAdded = true; 69 | } 70 | 71 | return config; 72 | } 73 | 74 | generateLoadersBuildComparison() { 75 | const objBuildData = { loaderInfo: [] }; 76 | const loaderFile = this.options.compareLoadersBuild.filePath; 77 | const outputObj = getLoadersOutput(this.timeEventData.loaders); 78 | 79 | if (!loaderFile) { 80 | throw new Error( 81 | "`options.compareLoadersBuild.filePath` is a required field" 82 | ); 83 | } 84 | 85 | if (!outputObj) { 86 | throw new Error("No output found!"); 87 | } 88 | 89 | const buildDetailsFile = fs.existsSync(loaderFile) 90 | ? fs.readFileSync(loaderFile) 91 | : "[]"; 92 | const buildDetails = JSON.parse(buildDetailsFile.toString()); 93 | const buildCount = buildDetails.length; 94 | const buildNo = 95 | buildCount > 0 ? buildDetails[buildCount - 1]["buildNo"] + 1 : 1; 96 | 97 | // create object format of current loader and write in the file 98 | outputObj.build.forEach((loaderObj) => { 99 | const loaderInfo = {}; 100 | loaderInfo["Name"] = loaderObj.loaders.join(",") || ""; 101 | loaderInfo["Time"] = loaderObj.activeTime || ""; 102 | loaderInfo["Count"] = 103 | this.options.outputFormat === "humanVerbose" 104 | ? loaderObj.averages.dataPoints 105 | : ""; 106 | loaderInfo[`Comparison`] = ""; 107 | 108 | // Getting the comparison from the previous build by default only 109 | // in case if build data is more then one 110 | if (buildCount > 0) { 111 | const prevBuildIndex = buildCount - 1; 112 | for ( 113 | var y = 0; 114 | y < buildDetails[prevBuildIndex]["loaderInfo"].length; 115 | y++ 116 | ) { 117 | const prevloaderDetails = 118 | buildDetails[prevBuildIndex]["loaderInfo"][y]; 119 | if ( 120 | loaderInfo["Name"] == prevloaderDetails["Name"] && 121 | prevloaderDetails["Time"] 122 | ) { 123 | const previousBuildTime = 124 | buildDetails[prevBuildIndex]["loaderInfo"][y]["Time"]; 125 | const savedTime = previousBuildTime > loaderObj.activeTime; 126 | 127 | loaderInfo[`Comparison`] = `${savedTime ? "-" : "+"}${Math.abs( 128 | loaderObj.activeTime - previousBuildTime 129 | )}ms | ${savedTime ? "(slower)" : "(faster)"}`; 130 | } 131 | } 132 | } 133 | 134 | objBuildData["loaderInfo"].push(loaderInfo); 135 | }); 136 | 137 | buildDetails.push({ buildNo, loaderInfo: objBuildData["loaderInfo"] }); 138 | 139 | fs.writeFileSync(loaderFile, JSON.stringify(buildDetails)); 140 | 141 | for (let i = 0; i < buildDetails.length; i++) { 142 | const outputTable = []; 143 | console.log("--------------------------------------------"); 144 | console.log("Build No ", buildDetails[i]["buildNo"]); 145 | console.log("--------------------------------------------"); 146 | 147 | if (buildDetails[i]["loaderInfo"]) { 148 | buildDetails[i]["loaderInfo"].forEach((buildInfo) => { 149 | const objCurrentBuild = {}; 150 | objCurrentBuild["Name"] = buildInfo["Name"] || ""; 151 | objCurrentBuild["Time (ms)"] = buildInfo["Time"] || ""; 152 | if (this.options.outputFormat === "humanVerbose") 153 | objCurrentBuild["Count"] = buildInfo["Count"] || 0; 154 | objCurrentBuild["Comparison"] = buildInfo["Comparison"] || ""; 155 | outputTable.push(objCurrentBuild); 156 | }); 157 | } 158 | console.table(outputTable); 159 | } 160 | } 161 | 162 | getOutput() { 163 | const outputObj = {}; 164 | if (this.timeEventData.misc) 165 | outputObj.misc = getMiscOutput(this.timeEventData.misc); 166 | if (this.timeEventData.plugins) 167 | outputObj.plugins = getPluginsOutput(this.timeEventData.plugins); 168 | if (this.timeEventData.loaders) 169 | outputObj.loaders = getLoadersOutput(this.timeEventData.loaders); 170 | 171 | if (this.options.outputFormat === "json") 172 | return JSON.stringify(outputObj, null, 2); 173 | if (typeof this.options.outputFormat === "function") 174 | return this.options.outputFormat(outputObj); 175 | return getHumanOutput( 176 | outputObj, 177 | Object.assign( 178 | { verbose: this.options.outputFormat === "humanVerbose" }, 179 | this.options 180 | ) 181 | ); 182 | } 183 | 184 | addTimeEvent(category, event, eventType, data = {}) { 185 | const allowFailure = data.allowFailure; 186 | delete data.allowFailure; 187 | 188 | const tED = this.timeEventData; 189 | if (!tED[category]) tED[category] = {}; 190 | if (!tED[category][event]) tED[category][event] = []; 191 | const eventList = tED[category][event]; 192 | const curTime = new Date().getTime(); 193 | 194 | if (eventType === "start") { 195 | data.start = curTime; 196 | eventList.push(data); 197 | } else if (eventType === "end") { 198 | const matchingEvent = eventList.find((e) => { 199 | const allowOverwrite = !e.end || !data.fillLast; 200 | const idMatch = e.id !== undefined && e.id === data.id; 201 | const nameMatch = 202 | !data.id && e.name !== undefined && e.name === data.name; 203 | return allowOverwrite && (idMatch || nameMatch); 204 | }); 205 | const eventToModify = 206 | matchingEvent || (data.fillLast && eventList.find((e) => !e.end)); 207 | if (!eventToModify) { 208 | console.error( 209 | "Could not find a matching event to end", 210 | category, 211 | event, 212 | data 213 | ); 214 | if (allowFailure) return; 215 | throw new Error("No matching event!"); 216 | } 217 | 218 | eventToModify.end = curTime; 219 | } 220 | } 221 | 222 | apply(compiler) { 223 | if (this.options.disable) return; 224 | 225 | tap(compiler, "compile", () => { 226 | this.addTimeEvent("misc", "compile", "start", { watch: false }); 227 | }); 228 | tap(compiler, "done", () => { 229 | clear(); 230 | this.addTimeEvent("misc", "compile", "end", { fillLast: true }); 231 | 232 | const outputToFile = typeof this.options.outputTarget === "string"; 233 | chalk.enabled = !outputToFile; 234 | const output = this.getOutput(); 235 | chalk.enabled = true; 236 | if (outputToFile) { 237 | const writeMethod = fs.existsSync(this.options.outputTarget) 238 | ? fs.appendFileSync 239 | : fs.writeFileSync; 240 | writeMethod(this.options.outputTarget, output + "\n"); 241 | console.log( 242 | smpTag() + "Outputted timing info to " + this.options.outputTarget 243 | ); 244 | } else { 245 | const outputFunc = this.options.outputTarget || console.log; 246 | outputFunc(output); 247 | } 248 | 249 | if (this.options.compareLoadersBuild) 250 | this.generateLoadersBuildComparison(); 251 | 252 | this.timeEventData = {}; 253 | }); 254 | 255 | tap(compiler, "compilation", (compilation) => { 256 | tap(compilation, "normal-module-loader", (loaderContext) => { 257 | loaderContext[NS] = this.provideLoaderTiming; 258 | }); 259 | 260 | tap(compilation, "build-module", (module) => { 261 | const name = getModuleName(module); 262 | if (name) { 263 | this.addTimeEvent("loaders", "build", "start", { 264 | name, 265 | fillLast: true, 266 | loaders: getLoaderNames(module.loaders), 267 | }); 268 | } 269 | }); 270 | 271 | tap(compilation, "succeed-module", (module) => { 272 | const name = getModuleName(module); 273 | if (name) { 274 | this.addTimeEvent("loaders", "build", "end", { 275 | name, 276 | fillLast: true, 277 | }); 278 | } 279 | }); 280 | }); 281 | } 282 | 283 | provideLoaderTiming(info) { 284 | const infoData = { id: info.id }; 285 | if (info.type !== "end") { 286 | infoData.loader = info.loaderName; 287 | infoData.name = info.module; 288 | } 289 | 290 | this.addTimeEvent("loaders", "build-specific", info.type, infoData); 291 | } 292 | }; 293 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | Speed Measure Plugin 5 |
(for webpack)
6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | The first step to optimising your webpack build speed, is to know where to focus your attention. 18 | 19 | This plugin measures your webpack build speed, giving an output like this: 20 | 21 | ![Preview of Speed Measure Plugin's output](preview.png) 22 | 23 | ## Install 24 | 25 | ```bash 26 | npm install --save-dev speed-measure-webpack-plugin 27 | ``` 28 | 29 | or 30 | 31 | ```bash 32 | yarn add -D speed-measure-webpack-plugin 33 | ``` 34 | 35 | ## Requirements 36 | 37 | SMP requires at least **Node v6**. But otherwise, accepts **all webpack** versions (1, 2, 3, and 4). 38 | 39 | ## Usage 40 | 41 | Change your webpack config from 42 | 43 | ```javascript 44 | const webpackConfig = { 45 | plugins: [new MyPlugin(), new MyOtherPlugin()], 46 | }; 47 | ``` 48 | 49 | to 50 | 51 | ```javascript 52 | const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); 53 | 54 | const smp = new SpeedMeasurePlugin(); 55 | 56 | const webpackConfig = smp.wrap({ 57 | plugins: [new MyPlugin(), new MyOtherPlugin()], 58 | }); 59 | ``` 60 | 61 | and you're done! SMP will now be printing timing output to the console by default. 62 | 63 | Check out the [examples folder](/examples) for some more examples. 64 | 65 | ## Options 66 | 67 | Pass these into the constructor, as an object: 68 | 69 | ```javascript 70 | const smp = new SpeedMeasurePlugin(options); 71 | ``` 72 | 73 | ### `options.disable` 74 | 75 | Type: `Boolean`
76 | Default: `false` 77 | 78 | If truthy, this plugin does nothing at all. 79 | 80 | `{ disable: !process.env.MEASURE }` allows opt-in measurements with `MEASURE=true npm run build`. 81 | 82 | ### `options.outputFormat` 83 | 84 | Type: `String|Function`
85 | Default: `"human"` 86 | 87 | Determines in what format this plugin prints its measurements 88 | 89 | - `"json"` - produces a JSON blob 90 | - `"human"` - produces a human readable output 91 | - `"humanVerbose"` - produces a more verbose version of the human readable output 92 | - If a function, it will call the function with the JSON blob, and output the response 93 | 94 | ### `options.outputTarget` 95 | 96 | Type: `String|Function`
97 | Default: `console.log` 98 | 99 | - If a string, it specifies the path to a file to output to. 100 | - If a function, it will call the function with the output as the first parameter 101 | 102 | ### `options.pluginNames` 103 | 104 | Type: `Object`
105 | Default: `{}` 106 | 107 | By default, SMP derives plugin names through `plugin.constructor.name`. For some 108 | plugins this doesn't work (or you may want to override this default). This option 109 | takes an object of `pluginName: PluginConstructor`, e.g. 110 | 111 | ```javascript 112 | const uglify = new UglifyJSPlugin(); 113 | const smp = new SpeedMeasurePlugin({ 114 | pluginNames: { 115 | customUglifyName: uglify, 116 | }, 117 | }); 118 | 119 | const webpackConfig = smp.wrap({ 120 | plugins: [uglify], 121 | }); 122 | ``` 123 | 124 | ### `options.loaderTopFiles` 125 | 126 | Type: `Number`
127 | Default: `0` 128 | 129 | You can configure SMP to include the files that take the most time per loader, when using `outputFormat: 'humanVerbose'`. E.g., to show the top 10 files per loader: 130 | 131 | ```javascript 132 | const smp = new SpeedMeasurePlugin({ 133 | outputFormat: "humanVerbose", 134 | loaderTopFiles: 10, 135 | }); 136 | ``` 137 | 138 | ### `options.compareLoadersBuild` 139 | 140 | Type: `Object`
141 | Default: `{}` 142 | 143 | This option gives you a comparison over time of the module count and time spent, per loader. This option provides more data when `outputFormat: "humanVerbose"`. 144 | 145 | Given a required `filePath` to store the build information, this option allows you to compare differences to your codebase over time. E.g. 146 | 147 | ```javascript 148 | const smp = new SpeedMeasurePlugin({ 149 | compareLoadersBuild: { 150 | filePath: "./buildInfo.json", 151 | }, 152 | }); 153 | ``` 154 | 155 | ### `options.granularLoaderData` _(experimental)_ 156 | 157 | Type: `Boolean`
158 | Default: `false` 159 | 160 | By default, SMP measures loaders in groups. If truthy, this plugin will give per-loader timing information. 161 | 162 | This flag is _experimental_. Some loaders will have inaccurate results: 163 | 164 | - loaders using separate processes (e.g. `thread-loader`) 165 | - loaders emitting file output (e.g. `file-loader`) 166 | 167 | We will find solutions to these issues before removing the _(experimental)_ flag on this option. 168 | 169 | ## FAQ 170 | 171 | ### What does general output time mean? 172 | 173 | This tends to be down to webpack reading in from the file-system, but in general it's anything outside of what SMP can actually measure. 174 | 175 | ### What does modules without loaders mean? 176 | 177 | It means vanilla JS files, which webpack can handle out of the box. 178 | 179 | ## Contributing 180 | 181 | Contributors are welcome! 😊 182 | 183 | Please check out the [CONTRIBUTING.md](./CONTRIBUTING.md). 184 | 185 | ## Migrating 186 | 187 | SMP follows [semver](https://semver.org/). If upgrading a major version, you can consult [the migration guide](./migration.md). 188 | 189 | ## License 190 | 191 | [MIT](/LICENSE) 192 | 193 | ## Contributors ✨ 194 | 195 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 |

Stephen Cook

💻 📖 📝 🎨 💬 👀

scarletsky

💻

牛さん

💻 🐛

Thomas Bentkowski

📖

Alan Agius

💻 🐛

Ximing

💻 🐛

Tan Li Hau

💻 🐛 ⚠️

Björn Brauer

💻 🐛

Suraj Patel

💻

Jm

💻 🐛 ⚠️
216 | 217 | 218 | 219 | 220 | 221 | 222 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 223 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 71 | 79 | 87 | 97 | 104 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 175 | 180 | 185 | 190 | 195 | 200 | 205 | 210 | 215 | 216 | 217 | 218 | --------------------------------------------------------------------------------