├── .babelrc ├── .circleci └── config.yml ├── .editorconfig ├── .github └── issue_template.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __mocks__ ├── bluebird.js └── worker-farm.js ├── __tests__ ├── __snapshots__ │ └── index.spec.js.snap └── index.spec.js ├── bin └── run.js ├── index.js ├── jest.config.js ├── package.json ├── schema.json └── src ├── __tests__ ├── __snapshots__ │ ├── createVariants.spec.js.snap │ └── webpackWorker.spec.js.snap ├── createVariants.spec.js ├── findConfigFile.spec.js ├── loadConfigurationFile.spec.js ├── watchDoneHandler.spec.js ├── watchModeIPC.spec.js └── webpackWorker.spec.js ├── createVariants.js ├── findConfigFile.js ├── loadConfigurationFile.js ├── watchDoneHandler.js ├── watchModeIPC.js └── webpackWorker.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": {"node": 4} 5 | } 6 | ]] 7 | } 8 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | # specify the version you desire here 6 | - image: circleci/node:6 7 | - image: circleci/node:latest 8 | 9 | working_directory: ~/repo 10 | 11 | steps: 12 | - checkout 13 | 14 | - run: npm install 15 | 16 | - run: npm run test-only 17 | 18 | 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | #### Explain the problem 2 | 3 | 4 | #### Expected Behaviour 5 | 6 | 7 | #### Actual Behaviour 8 | 9 | 10 | #### Steps to reproduce 11 | 12 | 13 | #### Provide your webpack config 14 | 15 | 16 | #### Provide your Environment details 17 | - Node version: 18 | 19 | - Operating System: 20 | 21 | - webpack version: 22 | 23 | - parallel-webpack version: 24 | 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "6" 5 | 6 | install: 7 | - npm install 8 | 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v2.6.0](https://github.com/trivago/parallel-webpack/tree/v2.6.0) (2020-04-08) 4 | 5 | [Full Changelog](https://github.com/trivago/parallel-webpack/compare/v2.5.0...v2.6.0) 6 | 7 | **Merged pull requests:** 8 | 9 | - Fail build and exit child processes on unhandledRejection or disconnect [\#100](https://github.com/trivago/parallel-webpack/pull/100) ([uzi88](https://github.com/uzi88)) 10 | 11 | ## [v2.5.0](https://github.com/trivago/parallel-webpack/tree/v2.5.0) (2020-04-03) 12 | 13 | [Full Changelog](https://github.com/trivago/parallel-webpack/compare/v2.3.0...v2.5.0) 14 | 15 | **Merged pull requests:** 16 | 17 | - Handle config function return promise of config object array [\#84](https://github.com/trivago/parallel-webpack/pull/84) ([chesan](https://github.com/chesan)) 18 | - Give more time to workers to finish the process [\#89](https://github.com/trivago/parallel-webpack/pull/89) ([byara](https://github.com/byara)) 19 | - Signal fail worker process exit and finish idle process [\#99](https://github.com/trivago/parallel-webpack/pull/99) ([uzi88](https://github.com/uzi88)) 20 | 21 | ## [v2.3.0](https://github.com/trivago/parallel-webpack/tree/v2.3.0) (2018-02-26) 22 | 23 | [Full Changelog](https://github.com/trivago/parallel-webpack/compare/v2.2.0...v2.3.0) 24 | 25 | **Merged pull requests:** 26 | 27 | - Bump webpack peer dependency [\#78](https://github.com/trivago/parallel-webpack/pull/78) ([efegurkan](https://github.com/efegurkan)) 28 | - injected Stats.presetToOptions to parse { stats: String } into Object [\#73](https://github.com/trivago/parallel-webpack/pull/73) ([oleg-andreyev](https://github.com/oleg-andreyev)) 29 | - update lodash.endswith require [\#72](https://github.com/trivago/parallel-webpack/pull/72) ([oleg-andreyev](https://github.com/oleg-andreyev)) 30 | - Add issue_template [\#69](https://github.com/trivago/parallel-webpack/pull/69) ([efegurkan](https://github.com/efegurkan)) 31 | - docs\(README\): add npm version badge [\#64](https://github.com/trivago/parallel-webpack/pull/64) ([alan-agius4](https://github.com/alan-agius4)) 32 | - Fix pull request links in CHANGELOG.md [\#62](https://github.com/trivago/parallel-webpack/pull/62) ([valscion](https://github.com/valscion)) 33 | - Incomplete replacement fix [\#61](https://github.com/trivago/parallel-webpack/pull/61) ([Luixo](https://github.com/Luixo)) 34 | 35 | ## [v2.2.0](https://github.com/trivago/parallel-webpack/tree/v2.2.0) (2017-09-19) 36 | 37 | [Full Changelog](https://github.com/trivago/parallel-webpack/compare/v2.1.0...v2.2.0) 38 | 39 | **Implemented enhancements:** 40 | 41 | - On complete hook [\#39](https://github.com/trivago/parallel-webpack/issues/39) 42 | - Webpack 2 config export as function [\#34](https://github.com/trivago/parallel-webpack/issues/34) 43 | - watch mode done callback [\#55](https://github.com/trivago/parallel-webpack/pull/55) ([natedanner](https://github.com/natedanner)) 44 | 45 | **Merged pull requests:** 46 | 47 | - Update package.json to support webpack 3.1.0 [\#59](https://github.com/trivago/parallel-webpack/pull/59) ([clowNay](https://github.com/clowNay)) 48 | - Remove coveralls from circle-ci, disable colors [\#53](https://github.com/trivago/parallel-webpack/pull/53) ([efegurkan](https://github.com/efegurkan)) 49 | - Add coverage support [\#50](https://github.com/trivago/parallel-webpack/pull/50) ([efegurkan](https://github.com/efegurkan)) 50 | 51 | ## [v2.1.0](https://github.com/trivago/parallel-webpack/tree/v2.1.0) (2017-07-28) 52 | 53 | [Full Changelog](https://github.com/trivago/parallel-webpack/compare/v2.0.0...v2.1.0) 54 | 55 | **Merged pull requests:** 56 | 57 | - travis and circle-ci integrations [\#49](https://github.com/trivago/parallel-webpack/pull/49) ([efegurkan](https://github.com/efegurkan)) 58 | - support config as export function v2 [\#48](https://github.com/trivago/parallel-webpack/pull/48) ([Robbilie](https://github.com/Robbilie)) 59 | - Adjust basic example [\#47](https://github.com/trivago/parallel-webpack/pull/47) ([Robbilie](https://github.com/Robbilie)) 60 | - Pass watch options to the compiler [\#46](https://github.com/trivago/parallel-webpack/pull/46) ([BenoitZugmeyer](https://github.com/BenoitZugmeyer)) 61 | - Implement tests [\#44](https://github.com/trivago/parallel-webpack/pull/44) ([efegurkan](https://github.com/efegurkan)) 62 | 63 | ## [v2.0.0](https://github.com/trivago/parallel-webpack/tree/v2.0.0) (2017-06-28) 64 | 65 | [Full Changelog](https://github.com/trivago/parallel-webpack/compare/v1.6.1...v2.0.0) 66 | 67 | **Closed issues:** 68 | 69 | - freezed on parallel-webpack run on circleci [\#41](https://github.com/trivago/parallel-webpack/issues/41) 70 | - \[Webpack 2\] Using --optimize-minimize argument with parallel-webpack [\#40](https://github.com/trivago/parallel-webpack/issues/40) 71 | - some childprocesses don't get killed when quitting [\#33](https://github.com/trivago/parallel-webpack/issues/33) 72 | 73 | **Merged pull requests:** 74 | 75 | - Moved webpack to peerDependencies [\#42](https://github.com/trivago/parallel-webpack/pull/42) ([gandazgul](https://github.com/gandazgul)) 76 | - make parallel-webpack work with webpack@^2.2.0 [\#32](https://github.com/trivago/parallel-webpack/pull/32) ([anilanar](https://github.com/anilanar)) 77 | 78 | ## [v1.6.1](https://github.com/trivago/parallel-webpack/tree/v1.6.1) (2017-01-20) 79 | 80 | [Full Changelog](https://github.com/trivago/parallel-webpack/compare/v1.6.0...v1.6.1) 81 | 82 | **Fixed bugs:** 83 | 84 | - After ctrl+c build/watch restarts [\#29](https://github.com/trivago/parallel-webpack/issues/29) 85 | 86 | ## [v1.6.0](https://github.com/trivago/parallel-webpack/tree/v1.6.0) (2016-12-16) 87 | 88 | [Full Changelog](https://github.com/trivago/parallel-webpack/compare/v1.5.0...v1.6.0) 89 | 90 | **Implemented enhancements:** 91 | 92 | - Don't have options propagation to worker-farm expect: 'maxConcurrentWorkers' and 'maxRetries' [\#25](https://github.com/trivago/parallel-webpack/issues/25) 93 | - EventEmitter memory leak warnings [\#22](https://github.com/trivago/parallel-webpack/issues/22) 94 | - Please use a changelog [\#15](https://github.com/trivago/parallel-webpack/issues/15) 95 | - \[name\] logging [\#14](https://github.com/trivago/parallel-webpack/issues/14) 96 | 97 | **Fixed bugs:** 98 | 99 | - Don't forget call process.removeListener. [\#24](https://github.com/trivago/parallel-webpack/issues/24) 100 | 101 | **Closed issues:** 102 | 103 | - Pass config directly to Node API [\#21](https://github.com/trivago/parallel-webpack/issues/21) 104 | - \[Question\] Does it work with webpack dev server or middleware ? [\#18](https://github.com/trivago/parallel-webpack/issues/18) 105 | 106 | **Merged pull requests:** 107 | 108 | - Implement options propagation to worker-farm. Add schema json validation. [\#28](https://github.com/trivago/parallel-webpack/pull/28) ([wKich](https://github.com/wKich)) 109 | - Add calls process.removeListener [\#27](https://github.com/trivago/parallel-webpack/pull/27) ([wKich](https://github.com/wKich)) 110 | 111 | ## [v1.5.0](https://github.com/trivago/parallel-webpack/tree/v1.5.0) (2016-06-14) 112 | 113 | [Full Changelog](https://github.com/trivago/parallel-webpack/compare/v1.4.0...v1.5.0) 114 | 115 | **Merged pull requests:** 116 | 117 | - Fix typo from statics to statistics [\#16](https://github.com/trivago/parallel-webpack/pull/16) ([nickpresta](https://github.com/nickpresta)) 118 | 119 | ## [v1.4.0](https://github.com/trivago/parallel-webpack/tree/v1.4.0) (2016-05-10) 120 | 121 | [Full Changelog](https://github.com/trivago/parallel-webpack/compare/v1.3.1...v1.4.0) 122 | 123 | **Implemented enhancements:** 124 | 125 | - Add timestamp for continously rebuilds with the watcher in the console output [\#12](https://github.com/trivago/parallel-webpack/issues/12) 126 | - Add documentation for configurable configurations [\#6](https://github.com/trivago/parallel-webpack/issues/6) 127 | - various fixes for programmatic output \(mostly\) [\#10](https://github.com/trivago/parallel-webpack/pull/10) ([boneskull](https://github.com/boneskull)) 128 | 129 | **Closed issues:** 130 | 131 | - Add config extension interpretation for ".babel.js", ".coffee", etc [\#13](https://github.com/trivago/parallel-webpack/issues/13) 132 | 133 | ## [v1.3.1](https://github.com/trivago/parallel-webpack/tree/v1.3.1) (2016-02-16) 134 | 135 | [Full Changelog](https://github.com/trivago/parallel-webpack/compare/v1.3.0...v1.3.1) 136 | 137 | **Closed issues:** 138 | 139 | - Watcher for 'npm start' gives wrong statistics about build time [\#7](https://github.com/trivago/parallel-webpack/issues/7) 140 | 141 | **Merged pull requests:** 142 | 143 | - Use timing stats directly from Webpack [\#9](https://github.com/trivago/parallel-webpack/pull/9) ([lime](https://github.com/lime)) 144 | - Use global parseInt instead of Number.parseInt [\#8](https://github.com/trivago/parallel-webpack/pull/8) ([lime](https://github.com/lime)) 145 | 146 | ## [v1.3.0](https://github.com/trivago/parallel-webpack/tree/v1.3.0) (2016-01-26) 147 | 148 | [Full Changelog](https://github.com/trivago/parallel-webpack/compare/v1.2.0...v1.3.0) 149 | 150 | ## [v1.2.0](https://github.com/trivago/parallel-webpack/tree/v1.2.0) (2016-01-11) 151 | 152 | [Full Changelog](https://github.com/trivago/parallel-webpack/compare/v1.1.0...v1.2.0) 153 | 154 | **Implemented enhancements:** 155 | 156 | - Can you support --bail and --json flags? [\#2](https://github.com/trivago/parallel-webpack/issues/2) 157 | 158 | ## [v1.1.0](https://github.com/trivago/parallel-webpack/tree/v1.1.0) (2016-01-01) 159 | 160 | [Full Changelog](https://github.com/trivago/parallel-webpack/compare/v1.0.0...v1.1.0) 161 | 162 | **Fixed bugs:** 163 | 164 | - Adjust documentation regarding createVariants baseOptions parameter [\#4](https://github.com/trivago/parallel-webpack/issues/4) 165 | 166 | **Closed issues:** 167 | 168 | - using babel-loader [\#3](https://github.com/trivago/parallel-webpack/issues/3) 169 | 170 | ## [v1.0.0](https://github.com/trivago/parallel-webpack/tree/v1.0.0) (2015-12-15) 171 | 172 | \* _This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)_ 173 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at efe-gurkan.yalaman@trivago.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to parallel-webpack 2 | 3 | Thank you for contributing to `parallel-webpack`! 4 | 5 | This project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md) By participating you agree to comply with its terms. 6 | 7 | #### Table Of Contents 8 | 9 | [How Can I Contribute?](#how-can-i-contribute) 10 | * [Improve Documentation](#improve-documentation) 11 | * [Reporting Bugs](#reporting-bugs) 12 | * [Writing Code](#writing-code) 13 | * [Pull Requests](#pull-requests) 14 | 15 | 16 | ## How can I contribute? 17 | 18 | ### Improve documentation 19 | Most simple way to contribute is improving our documentation. Fixing typo's, fixing errors, explaining something better, 20 | more examples etc. If your work would be a big change, open an issue first. For smaller changes feel free to open a PR 21 | directly. 22 | 23 | Use common sense to decide if you need an issue or not. Generally if you change more than a few paragraphs, multiple 24 | files etc, it is better to open an issue and explain your change. 25 | 26 | ### Reporting Bugs 27 | 28 | If you encounter any issues with `parallel-webpack` don't hesitate to report it. While reporting your bug make sure you 29 | follow the guidelines below. It helps maintainers to understand and reproduce the problem. 30 | 31 | * **Use a clear and descriptive title** for the issue to identify the problem. 32 | * **Provide configuration you used** which is critical for reproducing problem in most cases. 33 | * **Provide system details you used** to identify if the problem is system specific. I.e: operating system, node version, webpack version you used etc. 34 | * **Describe the exact steps which reproduce the problem** in as many details as possible. 35 | * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. 36 | * **Explain which behavior you expected to see instead and why.** 37 | 38 | > **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. 39 | 40 | 41 | ### Writing code 42 | 43 | Find an issue you want to work on, or if you have your own idea create an issue. You might find an issue assigned. Double-check 44 | if somebody else is working on the same issue. 45 | 46 | #### Local development 47 | 48 | It usually is a good idea to create a dummy repository to run your changes. 49 | 50 | While writing code make sure you follow this guidelines: 51 | * Use 4 space indentation. 52 | * Always use strict equality checks `===` instead of `==`. 53 | * Make sure your code runs on node 6. 54 | * Write tests and run them. Check coverage before submitting. 55 | * Write documentation for your code. 56 | 57 | ### Pull Requests 58 | 59 | * Non-trivial changes are often best discussed in an issue first, to prevent you from doing unnecessary work. 60 | * For ambitious tasks, you should try to get your work in front of the community for feedback as soon as possible. Open a pull request as soon as you have done the minimum needed to demonstrate your idea. At this early stage, don't worry about making things perfect, or 100% complete. Add a [WIP] prefix to the title, and describe what you still need to do. This lets reviewers know not to nit-pick small details or point out improvements you already know you need to make. 61 | * New features should be accompanied with tests and documentation. 62 | * Don't include unrelated changes. 63 | * Make the pull request from a [topic branch](https://github.com/dchelimsky/rspec/wiki/Topic-Branches), not master. 64 | * Use a clear and descriptive title for the pull request and commits. 65 | * Write a convincing description of why we should land your pull request. It's your job to convince us. Answer "why" it's needed and provide use-cases. 66 | * You might be asked to do changes to your pull request. There's never a need to open another pull request. [Just update the existing one.](https://github.com/RichardLitt/knowledge/blob/master/github/amending-a-commit-guide.md) 67 | * Be patient, we might not find time to check on your pull requests immediately. It will be checked eventually. 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, trivago GmbH 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer 10 | in the documentation and/or other materials provided with the distribution. 11 | 12 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived 13 | from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 16 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 17 | THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 20 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/parallel-webpack.svg)](https://badge.fury.io/js/parallel-webpack) 2 | [![Build Status](https://travis-ci.org/trivago/parallel-webpack.svg?branch=master)](https://travis-ci.org/trivago/parallel-webpack) [![CircleCI](https://circleci.com/gh/trivago/parallel-webpack.svg?style=svg)](https://circleci.com/gh/trivago/parallel-webpack) [![Coverage Status](https://coveralls.io/repos/github/trivago/parallel-webpack/badge.svg?branch=coverage)](https://coveralls.io/github/trivago/parallel-webpack?branch=coverage) 3 | [![Install Size](https://packagephobia.now.sh/badge?p=parallel-webpack)](https://packagephobia.now.sh/result?p=parallel-webpack) 4 | # parallel-webpack - Building multi-configs in parallel 5 | 6 | `parallel-webpack` allows you to run multiple webpack builds in parallel, 7 | spreading the work across your processors and thus helping to significantly speed 8 | up your build. For us at [trivago](http://www.trivago.com) it has reduced 9 | the build from 16 minutes to just 2 minutes - for 32 variants. That performance 10 | improvement naturally comes at the expense of utilizing all available CPU cores. 11 | 12 | ## Installation 13 | 14 | ```sh 15 | npm install parallel-webpack --save-dev 16 | ``` 17 | 18 | You can choose whether to install `parallel-webpack` globally or locally. 19 | At [trivago](http://www.trivago.com), we keep our build tools locally to the project 20 | so that we have full control over its versions. 21 | 22 | ## Basic example 23 | 24 | Given a `webpack.config.js` like this: 25 | 26 | ```javascript 27 | var path = require('path'); 28 | module.exports = [{ 29 | entry: './pageA.js', 30 | output: { 31 | path: path.resolve(__dirname, './dist'), 32 | filename: 'pageA.bundle.js' 33 | } 34 | }, { 35 | entry: './pageB.js', 36 | output: { 37 | path: path.resolve(__dirname, './dist'), 38 | filename: 'pageB.bundle.js' 39 | } 40 | }]; 41 | ``` 42 | 43 | `parallel-webpack` will run both specified builds in parallel. 44 | 45 | ## Variants example 46 | 47 | Sometimes, just using different configurations like above won't be enough and what 48 | you really want or need is the same configuration with some adjustments. 49 | `parallel-webpack` can help you with generating those `configuration variants` as 50 | well. 51 | 52 | ```javascript 53 | var createVariants = require('parallel-webpack').createVariants; 54 | 55 | // Those options will be mixed into every variant 56 | // and passed to the `createConfig` callback. 57 | var baseOptions = { 58 | preferredDevTool: process.env.DEVTOOL || 'eval' 59 | }; 60 | 61 | // This object defines the potential option variants 62 | // the key of the object is used as the option name, its value must be an array 63 | // which contains all potential values of your build. 64 | var variants = { 65 | minified: [true, false], 66 | debug: [true, false], 67 | target: ['commonjs2', 'var', 'umd', 'amd'] 68 | }; 69 | 70 | function createConfig(options) { 71 | var plugins = [ 72 | new webpack.optimize.DedupePlugin(), 73 | new webpack.optimize.OccurenceOrderPlugin(), 74 | new webpack.DefinePlugin({ 75 | DEBUG: JSON.stringify(JSON.parse(options.debug)) 76 | }) 77 | ]; 78 | if(options.minified) { 79 | plugins.push(new webpack.optimize.UglifyJsPlugin({ 80 | sourceMap: false, 81 | compress: { 82 | warnings: false 83 | } 84 | })); 85 | } 86 | return { 87 | entry: './index.js', 88 | devtool: options.preferredDevTool, 89 | output: { 90 | path: './dist/', 91 | filename: 'MyLib.' + 92 | options.target + 93 | (options.minified ? '.min' : '') + 94 | (options.debug ? '.debug' : '') 95 | + '.js', 96 | libraryTarget: options.target 97 | }, 98 | plugins: plugins 99 | }; 100 | } 101 | 102 | module.exports = createVariants(baseOptions, variants, createConfig); 103 | ``` 104 | 105 | The above configuration will create 16 variations of the build for you, which 106 | `parallel-webpack` will distribute among your processors for building. 107 | 108 | ``` 109 | [WEBPACK] Building 16 targets in parallel 110 | [WEBPACK] Started building MyLib.umd.js 111 | [WEBPACK] Started building MyLib.umd.min.js 112 | [WEBPACK] Started building MyLib.umd.debug.js 113 | [WEBPACK] Started building MyLib.umd.min.debug.js 114 | 115 | [WEBPACK] Started building MyLib.amd.js 116 | [WEBPACK] Started building MyLib.amd.min.js 117 | [WEBPACK] Started building MyLib.amd.debug.js 118 | [WEBPACK] Started building MyLib.amd.min.debug.js 119 | 120 | [WEBPACK] Started building MyLib.commonjs2.js 121 | [WEBPACK] Started building MyLib.commonjs2.min.js 122 | [WEBPACK] Started building MyLib.commonjs2.debug.js 123 | [WEBPACK] Started building MyLib.commonjs2.min.debug.js 124 | 125 | [WEBPACK] Started building MyLib.var.js 126 | [WEBPACK] Started building MyLib.var.min.js 127 | [WEBPACK] Started building MyLib.var.debug.js 128 | [WEBPACK] Started building MyLib.var.min.debug.js 129 | ``` 130 | 131 | ## Running the watcher 132 | 133 | One of the features that made webpack so popular is certainly its watcher which 134 | continously rebuilds your application. 135 | 136 | When using `parallel-webpack`, you can easily use the same feature as well by 137 | specifying the `--watch` option on the command line: 138 | 139 | ``` 140 | parallel-webpack --watch 141 | ``` 142 | 143 | ## Specifying retry limits 144 | 145 | As a side-effect of using `parallel-webpack`, an error will no longer lead to 146 | you having to restart webpack. Instead, `parallel-webpack` will keep retrying to 147 | build your application until you've fixed the problem. 148 | 149 | While that is highly useful for development it can be a nightmare for 150 | CI builds. Thus, when building with `parallel-webpack` in a CI context, you should 151 | consider to use the `--max-retries` (or `-m` option) to force `parallel-webpack` to give 152 | up on your build after a certain amount of retries: 153 | 154 | ``` 155 | parallel-webpack --max-retries=3 156 | ``` 157 | 158 | ## Specifying the configuration file 159 | 160 | When you need to use a configuration file that is not `webpack.config.js`, you can 161 | specify its name using the `--config` parameter: 162 | 163 | ``` 164 | parallel-webpack --config=myapp.webpack.config.js 165 | ``` 166 | 167 | ## Switch off statistics (improves performance) 168 | 169 | While the statistics generated by Webpack are very usually very useful, they also 170 | take time to generate and print and create a lot of visual overload if you don't 171 | actually need them. 172 | 173 | Since version *1.3.0*, generating them can be turned off: 174 | 175 | ``` 176 | parallel-webpack --no-stats 177 | ``` 178 | 179 | ## Limiting parallelism 180 | 181 | Under certain circumstances you might not want `parallel-webpack` to use all of your 182 | available CPUs for building your assets. In those cases, you can specify the `parallel`, 183 | or `p` for short, option to tell `parallel-webpack` how many CPUs it may use. 184 | 185 | ``` 186 | parallel-webpack -p=2 187 | ``` 188 | 189 | 190 | ## Configurable configuration 191 | 192 | Sometimes, you might want to access command line arguments within your `webpack.config.js` 193 | in order to create a more specific configuration. 194 | 195 | `parallel-webpack` will forward every parameter specified after `--` to the configuration 196 | as is: 197 | 198 | ``` 199 | parallel-webpack -- --app=trivago 200 | ``` 201 | 202 | 203 | Within `webpack.config.js`: 204 | 205 | ``` 206 | console.log(process.argv); 207 | // => [ 'node', 'parallel-webpack', '--app=trivago' ] 208 | ``` 209 | 210 | `parallel-webpack` adds the first two values to `process.argv` to ensure that there 211 | are no differences between various ways of invoking the `webpack.config.js`. 212 | 213 | ## Node.js API 214 | 215 | Just like webpack, you can also use `parallel-webpack` as an API from node.js 216 | (You can specify any other option used in [worker-farm](https://www.npmjs.com/package/worker-farm)): 217 | 218 | ```javascript 219 | var run = require('parallel-webpack').run, 220 | configPath = require.resolve('./webpack.config.js'); 221 | 222 | run(configPath, { 223 | watch: false, 224 | maxRetries: 1, 225 | stats: true, // defaults to false 226 | maxConcurrentWorkers: 2 // use 2 workers 227 | }); 228 | ``` 229 | 230 | You can pass a notify callback as well. 231 | ```javascript 232 | var run = require('parallel-webpack').run, 233 | configPath = require.resolve('./webpack.config.js'), 234 | options = {/*...*/}; 235 | 236 | function notify() { 237 | // do things 238 | } 239 | 240 | run(configPath, options, notify); 241 | ``` 242 | **NOTE:** In watch mode notify callback provided with Node.js API will run **only once** 243 | when all of the builds are finished. 244 | 245 | ### createVariants 246 | 247 | --- 248 | 249 | #### createVariants(baseConfig: Object, variants: Object, configCallback: Function): Object[] 250 | 251 | Alters the given `baseConfig` with all possible `variants` and maps the result into 252 | a valid webpack configuration using the given `configCallback`. 253 | 254 | #### createVariants(variants: Object, configCallback: Function): Object[] 255 | 256 | Creates all possible variations as specified in the `variants` object and 257 | maps the result into a valid webpack configuration using the given `configCallback`. 258 | 259 | #### createVariants(baseConfig: Object, variants: Object): Object[] 260 | 261 | Alters the given `baseConfig` with all possible `variants` and returns it. 262 | 263 | #### createVariants(variants: Object): Object[] 264 | 265 | Creates all possible variations from the given `variants` and returns them as a flat array. 266 | -------------------------------------------------------------------------------- /__mocks__/bluebird.js: -------------------------------------------------------------------------------- 1 | const api = { 2 | error: jest.fn().mockReturnThis(), 3 | then: jest.fn().mockReturnThis(), 4 | resolve: jest.fn().mockReturnThis(), 5 | reject: jest.fn().mockReturnThis(), 6 | promisify: jest.fn().mockReturnThis(), 7 | settle: jest.fn().mockReturnThis(), 8 | all: jest.fn().mockReturnThis(), 9 | finally: jest.fn().mockReturnThis(), 10 | asCallback: jest.fn().mockReturnThis(), 11 | }; 12 | 13 | module.exports = api; 14 | -------------------------------------------------------------------------------- /__mocks__/worker-farm.js: -------------------------------------------------------------------------------- 1 | const end = jest.fn(); 2 | const workerFarm = jest.fn(() => end); 3 | workerFarm.end = end; 4 | module.exports = workerFarm; 5 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/index.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`index.js run error callback should log and reject with error 1`] = ` 4 | Array [ 5 | Array [ 6 | "%s Build failed after %s seconds", 7 | "[WEBPACK]", 8 | "0.3", 9 | ], 10 | ] 11 | `; 12 | 13 | exports[`index.js run should reject promise config cannot get loaded 1`] = ` 14 | "Error: [WEBPACK] Could not load configuration file ./does/not/exist.js 15 | Error: Cannot find module './does/not/exist.js' from 'loadConfigurationFile.js'" 16 | `; 17 | 18 | exports[`index.js run should reject promise if options validation fails 1`] = ` 19 | Array [ 20 | [Error: Options validation failed: 21 | Property: "options.maxConcurrentWorkers" should be number], 22 | ] 23 | `; 24 | 25 | exports[`index.js run shutdownCallback should call end and remove callback 1`] = ` 26 | Array [ 27 | Array [ 28 | "[WEBPACK] Forcefully shutting down", 29 | ], 30 | ] 31 | `; 32 | 33 | exports[`index.js run then callback should filter non-truthy and return results 1`] = ` 34 | Array [ 35 | Array [ 36 | "%s Finished build after %s seconds", 37 | "[WEBPACK]", 38 | "29.7", 39 | ], 40 | ] 41 | `; 42 | -------------------------------------------------------------------------------- /__tests__/index.spec.js: -------------------------------------------------------------------------------- 1 | jest.mock('testConfig.js', () => ({}), { virtual: true }); 2 | jest.useFakeTimers(); 3 | 4 | import { run } from '../index'; 5 | import workerFarm from 'worker-farm'; 6 | import Bluebird, { 7 | promisify, 8 | error, 9 | then, 10 | asCallback, 11 | } from 'bluebird'; 12 | 13 | describe('index.js', () => { 14 | describe('run', () => { 15 | beforeEach(() => { 16 | jest.clearAllMocks(); 17 | }); 18 | afterEach(() => { 19 | process.removeAllListeners('SIGINT'); 20 | }) 21 | 22 | it('should reject promise config cannot get loaded', () => { 23 | const returnPromise = run('./does/not/exist.js', { colors: false }, jest.fn()); 24 | expect(returnPromise.reject.mock.calls[0][0].toString()).toMatchSnapshot(); 25 | }); 26 | 27 | it('should reject promise if options validation fails', () => { 28 | const returnPromise = run('testConfig.js', { maxConcurrentWorkers: 'fail' }, jest.fn()); 29 | expect(returnPromise.reject.mock.calls[0]).toMatchSnapshot(); 30 | }); 31 | 32 | it('should call generate workers and return farm promise', () => { 33 | jest.spyOn(console, 'log').mockImplementation(() => {}); 34 | promisify.mockReturnValueOnce(jest.fn()); 35 | 36 | const returnPromise = run('testConfig.js', { colors: false }, jest.fn()); 37 | expect(workerFarm.mock.calls[0][0]).toEqual({ maxRetries: 0 }); 38 | 39 | expect(returnPromise).toBe(Bluebird); 40 | expect(promisify).toHaveBeenCalledTimes(1); 41 | expect(error).toHaveBeenCalledTimes(1); 42 | expect(then).toHaveBeenCalledTimes(2); 43 | expect(Bluebird.finally).toHaveBeenCalledTimes(1); 44 | expect(asCallback).toHaveBeenCalledTimes(1); 45 | }); 46 | 47 | describe('error callback', () => { 48 | beforeEach(() => { 49 | jest.spyOn(console, 'log').mockImplementation(() => {}); 50 | jest.spyOn(Date, 'now') 51 | .mockImplementationOnce(() => 0) 52 | .mockImplementationOnce(() => 300); 53 | }); 54 | 55 | const errorCbTest = options => { 56 | promisify.mockReturnValueOnce(jest.fn()); 57 | 58 | const returnPromise = run('testConfig.js', { 59 | json: options.silent, 60 | colors: false 61 | }, jest.fn()); 62 | const cb = returnPromise.error.mock.calls[0][0]; 63 | const response = cb('Exception on worker farm'); 64 | 65 | expect(response).toBe(Bluebird); 66 | expect(Bluebird.reject).toHaveBeenCalledWith('Exception on worker farm'); 67 | if (options.silent) { 68 | expect(console.log).not.toHaveBeenCalled(); 69 | } else { 70 | expect(console.log.mock.calls).toMatchSnapshot(); 71 | } 72 | }; 73 | 74 | it('should log and reject with error', () => { 75 | errorCbTest({ silent: false }); 76 | }); 77 | it('should only reject with error when silent', () => { 78 | errorCbTest({ silent: true }); 79 | }); 80 | }); 81 | 82 | describe('then callback', () => { 83 | beforeEach(() => { 84 | jest.spyOn(console, 'log').mockImplementation(() => {}); 85 | jest.spyOn(Date, 'now') 86 | .mockImplementationOnce(() => 30000) 87 | .mockImplementationOnce(() => 0); 88 | }); 89 | 90 | const thenCbTest = options => { 91 | promisify.mockReturnValueOnce(jest.fn()); 92 | 93 | const returnPromise = run('testConfig.js', { 94 | json: options.silent, 95 | colors: false 96 | }, jest.fn()); 97 | const cb = returnPromise.then.mock.calls[1][0]; 98 | const response = cb([true, true, false, undefined, '', 0]); 99 | 100 | expect(response).toEqual([ true, true ]); 101 | if(options.silent) { 102 | expect(console.log).not.toHaveBeenCalled(); 103 | } else { 104 | expect(console.log.mock.calls).toMatchSnapshot(); 105 | } 106 | }; 107 | 108 | it('should filter non-truthy and return results', () => { 109 | thenCbTest({ silent: false }); 110 | }); 111 | it('should not log when silent', () => { 112 | thenCbTest({ silent: true }); 113 | }); 114 | }); 115 | 116 | describe('finally callback', () => { 117 | it('should call end workerFarm and remove SIGINT listener', () => { 118 | promisify.mockReturnValueOnce(jest.fn()); 119 | 120 | const returnPromise = run('testConfig.js', { colors: false }, jest.fn()); 121 | const cb = returnPromise.finally.mock.calls[0][0]; 122 | 123 | expect(process.listenerCount('SIGINT')).toBe(1); 124 | cb(); 125 | jest.runOnlyPendingTimers(); 126 | expect(process.listenerCount('SIGINT')).toBe(0); 127 | 128 | // called with workers 129 | expect(workerFarm.end.mock.calls[0][0]).toBe(workerFarm.end); 130 | }); 131 | it('should call keepAliveAfterFinishCallback if flag is set', () => { 132 | const options = {}; 133 | Object.defineProperty(options, 'keepAliveAfterFinish', { 134 | value: 500 135 | }); 136 | const keepAliveAfterFinishCallback = jest.fn(() => { 137 | setTimeout(expect.any(Function), options.keepAliveAfterFinish); 138 | }); 139 | keepAliveAfterFinishCallback(); 140 | expect(setTimeout).toHaveBeenCalledTimes(1); 141 | expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 500); 142 | }); 143 | }); 144 | 145 | describe('shutdownCallback', () => { 146 | const shutdownTest = options => { 147 | promisify.mockReturnValueOnce(jest.fn()); 148 | 149 | const returnPromise = run('testConfig.js', { 150 | json: options.silent, 151 | colors: false 152 | }, jest.fn()); 153 | 154 | expect(process.listenerCount('SIGINT')).toBe(1); 155 | 156 | process.emit('SIGINT'); 157 | // called with workers 158 | expect(workerFarm.end.mock.calls[0][0]).toBe(workerFarm.end); 159 | if(options.silent) { 160 | expect(console.log).not.toHaveBeenCalled(); 161 | } else { 162 | expect(console.log.mock.calls).toMatchSnapshot(); 163 | } 164 | }; 165 | 166 | it('should call end and remove callback', () => { 167 | shutdownTest({ silent: false }); 168 | }); 169 | 170 | it('should call end and remove callback silently', () => { 171 | shutdownTest({ silent: true }); 172 | 173 | }); 174 | }); 175 | }); 176 | 177 | }); 178 | -------------------------------------------------------------------------------- /bin/run.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | /** 4 | * Created by pgotthardt on 07/12/15. 5 | */ 6 | var run = require('../index').run, 7 | path = require('path'), 8 | chalk = require('chalk'), 9 | findConfigFile = require('../src/findConfigFile'), 10 | argv = require('minimist')(process.argv.slice(2), { 11 | '--': true, 12 | default: { 13 | watch: false, 14 | 'max-retries': Infinity, 15 | // leave off file extension so that we can find the most appropriate one 16 | config: 'webpack.config', 17 | 'parallel': require('os').cpus().length, 18 | json: false, 19 | colors: require('supports-color'), 20 | bail: true, 21 | stats: true 22 | }, 23 | alias: { 24 | 'm': 'max-retries', 25 | 'p': 'parallel', 26 | 'v': 'version' 27 | } 28 | }), 29 | configPath; 30 | 31 | if(argv.version) { 32 | process.stdout.write('parallel-webpack ' + chalk.blue(require('../package').version) + "\n"); 33 | } else { 34 | try { 35 | chalk.enabled = argv.colors; 36 | configPath = findConfigFile(path.resolve(argv.config)); 37 | 38 | run(configPath, { 39 | watch: argv.watch, 40 | maxRetries: argv['max-retries'], 41 | maxConcurrentWorkers: argv['parallel'], 42 | bail: argv.bail, 43 | json: argv.json, 44 | modulesSort: argv['sort-modules-by'], 45 | chunksSort: argv['sort-chunks-by'], 46 | assetsSort: argv['sort-assets-by'], 47 | exclude: argv['display-exclude'], 48 | colors: argv['colors'], 49 | stats: argv['stats'], 50 | keepAliveAfterFinish: argv['keep-alive-after-finish'], 51 | argv: argv['--'] 52 | }).then(function(stats) { 53 | if(argv.json && stats) { 54 | process.stdout.write(JSON.stringify(stats.map(function(stat) { 55 | return JSON.parse(stat); 56 | }), null, 2) + "\n"); 57 | } 58 | }).catch(function(err) { 59 | console.log(err.message); 60 | process.exit(1); 61 | }); 62 | } catch (e) { 63 | if(e.message) { 64 | process.stdout.write(e.message + "\n"); 65 | console.error(e.error); 66 | } else { 67 | process.stdout.write(chalk.red('[WEBPACK]') + ' Could not load configuration ' + chalk.underline(process.cwd() + '/' + argv.config) + "\n"); 68 | } 69 | process.exit(1); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var workerFarm = require('worker-farm'), 2 | Ajv = require('ajv'), 3 | Promise = require('bluebird'), 4 | chalk = require('chalk'), 5 | assign = require('lodash.assign'), 6 | pluralize = require('pluralize'), 7 | schema = require('./schema.json'), 8 | loadConfigurationFile = require('./src/loadConfigurationFile').default, 9 | startWatchIPCServer = require('./src/watchModeIPC').startWatchIPCServer; 10 | 11 | var ajv = new Ajv({ 12 | allErrors: true, 13 | coerceTypes: true, 14 | removeAdditional: 'all', 15 | useDefaults: true 16 | }); 17 | var validate = ajv.compile(schema); 18 | 19 | function notSilent(options) { 20 | return !options.json; 21 | } 22 | 23 | function startFarm(config, configPath, options, runWorker, callback) { 24 | return Promise.resolve(config).then(function(config) { 25 | config = Array.isArray(config) ? config : [config]; 26 | options = options || {}; 27 | 28 | // When in watch mode and a callback is provided start IPC server to invoke callback 29 | // once all webpack configurations have been compiled 30 | if (options.watch) { 31 | startWatchIPCServer(callback, Object.keys(config)); 32 | } 33 | 34 | if(notSilent(options)) { 35 | console.log(chalk.blue('[WEBPACK]') + ' Building ' + chalk.yellow(config.length) + ' ' + pluralize('target', config.length)); 36 | } 37 | 38 | var builds = config.map(function (c, i) { 39 | return runWorker(configPath, options, i, config.length); 40 | }); 41 | if(options.bail) { 42 | return Promise.all(builds); 43 | } else { 44 | return Promise.settle(builds).then(function(results) { 45 | return Promise.all(results.map(function (result) { 46 | if(result.isFulfilled()) { 47 | return result.value(); 48 | } 49 | return Promise.reject(result.reason()); 50 | })); 51 | }); 52 | } 53 | }) 54 | } 55 | 56 | /** 57 | * Runs the specified webpack configuration in parallel. 58 | * @param {String} configPath The path to the webpack.config.js 59 | * @param {Object} options 60 | * @param {Boolean} [options.watch=false] If `true`, Webpack will run in 61 | * `watch-mode`. 62 | * @param {Number} [options.maxCallsPerWorker=Infinity] The maximum amount of calls 63 | * per parallel worker 64 | * @param {Number} [options.maxConcurrentWorkers=require('os').cpus().length] The 65 | * maximum number of parallel workers 66 | * @param {Number} [options.maxConcurrentCallsPerWorker=10] The maximum number of 67 | * concurrent call per prallel worker 68 | * @param {Number} [options.maxConcurrentCalls=Infinity] The maximum number of 69 | * concurrent calls 70 | * @param {Number} [options.maxRetries=0] The maximum amount of retries 71 | * on build error 72 | * @param {Function} [callback] A callback to be invoked once the build has 73 | * been completed 74 | * @return {Promise} A Promise that is resolved once all builds have been 75 | * created 76 | */ 77 | function run(configPath, options, callback) { 78 | var config, 79 | argvBackup = process.argv, 80 | farmOptions = assign({}, options); 81 | options = options || {}; 82 | if(options.colors === undefined) { 83 | options.colors = chalk.supportsColor; 84 | } 85 | if(!options.argv) { 86 | options.argv = []; 87 | } 88 | options.argv.unshift(process.execPath, 'parallel-webpack'); 89 | try { 90 | process.argv = options.argv; 91 | config = loadConfigurationFile(configPath); 92 | process.argv = argvBackup; 93 | } catch(e) { 94 | process.argv = argvBackup; 95 | return Promise.reject(new Error( 96 | chalk.red('[WEBPACK]') + ' Could not load configuration file ' + chalk.underline(configPath) + "\n" 97 | + e 98 | )); 99 | } 100 | 101 | if (!validate(farmOptions)) { 102 | return Promise.reject(new Error( 103 | 'Options validation failed:\n' + 104 | validate.errors.map(function(error) { 105 | return 'Property: "options' + error.dataPath + '" ' + error.message; 106 | }).join('\n') 107 | )); 108 | } 109 | 110 | var workers = workerFarm(farmOptions, require.resolve('./src/webpackWorker')); 111 | 112 | var shutdownCallback = function() { 113 | if (notSilent(options)) { 114 | console.log(chalk.red('[WEBPACK]') + ' Forcefully shutting down'); 115 | } 116 | workerFarm.end(workers); 117 | }; 118 | 119 | function keepAliveAfterFinishCallback(cb){ 120 | if(options.keepAliveAfterFinish){ 121 | setTimeout(cb, options.keepAliveAfterFinish); 122 | } else { 123 | cb(); 124 | } 125 | } 126 | 127 | function finalCallback(){ 128 | workerFarm.end(workers); 129 | process.removeListener("SIGINT", shutdownCallback); 130 | } 131 | 132 | process.on('SIGINT', shutdownCallback); 133 | 134 | var startTime = Date.now(); 135 | var farmPromise = startFarm( 136 | config, 137 | configPath, 138 | options, 139 | Promise.promisify(workers), 140 | callback 141 | ).error(function(err) { 142 | if(notSilent(options)) { 143 | console.log('%s Build failed after %s seconds', chalk.red('[WEBPACK]'), chalk.blue((Date.now() - startTime) / 1000)); 144 | } 145 | return Promise.reject(err); 146 | }).then(function (results) { 147 | if(notSilent(options)) { 148 | console.log('%s Finished build after %s seconds', chalk.blue('[WEBPACK]'), chalk.blue((Date.now() - startTime) / 1000)); 149 | } 150 | results = results.filter(function(result) { 151 | return result; 152 | }); 153 | if(results.length) { 154 | return results; 155 | } 156 | }).finally(function() { 157 | keepAliveAfterFinishCallback(finalCallback); 158 | }); 159 | 160 | if (!options.watch) { 161 | farmPromise.asCallback(callback); 162 | } 163 | return farmPromise; 164 | } 165 | 166 | module.exports = { 167 | createVariants: require('./src/createVariants'), 168 | run: run 169 | }; 170 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | collectCoverageFrom : [ 4 | '**/*.js', 5 | '!**/node_modules/**', 6 | '!*jest.config.js', 7 | '!**/coverage/**' 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parallel-webpack", 3 | "version": "2.6.0", 4 | "description": "Builds multiple webpack configurations in parallel and allows you to easily create variants to those configurations.", 5 | "main": "index.js", 6 | "bin": { 7 | "parallel-webpack": "bin/run.js" 8 | }, 9 | "scripts": { 10 | "test": "jest --coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 11 | "test-only": "jest" 12 | }, 13 | "keywords": [ 14 | "webpack", 15 | "parallel" 16 | ], 17 | "author": "Patrick Gotthardt ", 18 | "license": "BSD-3-Clause", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/trivago/parallel-webpack" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/trivago/parallel-webpack/issues" 25 | }, 26 | "peerDependencies": { 27 | "webpack": "^1.12.9 || ^2.2.0 || ^3.x || ^4.x || ^5.x" 28 | }, 29 | "dependencies": { 30 | "ajv": "^4.9.2", 31 | "bluebird": "^3.0.6", 32 | "chalk": "^1.1.1", 33 | "interpret": "^1.0.1", 34 | "lodash.assign": "^4.0.8", 35 | "lodash.endswith": "^4.0.1", 36 | "lodash.flatten": "^4.2.0", 37 | "minimist": "^1.2.0", 38 | "node-ipc": "^9.1.0", 39 | "pluralize": "^1.2.1", 40 | "supports-color": "^3.1.2", 41 | "worker-farm": "^1.3.1" 42 | }, 43 | "devDependencies": { 44 | "babel-jest": "^20.0.3", 45 | "babel-preset-env": "^1.5.2", 46 | "coveralls": "^2.13.1", 47 | "jest": "^20.0.4", 48 | "webpack": "^3.4.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "maxCallsPerWorker": { "type": "number" }, 5 | "maxConcurrentWorkers": { "type": "number" }, 6 | "maxConcurrentCallsPerWorker": { "type": "number" }, 7 | "maxConcurrentCalls": { "type": "number" }, 8 | "maxCallTime": { "type": "number" }, 9 | "maxRetries": { "type": "number", "default": 0 }, 10 | "keep-alive-after-finish": { "type": "number" } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/createVariants.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`createVariants should generate configs with "baseConfig, variants, configCallback" as params 1`] = ` 4 | Array [ 5 | Object { 6 | "debug": true, 7 | "features": 0, 8 | "name": "bundle.rtl.dbg.0.js", 9 | "rtl": true, 10 | }, 11 | Object { 12 | "debug": true, 13 | "features": 1, 14 | "name": "bundle.rtl.dbg.1.js", 15 | "rtl": true, 16 | }, 17 | Object { 18 | "debug": true, 19 | "features": 2, 20 | "name": "bundle.rtl.dbg.2.js", 21 | "rtl": true, 22 | }, 23 | Object { 24 | "debug": false, 25 | "features": 0, 26 | "name": "bundle.rtl.no-debug.0.js", 27 | "rtl": true, 28 | }, 29 | Object { 30 | "debug": false, 31 | "features": 1, 32 | "name": "bundle.rtl.no-debug.1.js", 33 | "rtl": true, 34 | }, 35 | Object { 36 | "debug": false, 37 | "features": 2, 38 | "name": "bundle.rtl.no-debug.2.js", 39 | "rtl": true, 40 | }, 41 | Object { 42 | "debug": true, 43 | "features": 0, 44 | "name": "bundle.no-rtl.dbg.0.js", 45 | "rtl": false, 46 | }, 47 | Object { 48 | "debug": true, 49 | "features": 1, 50 | "name": "bundle.no-rtl.dbg.1.js", 51 | "rtl": false, 52 | }, 53 | Object { 54 | "debug": true, 55 | "features": 2, 56 | "name": "bundle.no-rtl.dbg.2.js", 57 | "rtl": false, 58 | }, 59 | Object { 60 | "debug": false, 61 | "features": 0, 62 | "name": "bundle.no-rtl.no-debug.0.js", 63 | "rtl": false, 64 | }, 65 | Object { 66 | "debug": false, 67 | "features": 1, 68 | "name": "bundle.no-rtl.no-debug.1.js", 69 | "rtl": false, 70 | }, 71 | Object { 72 | "debug": false, 73 | "features": 2, 74 | "name": "bundle.no-rtl.no-debug.2.js", 75 | "rtl": false, 76 | }, 77 | ] 78 | `; 79 | 80 | exports[`createVariants should generate configs with "variants" as params 1`] = ` 81 | Array [ 82 | Object { 83 | "debug": true, 84 | "features": 0, 85 | "rtl": true, 86 | }, 87 | Object { 88 | "debug": true, 89 | "features": 1, 90 | "rtl": true, 91 | }, 92 | Object { 93 | "debug": true, 94 | "features": 2, 95 | "rtl": true, 96 | }, 97 | Object { 98 | "debug": false, 99 | "features": 0, 100 | "rtl": true, 101 | }, 102 | Object { 103 | "debug": false, 104 | "features": 1, 105 | "rtl": true, 106 | }, 107 | Object { 108 | "debug": false, 109 | "features": 2, 110 | "rtl": true, 111 | }, 112 | Object { 113 | "debug": true, 114 | "features": 0, 115 | "rtl": false, 116 | }, 117 | Object { 118 | "debug": true, 119 | "features": 1, 120 | "rtl": false, 121 | }, 122 | Object { 123 | "debug": true, 124 | "features": 2, 125 | "rtl": false, 126 | }, 127 | Object { 128 | "debug": false, 129 | "features": 0, 130 | "rtl": false, 131 | }, 132 | Object { 133 | "debug": false, 134 | "features": 1, 135 | "rtl": false, 136 | }, 137 | Object { 138 | "debug": false, 139 | "features": 2, 140 | "rtl": false, 141 | }, 142 | ] 143 | `; 144 | 145 | exports[`createVariants should generate configs with "variants, configCallback" as params 1`] = ` 146 | Array [ 147 | Object { 148 | "debug": true, 149 | "features": 0, 150 | "name": "bundle.rtl.dbg.0.js", 151 | "rtl": true, 152 | }, 153 | Object { 154 | "debug": true, 155 | "features": 1, 156 | "name": "bundle.rtl.dbg.1.js", 157 | "rtl": true, 158 | }, 159 | Object { 160 | "debug": true, 161 | "features": 2, 162 | "name": "bundle.rtl.dbg.2.js", 163 | "rtl": true, 164 | }, 165 | Object { 166 | "debug": false, 167 | "features": 0, 168 | "name": "bundle.rtl.no-debug.0.js", 169 | "rtl": true, 170 | }, 171 | Object { 172 | "debug": false, 173 | "features": 1, 174 | "name": "bundle.rtl.no-debug.1.js", 175 | "rtl": true, 176 | }, 177 | Object { 178 | "debug": false, 179 | "features": 2, 180 | "name": "bundle.rtl.no-debug.2.js", 181 | "rtl": true, 182 | }, 183 | Object { 184 | "debug": true, 185 | "features": 0, 186 | "name": "bundle.no-rtl.dbg.0.js", 187 | "rtl": false, 188 | }, 189 | Object { 190 | "debug": true, 191 | "features": 1, 192 | "name": "bundle.no-rtl.dbg.1.js", 193 | "rtl": false, 194 | }, 195 | Object { 196 | "debug": true, 197 | "features": 2, 198 | "name": "bundle.no-rtl.dbg.2.js", 199 | "rtl": false, 200 | }, 201 | Object { 202 | "debug": false, 203 | "features": 0, 204 | "name": "bundle.no-rtl.no-debug.0.js", 205 | "rtl": false, 206 | }, 207 | Object { 208 | "debug": false, 209 | "features": 1, 210 | "name": "bundle.no-rtl.no-debug.1.js", 211 | "rtl": false, 212 | }, 213 | Object { 214 | "debug": false, 215 | "features": 2, 216 | "name": "bundle.no-rtl.no-debug.2.js", 217 | "rtl": false, 218 | }, 219 | ] 220 | `; 221 | 222 | exports[`createVariants should generate configs with baseConfig and variants 1`] = ` 223 | Array [ 224 | Object { 225 | "debug": true, 226 | "features": 0, 227 | "name": "bundle.base", 228 | "rtl": true, 229 | }, 230 | Object { 231 | "debug": true, 232 | "features": 1, 233 | "name": "bundle.base", 234 | "rtl": true, 235 | }, 236 | Object { 237 | "debug": true, 238 | "features": 2, 239 | "name": "bundle.base", 240 | "rtl": true, 241 | }, 242 | Object { 243 | "debug": false, 244 | "features": 0, 245 | "name": "bundle.base", 246 | "rtl": true, 247 | }, 248 | Object { 249 | "debug": false, 250 | "features": 1, 251 | "name": "bundle.base", 252 | "rtl": true, 253 | }, 254 | Object { 255 | "debug": false, 256 | "features": 2, 257 | "name": "bundle.base", 258 | "rtl": true, 259 | }, 260 | Object { 261 | "debug": true, 262 | "features": 0, 263 | "name": "bundle.base", 264 | "rtl": false, 265 | }, 266 | Object { 267 | "debug": true, 268 | "features": 1, 269 | "name": "bundle.base", 270 | "rtl": false, 271 | }, 272 | Object { 273 | "debug": true, 274 | "features": 2, 275 | "name": "bundle.base", 276 | "rtl": false, 277 | }, 278 | Object { 279 | "debug": false, 280 | "features": 0, 281 | "name": "bundle.base", 282 | "rtl": false, 283 | }, 284 | Object { 285 | "debug": false, 286 | "features": 1, 287 | "name": "bundle.base", 288 | "rtl": false, 289 | }, 290 | Object { 291 | "debug": false, 292 | "features": 2, 293 | "name": "bundle.base", 294 | "rtl": false, 295 | }, 296 | ] 297 | `; 298 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/webpackWorker.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`webpackWorker arguments options should call done with stats when there is stats object 1`] = ` 4 | Object { 5 | "message": "[WEBPACK] Errors building testApp 6 | module not found 7 | module not found 2", 8 | "stats": "{ 9 | \\"compilation\\": { 10 | \\"errors\\": [ 11 | { 12 | \\"message\\": \\"module not found\\" 13 | }, 14 | { 15 | \\"message\\": \\"module not found 2\\" 16 | } 17 | ] 18 | } 19 | }", 20 | } 21 | `; 22 | 23 | exports[`webpackWorker arguments options should log in watch mode instead of calling done callback 1`] = ` 24 | Array [ 25 | Array [ 26 | "%s Started %s %s", 27 | "[WEBPACK]", 28 | "watching", 29 | "testApp", 30 | ], 31 | Array [ 32 | "[WEBPACK] Errors building testApp 33 | module not found 34 | module not found 2", 35 | ], 36 | Array [ 37 | "%s Finished building %s within %s seconds", 38 | "[WEBPACK 12:05:54]", 39 | "testApp", 40 | "NaN", 41 | ], 42 | ] 43 | `; 44 | 45 | exports[`webpackWorker creator function finishedCallback should remove SIGINT and call done with stats 1`] = ` 46 | Array [ 47 | null, 48 | "\\"size: 321.kb\\"", 49 | ] 50 | `; 51 | 52 | exports[`webpackWorker creator function shutdownCallback should call done callback 1`] = ` 53 | Object { 54 | "message": "[WEBPACK] Forcefully shut down testApp", 55 | } 56 | `; 57 | 58 | exports[`webpackWorker creator function shutdownCallback should watcher.close and done in watch mode on SIGINT 1`] = ` 59 | Object { 60 | "message": "[WEBPACK] Forcefully shut down testApp", 61 | } 62 | `; 63 | 64 | exports[`webpackWorker module functions getOutputOptions should get stats options if they set 1`] = ` 65 | Array [ 66 | Array [ 67 | Object { 68 | "assetsSort": "name", 69 | "chunksSort": "size", 70 | "colors": true, 71 | "exclude": Array [ 72 | "file", 73 | ], 74 | "modulesSort": "name", 75 | }, 76 | ], 77 | ] 78 | `; 79 | -------------------------------------------------------------------------------- /src/__tests__/createVariants.spec.js: -------------------------------------------------------------------------------- 1 | import createVariants from '../createVariants'; 2 | 3 | describe('createVariants', () => { 4 | function test(withBase, withCallback) { 5 | // these are stupid examples but enough to prove transformation works 6 | const baseConfig = { 7 | name: 'bundle.base' 8 | }; 9 | const variants = { 10 | rtl: [true, false], 11 | debug: [true, false], 12 | features: [0, 1, 2] 13 | }; 14 | const callback = (x) => { 15 | const ret = Object.assign({}, x); 16 | ret.name = `bundle.${x.rtl ? 'rtl' : 'no-rtl'}.${x.debug ? 'dbg': 'no-debug'}.${x.features}.js`; 17 | return ret; 18 | }; 19 | 20 | let result; 21 | 22 | if (withBase && withCallback) { 23 | result = createVariants(baseConfig, variants, callback); 24 | } else if (withBase && !withCallback) { 25 | result = createVariants(baseConfig, variants); 26 | } else if (!withBase && withCallback) { 27 | result = createVariants(variants, callback); 28 | } else { //!withBase && !withCallback 29 | result = createVariants(variants); 30 | } 31 | expect(result).toMatchSnapshot(); 32 | } 33 | it('should generate configs with "baseConfig, variants, configCallback" as params', () => { 34 | test(true, true); 35 | }); 36 | it('should generate configs with "variants, configCallback" as params', () => { 37 | test(false, true); 38 | }); 39 | it('should generate configs with "variants" as params', () => { 40 | test(false, false); 41 | }); 42 | 43 | it('should generate configs with baseConfig and variants', () => { 44 | test(true, false); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/__tests__/findConfigFile.spec.js: -------------------------------------------------------------------------------- 1 | jest.mock('fs'); 2 | 3 | import fs from 'fs'; 4 | 5 | import findConfigFile from '../findConfigFile'; 6 | 7 | const possibleExtensions = [ 8 | '', 9 | '.js', 10 | '.babel.js', 11 | '.babel.ts', 12 | '.buble.js', 13 | '.cirru', 14 | '.cjsx', 15 | '.co', 16 | '.coffee', 17 | '.coffee.md', 18 | '.eg', 19 | '.esm.js', 20 | '.iced', 21 | '.iced.md', 22 | '.jsx', 23 | '.litcoffee', 24 | '.liticed', 25 | '.ls', 26 | '.ts', 27 | '.tsx', 28 | '.wisp' 29 | ]; 30 | 31 | describe('findConfigFile', () => { 32 | it('should throw error when config file not exists', () => { 33 | fs.accessSync.mockImplementation(() => {throw new Error()}); 34 | 35 | expect(() => { 36 | findConfigFile('/path/to/file'); 37 | }).toThrow('File does not exist'); 38 | }); 39 | 40 | it('should try for all of possible file extensions', () => { 41 | fs.accessSync.mockImplementation(() => {throw new Error()}); 42 | 43 | expect(() => { 44 | findConfigFile('/path/to/file'); 45 | }).toThrow('File does not exist'); 46 | 47 | possibleExtensions.forEach((ext, index) => { 48 | expect(fs.accessSync.mock.calls[index][0]).toBe('/path/to/file' + ext); 49 | }); 50 | 51 | }); 52 | it('should use statSync when accessSync is not available', () => { 53 | fs.accessSync = null; 54 | fs.statSync = jest.fn(() => { isFile: jest.fn().mockReturnValue(false)}) 55 | 56 | expect(() => { 57 | findConfigFile('/path/to/file') 58 | }).toThrow(); 59 | 60 | expect(fs.statSync).toHaveBeenCalledTimes(possibleExtensions.length); 61 | }); 62 | 63 | it('should return file from accessSync', () => { 64 | fs.accessSync = jest.fn(); 65 | 66 | expect(findConfigFile('/path/to/file')).toBe('/path/to/file'); 67 | }); 68 | 69 | it('should return file from statSync', () => { 70 | fs.accessSync = null; 71 | fs.statSync = jest.fn(() => ({ isFile: jest.fn().mockReturnValue(true)})); 72 | 73 | expect(findConfigFile('/path/to/file')).toBe('/path/to/file'); 74 | }) 75 | }); 76 | -------------------------------------------------------------------------------- /src/__tests__/loadConfigurationFile.spec.js: -------------------------------------------------------------------------------- 1 | jest.mock('configPath', () => ({test: 'config'}), {virtual: true}); 2 | 3 | const module = require('../loadConfigurationFile'); 4 | const loadConfigurationFile = module.default; 5 | const getMatchingLoader = module.getMatchingLoader; 6 | const availableExtensions = module.availableExtensions; 7 | 8 | import { underline } from 'chalk'; 9 | import { jsVariants } from 'interpret'; 10 | 11 | describe('loadConfigurationFile module', () => { 12 | it('should require configPath when there is no matching loaders',() => { 13 | expect(loadConfigurationFile('configPath')).toEqual({ test: 'config'}); 14 | }); 15 | 16 | it('should sort the extensions', () => { 17 | let unsortedVariants = Object.keys(jsVariants); 18 | let sortedVariants = [ 19 | '.babel.js', 20 | '.babel.ts', 21 | '.buble.js', 22 | '.coffee.md', 23 | '.babel.js', 24 | '.iced.md', 25 | '.js', 26 | '.co', 27 | '.coffee', 28 | '.wisp', 29 | '.cirru', 30 | '.iced', 31 | '.cjsx', 32 | '.jsx', 33 | '.litcoffee', 34 | '.liticed', 35 | '.ls', 36 | '.ts', 37 | '.tsx', 38 | '.eg']; 39 | // based on the test, we only care if the array is sorted based on the compare function 40 | // the result of the sort function seems to be different on node 6 and 11 but the expected 41 | // values are sorted somewhat correctly, cause we only care if they have similar string 42 | // structure as .babel.js has. 43 | // therefore we get the split length value and compare those two 44 | 45 | expect(sortedVariants.map(val => val.split(/\./).length)).toEqual(availableExtensions.map(val => val.split(/\./).length)); 46 | expect(unsortedVariants).not.toEqual(availableExtensions); 47 | }); 48 | 49 | describe("getMatchingLoader", () => { 50 | it('should get correct matcher', () => { 51 | availableExtensions.forEach((extension) => { 52 | let expectedLoader = getMatchingLoader(`configpath.${extension}`); 53 | expect(expectedLoader).toEqual(jsVariants[extension]); 54 | }); 55 | }); 56 | 57 | it('should return null if cannot find loader', () => { 58 | let expectedLoader = getMatchingLoader(`configpath.unknownextension`); 59 | expect(expectedLoader).toEqual(null); 60 | }); 61 | }); 62 | 63 | describe('loadConfigurationFile function', () => { 64 | function loadertest(loaderType) { 65 | jest.doMock('testloader', () => 'loaded', { virtual: true }); 66 | jest.doMock('test.test', () => `testModuleFrom${loaderType}`, { virtual: true }); 67 | if (loaderType === 'Iteration') { 68 | jest.doMock('stopIterating', () => fail('this should not happen'), { virtual: true }); 69 | } 70 | jest.resetModules(); 71 | 72 | const mockLoaderAsString = () => 'testloader'; 73 | const mockLoaderAsObject = () => ({ 74 | module: 'testloader', 75 | register: s => expect(s).toEqual('loaded'), 76 | }); 77 | const mockLoaderAsArray = () => ['testloader']; 78 | const mockLoaderIteration = () => ['fail', {module: 'fail'}, 'testloader', 'stopIterating']; 79 | 80 | const mockGetLoader = loaderType === 'String' 81 | ? mockLoaderAsString 82 | : loaderType === 'Object' 83 | ? mockLoaderAsObject 84 | : loaderType === 'Array' 85 | ? mockLoaderAsArray 86 | : mockLoaderIteration; 87 | 88 | const result = loadConfigurationFile('test.test', mockGetLoader); 89 | expect(result).toBe(`testModuleFrom${loaderType}`); 90 | } 91 | it('should require loaders as string', () => { 92 | loadertest('String'); 93 | }); 94 | 95 | it('should require loaders as object', () => { 96 | loadertest('Object'); 97 | }); 98 | 99 | it('should require loaders as array', () => { 100 | loadertest('Array'); 101 | }); 102 | 103 | it('should iterate and stop when it is successfully loads a module', () => { 104 | loadertest('Iteration'); 105 | }); 106 | it('should throw when it cannot find module', () => { 107 | expect(() => { 108 | loadConfigurationFile('/does/not/exist.co') 109 | }).toThrow(`Could not load required module loading for ${underline('/does/not/exist.co')}`) 110 | }); 111 | 112 | // not a string, object or array. Should never happen anyways 113 | it('should throw when the loader is not possible to evaluate', () => { 114 | expect(() => { 115 | loadConfigurationFile('/does/not/exist.co', () => [null]); 116 | }).toThrow(`Could not load required module loading for ${underline('/does/not/exist.co')}`) 117 | }); 118 | 119 | it('should pass `env` when config exports a function', () => { 120 | jest.doMock('test.js', () => (env) => env, { virtual: true }); 121 | const oldArgv = process.argv; 122 | process.argv = ['--env.foo', 'bar', '--env.bar', '--foo', '--env.baz="foo bar"']; 123 | const result = loadConfigurationFile('test.js', () => null); 124 | expect(result).toEqual({foo: 'bar', bar: true, baz: '\"foo bar\"'}); 125 | process.argv = oldArgv; 126 | }); 127 | 128 | }); 129 | 130 | 131 | }); 132 | -------------------------------------------------------------------------------- /src/__tests__/watchDoneHandler.spec.js: -------------------------------------------------------------------------------- 1 | let watchDoneHandler; 2 | let ipc; 3 | 4 | describe('watchDoneHandler', () => { 5 | beforeEach(() => { 6 | watchDoneHandler = require('../watchDoneHandler'); 7 | ipc = { 8 | server: { 9 | stop: jest.fn() 10 | } 11 | } 12 | }); 13 | 14 | describe('watchDoneHandler', () => { 15 | it('should remove index 1 from config', () => { 16 | let configIndices = [0, 1]; 17 | let callback = jest.fn(); 18 | watchDoneHandler(null, null, configIndices, 1); 19 | expect(configIndices).toEqual([0]); 20 | }); 21 | 22 | it('should stop server and invoke callback', () => { 23 | let configIndices = [0, 1]; 24 | let callback = jest.fn(); 25 | watchDoneHandler(callback, ipc, configIndices, 0); 26 | watchDoneHandler(callback, ipc, configIndices, 1); 27 | expect(configIndices).toEqual([]); 28 | expect(callback).toHaveBeenCalled(); 29 | expect(ipc.server.stop).toHaveBeenCalled(); 30 | }); 31 | 32 | it('should stop server', () => { 33 | let configIndices = [0, 1]; 34 | let callback = jest.fn(); 35 | watchDoneHandler(null, ipc, configIndices, 0); 36 | watchDoneHandler(null, ipc, configIndices, 1); 37 | expect(configIndices).toEqual([]); 38 | expect(ipc.server.stop).toHaveBeenCalled(); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/__tests__/watchModeIPC.spec.js: -------------------------------------------------------------------------------- 1 | let webpackWorker; 2 | let nodeIpc; 3 | let serveSpy; 4 | jest.mock('node-ipc'); 5 | 6 | describe('watchModeIPC', () => { 7 | beforeEach(() => { 8 | webpackWorker = require('../watchModeIPC.js'); 9 | nodeIpc = require('node-ipc'); 10 | nodeIpc.server = { 11 | start: jest.fn() 12 | }; 13 | nodeIpc.of = { 14 | webpack: { 15 | emit: jest.fn() 16 | } 17 | }; 18 | 19 | nodeIpc.connectTo = function(serverName, onConnect) { 20 | onConnect(); 21 | } 22 | }); 23 | 24 | describe('startWatchIPCServer', () => { 25 | it('should start ipc socket server', () => { 26 | webpackWorker.startWatchIPCServer(); 27 | expect(nodeIpc.config.id).toEqual('webpack'); 28 | expect(nodeIpc.config.retry).toEqual(3); 29 | expect(nodeIpc.config.silent).toEqual(true); 30 | expect(nodeIpc.serve).toHaveBeenCalled(); 31 | expect(nodeIpc.server.start).toHaveBeenCalled(); 32 | }); 33 | }); 34 | 35 | describe('notifyIPCWatchCompileDone', () => { 36 | it('should call connectTo', () => { 37 | webpackWorker.notifyIPCWatchCompileDone(0); 38 | expect(nodeIpc.config.id).toEqual('webpack0'); 39 | expect(nodeIpc.config.stopRetrying).toEqual(3); 40 | expect(nodeIpc.config.silent).toEqual(true); 41 | expect(nodeIpc.of.webpack.emit).toHaveBeenCalledWith('done', 0); 42 | }) 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/__tests__/webpackWorker.spec.js: -------------------------------------------------------------------------------- 1 | const run = jest.fn(); 2 | const close = jest.fn(); 3 | const watch = jest.fn().mockReturnValue({ close }); 4 | const compiler = jest.fn().mockReturnValue({ run, watch }); 5 | 6 | jest.setMock('webpack', compiler); // try to get rid of this. 7 | jest.mock('../watchModeIPC'); 8 | jest.mock('webpack/lib/Stats'); 9 | 10 | let webpackWorker; 11 | let promiseMock; 12 | let webpackMock; 13 | let webpackStatsMock; 14 | let notifyIPCWatchCompileDone; 15 | 16 | describe('webpackWorker', () => { 17 | beforeEach(() => { 18 | promiseMock = require('bluebird'); 19 | webpackMock = require('webpack'); 20 | webpackStatsMock = require('webpack/lib/Stats'); 21 | webpackWorker = require('../webpackWorker.js'); 22 | notifyIPCWatchCompileDone = require('../watchModeIPC').notifyIPCWatchCompileDone; 23 | jest.doMock('testConfig', () => ({ webpack: 'config' }), { virtual: true }); 24 | jest.resetModules(); 25 | jest.clearAllMocks(); 26 | process.removeAllListeners(); 27 | }); 28 | 29 | describe('arguments', () => { 30 | describe('options', () => { 31 | 32 | let _argv; 33 | beforeEach(() => { 34 | _argv = process.argv; 35 | }); 36 | afterEach(() => { 37 | process.argv = _argv; 38 | }); 39 | 40 | const optionsTest = (options) => { 41 | const finishStats = { 42 | compilation: {}, 43 | startTime: 1500034498, 44 | endTime: 1500054500, 45 | }; 46 | const doneCallback = jest.fn(); 47 | jest.spyOn(console, 'log').mockImplementation(() => {}); 48 | 49 | 50 | webpackWorker('testConfig', options , 0, 1, doneCallback); 51 | const thenCb = promiseMock.then.mock.calls[0][0]; 52 | thenCb({ webpack: 'config', name: 'testApp' }); 53 | const finishedCallback = run.mock.calls[0][0]; 54 | finishedCallback(null, finishStats); 55 | 56 | expect(doneCallback).toHaveBeenCalled(); 57 | if (options.json) { 58 | expect(console.log).not.toHaveBeenCalled(); 59 | } else { 60 | expect(console.log).toHaveBeenCalledTimes(2); 61 | expect(console.log).toHaveBeenLastCalledWith( 62 | '%s Finished building %s within %s seconds', 63 | '[WEBPACK]', 64 | 'testApp', 65 | '20.002' 66 | ); 67 | } 68 | 69 | if (options.argv) { 70 | expect(process.argv).toEqual(options.argv); 71 | } 72 | }; 73 | 74 | 75 | it('should report json only when json: true', () => { 76 | optionsTest({ json: true }); 77 | }); 78 | 79 | it('should report to console as well only when json: false', () => { 80 | optionsTest({ json: false }); 81 | }); 82 | 83 | it('should set process arguments to the passed ones', () => { 84 | optionsTest({ json:true, argv: ['--watch', '--bail']}); 85 | }); 86 | 87 | it('should call done with stats when there is stats object', () => { 88 | const doneCallback = jest.fn(); 89 | const statsObj = { 90 | compilation: { 91 | errors: [ 92 | { message: 'module not found' }, 93 | { message: 'module not found 2' }, 94 | ] 95 | }, 96 | toJson: jest.fn().mockReturnThis() 97 | }; 98 | webpackWorker('testConfig', {}, 0, 1, doneCallback); 99 | const thenCb = promiseMock.then.mock.calls[0][0]; 100 | thenCb({ webpack: 'config', name: 'testApp' }); 101 | let finishedCallback = run.mock.calls[0][0]; 102 | 103 | expect(process.listenerCount('SIGINT')).toBe(1); 104 | finishedCallback(null, statsObj); 105 | 106 | expect(process.listenerCount('SIGINT')).toBe(0); 107 | expect(doneCallback).toHaveBeenCalled(); 108 | expect(doneCallback.mock.calls[0][0]).toMatchSnapshot(); 109 | 110 | }); 111 | 112 | it('should log in watch mode instead of calling done callback', () => { 113 | jest.useFakeTimers(); 114 | const _temp = global.Date; 115 | global.Date = jest.fn(() => ( 116 | { 117 | toTimeString: jest.fn().mockReturnValue({ 118 | split: jest.fn().mockReturnValue(["12:05:54"]) 119 | }) 120 | } 121 | )); 122 | jest.spyOn(console, 'log'); 123 | const doneCallback = jest.fn(); 124 | const statsObj = { 125 | compilation: { 126 | errors: [ 127 | { message: 'module not found' }, 128 | { message: 'module not found 2' }, 129 | ] 130 | }, 131 | toJson: jest.fn().mockReturnThis() 132 | }; 133 | webpackWorker('testConfig', { watch: true }, 0, 1, doneCallback); 134 | const thenCb = promiseMock.then.mock.calls[0][0]; 135 | thenCb({ webpack: 'config', name: 'testApp' }); 136 | let finishedCallback = watch.mock.calls[0][1]; 137 | 138 | expect(process.listenerCount('SIGINT')).toBe(1); 139 | finishedCallback(null, statsObj); 140 | 141 | expect(process.listenerCount('SIGINT')).toBe(1); 142 | expect(doneCallback).not.toHaveBeenCalled(); 143 | expect(console.log.mock.calls).toMatchSnapshot(); 144 | expect(notifyIPCWatchCompileDone).toHaveBeenCalledWith(0); 145 | process.removeAllListeners('SIGINT'); 146 | jest.useRealTimers(); 147 | global.Date = _temp; 148 | }); 149 | 150 | }); 151 | 152 | describe('multi config options', () => { 153 | const multiConfigTest = options => { 154 | const errorMessage = '[WEBPACK] There is a difference between the amount of the provided configs. Maybe you where expecting command line arguments to be passed to your webpack.config.js. If so, you\'ll need to separate them with a -- from the parallel-webpack options.' 155 | jest.doMock('multiTestConfig', () => ( [{ fail: true}, { webpack: 'config'}]), { virtual: true }); 156 | jest.doMock('testConfig', () => ({ webpack: 'config' }), { virtual: true }); 157 | jest.spyOn(console, 'error').mockImplementation(() => {}); 158 | 159 | webpackWorker( 160 | options.multi ? 'multiTestConfig' : 'testConfig', 161 | { json: true }, 162 | options.configIndex, 163 | options.expectedConfigs, 164 | jest.fn() 165 | ); 166 | const allConfigs = promiseMock.resolve.mock.calls[0][0]; 167 | const thenCb = promiseMock.then.mock.calls[0][0]; 168 | if (options.multi && options.expectedConfigs < 3) { 169 | const targetConfig = allConfigs[options.configIndex]; 170 | thenCb(allConfigs) 171 | expect(compiler.mock.calls[0][0]).toEqual(targetConfig); 172 | } else { 173 | expect(() => thenCb(allConfigs)).toThrow(errorMessage) 174 | expect(console.error).toHaveBeenCalled(); 175 | } 176 | } 177 | 178 | it('should select the correct indexed one if configs are array', () => { 179 | multiConfigTest({ multi: true, configIndex: 1, expectedConfigs: 2 }); 180 | }); 181 | 182 | it('should fail if expectedConfigLength > 1 in case of single config', () => { 183 | multiConfigTest({ multi: false, configIndex: 1, expectedConfigs: 2 }); 184 | }); 185 | 186 | it('should fail if expectedConfigLength dont match with config.length', () => { 187 | multiConfigTest({ multi: true, configIndex: 1, expectedConfigs: 3 }); 188 | }); 189 | 190 | it('should be able to handle config function return promise of array of config object', () => { 191 | const originalConfigs = [{ webpack: 'config' }, { webpack: 'config2' }]; 192 | jest.doMock('promiseReturnConfigArray', () => Promise.resolve(originalConfigs), { virtual: true }); 193 | const configIndex = 1 194 | const expectedConfigLength = 2 195 | webpackWorker( 196 | 'promiseReturnConfigArray', 197 | { json: true }, 198 | configIndex, 199 | expectedConfigLength, 200 | jest.fn() 201 | ) 202 | const thenCb = promiseMock.then.mock.calls[0][0]; 203 | const targetConfig = originalConfigs[configIndex]; 204 | thenCb(originalConfigs); 205 | expect(compiler.mock.calls[0][0]).toEqual(targetConfig); 206 | }) 207 | }) 208 | }); 209 | 210 | describe('module functions', () => { 211 | describe('getAppName', () => { 212 | const getAppNameTest = (options, appName) => { 213 | const doneCallback = jest.fn(); 214 | jest.spyOn(console, 'log'); 215 | webpackWorker('testConfig', { json: false }, 0, 1, doneCallback); 216 | 217 | const thenCb = promiseMock.then.mock.calls[0][0]; 218 | thenCb(Object.assign({ webpack: 'config' }, options)); 219 | 220 | let finishedCallback = run.mock.calls[0][0]; 221 | finishedCallback(null, { compilation: {}}); 222 | 223 | expect(console.log.mock.calls[1][2]).toBe(appName); 224 | }; 225 | 226 | it('should get the name from config.name', () => { 227 | getAppNameTest({ name: 'testApp' }, 'testApp'); 228 | }); 229 | 230 | it('should get the name from output.filename', () => { 231 | getAppNameTest( 232 | { 233 | output: { 234 | filename: 'test.app.js' 235 | } 236 | }, 237 | 'test.app.js' 238 | ); 239 | }); 240 | 241 | it('should replace "[name]" pattern if there is one entry point', () => { 242 | getAppNameTest( 243 | { 244 | output: { 245 | filename: 'bundle.[name].js', 246 | }, 247 | entry: { 248 | testApp: 'filepath', 249 | } 250 | }, 251 | 'bundle.testApp.js' 252 | ); 253 | }); 254 | 255 | it('should replace all matches of "[name]" pattern if there is one entry point', () => { 256 | getAppNameTest( 257 | { 258 | output: { 259 | filename: 'bundle/[name]/[name].js', 260 | }, 261 | entry: { 262 | testApp: 'filepath', 263 | } 264 | }, 265 | 'bundle/testApp/testApp.js' 266 | ); 267 | }); 268 | }); 269 | 270 | describe('getOutputOptions', () => { 271 | it('should get stats options if they set', () => { 272 | const statsObj = { 273 | compilation: { 274 | }, 275 | toString: jest.fn().mockReturnValue('size: 321.kb'), 276 | toJson: jest.fn().mockReturnValue('size: 321.kb') 277 | }; 278 | jest.spyOn(console, 'log'); 279 | const doneCallback = jest.fn(); 280 | webpackWorker('testConfig', { 281 | stats: true, 282 | modulesSort: 'name', 283 | chunksSort: 'size', 284 | assetsSort: 'name', 285 | exclude: ['file'], 286 | colors: true 287 | }, 0, 1, doneCallback); 288 | 289 | expect(promiseMock.resolve.mock.calls[0][0]).toEqual({ webpack: 'config' }); 290 | const thenCb = promiseMock.then.mock.calls[0][0]; 291 | thenCb({ webpack: 'config', name: 'testApp' }); 292 | 293 | 294 | let finishedCallback = run.mock.calls[0][0]; 295 | finishedCallback(null, statsObj); 296 | expect(statsObj.toString.mock.calls).toMatchSnapshot(); 297 | }); 298 | 299 | it('should translate stats string to object', () => { 300 | jest.spyOn(console, 'log'); 301 | let presetToOptions = jest.spyOn(webpackStatsMock, 'presetToOptions'); 302 | 303 | const doneCallback = jest.fn(); 304 | 305 | webpackWorker('testConfig', { 306 | stats: true, 307 | modulesSort: 'name', 308 | chunksSort: 'size', 309 | assetsSort: 'name', 310 | exclude: ['file'], 311 | colors: true 312 | }, 0, 1, doneCallback); 313 | 314 | expect(promiseMock.resolve.mock.calls[0][0]).toEqual({ webpack: 'config' }); 315 | const thenCb = promiseMock.then.mock.calls[0][0]; 316 | thenCb({ webpack: 'config', name: 'testApp', 'stats': 'verbose' }); 317 | 318 | expect(presetToOptions).toHaveBeenCalled(); 319 | }) 320 | }); 321 | }); 322 | 323 | describe('creator function', () => { 324 | describe('shutdownCallback', () => { 325 | 326 | let _exit; 327 | beforeEach(() => { 328 | _exit = process.exit; 329 | }); 330 | afterEach(() => { 331 | process.exit = _exit; 332 | process.removeAllListeners('SIGINT'); 333 | }); 334 | 335 | const shutdownCallbackTest = options => { 336 | const doneCallback = jest.fn(); 337 | const statsObj = { 338 | compilation: { 339 | errors: [ 340 | { message: 'module not found' }, 341 | { message: 'module not found 2' }, 342 | ] 343 | }, 344 | toJson: jest.fn().mockReturnThis() 345 | }; 346 | process.exit = jest.fn(); 347 | 348 | webpackWorker('testConfig', options , 0, 1, doneCallback); 349 | const thenCb = promiseMock.then.mock.calls[0][0]; 350 | thenCb({ webpack: 'config', name: 'testApp' }); 351 | const shutdownCallback = process.listeners('SIGINT')[0]; 352 | shutdownCallback(); 353 | 354 | 355 | if (options.watch) { 356 | expect(close.mock.calls[0][0]).toBe(doneCallback); 357 | expect(doneCallback.mock.calls[0][0]).toMatchSnapshot(); 358 | } else { 359 | expect(close).not.toHaveBeenCalled(); 360 | expect(doneCallback.mock.calls[0][0]).toMatchSnapshot(); 361 | } 362 | }; 363 | 364 | it('should watcher.close and done in watch mode on SIGINT', () => { 365 | shutdownCallbackTest({ watch: true }); 366 | }); 367 | 368 | it('should call done callback', () => { 369 | shutdownCallbackTest({ watch: false }); 370 | }); 371 | }); 372 | 373 | describe('finishedCallback', () => { 374 | const finishCbTest = options => { 375 | const statsObj = { 376 | compilation: { 377 | }, 378 | toString: jest.fn().mockReturnValue('size: 321.kb'), 379 | toJson: jest.fn().mockReturnValue('size: 321.kb') 380 | }; 381 | const doneCallback = jest.fn(); 382 | jest.spyOn(console, 'log'); 383 | jest.spyOn(console, 'error'); 384 | 385 | webpackWorker('testConfig', options.worker, 0, 1, doneCallback); 386 | expect(promiseMock.resolve.mock.calls[0][0]).toEqual({ webpack: 'config' }); 387 | 388 | const thenCb = promiseMock.then.mock.calls[0][0]; 389 | thenCb({ webpack: 'config', name: 'testApp' }); 390 | expect(process.listenerCount('SIGINT')).toBe(1); 391 | 392 | let finishedCallback = run.mock.calls[0][0]; 393 | 394 | 395 | if (options.isError) { 396 | finishedCallback({ error: 'Exception' }, { compilation: {}}); 397 | expect(console.error).toHaveBeenCalledTimes(2); 398 | expect(doneCallback.mock.calls[0]).toEqual([{error: 'Exception'}]); 399 | } else if (options.isStats) { 400 | finishedCallback(null, statsObj); 401 | expect(doneCallback.mock.calls[0]).toMatchSnapshot(); 402 | expect(console.log.mock.calls[1]).toEqual(['size: 321.kb']); 403 | } else { // success 404 | finishedCallback(null, statsObj); 405 | expect(doneCallback.mock.calls[0]).toEqual([null, '']); 406 | } 407 | expect(process.listenerCount('SIGINT')).toBe(0); 408 | 409 | }; 410 | 411 | it('should spit out error, remove SIGINT and close when an exception happen', () => { 412 | finishCbTest({ 413 | worker: { json: true }, 414 | isError: true 415 | }); 416 | }); 417 | 418 | it('should remove SIGINT and call done with stats', () => { 419 | finishCbTest({ 420 | worker: { stats: true }, 421 | isError: false, 422 | isStats: true 423 | }); 424 | }); 425 | 426 | it('should set the promise and call done on success', () => { 427 | finishCbTest({ 428 | worker: { json: true }, 429 | isError: false, 430 | isStats: false 431 | }); 432 | }); 433 | }); 434 | }); 435 | }); 436 | -------------------------------------------------------------------------------- /src/createVariants.js: -------------------------------------------------------------------------------- 1 | var assign = require('lodash.assign'); 2 | var flatten = require('lodash.flatten'); 3 | 4 | /** 5 | * Creates configuration variants. 6 | * 7 | * @param {Object} [baseConfig={}] The base configuration 8 | * @param {Object} variants The variants 9 | * @param {Function} [configCallback] A callback executed for each configuration to 10 | * transform the variant into a webpack configuration 11 | * @returns {*|Array} 12 | */ 13 | module.exports = function createVariants(baseConfig, variants, configCallback) { 14 | if(arguments.length < 3) { 15 | if(arguments.length === 2) { 16 | if(typeof variants === 'function') { 17 | // createVariants(variants: Object, configCallback: Function) 18 | configCallback = variants; 19 | variants = baseConfig; 20 | baseConfig = {}; 21 | } 22 | // createVariants(baseConfig: Object, variants: Object) 23 | // => don't do anything 24 | } else { 25 | // createVariants(variants: Object) 26 | variants = baseConfig; 27 | baseConfig = {}; 28 | } 29 | } 30 | 31 | // Okay, so this looks a little bit messy but it really does make some sense. 32 | // Essentially, for each base configuration, we want to create every 33 | // possible combination of the configuration variants specified above. 34 | var transforms = Object.keys(variants).map(function(key) { 35 | return function(config) { 36 | return variants[key].map(function(value) { 37 | var result = assign({}, config); 38 | result[key] = value; 39 | return result; 40 | }); 41 | }; 42 | }), 43 | configs = transforms.reduce(function(options, transform) { 44 | return flatten(options.map(transform)); 45 | }, [baseConfig]); 46 | 47 | 48 | return configCallback && configs.map(configCallback) || configs; 49 | }; 50 | -------------------------------------------------------------------------------- /src/findConfigFile.js: -------------------------------------------------------------------------------- 1 | var potentialExtensions = [''].concat(Object.keys(require('interpret').jsVariants)), 2 | fs = require('fs'); 3 | 4 | function existsWithAccess(path) { 5 | try { 6 | fs.accessSync(path); 7 | return true; 8 | } catch(ignore) { 9 | return false; 10 | } 11 | } 12 | 13 | function exists(path) { 14 | if(fs.accessSync) { 15 | return existsWithAccess(path); 16 | } else { 17 | try { 18 | var stats = fs.statSync(path); 19 | return stats.isFile(); 20 | } catch(ignore) { 21 | return false; 22 | } 23 | } 24 | } 25 | 26 | module.exports = function(configPath) { 27 | for(var i = 0, len = potentialExtensions.length; i < len; i++) { 28 | var ext = potentialExtensions[i]; 29 | if(exists(configPath + ext)) { 30 | // file exists, use that extension 31 | return configPath + ext; 32 | } 33 | } 34 | 35 | throw new Error('File does not exist'); 36 | } 37 | -------------------------------------------------------------------------------- /src/loadConfigurationFile.js: -------------------------------------------------------------------------------- 1 | var jsVars = require('interpret').jsVariants, 2 | endsWith = require('lodash.endswith'), 3 | availableExts = Object.keys(jsVars), 4 | chalk = require('chalk'); 5 | 6 | // sort extensions to ensure that .babel.js and 7 | // similar ones are always matched before .js 8 | availableExts.sort(function(a, b) { 9 | var res = -(a.split(/\./).length - b.split(/\./).length); 10 | // all things being equal, we need to 11 | // prioritize .js as it is most likely 12 | if(res === 0) { 13 | if(a === '.js') { 14 | return -1; 15 | } 16 | if(b === '.js') { 17 | return 1; 18 | } 19 | return 0; 20 | } 21 | return res; 22 | }); 23 | 24 | function getMatchingLoaderFn(configPath, extensions, variants) { 25 | let availableExtensions = extensions || availableExts; 26 | let jsVariants = variants || jsVars; 27 | for(var i = 0, len = availableExtensions.length; i < len; i++) { 28 | var ext = availableExtensions[i]; 29 | if(endsWith(configPath, ext)) { 30 | return jsVariants[ext]; 31 | } 32 | } 33 | return null; 34 | } 35 | 36 | function callConfigFunction(fn) { 37 | return fn(require('minimist')(process.argv, { '--': true }).env || {}); 38 | } 39 | 40 | function getConfig(configPath) { 41 | var configModule = require(configPath); 42 | var configDefault = configModule && configModule.__esModule ? configModule.default : configModule; 43 | return typeof configDefault === 'function' ? callConfigFunction(configDefault) : configDefault; 44 | } 45 | 46 | module.exports = { 47 | default: function(configPath, matchingLoader) { 48 | let getMatchingLoader = matchingLoader || getMatchingLoaderFn; 49 | 50 | var mod = getMatchingLoader(configPath); 51 | if(mod) { 52 | var mods = Array.isArray(mod) ? mod : [mod], 53 | installed = false; 54 | 55 | for(var i = 0, len = mods.length; i < len; i++) { 56 | mod = mods[i]; 57 | if(typeof mod === 'string') { 58 | try { 59 | require(mod); 60 | installed = true; 61 | } catch(ignored) {} 62 | } else if(typeof mod === 'object') { 63 | try { 64 | var s = require(mod.module); 65 | mod.register(s); 66 | installed = true; 67 | } catch(ignored) {} 68 | } 69 | 70 | if(installed) { 71 | break; 72 | } 73 | } 74 | 75 | if(!installed) { 76 | throw new Error('Could not load required module loading for ' + chalk.underline(configPath)); 77 | } 78 | } 79 | return getConfig(configPath); 80 | }, 81 | getMatchingLoader: getMatchingLoaderFn, 82 | availableExtensions: availableExts, 83 | }; 84 | -------------------------------------------------------------------------------- /src/watchDoneHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handler for done event 3 | * @param data - the configuration index of the completed webpack run 4 | */ 5 | module.exports = function watchDoneHandler(callback, ipc, configIndices, data) { 6 | // Once every configuration has completed once, stop the server and invoke the callback 7 | configIndices.splice(configIndices.indexOf(data), 1); 8 | if (!configIndices.length) { 9 | ipc.server.stop(); 10 | 11 | if (typeof callback === 'function') { 12 | callback(); 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/watchModeIPC.js: -------------------------------------------------------------------------------- 1 | var ipc = require('node-ipc'), 2 | serverName = 'webpack', 3 | watchDoneHandler = require('./watchDoneHandler'); 4 | 5 | module.exports = { 6 | 7 | /** 8 | * Start IPC server and listens for 'done' message from child processes 9 | * @param {any} callback - callback invoked once 'done' has been emitted by each confugration 10 | * @param {any} configIndices - array indices of configuration 11 | */ 12 | startWatchIPCServer: function startWatchIPCServer(callback, configIndices) { 13 | ipc.config.id = serverName; 14 | ipc.config.retry = 3; 15 | ipc.config.silent = true; 16 | 17 | ipc.serve( 18 | function() { 19 | ipc.server.on( 20 | 'done', 21 | watchDoneHandler.bind(this, callback, ipc, configIndices) 22 | ); 23 | } 24 | ); 25 | ipc.server.start(); 26 | }, 27 | 28 | /* 29 | * Notifies parent process that a complete compile has occured in watch mode 30 | * @param {any} index 31 | */ 32 | notifyIPCWatchCompileDone: function notifyIPCWatchCompileDone(index) { 33 | ipc.config.id = serverName + index; 34 | ipc.config.stopRetrying = 3; 35 | ipc.config.silent = true; 36 | 37 | ipc.connectTo( 38 | serverName, 39 | function() { 40 | ipc.of.webpack.emit('done', index); 41 | } 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/webpackWorker.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'), 2 | chalk = require('chalk'), 3 | loadConfigurationFile = require('./loadConfigurationFile').default, 4 | notifyIPCWatchCompileDone = require('./watchModeIPC').notifyIPCWatchCompileDone, 5 | presetToOptions = require('webpack/lib/Stats').presetToOptions; 6 | /** 7 | * Choose the most correct version of webpack, prefer locally installed version, 8 | * fallback to the own dependency if there's none. 9 | * @returns {*} 10 | */ 11 | function getWebpack() { 12 | try { 13 | return require(process.cwd() + '/node_modules/webpack'); 14 | } catch(e) { 15 | return require('webpack'); 16 | } 17 | } 18 | 19 | function getAppName(webpackConfig) { 20 | var appName = webpackConfig.name 21 | || webpackConfig.output && webpackConfig.output.filename 22 | || String(process.pid); 23 | if(~appName.indexOf('[name]') && typeof webpackConfig.entry === 'object') { 24 | var entryNames = Object.keys(webpackConfig.entry); 25 | if(entryNames.length === 1) { 26 | // we can only replace [name] with the entry point if there is only one entry point 27 | appName = appName.replace(/\[name]/g, entryNames[0]); 28 | } 29 | } 30 | return appName; 31 | } 32 | 33 | function getOutputOptions(webpackConfig, options) { 34 | var stats = webpackConfig.stats; 35 | // @see https://webpack.js.org/configuration/stats/ 36 | if (typeof stats === 'string') { 37 | stats = presetToOptions(stats); 38 | } 39 | var outputOptions = Object.create(stats || {}); 40 | if(typeof options.modulesSort !== 'undefined') { 41 | outputOptions.modulesSort = options.modulesSort; 42 | } 43 | if(typeof options.chunksSort !== 'undefined') { 44 | outputOptions.chunksSort = options.chunksSort; 45 | } 46 | if(typeof options.assetsSort !== 'undefined') { 47 | outputOptions.assetsSort = options.assetsSort; 48 | } 49 | if(typeof options.exclude !== 'undefined') { 50 | outputOptions.exclude = options.exclude; 51 | } 52 | if(typeof options.colors !== 'undefined') { 53 | outputOptions.colors = options.colors; 54 | } 55 | return outputOptions; 56 | } 57 | 58 | /** 59 | * Create a single webpack build using the specified configuration. 60 | * Calls the done callback once it has finished its work. 61 | * 62 | * @param {string} configuratorFileName The app configuration filename 63 | * @param {Object} options The build options 64 | * @param {boolean} options.watch If `true`, then the webpack watcher is being run; if `false`, runs only ones 65 | * @param {boolean} options.json If `true`, then the webpack watcher will only report the result as JSON but not produce any other output 66 | * @param {number} index The configuration index 67 | * @param {number} expectedConfigLength 68 | * @param {Function} done The callback that should be invoked once this worker has finished the build. 69 | */ 70 | module.exports = function(configuratorFileName, options, index, expectedConfigLength, done) { 71 | if(options.argv) { 72 | process.argv = options.argv; 73 | } 74 | chalk.enabled = options.colors; 75 | var config = loadConfigurationFile(configuratorFileName) 76 | 77 | Promise.resolve(config).then(function(config) { 78 | var watch = !!options.watch, 79 | silent = !!options.json; 80 | if(expectedConfigLength !== 1 && !Array.isArray(config) 81 | || (Array.isArray(config) && config.length !== expectedConfigLength)) { 82 | if(config.length !== expectedConfigLength) { 83 | var errorMessage = '[WEBPACK] There is a difference between the amount of the' 84 | + ' provided configs. Maybe you where expecting command line' 85 | + ' arguments to be passed to your webpack.config.js. If so,' 86 | + " you'll need to separate them with a -- from the parallel-webpack options."; 87 | console.error(errorMessage); 88 | throw Error(errorMessage); 89 | } 90 | } 91 | var webpackConfig; 92 | if(Array.isArray(config)) { 93 | webpackConfig = config[index]; 94 | } else { 95 | webpackConfig = config 96 | } 97 | 98 | var MSG_ERROR = chalk.red('[WEBPACK]'); 99 | var MSG_SUCCESS = chalk.blue('[WEBPACK]'); 100 | var MSG_APP = chalk.yellow(getAppName(webpackConfig)); 101 | 102 | var watcher; 103 | var webpack = getWebpack(); 104 | var hasCompletedOneCompile = false; 105 | var outputOptions = getOutputOptions(webpackConfig, options); 106 | var disconnected = false; 107 | 108 | if(!silent) { 109 | console.log('%s Started %s %s', MSG_SUCCESS, watch ? 'watching' : 'building', MSG_APP); 110 | } 111 | 112 | var compiler = webpack(webpackConfig); 113 | 114 | if(watch || webpack.watch) { 115 | watcher = compiler.watch(webpackConfig.watchOptions, finishedCallback); 116 | } else { 117 | compiler.run(finishedCallback); 118 | } 119 | 120 | process.on('SIGINT', shutdownCallback); 121 | process.on('exit', exitCallback); 122 | process.on('unhandledRejection', unhandledRejectionCallback); 123 | process.on('disconnect', disconnectCallback); 124 | 125 | function cleanup() { 126 | process.removeListener('SIGINT', shutdownCallback); 127 | process.removeListener('exit', exitCallback); 128 | process.removeListener('unhandledRejection', unhandledRejectionCallback); 129 | process.removeListener('disconnect', disconnectCallback); 130 | } 131 | 132 | function shutdownCallback() { 133 | if(watcher) { 134 | watcher.close(done); 135 | } 136 | done({ 137 | message: MSG_ERROR + ' Forcefully shut down ' + MSG_APP 138 | }); 139 | process.exit(0); 140 | } 141 | 142 | function unhandledRejectionCallback(error) { 143 | console.log(MSG_ERROR + 'Build child process error:', error); 144 | process.exit(1); 145 | } 146 | 147 | function exitCallback(code) { 148 | cleanup(); 149 | if (code === 0) { 150 | return; 151 | } 152 | if(watcher) { 153 | watcher.close(done); 154 | } 155 | done({ 156 | message: MSG_ERROR + ' Exit ' + MSG_APP + ' with code ' + code 157 | }); 158 | } 159 | 160 | function disconnectCallback(){ 161 | disconnected = true; 162 | console.log('%s Parent process terminated, exit building %s', MSG_ERROR, MSG_APP); 163 | process.exit(1); 164 | } 165 | 166 | function finishedCallback(err, stats) { 167 | if(err) { 168 | console.error('%s fatal error occured', MSG_ERROR); 169 | console.error(err); 170 | cleanup(); 171 | return done(err); 172 | } 173 | if(stats.compilation.errors && stats.compilation.errors.length) { 174 | var message = MSG_ERROR + ' Errors building ' + MSG_APP + "\n" 175 | + stats.compilation.errors.map(function(error) { 176 | return error.message; 177 | }).join("\n"); 178 | if(watch) { 179 | console.log(message); 180 | } else { 181 | cleanup(); 182 | if (disconnected) { 183 | return; 184 | } 185 | return done({ 186 | message: message, 187 | stats: JSON.stringify(stats.toJson(outputOptions), null, 2) 188 | }); 189 | } 190 | } 191 | if(!silent) { 192 | if(options.stats) { 193 | console.log(stats.toString(outputOptions)); 194 | } 195 | var timeStamp = watch 196 | ? ' ' + chalk.yellow(new Date().toTimeString().split(/ +/)[0]) 197 | : ''; 198 | console.log('%s Finished building %s within %s seconds', chalk.blue('[WEBPACK' + timeStamp + ']'), MSG_APP, chalk.blue((stats.endTime - stats.startTime) / 1000)); 199 | } 200 | if(!watch) { 201 | cleanup(); 202 | if (disconnected) { 203 | return; 204 | } 205 | done(null, options.stats ? JSON.stringify(stats.toJson(outputOptions), null, 2) : ''); 206 | } else if (!hasCompletedOneCompile) { 207 | notifyIPCWatchCompileDone(index); 208 | hasCompletedOneCompile = true; 209 | } 210 | } 211 | }); 212 | }; 213 | --------------------------------------------------------------------------------