├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── project.sublime-project ├── src ├── bundlers │ ├── rollup.js │ └── webpack.js └── run.js └── tests └── sample ├── helpers.js ├── index.js └── passed.js /.gitignore: -------------------------------------------------------------------------------- 1 | # test coverage folder 2 | /coverage/ 3 | 4 | # npm modules 5 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 6 | /node_modules/ 7 | 8 | # npm errors 9 | npm-debug.log 10 | 11 | # for OS X users 12 | .DS_Store 13 | 14 | # cache files for sublime text 15 | *.tmlanguage.cache 16 | *.tmPreferences.cache 17 | *.stTheme.cache 18 | 19 | # workspace files are user-specific 20 | *.sublime-workspace -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 @halt-hammerzeit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ES6 "tree shaking" test suite for javascript bundlers 2 | 3 | 7 | 8 | Test your javascript module bundler (Webpack, Rollup, etc) and find out which one is the smartest when it comes to intelligently eliminating unused code. 9 | 10 | See the [original discussion in `webpack` repo](https://github.com/webpack/webpack/issues/2867) 11 | 12 | ## Comparison table 13 | 14 | This is the test cases comparison table: `✓` means the test passes. 15 | 16 | | Bundler | "sample" | 17 | |----------------------|----------| 18 | | Webpack 2 + Babili | ✓ | 19 | | Webpack 2 + UglifyJS | ✓ | 20 | | Rollup | ✓ | 21 | 22 | Make up your own test cases which not all bundlers pass and send pull requests. Use the `tests/sample` folder as an example. 23 | 24 | When Webpack 2 detects an unused ES6 export it marks it with `/* unused harmony export */` but still does output it in the resulting bundle. Such unused exports will be removed when minimizing the bundle using either [Babili](https://github.com/webpack-contrib/babili-webpack-plugin) (recommended) or [`UglifyJSPlugin`](https://github.com/webpack-contrib/uglifyjs-webpack-plugin) (legacy) (therefore Webpack 2 doesn't remove unused code itself instead relying on a 3rd party plugin for doing that). In contrast, Rollup does remove unused ES6 exports before the output bundle is minimized. 25 | 26 | ## Running 27 | 28 | ```sh 29 | # Make sure Node.js >= 7.x.x is installed 30 | git clone https://github.com/halt-hammerzeit/es6-tree-shaking-test.git 31 | cd es6-tree-shaking-test 32 | npm install 33 | npm test [test-case-name] 34 | ``` 35 | 36 | ## License 37 | 38 | [MIT](LICENSE) 39 | 40 | [npm]: https://www.npmjs.org/package/universal-webpack 41 | [npm-badge]: https://img.shields.io/npm/v/universal-webpack.svg?style=flat-square 42 | 43 | [travis]: https://travis-ci.org/halt-hammerzeit/universal-webpack 44 | [travis-badge]: https://img.shields.io/travis/halt-hammerzeit/universal-webpack/master.svg?style=flat-square 45 | 46 | [coveralls]: https://coveralls.io/r/halt-hammerzeit/universal-webpack?branch=master 47 | [coveralls-badge]: https://img.shields.io/coveralls/halt-hammerzeit/universal-webpack/master.svg?style=flat-square 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "es6-tree-shaking-test", 3 | "version": "0.1.0", 4 | "description": "Test your javascript module bundler (Webpack, Rollup, etc) and find out which one is the smartest when it comes to intelligently eliminating unused code", 5 | "main": "src/run.js", 6 | "scripts": { 7 | "test": "node src/run.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/halt-hammerzeit/es6-tree-shaking-test.git" 12 | }, 13 | "keywords": [ 14 | "webpack", 15 | "isomorphic", 16 | "universal", 17 | "render", 18 | "server", 19 | "react" 20 | ], 21 | "author": "Halt Hammerzeit ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/halt-hammerzeit/es6-tree-shaking-test/issues" 25 | }, 26 | "homepage": "https://github.com/halt-hammerzeit/es6-tree-shaking-test#readme", 27 | "engines": { 28 | "node": ">=7.0.0" 29 | }, 30 | "dependencies": { 31 | "babili": "^0.1.2", 32 | "babili-webpack-plugin": "^0.1.1", 33 | "memory-fs": "^0.4.1", 34 | "rollup": "^0.41.6", 35 | "uglifyjs-webpack-plugin": "^0.3.0", 36 | "webpack": "^2.2.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /project.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "follow_symlinks": true, 6 | "path": ".", 7 | "folder_exclude_patterns": ["node_modules", "coverage", "build", "project.sublime-workspace"] 8 | } 9 | ], 10 | "settings": 11 | { 12 | "tab_size": 3 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/bundlers/rollup.js: -------------------------------------------------------------------------------- 1 | const rollup = require('rollup') 2 | const path = require('path') 3 | 4 | // https://github.com/rollup/rollup/wiki/JavaScript-API 5 | module.exports = ({ folder, file }) => { 6 | const entry = path.join(folder, file, 'index.js') 7 | return rollup.rollup({ 8 | entry 9 | }) 10 | .then((bundle) => { 11 | const output = bundle.generate({ 12 | format: 'es' 13 | }).code 14 | 15 | // Debugging 16 | console.log() 17 | console.log(output) 18 | 19 | return output 20 | }) 21 | } -------------------------------------------------------------------------------- /src/bundlers/webpack.js: -------------------------------------------------------------------------------- 1 | const MemoryFS = require('memory-fs') 2 | const webpack = require('webpack') 3 | // const UglifyJSPlugin = require('uglifyjs-webpack-plugin') 4 | const BabiliPlugin = require('babili-webpack-plugin') 5 | 6 | // https://webpack.js.org/api/node/ 7 | module.exports = async ({ folder, file }, { minifier }) => { 8 | 9 | const output = await bundle({ folder, file }) 10 | 11 | const startMarker = '/******/ ([' 12 | const endMarker = '/******/ ]);' 13 | 14 | // Debugging 15 | console.log() 16 | console.log(output.slice(output.indexOf(`${startMarker}\n/* 0 */`) + `${startMarker}\n`.length, `${endMarker}\n`.length * -1)) 17 | console.log() 18 | 19 | const minimized = await bundle({ folder, file }, [minifierPlugin(minifier)]) 20 | 21 | // Debugging minimized output 22 | console.log('------------------------------------------------') 23 | console.log('- Minimized -') 24 | console.log('------------------------------------------------') 25 | console.log() 26 | 27 | console.log(minimized) 28 | console.log() 29 | 30 | return minimized 31 | } 32 | 33 | function bundle({ folder, file }, plugins = []) { 34 | return new Promise((resolve, reject) => { 35 | const compiler = webpack({ 36 | context: folder, 37 | entry: `./${file}`, 38 | output: { 39 | path: '/', 40 | filename: 'bundle.js' 41 | }, 42 | plugins 43 | }) 44 | 45 | const fs = new MemoryFS() 46 | compiler.outputFileSystem = fs 47 | 48 | compiler.run((error, stats) => { 49 | if (error) { 50 | return reject(error) 51 | } 52 | 53 | // stats.toJson("minimal") 54 | // more options: "verbose", etc. 55 | const info = stats.toJson() 56 | 57 | if (stats.hasErrors()) { 58 | return reject(new Error(`Webpack build errors: ${JSON.stringify(info.errors, null, 2)}`)) 59 | } 60 | 61 | if (stats.hasWarnings()) { 62 | return reject(new Error(`Webpack build warnings: ${JSON.stringify(info.warnings, null, 2)}`)) 63 | } 64 | 65 | resolve(fs.readFileSync('/bundle.js', 'utf8')) 66 | }) 67 | }) 68 | } 69 | 70 | function minifierPlugin(minifier) { 71 | switch (minifier) { 72 | case 'babili': 73 | return new BabiliPlugin() 74 | case 'uglifyjs': 75 | return new webpack.optimize.UglifyJsPlugin() 76 | } 77 | } -------------------------------------------------------------------------------- /src/run.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const webpackRunTest = require('./bundlers/webpack') 5 | const rollupRunTest = require('./bundlers/rollup') 6 | 7 | const testsFolder = path.join(__dirname, '../tests') 8 | 9 | function getTests() { 10 | return fs.readdirSync(testsFolder) 11 | .filter(file => fs.statSync(path.join(testsFolder, file)).isDirectory()) 12 | } 13 | 14 | const testName = process.argv[2] 15 | 16 | async function runTests() { 17 | const results = [] 18 | 19 | for (const test of testName ? [testName] : getTests()) { 20 | console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') 21 | console.log(`~ Running test "${test}"`) 22 | console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') 23 | 24 | // Determines if a test passed 25 | const passed = require(path.join(testsFolder, test, 'passed.js')) 26 | 27 | const options = { 28 | file: test, 29 | folder: testsFolder 30 | } 31 | 32 | const output = {} 33 | 34 | console.log('==========================================================') 35 | console.log('= Webpack (with UglifyJS) =') 36 | console.log('==========================================================') 37 | 38 | output.webpackUglifyJS = await webpackRunTest(options, { minifier: 'uglifyjs' }) 39 | reportPassedOrFailed(passed(output.webpackUglifyJS)) 40 | 41 | console.log('==========================================================') 42 | console.log('= Webpack (with Babili) =') 43 | console.log('==========================================================') 44 | 45 | output.webpackBabili = await webpackRunTest(options, { minifier: 'babili' }) 46 | reportPassedOrFailed(passed(output.webpackBabili)) 47 | 48 | console.log('==========================================================') 49 | console.log('= Rollup =') 50 | console.log('==========================================================') 51 | 52 | output.rollup = await rollupRunTest(options) 53 | reportPassedOrFailed(passed(output.rollup)) 54 | 55 | results.push({ 56 | test, 57 | webpack_UglifyJS: passed(output.webpackUglifyJS), 58 | webpack_Babili: passed(output.webpackBabili), 59 | rollup: passed(output.rollup) 60 | }) 61 | } 62 | 63 | console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') 64 | console.log('~ Tests finished ~') 65 | console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') 66 | 67 | // Can output a GitHub markdown table here 68 | // which can then be injected into README.md 69 | console.log(JSON.stringify(results, null, 2)) 70 | } 71 | 72 | function reportPassedOrFailed(passed) { 73 | if (passed) { 74 | console.log('✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓') 75 | console.log('✓ PASSED ✓') 76 | console.log('✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓') 77 | } 78 | else { 79 | console.log('✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕') 80 | console.log('✕ FAILED ✕') 81 | console.log('✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕✕') 82 | } 83 | } 84 | 85 | runTests().catch(error => console.error(error)) -------------------------------------------------------------------------------- /tests/sample/helpers.js: -------------------------------------------------------------------------------- 1 | export function a() { 2 | console.log('I have a pen') 3 | } 4 | 5 | export function b() { 6 | console.log('I have an apple') 7 | } -------------------------------------------------------------------------------- /tests/sample/index.js: -------------------------------------------------------------------------------- 1 | import { a, b } from './helpers' 2 | 3 | export default function() { 4 | a() 5 | } -------------------------------------------------------------------------------- /tests/sample/passed.js: -------------------------------------------------------------------------------- 1 | module.exports = (output) => { 2 | return output.indexOf('I have a pen') >= 0 && output.indexOf('I have an apple') < 0 3 | } --------------------------------------------------------------------------------