├── .circleci
└── config.yml
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .github
├── CODEOWNERS
├── CONTRIBUTING-OLD.md
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE.md
├── ISSUE_TEMPLATE
│ ├── BUG.md
│ ├── DOCS.md
│ ├── FEATURE.md
│ ├── MODIFICATION.md
│ └── SUPPORT.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── client
└── .gitkeep
├── codecov.yml
├── commitlint.config.js
├── docs
├── REMOTE.md
└── WEBSOCKETS.md
├── lib
├── HotClientError.js
├── client
│ ├── .babelrc
│ ├── .eslintrc
│ ├── hot.js
│ ├── index.js
│ ├── log.js
│ └── socket.js
├── compiler.js
├── index.js
├── options.js
└── socket-server.js
├── package-lock.json
├── package.json
├── schemas
└── options.json
└── test
├── __snapshots__
├── compiler.test.js.snap
├── index.test.js.snap
├── options.test.js.snap
└── socket-server.test.js.snap
├── compiler.test.js
├── fixtures
├── app-clean.js
├── app.js
├── component.js
├── foo.js
├── index.html
├── multi
│ ├── client.js
│ ├── server.js
│ └── webpack.config.js
├── sub
│ └── resource.html
├── test-cert.pfx
├── webpack.config-allentries.js
├── webpack.config-array.js
├── webpack.config-function-invalid.js
├── webpack.config-function.js
├── webpack.config-invalid-object.js
├── webpack.config-invalid-plugin.js
├── webpack.config-invalid.js
├── webpack.config-object.js
└── webpack.config-watch.js
├── index.test.js
├── options.test.js
├── socket-server.test.js
└── watch.test.js
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | unit_tests: &unit_tests
2 | steps:
3 | - checkout
4 | - restore_cache:
5 | key: dependency-cache-{{ checksum "package-lock.json" }}
6 | - run:
7 | name: NPM Rebuild
8 | command: npm install
9 | - run:
10 | name: Run unit tests.
11 | command: npm run ci:test
12 | canary_tests: &canary_tests
13 | steps:
14 | - checkout
15 | - restore_cache:
16 | key: dependency-cache-{{ checksum "package-lock.json" }}
17 | - run:
18 | name: NPM Rebuild
19 | command: npm install
20 | - run:
21 | name: Install Webpack Canary
22 | command: npm i --no-save webpack@next
23 | - run:
24 | name: Run unit tests.
25 | command: if [[ $(compver --name webpack --gte next --lt latest) < 1 ]] ; then printf "Next is older than Latest - Skipping Canary Suite"; else npm run ci:test ; fi
26 |
27 | version: 2
28 | jobs:
29 | dependency_cache:
30 | docker:
31 | - image: webpackcontrib/circleci-node-base:latest
32 | steps:
33 | - checkout
34 | - restore_cache:
35 | key: dependency-cache-{{ checksum "package-lock.json" }}
36 | - run:
37 | name: Install Dependencies
38 | command: npm install
39 | - save_cache:
40 | key: dependency-cache-{{ checksum "package-lock.json" }}
41 | paths:
42 | - ./node_modules
43 |
44 | node8-latest:
45 | docker:
46 | - image: webpackcontrib/circleci-node8:latest
47 | steps:
48 | - checkout
49 | - restore_cache:
50 | key: dependency-cache-{{ checksum "package-lock.json" }}
51 | - run:
52 | name: NPM Rebuild
53 | command: npm install
54 | - run:
55 | name: Run unit tests.
56 | command: npm run ci:coverage
57 | - run:
58 | name: Submit coverage data to codecov.
59 | command: bash <(curl -s https://codecov.io/bash)
60 | when: on_success
61 | node6-latest:
62 | docker:
63 | - image: webpackcontrib/circleci-node6:latest
64 | <<: *unit_tests
65 | node10-latest:
66 | docker:
67 | - image: webpackcontrib/circleci-node10:latest
68 | <<: *unit_tests
69 | node8-canary:
70 | docker:
71 | - image: webpackcontrib/circleci-node8:latest
72 | <<: *canary_tests
73 | analysis:
74 | docker:
75 | - image: webpackcontrib/circleci-node-base:latest
76 | steps:
77 | - checkout
78 | - restore_cache:
79 | key: dependency-cache-{{ checksum "package-lock.json" }}
80 | - run:
81 | name: NPM Rebuild
82 | command: npm install
83 | - run:
84 | name: Run linting.
85 | command: npm run lint
86 | - run:
87 | name: Run NSP Security Check.
88 | command: npm run security
89 | - run:
90 | name: Validate Commit Messages
91 | command: npm run ci:lint:commits
92 | publish:
93 | docker:
94 | - image: webpackcontrib/circleci-node-base:latest
95 | steps:
96 | - checkout
97 | - restore_cache:
98 | key: dependency-cache-{{ checksum "package-lock.json" }}
99 | - run:
100 | name: NPM Rebuild
101 | command: npm install
102 | # - run:
103 | # name: Validate Commit Messages
104 | # command: npm run release:validate
105 | - run:
106 | name: Publish to NPM
107 | command: printf "noop running conventional-github-releaser"
108 |
109 | version: 2.0
110 | workflows:
111 | version: 2
112 | validate-publish:
113 | jobs:
114 | - dependency_cache
115 | - node6-latest:
116 | requires:
117 | - dependency_cache
118 | filters:
119 | tags:
120 | only: /.*/
121 | - analysis:
122 | requires:
123 | - dependency_cache
124 | filters:
125 | tags:
126 | only: /.*/
127 | - node8-latest:
128 | requires:
129 | - analysis
130 | - node6-latest
131 | filters:
132 | tags:
133 | only: /.*/
134 | - node10-latest:
135 | requires:
136 | - analysis
137 | - node6-latest
138 | filters:
139 | tags:
140 | only: /.*/
141 | - node8-canary:
142 | requires:
143 | - analysis
144 | - node6-latest
145 | filters:
146 | tags:
147 | only: /.*/
148 | - publish:
149 | requires:
150 | - node8-latest
151 | - node8-canary
152 | - node10-latest
153 | filters:
154 | branches:
155 | only:
156 | - master
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | insert_final_newline = false
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | example/*
3 | example/node_modules/*
4 | coverage/*
5 | /client
6 | output.js
7 | /node_modules
8 | /dist
9 | /test-old
10 | *.snap
11 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | plugins: ['prettier'],
4 | extends: ['@webpack-contrib/eslint-config-webpack'],
5 | rules: {
6 | 'prettier/prettier': [
7 | 'error',
8 | { singleQuote: true, trailingComma: 'es5', arrowParens: 'always' },
9 | ],
10 | },
11 | };
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | package-lock.json -diff
2 | * text=auto
3 | bin/* eol=lf
4 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # These are the default owners for everything in
2 | # webpack-contrib
3 | @webpack-contrib/org-maintainers
4 |
5 | # Add repository specific users / groups
6 | # below here for libs that are not maintained by the org.
--------------------------------------------------------------------------------
/.github/CONTRIBUTING-OLD.md:
--------------------------------------------------------------------------------
1 | # Welcome!
2 | :heart: Thanks for your interest and time in contributing to this project.
3 |
4 | ## What We Use
5 |
6 | - Building: [Webpack](https://webpack.js.org)
7 | - Linting: [ESLint](http://eslint.org/)
8 | - NPM: [NPM as a Build Tool](https://css-tricks.com/using-npm-build-tool/)
9 | - Testing: [Mocha](https://mochajs.org)
10 |
11 | ## Forking and Cloning
12 |
13 | You'll need to first fork this repository, and then clone it locally before you
14 | can submit a Pull Request with your proposed changes.
15 |
16 | Please see the following articles for help getting started with git:
17 |
18 | [Forking a Repository](https://help.github.com/articles/fork-a-repo/)
19 | [Cloning a Repository](https://help.github.com/articles/cloning-a-repository/)
20 |
21 | ## Pull Requests
22 |
23 | Please lint and test your changes before submitting a Pull Request. You can lint your
24 | changes by running:
25 |
26 | ```console
27 | $ npm run lint
28 | ```
29 |
30 | You can test your changes against the test suite for this module by running:
31 |
32 | ```console
33 | $ npm run test
34 | ```
35 |
36 | _Note: Please avoid committing `package-lock.json` files!_
37 |
38 | Please don't change variable or parameter names to match your personal
39 | preferences, unless the change is part of a refactor or significant modification
40 | of the codebase (which is very rare).
41 |
42 | Please remember to thoroughly explain your Pull Request if it doesn't have an
43 | associated issue. If you're changing code significantly, please remember to add
44 | inline or block comments in the code as appropriate.
45 |
46 | ## Testing Your Pull Request
47 |
48 | You may have the need to test your changes in a real-world project or dependent
49 | module. Thankfully, Github provides a means to do this. Add a dependency to the
50 | `package.json` for such a project as follows:
51 |
52 | ```json
53 | "webpack-hot-client": "webpack-contrib/webpack-hot-client#{id}/head"
54 | ```
55 |
56 | Where `{id}` is the # ID of your Pull Request.
57 |
58 | ## Thanks
59 |
60 | For your interest, time, understanding, and for following this simple guide.
61 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing in @webpack-contrib
2 |
3 | We'd always love contributions to further improve the webpack / webpack-contrib ecosystem!
4 | Here are the guidelines we'd like you to follow:
5 |
6 | * [Questions and Problems](#question)
7 | * [Issues and Bugs](#issue)
8 | * [Feature Requests](#feature)
9 | * [Pull Request Submission Guidelines](#submit-pr)
10 | * [Commit Message Conventions](#commit)
11 |
12 | ### Got a Question or Problem?
13 |
14 | Please submit support requests and questions to StackOverflow using the tag [[webpack]](http://stackoverflow.com/tags/webpack).
15 | StackOverflow is better suited for this kind of support though you may also inquire in [Webpack Gitter](https://gitter.im/webpack/webpack).
16 | The issue tracker is for bug reports and feature discussions.
17 |
18 | ### Found an Issue or Bug?
19 |
20 | Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
21 |
22 | We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs, we ask that you to provide a minimal reproduction scenario (github repo or failing test case). Having a live, reproducible scenario gives us a wealth of important information without going back & forth to you with additional questions like:
23 |
24 | - version of Webpack used
25 | - version of the loader / plugin you are creating a bug report for
26 | - the use-case that fails
27 |
28 | A minimal reproduce scenario allows us to quickly confirm a bug (or point out config problems) as well as confirm that we are fixing the right problem.
29 |
30 | We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it.
31 |
32 | Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that doesn't have enough info to be reproduced.
33 |
34 | ### Feature Requests?
35 |
36 | You can *request* a new feature by creating an issue on Github.
37 |
38 | If you would like to *implement* a new feature, please submit an issue with a proposal for your work `first`, to be sure that particular makes sense for the project.
39 |
40 | ### Pull Request Submission Guidelines
41 |
42 | Before you submit your Pull Request (PR) consider the following guidelines:
43 |
44 | - Search Github for an open or closed PR that relates to your submission. You don't want to duplicate effort.
45 | - Commit your changes using a descriptive commit message that follows our [commit message conventions](#commit). Adherence to these conventions is necessary because release notes are automatically generated from these messages.
46 | - Fill out our `Pull Request Template`. Your pull request will not be considered if it is ignored.
47 | - Please sign the `Contributor License Agreement (CLA)` when a pull request is opened. We cannot accept your pull request without this. Make sure you sign with the primary email address associated with your local / github account.
48 |
49 | ### Webpack Contrib Commit Conventions
50 |
51 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
52 | format that includes a **type**, a **scope** and a **subject**:
53 |
54 | ```
55 | ():
56 |
57 |
58 |
59 |
60 | ```
61 |
62 | The **header** is mandatory and the **scope** of the header is optional.
63 |
64 | Any line of the commit message cannot be longer 100 characters! This allows the message to be easier
65 | to read on GitHub as well as in various git tools.
66 |
67 | The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
68 |
69 | Examples:
70 | ```
71 | docs(readme): update install instructions
72 | ```
73 | ```
74 | fix: refer to the `entrypoint` instead of the first `module`
75 | ```
76 |
77 | #### Revert
78 | If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit.
79 | In the body it should say: `This reverts commit .`, where the hash is the SHA of the commit being reverted.
80 |
81 | #### Type
82 | Must be one of the following:
83 |
84 | * **build**: Changes that affect the build system or external dependencies (example scopes: babel, npm)
85 | * **chore**: Changes that fall outside of build / docs that do not effect source code (example scopes: package, defaults)
86 | * **ci**: Changes to our CI configuration files and scripts (example scopes: circleci, travis)
87 | * **docs**: Documentation only changes (example scopes: readme, changelog)
88 | * **feat**: A new feature
89 | * **fix**: A bug fix
90 | * **perf**: A code change that improves performance
91 | * **refactor**: A code change that neither fixes a bug nor adds a feature
92 | * **revert**: Used when reverting a committed change
93 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons)
94 | * **test**: Addition of or updates to Jest tests
95 |
96 | #### Scope
97 | The scope is subjective & depends on the `type` see above. A good example would be a change to a particular class / module.
98 |
99 | #### Subject
100 | The subject contains a succinct description of the change:
101 |
102 | * use the imperative, present tense: "change" not "changed" nor "changes"
103 | * don't capitalize the first letter
104 | * no dot (.) at the end
105 |
106 | #### Body
107 | Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
108 | The body should include the motivation for the change and contrast this with previous behavior.
109 |
110 | #### Footer
111 | The footer should contain any information about **Breaking Changes** and is also the place to
112 | reference GitHub issues that this commit **Closes**.
113 |
114 | **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
115 |
116 | Example
117 |
118 | ```
119 | BREAKING CHANGE: Updates to `Chunk.mapModules`.
120 |
121 | This release is not backwards compatible with `Webpack 2.x` due to breaking changes in webpack/webpack#4764
122 | Migration: see webpack/webpack#5225
123 |
124 | ```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug Report
3 | about: Something went awry and you'd like to tell us about it.
4 |
5 | ---
6 |
7 |
17 |
18 | * Operating System:
19 | * Node Version:
20 | * NPM Version:
21 | * webpack Version:
22 | * webpack-hot-client Version:
23 |
24 |
25 | ### Expected Behavior
26 |
27 |
28 |
29 | ### Actual Behavior
30 |
31 |
32 |
33 | ### Code
34 |
35 | ```js
36 | // webpack.config.js
37 | // If your bitchin' code blocks are over 20 lines, please paste a link to a gist
38 | // (https://gist.github.com).
39 | ```
40 |
41 | ```js
42 | // additional code, HEY YO remove this block if you don't need it
43 | ```
44 |
45 | ### How Do We Reproduce?
46 |
47 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/DOCS.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 📚 Documentation
3 | about: Are the docs lacking or missing something? Do they need some new 🔥 hotness? Tell us here.
4 |
5 | ---
6 |
7 |
17 |
18 | Documentation Is:
19 |
20 |
21 |
22 | - [ ] Missing
23 | - [ ] Needed
24 | - [ ] Confusing
25 | - [ ] Not Sure?
26 |
27 |
28 | ### Please Explain in Detail...
29 |
30 |
31 | ### Your Proposal for Changes
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/FEATURE.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: ✨ Feature Request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 |
17 |
18 | * Operating System:
19 | * Node Version:
20 | * NPM Version:
21 | * webpack Version:
22 | * webpack-hot-client Version:
23 |
24 |
25 | ### Feature Proposal
26 |
27 |
28 |
29 | ### Feature Use Case
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/MODIFICATION.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🔧 Modification Request
3 | about: Would you like something work differently? Have an alternative approach? This is the template for you.
4 |
5 | ---
6 |
7 |
17 |
18 | * Operating System:
19 | * Node Version:
20 | * NPM Version:
21 | * webpack Version:
22 | * webpack-hot-client Version:
23 |
24 |
25 | ### Expected Behavior / Situation
26 |
27 |
28 |
29 | ### Actual Behavior / Situation
30 |
31 |
32 |
33 | ### Modification Proposal
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/SUPPORT.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🆘 Support, Help, and Advice
3 | about: 👉🏽 Need support, help, or advice? Don't open an issue! Head to StackOverflow or https://gitter.im/webpack/webpack.
4 |
5 | ---
6 |
7 | Hey there! If you need support, help, or advice then this is not the place to ask.
8 | Please visit [StackOverflow](https://stackoverflow.com/questions/tagged/webpack)
9 | or [the Webpack Gitter](https://gitter.im/webpack/webpack) instead.
10 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
10 |
11 | This PR contains a:
12 |
13 | - [ ] **bugfix**
14 | - [ ] new **feature**
15 | - [ ] **code refactor**
16 | - [ ] **test update**
17 | - [ ] **typo fix**
18 | - [ ] **metadata update**
19 |
20 | ### Motivation / Use-Case
21 |
22 |
27 |
28 | ### Breaking Changes
29 |
30 |
34 |
35 | ### Additional Info
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .nyc_output
2 | node_modules
3 | coverage
4 | test/fixtures/output.js*
5 | client/*.js
6 | coverage.lcov
7 | *.log
8 | logs
9 | npm-debug.log*
10 | .eslintcache
11 | /coverage
12 | /dist
13 | /local
14 | /reports
15 | /node_modules
16 | .DS_Store
17 | Thumbs.db
18 | .idea
19 | .vscode
20 | *.sublime-project
21 | *.sublime-workspace
22 | *hot-update*.*
23 | output*.js
24 | output.js.map
25 | test/output
26 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5",
4 | "arrowParens": "always"
5 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright JS Foundation and other contributors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | 'Software'), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | [![npm][npm]][npm-url]
8 | [![node][node]][node-url]
9 | [![deps][deps]][deps-url]
10 | [![tests][tests]][tests-url]
11 | [![chat][chat]][chat-url]
12 | [![size][size]][size-url]
13 |
14 | # webpack-hot-client
15 |
16 | A client for enabling, and interacting with, webpack [Hot Module Replacement][hmr-docs].
17 |
18 | This is intended to work in concert with [`webpack-dev-middleware`][dev-middleware]
19 | and allows for adding Hot Module Replacement to an existing server, without a
20 | dependency upon [`webpack-dev-server`][dev-server]. This comes in handy for testing
21 | in projects that already use server frameworks such as `Express` or `Koa`.
22 |
23 | `webpack-hot-client` accomplishes this by creating a `WebSocket` server, providing
24 | the necessary client (browser) scripts that communicate via `WebSocket`s, and
25 | automagically adding the necessary webpack plugins and config entries. All of
26 | that allows for a seamless integration of Hot Module Replacement Support.
27 |
28 | Curious about the differences between this module and `webpack-hot-middleware`?
29 | [Read more here](https://github.com/webpack-contrib/webpack-hot-client/issues/18).
30 |
31 | ## Requirements
32 |
33 | This module requires a minimum of Node v6.9.0 and Webpack v4.0.0.
34 |
35 | ## Getting Started
36 |
37 | To begin, you'll need to install `webpack-hot-client`:
38 |
39 | ```console
40 | $ npm install webpack-hot-client --save-dev
41 | ```
42 |
43 | ## Gotchas
44 |
45 | ### Entries
46 |
47 | In order to use `webpack-hot-client`, your `webpack` config should include an
48 | `entry` option that is set to an `Array` of `String`, or an `Object` who's keys
49 | are set to an `Array` of `String`. You may also use a `Function`, but that
50 | function should return a value in one of the two valid formats.
51 |
52 | This is primarily due to restrictions in `webpack` itself and the way that it
53 | processes options and entries. For users of webpack v4+ that go the zero-config
54 | route, you must specify an `entry` option.
55 |
56 | ### Automagical Configuration
57 |
58 | As a convenience `webpack-hot-client` adds `HotModuleReplacementPlugin`
59 | and the necessary entries to your `webpack` config for you at runtime. Including
60 | the plugin in your config manually while using this module may produce unexpected
61 | or wonky results. If you have a need to configure entries and plugins for HMR
62 | manually, use the `autoConfigure` option.
63 |
64 | ### Best Practices
65 |
66 | When using this module manually, along with the default `port` option value of
67 | `0`, starting compilation (or passing a compiler to `webpack-dev-middleware`)
68 | should be done _after_ the socket server has finished listening and allocating
69 | a port. This ensures that the build will contain the allocated port. (See the
70 | Express example below.) This condition does not apply if providing a static
71 | `port` option to the API.
72 |
73 | ### Express
74 |
75 | For setting up the module for use with an `Express` server, try the following:
76 |
77 | ```js
78 | const hotClient = require('webpack-hot-client');
79 | const middleware = require('webpack-dev-middleware');
80 | const webpack = require('webpack');
81 | const config = require('./webpack.config');
82 |
83 | const compiler = webpack(config);
84 | const { publicPath } = config.output;
85 | const options = { ... }; // webpack-hot-client options
86 |
87 | // we recommend calling the client _before_ adding the dev middleware
88 | const client = hotClient(compiler, options);
89 | const { server } = client;
90 |
91 | server.on('listening', () => {
92 | app.use(middleware(compiler, { publicPath }));
93 | });
94 | ```
95 |
96 | ### Koa
97 |
98 | Since `Koa`@2.0.0 was released, the patterns and requirements for using
99 | `webpack-dev-middleware` have changed somewhat, due to use of `async/await` in
100 | Koa. As such, one potential solution is to use [`koa-webpack`][koa-webpack],
101 | which wires up the dev middleware properly for Koa, and also implements this
102 | module. If you'd like to use both modules without `koa-webpack`, you may examine
103 | that module's code for implementation details.
104 |
105 | ## Browser Support
106 |
107 | Because this module leverages _native_ `WebSockets`, the browser support for this
108 | module is limited to only those browsers which support native `WebSocket`. That
109 | typically means the last two major versions of a particular browser.
110 |
111 | Please visit [caniuse.com](https://caniuse.com/#feat=websockets) for a full
112 | compatibility table.
113 |
114 | _Note: We won't be accepting requests for changes to this facet of the module._
115 |
116 | ## API
117 |
118 | ### client(compiler, [options])
119 |
120 | Returns an `Object` containing:
121 |
122 | - `close()` *(Function)* - Closes the WebSocketServer started by the module.
123 | - `wss` *(WebSocketServer)* - A WebSocketServer instance.
124 |
125 | #### options
126 |
127 | Type: `Object`
128 |
129 | ##### allEntries
130 |
131 | Type: `Boolean`
132 | Default: `false`
133 |
134 | If true and `autoConfigure` is true, will automatically configures each `entry`
135 | key for the webpack compiler. Typically used when working with or manipulating
136 | different chunks in the same compiler configuration.
137 |
138 | ##### autoConfigure
139 |
140 | Type: `Boolean`
141 | Default: `true`
142 |
143 | If true, automatically configures the `entry` for the webpack compiler, and adds
144 | the `HotModuleReplacementPlugin` to the compiler.
145 |
146 | ##### host
147 |
148 | Type: `String|Object`
149 | Default: `'localhost'`
150 |
151 | Sets the host that the `WebSocket` server will listen on. If this doesn't match
152 | the host of the server the module is used with, the module may not function
153 | properly. If the `server` option is defined, and the server has been instructed
154 | to listen, this option is ignored.
155 |
156 | If using the module in a specialized environment, you may choose to specify an
157 | `object` to define `client` and `server` host separately. The `object` value
158 | should match `{ client: , server: }`. Be aware that the `client`
159 | host will be used _in the browser_ by `WebSockets`. You should not use this
160 | option in this way unless _you know what you're doing._ Using a mismatched
161 | `client` and `server` host will be **unsupported by the project** as the behavior
162 | in the browser can be unpredictable and is specific to a particular environment.
163 |
164 | The value of `host.client` can also be set to a wildcard character for
165 | [Remote Machine Testing](./docs/REMOTE.md).
166 |
167 | ##### hmr
168 |
169 | Type: `Boolean`
170 | Default: `true`
171 |
172 | If true, instructs the client script to attempt Hot Module Replacement patching
173 | of modules.
174 |
175 | ##### https
176 |
177 | Type: `Boolean`
178 | Default: `false`
179 |
180 | If true, instructs the client script to use `wss://` as the `WebSocket` protocol.
181 |
182 | When using the `server` option and passing an instance of `https.Server`, this
183 | flag must also be true. Otherwise, the sockets cannot communicate and this
184 | module won't function properly. The module will examine the `server` instance
185 | passed and if `server` _is an instance of `https.Server`, and `https` is not
186 | already set_, will set `https` to `true`.
187 |
188 | _Note: When using a self-signed certificate on Firefox, you must add a "Server
189 | Exception" for `localhost:{port}` where `{port}` is either the `port` or the
190 | `port.server` option for secure `WebSocket` to work correctly._
191 |
192 | ##### logLevel
193 |
194 | Type: `String`
195 | Default: `'info'`
196 |
197 | Sets the minimum level of logs that will be displayed in the console. Please see
198 | [webpack-log/#levels][levels] for valid values.
199 |
200 | ##### logTime
201 |
202 | Type: `Boolean`
203 | Default: `false`
204 |
205 | If true, instructs the internal logger to prepend log output with a timestamp.
206 |
207 | ##### port
208 |
209 | Type: `Number|Object`
210 | Default: `0`
211 |
212 | The port the `WebSocket` server should listen on. By default, the socket server
213 | will allocate a port. If a different port is chosen, the consumer of the module
214 | must ensure that the port is free before hand. If the `server` option is defined,
215 | and the server has been instructed to listen, this option is ignored.
216 |
217 | If using the module in a specialized environment, you may choose to specify an
218 | `object` to define `client` and `server` port separately. The `object` value
219 | should match `{ client: , server: }`. Be aware that the `client`
220 | port will be used _in the browser_ by `WebSockets`. You should not use this
221 | option in this way unless _you know what you're doing._ Using a mismatched
222 | `client` and `server` port will be **unsupported by the project** as the behavior
223 | in the browser can be unpredictable and is specific to a particular environment.
224 |
225 | ##### reload
226 |
227 | Type: `Boolean`
228 | Default: `true`
229 |
230 | If true, instructs the browser to physically refresh the entire page if / when
231 | webpack indicates that a hot patch cannot be applied and a full refresh is needed.
232 |
233 | This option also instructs the browser whether or not to refresh the entire page
234 | when `hmr: false` is used.
235 |
236 | _Note: If both `hmr` and `reload` are false, and these are permanent settings,
237 | it makes this module fairly useless._
238 |
239 | ##### server
240 |
241 | Type: `Object`
242 | Default: `null`
243 |
244 | If a server instance (eg. Express or Koa) is provided, the `WebSocket` server
245 | will attempt to attach to the server instance instead of using a separate port.
246 |
247 | ##### stats
248 |
249 | Type: `Object`
250 | Default: `{ context: process.cwd() }`
251 |
252 | An object specifying the webpack [stats][stats] configuration. This does not
253 | typically need to be modified.
254 |
255 | ##### validTargets
256 |
257 | Type: `Array[String]`
258 | Default: `['web']`
259 |
260 | By default, `webpack-hot-client` is meant to, and expects to function on the
261 | [`'web'` build target](https://webpack.js.org/configuration/target). However,
262 | you can manipulate this by adding targets to this property. eg.
263 |
264 | ```
265 | // will be merged with the default 'web' target
266 | validTargets: ['batmobile']
267 | ```
268 |
269 | ## Communicating with Client WebSockets
270 |
271 | Please see the [WebSockets](./docs/WEBSOCKETS.md) documentation.
272 |
273 | ## Remote Machine Testing
274 |
275 | Please see the [Remote Machine Testing](./docs/REMOTE.md) documentation.
276 |
277 | ## Contributing
278 |
279 | We welcome your contributions! Please have a read of
280 | [CONTRIBUTING](./.github/CONTRIBUTING.md) for more information on how to get involved.
281 |
282 | ## License
283 |
284 | #### [MIT](./LICENSE)
285 |
286 | [npm]: https://img.shields.io/npm/v/webpack-hot-client.svg
287 | [npm-url]: https://npmjs.com/package/webpack-hot-client
288 |
289 | [node]: https://img.shields.io/node/v/webpack-hot-client.svg
290 | [node-url]: https://nodejs.org
291 |
292 | [deps]: https://david-dm.org/webpack-contrib/webpack-hot-client.svg
293 | [deps-url]: https://david-dm.org/webpack-contrib/webpack-hot-client
294 |
295 | [tests]: https://img.shields.io/circleci/project/github/webpack-contrib/webpack-hot-client.svg
296 | [tests-url]: https://circleci.com/gh/webpack-contrib/webpack-hot-client
297 |
298 | [cover]: https://codecov.io/gh/webpack-contrib/webpack-hot-client/branch/master/graph/badge.svg
299 | [cover-url]: https://codecov.io/gh/webpack-contrib/webpack-hot-client
300 |
301 | [chat]: https://img.shields.io/badge/gitter-webpack%2Fwebpack-brightgreen.svg
302 | [chat-url]: https://gitter.im/webpack/webpack
303 |
304 | [size]: https://packagephobia.now.sh/badge?p=webpack-hot-client
305 | [size-url]: https://packagephobia.now.sh/result?p=webpack-hot-client
306 |
307 | [dev-middleware]: https://github.com/webpack/webpack-dev-middleware
308 | [dev-server]: https://github.com/webpack/webpack-dev-server
309 | [hmr-docs]: https://webpack.js.org/concepts/hot-module-replacement/
310 | [koa-webpack]: https://github.com/shellscape/koa-webpack
311 | [levels]: https://github.com/webpack-contrib/webpack-log#level
312 | [stats]: https://webpack.js.org/configuration/stats/#stats
313 |
--------------------------------------------------------------------------------
/client/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webpack-contrib/webpack-hot-client/1b7f221918217be0db7a6089fb77fffde9a973f6/client/.gitkeep
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | branch: master
3 | coverage:
4 | precision: 2
5 | round: down
6 | range: 70...100
7 | status:
8 | project: 'no'
9 | patch: 'yes'
10 | comment: 'off'
11 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const Configuration = {
3 | extends: ['@commitlint/config-conventional'],
4 |
5 | rules: {
6 | 'body-leading-blank': [1, 'always'],
7 | 'footer-leading-blank': [1, 'always'],
8 | 'header-max-length': [2, 'always', 72],
9 | 'scope-case': [2, 'always', 'lower-case'],
10 | 'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']],
11 | 'subject-empty': [2, 'never'],
12 | 'subject-full-stop': [2, 'never', '.'],
13 | 'type-case': [2, 'always', 'lower-case'],
14 | 'type-empty': [2, 'never'],
15 | 'type-enum': [2, 'always', [
16 | 'build',
17 | 'chore',
18 | 'ci',
19 | 'docs',
20 | 'feat',
21 | 'fix',
22 | 'perf',
23 | 'refactor',
24 | 'revert',
25 | 'style',
26 | 'test',
27 | ],
28 | ],
29 | },
30 | };
31 |
32 | module.exports = Configuration;
--------------------------------------------------------------------------------
/docs/REMOTE.md:
--------------------------------------------------------------------------------
1 | # Remote Machine Testing
2 |
3 | If you're working in an environment where you have the need to run the server on
4 | one machine (or VM) and need to test your app on another, you'll need to properly
5 | configure both `webpack-hot-client` and the remote machine. The most stable and
6 | least error-prone method will involve setting options statically:
7 |
8 | ## Client Host and HOSTS
9 |
10 | Update your HOSTS file with an entry akin to:
11 |
12 | ```
13 | 127.0.0.1 mytesthost # where 127.0.0.1 is the IP of the machine hosting the tests
14 | ```
15 |
16 | And modifying your options in a similar fashion:
17 |
18 | ```js
19 | host: {
20 | client: 'mytesthost',
21 | server: '0.0.0.0',
22 | }
23 | ```
24 |
25 | ### Use `public-ip`
26 |
27 | If hostnames aren't your flavor, you can also use packages like
28 | [`public-ip`](https://www.npmjs.com/package/public-ip) to set the host to your
29 | machine's public IP address.
30 |
31 | If the need to use `public-ip` in a synchronous environment arises, you might
32 | look at using `public-ip-cli` in conjunction with `exceca.sync`:
33 |
34 | ```js
35 | const { stdout: ip } = execa.sync('public-ip', { preferLocal: true });
36 | ```
37 |
38 | ### The Wildcard Host `*`
39 |
40 | New in v5.0.0 is the ability to set the `host.client` value to a wildcard symbol.
41 | Setting the client property to `*` will tell the client scripts to connect to
42 | any hostname the current page is being accessed on:
43 |
44 | ```js
45 | host: {
46 | client: '*',
47 | server: '0.0.0.0',
48 | }
49 | ```
50 |
51 | This setting can create an environment of _unpredictable results_ in the
52 | browser and is **unsupported**. Please make sure you know what you're doing if
53 | using the wildcard option.
--------------------------------------------------------------------------------
/docs/WEBSOCKETS.md:
--------------------------------------------------------------------------------
1 | # WebSocket Documentation
2 |
3 | ## Communicating with Client WebSockets
4 |
5 | In some rare situations, you may have the need to communicate with the attached
6 | `WebSockets` in the browser. To accomplish this, open a new `WebSocket` to the
7 | server, and send a `broadcast` message. eg.
8 |
9 | ```js
10 | const stringify = require('json-stringify-safe');
11 | const { WebSocket } = require('ws');
12 |
13 | const socket = new WebSocket('ws://localhost:8081'); // this should match the server settings
14 | const data = {
15 | type: 'broadcast',
16 | data: { // the message you want to broadcast
17 | type: '', // the message type you want to broadcast
18 | data: { ... } // the message data you want to broadcast
19 | }
20 | };
21 |
22 | socket.send(stringify(data));
23 | ```
24 |
25 | _Note: The `data` property of the message should contain the enveloped message
26 | you wish to broadcast to all other client `WebSockets`._
--------------------------------------------------------------------------------
/lib/HotClientError.js:
--------------------------------------------------------------------------------
1 | module.exports = class HotClientError extends Error {
2 | constructor(message) {
3 | super(`webpack-hot-client: ${message}`);
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/lib/client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", {
4 | "targets": {
5 | "browsers": ["last 2 versions"]
6 | }
7 | }]
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/lib/client/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "WebSocket": true,
4 | "window": true,
5 | "__hotClientOptions__": true,
6 | "__webpack_hash__": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/lib/client/hot.js:
--------------------------------------------------------------------------------
1 | const log = require('./log');
2 |
3 | const refresh = 'Please refresh the page.';
4 | const hotOptions = {
5 | ignoreUnaccepted: true,
6 | ignoreDeclined: true,
7 | ignoreErrored: true,
8 | onUnaccepted(data) {
9 | const chain = [].concat(data.chain);
10 | const last = chain[chain.length - 1];
11 |
12 | if (last === 0) {
13 | chain.pop();
14 | }
15 |
16 | log.warn(`Ignored an update to unaccepted module ${chain.join(' ➭ ')}`);
17 | },
18 | onDeclined(data) {
19 | log.warn(`Ignored an update to declined module ${data.chain.join(' ➭ ')}`);
20 | },
21 | onErrored(data) {
22 | log.warn(
23 | `Ignored an error while updating module ${data.moduleId} <${data.type}>`
24 | );
25 | log.warn(data.error);
26 | },
27 | };
28 |
29 | let lastHash;
30 |
31 | function upToDate() {
32 | return lastHash.indexOf(__webpack_hash__) >= 0;
33 | }
34 |
35 | function result(modules, appliedModules) {
36 | const unaccepted = modules.filter(
37 | (moduleId) => appliedModules && appliedModules.indexOf(moduleId) < 0
38 | );
39 |
40 | if (unaccepted.length > 0) {
41 | let message = 'The following modules could not be updated:';
42 |
43 | for (const moduleId of unaccepted) {
44 | message += `\n ⦻ ${moduleId}`;
45 | }
46 | log.warn(message);
47 | }
48 |
49 | if (!(appliedModules || []).length) {
50 | log.info('No Modules Updated.');
51 | } else {
52 | const message = ['The following modules were updated:'];
53 |
54 | for (const moduleId of appliedModules) {
55 | message.push(` ↻ ${moduleId}`);
56 | }
57 |
58 | log.info(message.join('\n'));
59 |
60 | const numberIds = appliedModules.every(
61 | (moduleId) => typeof moduleId === 'number'
62 | );
63 | if (numberIds) {
64 | log.info(
65 | 'Please consider using the NamedModulesPlugin for module names.'
66 | );
67 | }
68 | }
69 | }
70 |
71 | function check(options) {
72 | module.hot
73 | .check()
74 | .then((modules) => {
75 | if (!modules) {
76 | log.warn(
77 | `Cannot find update. The server may have been restarted. ${refresh}`
78 | );
79 | if (options.reload) {
80 | window.location.reload();
81 | }
82 | return null;
83 | }
84 |
85 | const hotOpts = options.reload ? {} : hotOptions;
86 |
87 | return module.hot
88 | .apply(hotOpts)
89 | .then((appliedModules) => {
90 | if (!upToDate()) {
91 | check(options);
92 | }
93 |
94 | result(modules, appliedModules);
95 |
96 | if (upToDate()) {
97 | log.info('App is up to date.');
98 | }
99 | })
100 | .catch((err) => {
101 | const status = module.hot.status();
102 | if (['abort', 'fail'].indexOf(status) >= 0) {
103 | log.warn(`Cannot apply update. ${refresh}`);
104 | log.warn(err.stack || err.message);
105 | if (options.reload) {
106 | window.location.reload();
107 | }
108 | } else {
109 | log.warn(`Update failed: ${err.stack}` || err.message);
110 | }
111 | });
112 | })
113 | .catch((err) => {
114 | const status = module.hot.status();
115 | if (['abort', 'fail'].indexOf(status) >= 0) {
116 | log.warn(`Cannot check for update. ${refresh}`);
117 | log.warn(err.stack || err.message);
118 | if (options.reload) {
119 | window.location.reload();
120 | }
121 | } else {
122 | log.warn(`Update check failed: ${err.stack}` || err.message);
123 | }
124 | });
125 | }
126 |
127 | if (module.hot) {
128 | log.info('Hot Module Replacement Enabled. Waiting for signal.');
129 | } else {
130 | log.error('Hot Module Replacement is disabled.');
131 | }
132 |
133 | module.exports = function update(currentHash, options) {
134 | lastHash = currentHash;
135 | if (!upToDate()) {
136 | const status = module.hot.status();
137 |
138 | if (status === 'idle') {
139 | log.info('Checking for updates to the bundle.');
140 | check(options);
141 | } else if (['abort', 'fail'].indexOf(status) >= 0) {
142 | log.warn(
143 | `Cannot apply update. A previous update ${status}ed. ${refresh}`
144 | );
145 | if (options.reload) {
146 | window.location.reload();
147 | }
148 | }
149 | }
150 | };
151 |
--------------------------------------------------------------------------------
/lib/client/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require, consistent-return */
2 |
3 | (function hotClientEntry() {
4 | // eslint-disable-next-line no-underscore-dangle
5 | if (window.__webpackHotClient__) {
6 | return;
7 | }
8 |
9 | // eslint-disable-next-line no-underscore-dangle
10 | window.__webpackHotClient__ = {};
11 |
12 | // this is piped in at runtime build via DefinePlugin in /lib/plugins.js
13 | // eslint-disable-next-line no-unused-vars, no-undef
14 | const options = __hotClientOptions__;
15 |
16 | const log = require('./log'); // eslint-disable-line import/order
17 |
18 | log.level = options.logLevel;
19 |
20 | const update = require('./hot');
21 | const socket = require('./socket');
22 |
23 | if (!options) {
24 | throw new Error(
25 | 'Something went awry and __hotClientOptions__ is undefined. Possible bad build. HMR cannot be enabled.'
26 | );
27 | }
28 |
29 | let currentHash;
30 | let initial = true;
31 | let isUnloading;
32 |
33 | window.addEventListener('beforeunload', () => {
34 | isUnloading = true;
35 | });
36 |
37 | function reload() {
38 | if (isUnloading) {
39 | return;
40 | }
41 |
42 | if (options.hmr) {
43 | log.info('App Updated, Reloading Modules');
44 | update(currentHash, options);
45 | } else if (options.reload) {
46 | log.info('Refreshing Page');
47 | window.location.reload();
48 | } else {
49 | log.warn('Please refresh the page manually.');
50 | log.info('The `hot` and `reload` options are set to false.');
51 | }
52 | }
53 |
54 | socket(options, {
55 | compile({ compilerName }) {
56 | log.info(`webpack: Compiling (${compilerName})`);
57 | },
58 |
59 | errors({ errors }) {
60 | log.error(
61 | 'webpack: Encountered errors while compiling. Reload prevented.'
62 | );
63 |
64 | for (let i = 0; i < errors.length; i++) {
65 | log.error(errors[i]);
66 | }
67 | },
68 |
69 | hash({ hash }) {
70 | currentHash = hash;
71 | },
72 |
73 | invalid({ fileName }) {
74 | log.info(`App updated. Recompiling ${fileName}`);
75 | },
76 |
77 | ok() {
78 | if (initial) {
79 | initial = false;
80 | return initial;
81 | }
82 |
83 | reload();
84 | },
85 |
86 | 'window-reload': () => {
87 | window.location.reload();
88 | },
89 |
90 | warnings({ warnings }) {
91 | log.warn('Warnings while compiling.');
92 |
93 | for (let i = 0; i < warnings.length; i++) {
94 | log.warn(warnings[i]);
95 | }
96 |
97 | if (initial) {
98 | initial = false;
99 | return initial;
100 | }
101 |
102 | reload();
103 | },
104 | });
105 | })();
106 |
--------------------------------------------------------------------------------
/lib/client/log.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/no-extraneous-dependencies
2 | const loglevel = require('loglevelnext/dist/loglevelnext');
3 |
4 | const { MethodFactory } = loglevel.factories;
5 | const css = {
6 | prefix:
7 | 'color: #999; padding: 0 0 0 20px; line-height: 16px; background: url(https://webpack.js.org/6bc5d8cf78d442a984e70195db059b69.svg) no-repeat; background-size: 16px 16px; background-position: 0 -2px;',
8 | reset: 'color: #444',
9 | };
10 | const log = loglevel.getLogger({ name: 'hot', id: 'hot-middleware/client' });
11 |
12 | function IconFactory(logger) {
13 | MethodFactory.call(this, logger);
14 | }
15 |
16 | IconFactory.prototype = Object.create(MethodFactory.prototype);
17 | IconFactory.prototype.constructor = IconFactory;
18 |
19 | IconFactory.prototype.make = function make(methodName) {
20 | const og = MethodFactory.prototype.make.call(this, methodName);
21 |
22 | return function _(...params) {
23 | const args = [].concat(params);
24 | const prefix = '%c「hot」 %c';
25 | const [first] = args;
26 |
27 | if (typeof first === 'string') {
28 | args[0] = prefix + first;
29 | } else {
30 | args.unshift(prefix);
31 | }
32 |
33 | args.splice(1, 0, css.prefix, css.reset);
34 | og(...args);
35 | };
36 | };
37 |
38 | log.factory = new IconFactory(log, {});
39 |
40 | log.group = console.group; // eslint-disable-line no-console
41 | log.groupCollapsed = console.groupCollapsed; // eslint-disable-line no-console
42 | log.groupEnd = console.groupEnd; // eslint-disable-line no-console
43 |
44 | module.exports = log;
45 |
--------------------------------------------------------------------------------
/lib/client/socket.js:
--------------------------------------------------------------------------------
1 | const url = require('url');
2 |
3 | const log = require('./log');
4 |
5 | const maxRetries = 10;
6 | let retry = maxRetries;
7 |
8 | module.exports = function connect(options, handler) {
9 | const { host } = options.webSocket;
10 | const socketUrl = url.format({
11 | protocol: options.https ? 'wss' : 'ws',
12 | hostname: host === '*' ? window.location.hostname : host,
13 | port: options.webSocket.port,
14 | slashes: true,
15 | });
16 |
17 | let open = false;
18 | let socket = new WebSocket(socketUrl);
19 |
20 | socket.addEventListener('open', () => {
21 | open = true;
22 | retry = maxRetries;
23 | log.info('WebSocket connected');
24 | });
25 |
26 | socket.addEventListener('close', () => {
27 | log.warn('WebSocket closed');
28 |
29 | open = false;
30 | socket = null;
31 |
32 | // exponentation operator ** isn't supported by IE at all
33 | const timeout =
34 | // eslint-disable-next-line no-restricted-properties
35 | 1000 * Math.pow(maxRetries - retry, 2) + Math.random() * 100;
36 |
37 | if (open || retry <= 0) {
38 | log.warn(`WebSocket: ending reconnect after ${maxRetries} attempts`);
39 | return;
40 | }
41 |
42 | log.info(
43 | `WebSocket: attempting reconnect in ${parseInt(timeout / 1000, 10)}s`
44 | );
45 |
46 | setTimeout(() => {
47 | retry -= 1;
48 |
49 | connect(
50 | options,
51 | handler
52 | );
53 | }, timeout);
54 | });
55 |
56 | socket.addEventListener('message', (event) => {
57 | log.debug('WebSocket: message:', event.data);
58 |
59 | const message = JSON.parse(event.data);
60 |
61 | if (handler[message.type]) {
62 | handler[message.type](message.data);
63 | }
64 | });
65 | };
66 |
--------------------------------------------------------------------------------
/lib/compiler.js:
--------------------------------------------------------------------------------
1 | const ParserHelpers = require('webpack/lib/ParserHelpers');
2 | const stringify = require('json-stringify-safe');
3 | const uuid = require('uuid/v4');
4 | const { DefinePlugin, HotModuleReplacementPlugin } = require('webpack');
5 |
6 | const HotClientError = require('./HotClientError');
7 |
8 | function addEntry(entry, compilerName, options) {
9 | const clientEntry = [`webpack-hot-client/client?${compilerName || uuid()}`];
10 | let newEntry = {};
11 |
12 | if (!Array.isArray(entry) && typeof entry === 'object') {
13 | const keys = Object.keys(entry);
14 | const [first] = keys;
15 |
16 | for (const entryName of keys) {
17 | if (options.allEntries) {
18 | newEntry[entryName] = clientEntry.concat(entry[entryName]);
19 | } else if (entryName === first) {
20 | newEntry[first] = clientEntry.concat(entry[first]);
21 | } else {
22 | newEntry[entryName] = entry[entryName];
23 | }
24 | }
25 | } else {
26 | newEntry = clientEntry.concat(entry);
27 | }
28 |
29 | return newEntry;
30 | }
31 |
32 | function hotEntry(compiler, options) {
33 | if (!options.validTargets.includes(compiler.options.target)) {
34 | return false;
35 | }
36 |
37 | const { entry } = compiler.options;
38 | const { name } = compiler;
39 | let newEntry;
40 |
41 | if (typeof entry === 'function') {
42 | /* istanbul ignore next */
43 | // TODO: run the build in tests and examine the output
44 | newEntry = function enter(...args) {
45 | // the entry result from the original entry function in the config
46 | let result = entry(...args);
47 |
48 | validateEntry(result);
49 |
50 | result = addEntry(result, name, options);
51 |
52 | return result;
53 | };
54 | } else {
55 | newEntry = addEntry(entry, name, options);
56 | }
57 |
58 | compiler.hooks.entryOption.call(compiler.options.context, newEntry);
59 |
60 | return true;
61 | }
62 |
63 | function hotPlugin(compiler) {
64 | const hmrPlugin = new HotModuleReplacementPlugin();
65 |
66 | /* istanbul ignore next */
67 | compiler.hooks.compilation.tap(
68 | 'HotModuleReplacementPlugin',
69 | (compilation, { normalModuleFactory }) => {
70 | const handler = (parser) => {
71 | parser.hooks.evaluateIdentifier.for('module.hot').tap(
72 | {
73 | name: 'HotModuleReplacementPlugin',
74 | before: 'NodeStuffPlugin',
75 | },
76 | (expr) =>
77 | ParserHelpers.evaluateToIdentifier(
78 | 'module.hot',
79 | !!parser.state.compilation.hotUpdateChunkTemplate
80 | )(expr)
81 |
82 | );
83 | };
84 |
85 | normalModuleFactory.hooks.parser
86 | .for('javascript/auto')
87 | .tap('HotModuleReplacementPlugin', handler);
88 | normalModuleFactory.hooks.parser
89 | .for('javascript/dynamic')
90 | .tap('HotModuleReplacementPlugin', handler);
91 | }
92 | );
93 |
94 | hmrPlugin.apply(compiler);
95 | }
96 |
97 | function validateEntry(entry) {
98 | const type = typeof entry;
99 | const isArray = Array.isArray(entry);
100 |
101 | if (type !== 'function') {
102 | if (!isArray && type !== 'object') {
103 | throw new TypeError(
104 | 'webpack-hot-client: The value of `entry` must be an Array, Object, or Function. Please check your webpack config.'
105 | );
106 | }
107 |
108 | if (!isArray && type === 'object') {
109 | for (const key of Object.keys(entry)) {
110 | const value = entry[key];
111 | if (!Array.isArray(value)) {
112 | throw new TypeError(
113 | 'webpack-hot-client: `entry` Object values must be an Array or Function. Please check your webpack config.'
114 | );
115 | }
116 | }
117 | }
118 | }
119 |
120 | return true;
121 | }
122 |
123 | module.exports = {
124 | addEntry,
125 | hotEntry,
126 | hotPlugin,
127 | validateEntry,
128 |
129 | modifyCompiler(compiler, options) {
130 | for (const comp of [].concat(compiler.compilers || compiler)) {
131 | // since there's a baffling lack of means to un-tap a hook, we have to
132 | // keep track of a flag, per compiler indicating whether or not we should
133 | // add a DefinePlugin before each compile.
134 | comp.hotClient = { optionsDefined: false };
135 |
136 | comp.hooks.beforeCompile.tap('WebpackHotClient', () => {
137 | if (!comp.hotClient.optionsDefined) {
138 | comp.hotClient.optionsDefined = true;
139 |
140 | // we use the DefinePlugin to inject hot-client options into the
141 | // client script. we only want this to happen once per compiler. we
142 | // have to do it in a hook, since the port may not be available before
143 | // the server has finished listening. compiler's shouldn't be run
144 | // until setup in hot-client is complete.
145 | const definePlugin = new DefinePlugin({
146 | __hotClientOptions__: stringify(options),
147 | });
148 | options.log.info('Applying DefinePlugin:__hotClientOptions__');
149 | definePlugin.apply(comp);
150 | }
151 | });
152 |
153 | if (options.autoConfigure) {
154 | hotEntry(comp, options);
155 | hotPlugin(comp);
156 | }
157 | }
158 | },
159 |
160 | validateCompiler(compiler) {
161 | for (const comp of [].concat(compiler.compilers || compiler)) {
162 | const { entry, plugins } = comp.options;
163 | validateEntry(entry);
164 |
165 | const pluginExists = (plugins || []).some(
166 | (plugin) => plugin instanceof HotModuleReplacementPlugin
167 | );
168 |
169 | if (pluginExists) {
170 | throw new HotClientError(
171 | 'HotModuleReplacementPlugin is automatically added to compilers. Please remove instances from your config before proceeding, or use the `autoConfigure: false` option.'
172 | );
173 | }
174 | }
175 |
176 | return true;
177 | },
178 | };
179 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | const getOptions = require('./options');
2 | const { getServer, payload } = require('./socket-server');
3 | const { modifyCompiler, validateCompiler } = require('./compiler');
4 |
5 | module.exports = (compiler, opts) => {
6 | const options = getOptions(opts);
7 | const { log } = options;
8 |
9 | if (options.autoConfigure) {
10 | validateCompiler(compiler);
11 | }
12 |
13 | /* istanbul ignore if */
14 | if (options.host.client !== options.host.server) {
15 | log.warn(
16 | '`host.client` does not match `host.server`. This can cause unpredictable behavior in the browser.'
17 | );
18 | }
19 |
20 | const server = getServer(options);
21 |
22 | modifyCompiler(compiler, options);
23 |
24 | const compile = (comp) => {
25 | const compilerName = comp.name || '';
26 | options.stats = null;
27 | log.info('webpack: Compiling...');
28 | server.broadcast(payload('compile', { compilerName }));
29 | };
30 |
31 | const done = (result) => {
32 | log.info('webpack: Compiling Done');
33 | options.stats = result;
34 |
35 | const jsonStats = options.stats.toJson(options.stats);
36 |
37 | /* istanbul ignore if */
38 | if (!jsonStats) {
39 | options.log.error('compiler done: `stats` is undefined');
40 | }
41 |
42 | server.send(jsonStats);
43 | };
44 |
45 | const invalid = (filePath, comp) => {
46 | const context = comp.context || comp.options.context || process.cwd();
47 | const fileName = (filePath || '')
48 | .replace(context, '')
49 | .substring(1);
50 | log.info('webpack: Bundle Invalidated');
51 | server.broadcast(payload('invalid', { fileName }));
52 | };
53 |
54 | // as of webpack@4 MultiCompiler no longer exports the compile hook
55 | const compilers = compiler.compilers || [compiler];
56 | for (const comp of compilers) {
57 | comp.hooks.compile.tap('WebpackHotClient', () => {
58 | compile(comp);
59 | });
60 |
61 | // we need the compiler object reference here, otherwise we'd let the
62 | // MultiHook do it's thing in a MultiCompiler situation.
63 | comp.hooks.invalid.tap('WebpackHotClient', (filePath) => {
64 | invalid(filePath, comp);
65 | });
66 | }
67 |
68 | compiler.hooks.done.tap('WebpackHotClient', done);
69 |
70 | return {
71 | close: server.close,
72 | options: Object.freeze(Object.assign({}, options)),
73 | server,
74 | };
75 | };
76 |
--------------------------------------------------------------------------------
/lib/options.js:
--------------------------------------------------------------------------------
1 | const { Server: HttpsServer } = require('https');
2 |
3 | const validate = require('@webpack-contrib/schema-utils');
4 | const merge = require('merge-options').bind({ concatArrays: true });
5 | const weblog = require('webpack-log');
6 |
7 | const schema = require('../schemas/options.json');
8 |
9 | const HotClientError = require('./HotClientError');
10 |
11 | const defaults = {
12 | allEntries: false,
13 | autoConfigure: true,
14 | host: 'localhost',
15 | hmr: true,
16 | // eslint-disable-next-line no-undefined
17 | https: undefined,
18 | logLevel: 'info',
19 | logTime: false,
20 | port: 0,
21 | reload: true,
22 | send: {
23 | errors: true,
24 | warnings: true,
25 | },
26 | server: null,
27 | stats: {
28 | context: process.cwd(),
29 | },
30 | validTargets: ['web'],
31 | test: false,
32 | };
33 |
34 | module.exports = (opts = {}) => {
35 | validate({ name: 'webpack-hot-client', schema, target: opts });
36 |
37 | const options = merge({}, defaults, opts);
38 | const log = weblog({
39 | name: 'hot',
40 | id: options.test ? null : 'webpack-hot-client',
41 | level: options.logLevel,
42 | timestamp: options.logTime,
43 | });
44 |
45 | options.log = log;
46 |
47 | if (typeof options.host === 'string') {
48 | options.host = {
49 | client: options.host,
50 | server: options.host,
51 | };
52 | } else if (!options.host.server) {
53 | throw new HotClientError(
54 | '`host.server` must be defined when setting host to an Object'
55 | );
56 | } else if (!options.host.client) {
57 | throw new HotClientError(
58 | '`host.client` must be defined when setting host to an Object'
59 | );
60 | }
61 |
62 | if (typeof options.port === 'number') {
63 | options.port = {
64 | client: options.port,
65 | server: options.port,
66 | };
67 | } else if (isNaN(parseInt(options.port.server, 10))) {
68 | throw new HotClientError(
69 | '`port.server` must be defined when setting host to an Object'
70 | );
71 | } else if (isNaN(parseInt(options.port.client, 10))) {
72 | throw new HotClientError(
73 | '`port.client` must be defined when setting host to an Object'
74 | );
75 | }
76 |
77 | const { server } = options;
78 |
79 | if (
80 | server &&
81 | server instanceof HttpsServer &&
82 | typeof options.https === 'undefined'
83 | ) {
84 | options.https = true;
85 | }
86 |
87 | if (server && server.listening) {
88 | options.webSocket = {
89 | host: server.address().address,
90 | // a port.client value of 0 will be falsy, so it should pull the server port
91 | port: options.port.client || server.address().port,
92 | };
93 | } else {
94 | options.webSocket = {
95 | host: options.host.client,
96 | port: options.port.client,
97 | };
98 | }
99 |
100 | return options;
101 | };
102 |
--------------------------------------------------------------------------------
/lib/socket-server.js:
--------------------------------------------------------------------------------
1 | const stringify = require('json-stringify-safe');
2 | const strip = require('strip-ansi');
3 | const WebSocket = require('ws');
4 |
5 | function getServer(options) {
6 | const { host, log, port, server } = options;
7 | const wssOptions = server
8 | ? { server }
9 | : { host: host.server, port: port.server };
10 |
11 | if (server && !server.listening) {
12 | server.listen(port.server, host.server);
13 | }
14 |
15 | const wss = new WebSocket.Server(wssOptions);
16 |
17 | onConnection(wss, options);
18 | onError(wss, options);
19 | onListening(wss, options);
20 |
21 | const broadcast = (data) => {
22 | wss.clients.forEach((client) => {
23 | if (client.readyState === WebSocket.OPEN) {
24 | client.send(data);
25 | }
26 | });
27 | };
28 |
29 | const ogClose = wss.close;
30 | const close = (callback) => {
31 | try {
32 | ogClose.call(wss, callback);
33 | } catch (err) {
34 | /* istanbul ignore next */
35 | log.error(err);
36 | }
37 | };
38 |
39 | const send = sendData.bind(null, wss, options);
40 |
41 | return Object.assign(wss, { broadcast, close, send });
42 | }
43 |
44 | function onConnection(server, options) {
45 | const { log } = options;
46 |
47 | server.on('connection', (socket) => {
48 | log.info('WebSocket Client Connected');
49 |
50 | socket.on('error', (err) => {
51 | /* istanbul ignore next */
52 | if (err.errno !== 'ECONNRESET') {
53 | log.warn('client socket error', JSON.stringify(err));
54 | }
55 | });
56 |
57 | socket.on('message', (data) => {
58 | const message = JSON.parse(data);
59 |
60 | if (message.type === 'broadcast') {
61 | for (const client of server.clients) {
62 | if (client.readyState === WebSocket.OPEN) {
63 | client.send(stringify(message.data));
64 | }
65 | }
66 | }
67 | });
68 |
69 | // only send stats to newly connected clients, if no previous clients have
70 | // connected and stats has been modified by webpack
71 | if (options.stats && options.stats.toJson && server.clients.size === 1) {
72 | const jsonStats = options.stats.toJson(options.stats);
73 |
74 | /* istanbul ignore if */
75 | if (!jsonStats) {
76 | options.log.error('Client Connection: `stats` is undefined');
77 | }
78 |
79 | server.send(jsonStats);
80 | }
81 | });
82 | }
83 |
84 | function onError(server, options) {
85 | const { log } = options;
86 |
87 | server.on('error', (err) => {
88 | /* istanbul ignore next */
89 | log.error('WebSocket Server Error', err);
90 | });
91 | }
92 |
93 | function onListening(server, options) {
94 | /* eslint-disable no-underscore-dangle, no-param-reassign */
95 | const { host, log } = options;
96 |
97 | if (options.server && options.server.listening) {
98 | const addr = options.server.address();
99 | server.host = addr.address;
100 | server.port = addr.port;
101 | log.info(`WebSocket Server Attached to ${addr.address}:${addr.port}`);
102 | } else {
103 | server.on('listening', () => {
104 | const { address, port } = server._server.address();
105 | server.host = address;
106 | server.port = port;
107 | // a port.client value of 0 will be falsy, so it should pull the server port
108 | options.webSocket.port = options.port.client || port;
109 |
110 | log.info(`WebSocket Server Listening on ${host.server}:${port}`);
111 | });
112 | }
113 | }
114 |
115 | function payload(type, data) {
116 | return stringify({ type, data });
117 | }
118 |
119 | function sendData(server, options, stats) {
120 | const send = (type, data) => {
121 | server.broadcast(payload(type, data));
122 | };
123 |
124 | if (stats.errors && stats.errors.length > 0) {
125 | if (options.send.errors) {
126 | const errors = [].concat(stats.errors).map((error) => strip(error));
127 | send('errors', { errors });
128 | }
129 | return;
130 | }
131 |
132 | if (stats.assets && stats.assets.every((asset) => !asset.emitted)) {
133 | send('no-change');
134 | return;
135 | }
136 |
137 | const { hash, warnings } = stats;
138 |
139 | send('hash', { hash });
140 |
141 | if (warnings.length > 0) {
142 | if (options.send.warnings) {
143 | send('warnings', { warnings });
144 | }
145 | } else {
146 | send('ok', { hash });
147 | }
148 | }
149 |
150 | module.exports = {
151 | getServer,
152 | payload,
153 | };
154 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webpack-hot-client",
3 | "version": "4.1.1",
4 | "description": "A client for enabling, and interacting with, webpack Hot Module Replacement",
5 | "license": "MIT",
6 | "repository": "webpack-contrib/webpack-hot-client",
7 | "author": "Andrew Powell ",
8 | "homepage": "https://github.com/webpack-contrib/webpack-hot-client",
9 | "bugs": "https://github.com/webpack-contrib/webpack-hot-client/issues",
10 | "bin": "",
11 | "main": "lib/index.js",
12 | "engines": {
13 | "node": ">= 6.9.0 < 7.0.0 || >= 8.9.0"
14 | },
15 | "scripts": {
16 | "build:client": "babel lib/client --out-dir client",
17 | "commitlint": "commitlint",
18 | "commitmsg": "commitlint -e $GIT_PARAMS",
19 | "lint": "eslint --cache src test",
20 | "ci:lint:commits": "commitlint --from=${CIRCLE_BRANCH} --to=${CIRCLE_SHA1}",
21 | "lint-staged": "lint-staged",
22 | "prepublishOnly": "npm run build:client",
23 | "release": "standard-version",
24 | "release:ci": "conventional-github-releaser -p angular",
25 | "release:validate": "commitlint --from=$(git describe --tags --abbrev=0) --to=$(git rev-parse HEAD)",
26 | "security": "nsp check",
27 | "test": "NODE_TLS_REJECT_UNAUTHORIZED=0 npm run build:client && jest",
28 | "test:watch": "jest --watch",
29 | "test:coverage": "NODE_TLS_REJECT_UNAUTHORIZED=0 npm run build:client && jest --collectCoverageFrom='lib/*.js' --coverage",
30 | "ci:lint": "npm run lint && npm run security",
31 | "ci:test": "NODE_TLS_REJECT_UNAUTHORIZED=0 npm run test -- --runInBand",
32 | "ci:coverage": "NODE_TLS_REJECT_UNAUTHORIZED=0 npm run test:coverage -- --runInBand",
33 | "defaults": "defaults"
34 | },
35 | "files": [
36 | "client/",
37 | "lib/compiler.js",
38 | "lib/HotClientError.js",
39 | "lib/index.js",
40 | "lib/options.js",
41 | "lib/socket-server.js",
42 | "schemas/",
43 | "LICENSE",
44 | "README.md"
45 | ],
46 | "peerDependencies": {
47 | "webpack": "^4.0.0"
48 | },
49 | "dependencies": {
50 | "@webpack-contrib/schema-utils": "^1.0.0-beta.0",
51 | "json-stringify-safe": "^5.0.1",
52 | "loglevelnext": "^1.0.2",
53 | "merge-options": "^1.0.1",
54 | "strip-ansi": "^4.0.0",
55 | "uuid": "^3.1.0",
56 | "webpack-log": "^1.1.1",
57 | "ws": "^4.0.0"
58 | },
59 | "devDependencies": {
60 | "@babel/cli": "^7.0.0-beta.49",
61 | "@babel/core": "^7.0.0-beta.49",
62 | "@babel/polyfill": "^7.0.0-beta.49",
63 | "@babel/preset-env": "^7.0.0-beta.49",
64 | "@babel/register": "^7.0.0-beta.49",
65 | "@commitlint/cli": "^6.2.0",
66 | "@commitlint/config-conventional": "^6.1.3",
67 | "@webpack-contrib/defaults": "^2.4.0",
68 | "@webpack-contrib/eslint-config-webpack": "^2.0.4",
69 | "ansi-regex": "^3.0.0",
70 | "babel-jest": "^23.0.1",
71 | "codecov": "^3.0.0",
72 | "conventional-github-releaser": "^3.0.0",
73 | "cross-env": "^5.1.6",
74 | "del-cli": "^1.1.0",
75 | "eslint": "^4.6.1",
76 | "eslint-config-webpack": "^1.2.5",
77 | "eslint-plugin-import": "^2.8.0",
78 | "eslint-plugin-prettier": "^2.6.0",
79 | "expect": "^22.4.3",
80 | "husky": "^0.14.3",
81 | "jest": "^23.0.1",
82 | "jest-serializer-path": "^0.1.15",
83 | "killable": "^1.0.0",
84 | "lint-staged": "^7.1.2",
85 | "loud-rejection": "^1.6.0",
86 | "memory-fs": "^0.4.1",
87 | "mocha": "^5.0.0",
88 | "nsp": "^3.2.1",
89 | "nyc": "^11.4.1",
90 | "pre-commit": "^1.2.2",
91 | "prettier": "^1.13.2",
92 | "sinon": "^5.0.10",
93 | "standard-version": "^4.4.0",
94 | "time-fix-plugin": "^2.0.0",
95 | "touch": "^3.1.0",
96 | "webpack": "^4.0.1"
97 | },
98 | "keywords": [
99 | "webpack"
100 | ],
101 | "jest": {
102 | "snapshotSerializers": [
103 | "jest-serializer-path"
104 | ],
105 | "testEnvironment": "node"
106 | },
107 | "pre-commit": "lint-staged",
108 | "lint-staged": {
109 | "*.js": [
110 | "eslint --fix",
111 | "git add"
112 | ]
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/schemas/options.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "properties": {
4 | "allEntries": {
5 | "type": "boolean"
6 | },
7 | "autoConfigure": {
8 | "type": "boolean"
9 | },
10 | "host": {
11 | "anyOf": [
12 | {
13 | "type": "string"
14 | },
15 | {
16 | "properties": {
17 | "client": {
18 | "type": "string"
19 | },
20 | "server": {
21 | "type": "string"
22 | }
23 | },
24 | "type": "object"
25 | }
26 | ]
27 | },
28 | "hmr": {
29 | "type": "boolean"
30 | },
31 | "hot": {
32 | "type": "boolean"
33 | },
34 | "https": {
35 | "type": "boolean"
36 | },
37 | "logLevel": {
38 | "type": "string"
39 | },
40 | "logTime": {
41 | "type": "boolean"
42 | },
43 | "port": {
44 | "anyOf": [
45 | {
46 | "type": "integer"
47 | },
48 | {
49 | "properties": {
50 | "client": {
51 | "type": "integer"
52 | },
53 | "server": {
54 | "type": "integer"
55 | }
56 | },
57 | "type": "object"
58 | }
59 | ]
60 | },
61 | "reload": {
62 | "type": "boolean"
63 | },
64 | "send": {
65 | "properties": {
66 | "errors": {
67 | "type": "boolean"
68 | },
69 | "warnings": {
70 | "type": "boolean"
71 | }
72 | }
73 | },
74 | "server": {
75 | "additionalProperties": true,
76 | "type": "object"
77 | },
78 | "stats": {
79 | "additionalProperties": true,
80 | "type": "object"
81 | },
82 | "test": {
83 | "type": "boolean"
84 | },
85 | "validTargets": {
86 | "type": "array",
87 | "items": {
88 | "type": "string"
89 | }
90 | }
91 | },
92 | "additionalProperties": false
93 | }
94 |
--------------------------------------------------------------------------------
/test/__snapshots__/compiler.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`compiler addEntry: array 1`] = `
4 | Array [
5 | "webpack-hot-client/client?test",
6 | "index.js",
7 | ]
8 | `;
9 |
10 | exports[`compiler addEntry: array 2`] = `
11 | Array [
12 | "webpack-hot-client/client?test",
13 | "index.js",
14 | ]
15 | `;
16 |
17 | exports[`compiler addEntry: array 3`] = `
18 | Array [
19 | "webpack-hot-client/client?test",
20 | "index.js",
21 | ]
22 | `;
23 |
24 | exports[`compiler addEntry: object 1`] = `
25 | Object {
26 | "a": Array [
27 | "webpack-hot-client/client?test",
28 | "index-a.js",
29 | ],
30 | "b": Array [
31 | "index-b.js",
32 | ],
33 | }
34 | `;
35 |
36 | exports[`compiler addEntry: object, allEntries: true 1`] = `
37 | Object {
38 | "a": Array [
39 | "webpack-hot-client/client?test",
40 | "index-a.js",
41 | ],
42 | "b": Array [
43 | "webpack-hot-client/client?test",
44 | "index-b.js",
45 | ],
46 | }
47 | `;
48 |
--------------------------------------------------------------------------------
/test/__snapshots__/index.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`api mismatched client/server options sanity check 1`] = `
4 | Object {
5 | "host": "0.0.0.0",
6 | "port": 7000,
7 | }
8 | `;
9 |
10 | exports[`api mismatched client/server options sanity check 2`] = `
11 | Object {
12 | "allEntries": false,
13 | "autoConfigure": true,
14 | "hmr": true,
15 | "host": Object {
16 | "client": "localhost",
17 | "server": "0.0.0.0",
18 | },
19 | "https": undefined,
20 | "log": LogLevel {
21 | "currentLevel": 5,
22 | "debug": [Function],
23 | "error": [Function],
24 | "info": [Function],
25 | "log": [Function],
26 | "methodFactory": PrefixFactory {
27 | "options": Object {
28 | "level": [Function],
29 | "name": [Function],
30 | "template": "{{level}} [90m「{{name}}」[39m: ",
31 | "time": [Function],
32 | },
33 | Symbol(a log instance): [Circular],
34 | Symbol(valid log levels): Object {
35 | "DEBUG": 1,
36 | "ERROR": 4,
37 | "INFO": 2,
38 | "SILENT": 5,
39 | "TRACE": 0,
40 | "WARN": 3,
41 | },
42 | },
43 | "name": "hot",
44 | "options": Object {
45 | "factory": null,
46 | "level": "silent",
47 | "name": "hot",
48 | "prefix": Object {
49 | "level": [Function],
50 | "template": "{{level}} [90m「{{name}}」[39m: ",
51 | },
52 | "timestamp": false,
53 | "unique": true,
54 | },
55 | "trace": [Function],
56 | "type": "LogLevel",
57 | "warn": [Function],
58 | },
59 | "logLevel": "silent",
60 | "logTime": false,
61 | "port": Object {
62 | "client": 6000,
63 | "server": 7000,
64 | },
65 | "reload": true,
66 | "send": Object {
67 | "errors": true,
68 | "warnings": true,
69 | },
70 | "server": null,
71 | "stats": Object {
72 | "context": "",
73 | },
74 | "test": false,
75 | "validTargets": Array [
76 | "web",
77 | ],
78 | "webSocket": Object {
79 | "host": "localhost",
80 | "port": 6000,
81 | },
82 | }
83 | `;
84 |
85 | exports[`api options sanity check 1`] = `
86 | Object {
87 | "host": "127.0.0.1",
88 | "port": Any,
89 | }
90 | `;
91 |
92 | exports[`api options sanity check 2`] = `
93 | Object {
94 | "allEntries": false,
95 | "autoConfigure": true,
96 | "hmr": true,
97 | "host": Object {
98 | "client": "localhost",
99 | "server": "localhost",
100 | },
101 | "https": undefined,
102 | "log": LogLevel {
103 | "currentLevel": 5,
104 | "debug": [Function],
105 | "error": [Function],
106 | "info": [Function],
107 | "log": [Function],
108 | "methodFactory": PrefixFactory {
109 | "options": Object {
110 | "level": [Function],
111 | "name": [Function],
112 | "template": "{{level}} [90m「{{name}}」[39m: ",
113 | "time": [Function],
114 | },
115 | Symbol(a log instance): [Circular],
116 | Symbol(valid log levels): Object {
117 | "DEBUG": 1,
118 | "ERROR": 4,
119 | "INFO": 2,
120 | "SILENT": 5,
121 | "TRACE": 0,
122 | "WARN": 3,
123 | },
124 | },
125 | "name": "hot",
126 | "options": Object {
127 | "factory": null,
128 | "level": "silent",
129 | "name": "hot",
130 | "prefix": Object {
131 | "level": [Function],
132 | "template": "{{level}} [90m「{{name}}」[39m: ",
133 | },
134 | "timestamp": false,
135 | "unique": true,
136 | },
137 | "trace": [Function],
138 | "type": "LogLevel",
139 | "warn": [Function],
140 | },
141 | "logLevel": "silent",
142 | "logTime": false,
143 | "port": Object {
144 | "client": 0,
145 | "server": 0,
146 | },
147 | "reload": true,
148 | "send": Object {
149 | "errors": true,
150 | "warnings": true,
151 | },
152 | "server": null,
153 | "stats": Object {
154 | "context": "",
155 | },
156 | "test": false,
157 | "validTargets": Array [
158 | "web",
159 | ],
160 | "webSocket": Object {
161 | "port": Any,
162 | },
163 | }
164 | `;
165 |
--------------------------------------------------------------------------------
/test/__snapshots__/options.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`options altered options 1`] = `
4 | Object {
5 | "allEntries": true,
6 | "autoConfigure": false,
7 | "hmr": false,
8 | "host": Object {
9 | "client": "localhost",
10 | "server": "localhost",
11 | },
12 | "https": true,
13 | "log": LogLevel {
14 | "currentLevel": 0,
15 | "debug": [Function],
16 | "error": [Function],
17 | "info": [Function],
18 | "log": [Function],
19 | "methodFactory": PrefixFactory {
20 | "options": Object {
21 | "level": [Function],
22 | "name": [Function],
23 | "template": "[{{time}}] {{level}} [90m「{{name}}」[39m: ",
24 | "time": [Function],
25 | },
26 | Symbol(a log instance): [Circular],
27 | Symbol(valid log levels): Object {
28 | "DEBUG": 1,
29 | "ERROR": 4,
30 | "INFO": 2,
31 | "SILENT": 5,
32 | "TRACE": 0,
33 | "WARN": 3,
34 | },
35 | },
36 | "name": "hot",
37 | "options": Object {
38 | "factory": null,
39 | "level": "trace",
40 | "name": "hot",
41 | "prefix": Object {
42 | "level": [Function],
43 | "template": "[{{time}}] {{level}} [90m「{{name}}」[39m: ",
44 | },
45 | "timestamp": true,
46 | "unique": true,
47 | },
48 | "trace": [Function],
49 | "type": "LogLevel",
50 | "warn": [Function],
51 | },
52 | "logLevel": "trace",
53 | "logTime": true,
54 | "port": Object {
55 | "client": 0,
56 | "server": 0,
57 | },
58 | "reload": false,
59 | "send": Object {
60 | "errors": false,
61 | "warnings": false,
62 | },
63 | "server": null,
64 | "stats": Object {
65 | "context": "/",
66 | },
67 | "test": true,
68 | "validTargets": Array [
69 | "web",
70 | "batman",
71 | ],
72 | "webSocket": Object {
73 | "host": "localhost",
74 | "port": 0,
75 | },
76 | }
77 | `;
78 |
79 | exports[`options defaults 1`] = `
80 | Object {
81 | "allEntries": false,
82 | "autoConfigure": true,
83 | "hmr": true,
84 | "host": Object {
85 | "client": "localhost",
86 | "server": "localhost",
87 | },
88 | "https": undefined,
89 | "log": LogLevel {
90 | "currentLevel": 2,
91 | "debug": [Function],
92 | "error": [Function],
93 | "info": [Function],
94 | "log": [Function],
95 | "methodFactory": PrefixFactory {
96 | "options": Object {
97 | "level": [Function],
98 | "name": [Function],
99 | "template": "{{level}} [90m「{{name}}」[39m: ",
100 | "time": [Function],
101 | },
102 | Symbol(a log instance): [Circular],
103 | Symbol(valid log levels): Object {
104 | "DEBUG": 1,
105 | "ERROR": 4,
106 | "INFO": 2,
107 | "SILENT": 5,
108 | "TRACE": 0,
109 | "WARN": 3,
110 | },
111 | },
112 | "name": "hot",
113 | "options": Object {
114 | "factory": null,
115 | "level": "info",
116 | "name": "hot",
117 | "prefix": Object {
118 | "level": [Function],
119 | "template": "{{level}} [90m「{{name}}」[39m: ",
120 | },
121 | "timestamp": false,
122 | "unique": true,
123 | },
124 | "trace": [Function],
125 | "type": "LogLevel",
126 | "warn": [Function],
127 | },
128 | "logLevel": "info",
129 | "logTime": false,
130 | "port": Object {
131 | "client": 0,
132 | "server": 0,
133 | },
134 | "reload": true,
135 | "send": Object {
136 | "errors": true,
137 | "warnings": true,
138 | },
139 | "server": null,
140 | "stats": Object {
141 | "context": StringMatching /\\(webpack-hot-client\\|project\\)\\$/,
142 | },
143 | "test": false,
144 | "validTargets": Array [
145 | "web",
146 | ],
147 | "webSocket": Object {
148 | "host": "localhost",
149 | "port": 0,
150 | },
151 | }
152 | `;
153 |
154 | exports[`options https.Server 1`] = `
155 | Object {
156 | "allEntries": false,
157 | "autoConfigure": true,
158 | "hmr": true,
159 | "host": Object {
160 | "client": "localhost",
161 | "server": "localhost",
162 | },
163 | "https": true,
164 | "log": LogLevel {
165 | "currentLevel": 2,
166 | "debug": [Function],
167 | "error": [Function],
168 | "info": [Function],
169 | "log": [Function],
170 | "methodFactory": PrefixFactory {
171 | "options": Object {
172 | "level": [Function],
173 | "name": [Function],
174 | "template": "{{level}} [90m「{{name}}」[39m: ",
175 | "time": [Function],
176 | },
177 | Symbol(a log instance): [Circular],
178 | Symbol(valid log levels): Object {
179 | "DEBUG": 1,
180 | "ERROR": 4,
181 | "INFO": 2,
182 | "SILENT": 5,
183 | "TRACE": 0,
184 | "WARN": 3,
185 | },
186 | },
187 | "name": "hot",
188 | "options": Object {
189 | "factory": null,
190 | "level": "info",
191 | "name": "hot",
192 | "prefix": Object {
193 | "level": [Function],
194 | "template": "[{{time}}] {{level}} [90m「{{name}}」[39m: ",
195 | },
196 | "timestamp": false,
197 | "unique": true,
198 | },
199 | "trace": [Function],
200 | "type": "LogLevel",
201 | "warn": [Function],
202 | },
203 | "logLevel": "info",
204 | "logTime": false,
205 | "port": Object {
206 | "client": 0,
207 | "server": 0,
208 | },
209 | "reload": true,
210 | "send": Object {
211 | "errors": true,
212 | "warnings": true,
213 | },
214 | "server": Any,
215 | "stats": Object {
216 | "context": "",
217 | },
218 | "test": false,
219 | "validTargets": Array [
220 | "web",
221 | ],
222 | "webSocket": Object {
223 | "host": "::",
224 | "port": Any,
225 | },
226 | }
227 | `;
228 |
229 | exports[`options https: false, https.Server 1`] = `
230 | Object {
231 | "allEntries": false,
232 | "autoConfigure": true,
233 | "hmr": true,
234 | "host": Object {
235 | "client": "localhost",
236 | "server": "localhost",
237 | },
238 | "https": false,
239 | "log": LogLevel {
240 | "currentLevel": 2,
241 | "debug": [Function],
242 | "error": [Function],
243 | "info": [Function],
244 | "log": [Function],
245 | "methodFactory": PrefixFactory {
246 | "options": Object {
247 | "level": [Function],
248 | "name": [Function],
249 | "template": "{{level}} [90m「{{name}}」[39m: ",
250 | "time": [Function],
251 | },
252 | Symbol(a log instance): [Circular],
253 | Symbol(valid log levels): Object {
254 | "DEBUG": 1,
255 | "ERROR": 4,
256 | "INFO": 2,
257 | "SILENT": 5,
258 | "TRACE": 0,
259 | "WARN": 3,
260 | },
261 | },
262 | "name": "hot",
263 | "options": Object {
264 | "factory": null,
265 | "level": "info",
266 | "name": "hot",
267 | "prefix": Object {
268 | "level": [Function],
269 | "template": "[{{time}}] {{level}} [90m「{{name}}」[39m: ",
270 | },
271 | "timestamp": false,
272 | "unique": true,
273 | },
274 | "trace": [Function],
275 | "type": "LogLevel",
276 | "warn": [Function],
277 | },
278 | "logLevel": "info",
279 | "logTime": false,
280 | "port": Object {
281 | "client": 0,
282 | "server": 0,
283 | },
284 | "reload": true,
285 | "send": Object {
286 | "errors": true,
287 | "warnings": true,
288 | },
289 | "server": Any,
290 | "stats": Object {
291 | "context": "",
292 | },
293 | "test": false,
294 | "validTargets": Array [
295 | "web",
296 | ],
297 | "webSocket": Object {
298 | "host": "::",
299 | "port": Any,
300 | },
301 | }
302 | `;
303 |
304 | exports[`options throws if port.client is missing 1`] = `"webpack-hot-client: \`port.client\` must be defined when setting host to an Object"`;
305 |
306 | exports[`options throws if port.server is missing 1`] = `"webpack-hot-client: \`port.server\` must be defined when setting host to an Object"`;
307 |
--------------------------------------------------------------------------------
/test/__snapshots__/socket-server.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`socket server broadcast 1`] = `"{\\"type\\":\\"broadcast\\",\\"data\\":{\\"received\\":true}}"`;
4 |
5 | exports[`socket server errors 1`] = `
6 | Object {
7 | "data": Object {
8 | "errors": Array [
9 | "test error",
10 | ],
11 | },
12 | "type": "errors",
13 | }
14 | `;
15 |
16 | exports[`socket server hash-ok 1`] = `
17 | Object {
18 | "data": Object {
19 | "hash": "000000",
20 | },
21 | "type": "ok",
22 | }
23 | `;
24 |
25 | exports[`socket server hash-ok 2`] = `
26 | Object {
27 | "data": Object {
28 | "warnings": Array [
29 | "test warning",
30 | ],
31 | },
32 | "type": "warnings",
33 | }
34 | `;
35 |
36 | exports[`socket server no-change 1`] = `"{\\"type\\":\\"no-change\\"}"`;
37 |
38 | exports[`socket server payload 1`] = `"{\\"type\\":\\"test\\",\\"data\\":{\\"batman\\":\\"superman \\"}}"`;
39 |
40 | exports[`socket server send via stats 1`] = `
41 | Object {
42 | "data": Object {
43 | "hash": "111111",
44 | },
45 | "type": "ok",
46 | }
47 | `;
48 |
49 | exports[`socket server socket broadcast 1`] = `"{\\"received\\":true}"`;
50 |
--------------------------------------------------------------------------------
/test/compiler.test.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 |
3 | const {
4 | addEntry,
5 | hotEntry,
6 | modifyCompiler,
7 | validateCompiler,
8 | validateEntry,
9 | } = require('../lib/compiler');
10 | const HotClientError = require('../lib/HotClientError');
11 | const getOptions = require('../lib/options');
12 |
13 | const compilerName = 'test';
14 | const options = getOptions({});
15 |
16 | // eslint-disable-next-line import/no-dynamic-require, global-require
17 | const getConfig = (name) => require(`./fixtures/webpack.config-${name}`);
18 |
19 | describe('compiler', () => {
20 | test('validateEntry: array', () => {
21 | const result = validateEntry([]);
22 | expect(result).toBe(true);
23 | });
24 |
25 | test('validateEntry: object', () => {
26 | const result = validateEntry({ a: [], b: [] });
27 | expect(result).toBe(true);
28 | });
29 |
30 | test('validateEntry: string', () => {
31 | const t = () => validateEntry('');
32 | expect(t).toThrow();
33 | });
34 |
35 | test('validateEntry: object, string', () => {
36 | const t = () => validateEntry({ a: [], b: '' });
37 | expect(t).toThrow(TypeError);
38 | });
39 |
40 | test('validateCompiler: string', () => {
41 | const t = () => validateEntry('');
42 | expect(t).toThrow(TypeError);
43 | });
44 |
45 | test('validateCompiler', () => {
46 | const config = getConfig('array');
47 | const compiler = webpack(config);
48 | const result = validateCompiler(compiler);
49 | expect(result).toBe(true);
50 | });
51 |
52 | test('validateCompiler: HotModuleReplacementPlugin', () => {
53 | const config = getConfig('invalid-plugin');
54 | const compiler = webpack(config);
55 | const t = () => validateCompiler(compiler);
56 | expect(t).toThrow(HotClientError);
57 | });
58 |
59 | test('addEntry: array', () => {
60 | const entry = ['index.js'];
61 | const entries = addEntry(entry, compilerName, options);
62 | expect(entries).toMatchSnapshot();
63 | });
64 |
65 | test('addEntry: array', () => {
66 | const entry = ['index.js'];
67 | const entries = addEntry(entry, compilerName, options);
68 | expect(entries).toMatchSnapshot();
69 | });
70 |
71 | test('addEntry: array', () => {
72 | const entry = ['index.js'];
73 | const entries = addEntry(entry, compilerName, options);
74 | expect(entries).toMatchSnapshot();
75 | });
76 |
77 | test('addEntry: object', () => {
78 | const entry = {
79 | a: ['index-a.js'],
80 | b: ['index-b.js'],
81 | };
82 | const entries = addEntry(entry, compilerName, options);
83 | expect(entries).toMatchSnapshot();
84 | });
85 |
86 | test('addEntry: object, allEntries: true', () => {
87 | const entry = {
88 | a: ['index-a.js'],
89 | b: ['index-b.js'],
90 | };
91 | const opts = getOptions({ allEntries: true });
92 | const entries = addEntry(entry, compilerName, opts);
93 | expect(entries).toMatchSnapshot();
94 | });
95 |
96 | test('hotEntry: invalid target', () => {
97 | const config = getConfig('array');
98 | config.target = 'node';
99 |
100 | const compiler = webpack(config);
101 | const result = hotEntry(compiler, options);
102 |
103 | expect(result).toBe(false);
104 | });
105 |
106 | const configTypes = ['array', 'function', 'object'];
107 |
108 | for (const configType of configTypes) {
109 | test(`modifyCompiler: ${configType}`, () => {
110 | const config = getConfig(configType);
111 | const compiler = webpack(config);
112 |
113 | // at this time we don't have a meaningful way of inspecting which plugins
114 | // have been applied to the compiler, unfortunately. the best we can do is
115 | // perform the hotEntry and hotPlugin actions and pass if there's no
116 | // exceptions thrown.
117 | modifyCompiler(compiler, options);
118 |
119 | expect(true);
120 | });
121 | }
122 | });
123 |
--------------------------------------------------------------------------------
/test/fixtures/app-clean.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console: off */
2 |
3 | require('./component');
4 |
5 | if (module.hot) {
6 | module.hot.accept((err) => {
7 | if (err) {
8 | console.error('Cannot apply HMR update.', err);
9 | }
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/test/fixtures/app.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console: off */
2 |
3 | require('./component');
4 |
5 | if (module.hot) {
6 | module.hot.accept((err) => {
7 | if (err) {
8 | console.error('Cannot apply HMR update.', err);
9 | }
10 | });
11 | }
12 |
13 | console.log('dirty');
14 | console.log('dirty');
15 | console.log('dirty');
16 | console.log('dirty');
17 | console.log('dirty');
18 |
19 | console.log('dirty');
20 |
21 | console.log('dirty');
22 | console.log('dirty');
23 | console.log('dirty');
24 |
25 | console.log('dirty');
26 | console.log('dirty');
27 | console.log('dirty');
28 | console.log('dirty');
29 | console.log('dirty');
30 | console.log('dirty');
31 |
32 | console.log('dirty');
33 | console.log('dirty');
34 | console.log('dirty');
35 |
--------------------------------------------------------------------------------
/test/fixtures/component.js:
--------------------------------------------------------------------------------
1 | /* global document */
2 |
3 | const target = document.querySelector('#target');
4 | target.innerHTML = 'component';
5 |
--------------------------------------------------------------------------------
/test/fixtures/foo.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line
2 | console.log('foo');
3 |
--------------------------------------------------------------------------------
/test/fixtures/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | yay
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/test/fixtures/multi/client.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | console.log('client');
4 |
--------------------------------------------------------------------------------
/test/fixtures/multi/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | console.log('server');
4 |
--------------------------------------------------------------------------------
/test/fixtures/multi/webpack.config.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 |
3 | module.exports = [
4 | {
5 | resolve: {
6 | alias: {
7 | 'webpack-hot-client/client': resolve(__dirname, '../../../lib/client'),
8 | },
9 | },
10 | context: __dirname,
11 | entry: [resolve(__dirname, './client.js')],
12 | mode: 'development',
13 | output: {
14 | filename: './output.client.js',
15 | path: resolve(__dirname),
16 | },
17 | },
18 | {
19 | resolve: {
20 | alias: {
21 | 'webpack-hot-client/client': resolve(__dirname, '../../../lib/client'),
22 | },
23 | },
24 | context: __dirname,
25 | entry: [resolve(__dirname, './server.js')],
26 | mode: 'development',
27 | output: {
28 | filename: './output.server.js',
29 | path: resolve(__dirname),
30 | },
31 | },
32 | ];
33 |
--------------------------------------------------------------------------------
/test/fixtures/sub/resource.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | subdirectory
7 |
8 |
9 |
--------------------------------------------------------------------------------
/test/fixtures/test-cert.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webpack-contrib/webpack-hot-client/1b7f221918217be0db7a6089fb77fffde9a973f6/test/fixtures/test-cert.pfx
--------------------------------------------------------------------------------
/test/fixtures/webpack.config-allentries.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const TimeFixPlugin = require('time-fix-plugin');
4 | const webpack = require('webpack');
5 |
6 | module.exports = {
7 | resolve: {
8 | alias: {
9 | 'webpack-hot-client/client': path.resolve(__dirname, '../../client'),
10 | },
11 | },
12 | context: __dirname,
13 | devtool: 'source-map',
14 | entry: {
15 | main: ['./app.js'],
16 | server: ['./sub/client'],
17 | },
18 | output: {
19 | filename: './output.js',
20 | path: path.resolve(__dirname),
21 | },
22 | plugins: [new webpack.NamedModulesPlugin(), new TimeFixPlugin()],
23 | };
24 |
--------------------------------------------------------------------------------
/test/fixtures/webpack.config-array.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const TimeFixPlugin = require('time-fix-plugin');
4 | const webpack = require('webpack');
5 |
6 | module.exports = {
7 | resolve: {
8 | alias: {
9 | 'webpack-hot-client/client': path.resolve(__dirname, '../../client'),
10 | },
11 | },
12 | context: __dirname,
13 | devtool: 'source-map',
14 | entry: ['./app.js'],
15 | // mode: 'development',
16 | output: {
17 | filename: './output.js',
18 | path: path.resolve(__dirname),
19 | },
20 | plugins: [new webpack.NamedModulesPlugin(), new TimeFixPlugin()],
21 | };
22 |
--------------------------------------------------------------------------------
/test/fixtures/webpack.config-function-invalid.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const TimeFixPlugin = require('time-fix-plugin');
4 | const webpack = require('webpack');
5 |
6 | module.exports = {
7 | resolve: {
8 | alias: {
9 | 'webpack-hot-client/client': path.resolve(__dirname, '../../client'),
10 | },
11 | },
12 | context: __dirname,
13 | devtool: 'source-map',
14 | entry: () => './app.js',
15 | mode: 'development',
16 | output: {
17 | filename: './output.js',
18 | path: path.resolve(__dirname),
19 | },
20 | plugins: [new webpack.NamedModulesPlugin(), new TimeFixPlugin()],
21 | };
22 |
--------------------------------------------------------------------------------
/test/fixtures/webpack.config-function.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const TimeFixPlugin = require('time-fix-plugin');
4 | const webpack = require('webpack');
5 |
6 | module.exports = {
7 | resolve: {
8 | alias: {
9 | 'webpack-hot-client/client': path.resolve(__dirname, '../../client'),
10 | },
11 | },
12 | context: __dirname,
13 | devtool: 'source-map',
14 | entry: () => ['./app.js'],
15 | mode: 'development',
16 | output: {
17 | filename: './output.js',
18 | path: path.resolve(__dirname),
19 | },
20 | plugins: [new webpack.NamedModulesPlugin(), new TimeFixPlugin()],
21 | };
22 |
--------------------------------------------------------------------------------
/test/fixtures/webpack.config-invalid-object.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const TimeFixPlugin = require('time-fix-plugin');
4 | const webpack = require('webpack');
5 |
6 | module.exports = {
7 | resolve: {
8 | alias: {
9 | 'webpack-hot-client/client': path.resolve(__dirname, '../../client'),
10 | },
11 | },
12 | context: __dirname,
13 | devtool: 'source-map',
14 | entry: { index: './app.js' },
15 | // mode: 'development',
16 | output: {
17 | filename: './output.js',
18 | path: path.resolve(__dirname),
19 | },
20 | plugins: [new webpack.NamedModulesPlugin(), new TimeFixPlugin()],
21 | };
22 |
--------------------------------------------------------------------------------
/test/fixtures/webpack.config-invalid-plugin.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const TimeFixPlugin = require('time-fix-plugin');
4 | const webpack = require('webpack');
5 |
6 | module.exports = {
7 | resolve: {
8 | alias: {
9 | 'webpack-hot-client/client': path.resolve(__dirname, '../../client'),
10 | },
11 | },
12 | context: __dirname,
13 | devtool: 'source-map',
14 | entry: [path.resolve(__dirname, './app.js')],
15 | // mode: 'development',
16 | output: {
17 | filename: 'output.js',
18 | path: path.resolve(__dirname),
19 | },
20 | plugins: [
21 | new webpack.NamedModulesPlugin(),
22 | new webpack.HotModuleReplacementPlugin(),
23 | new TimeFixPlugin(),
24 | ],
25 | };
26 |
--------------------------------------------------------------------------------
/test/fixtures/webpack.config-invalid.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const TimeFixPlugin = require('time-fix-plugin');
4 | const webpack = require('webpack');
5 |
6 | module.exports = {
7 | resolve: {
8 | alias: {
9 | 'webpack-hot-client/client': path.resolve(__dirname, '../../client'),
10 | },
11 | },
12 | context: __dirname,
13 | devtool: 'source-map',
14 | entry: './app.js',
15 | // mode: 'development',
16 | output: {
17 | filename: './output.js',
18 | path: path.resolve(__dirname),
19 | },
20 | plugins: [new webpack.NamedModulesPlugin(), new TimeFixPlugin()],
21 | };
22 |
--------------------------------------------------------------------------------
/test/fixtures/webpack.config-object.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const TimeFixPlugin = require('time-fix-plugin');
4 | const webpack = require('webpack');
5 |
6 | module.exports = {
7 | resolve: {
8 | alias: {
9 | 'webpack-hot-client/client': path.resolve(__dirname, '../../client'),
10 | },
11 | },
12 | context: __dirname,
13 | devtool: 'source-map',
14 | entry: {
15 | main: ['./app.js'],
16 | },
17 | output: {
18 | filename: './output.js',
19 | path: path.resolve(__dirname),
20 | },
21 | plugins: [new webpack.NamedModulesPlugin(), new TimeFixPlugin()],
22 | };
23 |
--------------------------------------------------------------------------------
/test/fixtures/webpack.config-watch.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | resolve: {
5 | alias: {
6 | 'webpack-hot-client/client': path.resolve(__dirname, '../../client'),
7 | },
8 | },
9 | context: __dirname,
10 | devtool: 'source-map',
11 | entry: [path.resolve(__dirname, './app.js')],
12 | mode: 'development',
13 | output: {
14 | filename: 'output.js',
15 | path: path.resolve(__dirname, 'output'),
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 |
3 | const webpack = require('webpack');
4 | const WebSocket = require('ws');
5 |
6 | const client = require('../lib');
7 |
8 | const logLevel = 'silent';
9 | const options = { logLevel };
10 | const validTypes = ['compile', 'hash', 'ok', 'warnings'];
11 |
12 | describe('api', () => {
13 | test('array config', (done) => {
14 | const config = require('./fixtures/webpack.config-array.js');
15 | const compiler = webpack(config);
16 | const { server } = client(compiler, options);
17 |
18 | server.on('listening', () => {
19 | const { host, port } = server;
20 | const socket = new WebSocket(`ws://${host}:${port}`);
21 |
22 | socket.on('message', (raw) => {
23 | const data = JSON.parse(raw);
24 |
25 | if (data.type === 'errors') {
26 | console.log(data); // eslint-disable-line no-console
27 | }
28 |
29 | expect(validTypes).toContain(data.type);
30 |
31 | if (data.type === 'hash') {
32 | server.close(done);
33 | }
34 | });
35 |
36 | socket.on('open', () => {
37 | compiler.run(() => {});
38 | });
39 | });
40 | });
41 |
42 | test('object config', (done) => {
43 | const config = require('./fixtures/webpack.config-object.js');
44 | const compiler = webpack(config);
45 | const { server } = client(compiler, options);
46 |
47 | server.on('listening', () => {
48 | const { host, port } = server;
49 | const socket = new WebSocket(`ws://${host}:${port}`);
50 |
51 | socket.on('message', (raw) => {
52 | const data = JSON.parse(raw);
53 |
54 | expect(validTypes).toContain(data.type);
55 |
56 | if (data.type === 'hash') {
57 | server.close(done);
58 | }
59 | });
60 |
61 | socket.on('open', () => {
62 | compiler.run(() => {});
63 | });
64 | });
65 | });
66 |
67 | test('function returns array', (done) => {
68 | const config = require('./fixtures/webpack.config-function.js');
69 | const compiler = webpack(config);
70 | const { server } = client(compiler, options);
71 |
72 | server.on('listening', () => {
73 | const { host, port } = server;
74 | const socket = new WebSocket(`ws://${host}:${port}`);
75 |
76 | socket.on('message', (raw) => {
77 | const data = JSON.parse(raw);
78 |
79 | expect(validTypes).toContain(data.type);
80 |
81 | if (data.type === 'hash') {
82 | server.close(done);
83 | }
84 | });
85 |
86 | socket.on('open', () => {
87 | compiler.run(() => {});
88 | });
89 | });
90 | });
91 |
92 | test('MultiCompiler config', (done) => {
93 | const config = require('./fixtures/multi/webpack.config.js');
94 | const compiler = webpack(config);
95 | const { server } = client(compiler, options);
96 |
97 | server.on('listening', () => {
98 | const { host, port } = server;
99 | const socket = new WebSocket(`ws://${host}:${port}`);
100 |
101 | socket.on('message', (raw) => {
102 | const data = JSON.parse(raw);
103 |
104 | expect(validTypes).toContain(data.type);
105 |
106 | if (data.type === 'hash') {
107 | server.close(done);
108 | }
109 | });
110 |
111 | socket.on('open', () => {
112 | compiler.run(() => {});
113 | });
114 | });
115 | });
116 |
117 | test('options sanity check', (done) => {
118 | const config = require('./fixtures/webpack.config-object.js');
119 | const compiler = webpack(config);
120 | const { options: opts, server } = client(compiler, options);
121 |
122 | server.on('listening', () => {
123 | const { host, port } = server;
124 | expect({ host, port }).toMatchSnapshot({
125 | port: expect.any(Number),
126 | });
127 | // this assign is necessary as Jest cannot manipulate a frozen object
128 | expect(Object.assign({}, opts)).toMatchSnapshot({
129 | webSocket: {
130 | port: expect.any(Number),
131 | },
132 | });
133 |
134 | setTimeout(() => server.close(done), 500);
135 | });
136 | });
137 |
138 | test('mismatched client/server options sanity check', (done) => {
139 | const config = require('./fixtures/webpack.config-object.js');
140 | const compiler = webpack(config);
141 | const clientOptions = Object.assign({}, options, {
142 | host: {
143 | client: 'localhost',
144 | server: '0.0.0.0',
145 | },
146 | port: {
147 | client: 6000,
148 | server: 7000,
149 | },
150 | });
151 | const { options: opts, server } = client(compiler, clientOptions);
152 |
153 | server.on('listening', () => {
154 | const { host, port } = server;
155 | expect({ host, port }).toMatchSnapshot();
156 | expect(opts).toMatchSnapshot();
157 |
158 | setTimeout(() => server.close(done), 500);
159 | });
160 | });
161 | });
162 |
--------------------------------------------------------------------------------
/test/options.test.js:
--------------------------------------------------------------------------------
1 | const { readFileSync: read } = require('fs');
2 | const { resolve } = require('path');
3 | const https = require('https');
4 |
5 | const killable = require('killable');
6 |
7 | const getOptions = require('../lib/options');
8 |
9 | describe('options', () => {
10 | test('defaults', () => {
11 | const options = getOptions();
12 | expect(options).toMatchSnapshot({
13 | stats: {
14 | context: expect.stringMatching(/(webpack-hot-client|project)$/),
15 | },
16 | });
17 | });
18 |
19 | test('altered options', () => {
20 | const altered = {
21 | allEntries: true,
22 | autoConfigure: false,
23 | host: {
24 | client: 'localhost',
25 | server: 'localhost',
26 | },
27 | hmr: false,
28 | https: true,
29 | logLevel: 'trace',
30 | logTime: true,
31 | port: 0,
32 | reload: false,
33 | send: {
34 | errors: false,
35 | warnings: false,
36 | },
37 | // this property is tested later
38 | // server: null,
39 | stats: {
40 | context: '/',
41 | },
42 | validTargets: ['batman'],
43 | // we pass this to force the log instance to be unique, to assert log
44 | // option differences
45 | test: true,
46 | };
47 | const options = getOptions(altered);
48 | // console.log(JSON.stringify(options, null, 2));
49 | expect(options).toMatchSnapshot();
50 | });
51 |
52 | test('https.Server', (done) => {
53 | const passphrase = 'sample';
54 | const pfx = read(resolve(__dirname, './fixtures/test-cert.pfx'));
55 | const server = https.createServer({ passphrase, pfx });
56 |
57 | killable(server);
58 | server.listen(0, () => {
59 | const options = getOptions({ server });
60 | expect(options).toMatchSnapshot({
61 | server: expect.any(https.Server),
62 | webSocket: {
63 | host: '::',
64 | port: expect.any(Number),
65 | },
66 | });
67 | server.kill(done);
68 | });
69 | });
70 |
71 | test('https: false, https.Server', (done) => {
72 | const passphrase = 'sample';
73 | const pfx = read(resolve(__dirname, './fixtures/test-cert.pfx'));
74 | const server = https.createServer({ passphrase, pfx });
75 |
76 | killable(server);
77 | server.listen(0, () => {
78 | const options = getOptions({ server, https: false });
79 | expect(options).toMatchSnapshot({
80 | server: expect.any(https.Server),
81 | webSocket: {
82 | host: '::',
83 | port: expect.any(Number),
84 | },
85 | });
86 | server.kill(done);
87 | });
88 | });
89 |
90 | test('throws on invalid host option', () => {
91 | const t = () => getOptions({ host: true });
92 | expect(t).toThrow();
93 | });
94 |
95 | test('throws if host.client is missing', () => {
96 | const t = () => getOptions({ host: { server: 'localhost' } });
97 | expect(t).toThrow();
98 | });
99 |
100 | test('throws if host.server is missing', () => {
101 | const t = () => getOptions({ host: { client: 'localhost' } });
102 | expect(t).toThrow();
103 | });
104 |
105 | test('throws if port.client is missing', () => {
106 | const t = () => getOptions({ port: { server: 0 } });
107 | expect(t).toThrowErrorMatchingSnapshot();
108 | });
109 |
110 | test('throws if port.server is missing', () => {
111 | const t = () => getOptions({ port: { client: 9000 } });
112 | expect(t).toThrowErrorMatchingSnapshot();
113 | });
114 | });
115 |
--------------------------------------------------------------------------------
/test/socket-server.test.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 |
3 | const WebSocket = require('ws');
4 |
5 | const { getServer, payload } = require('../lib/socket-server');
6 | const getOptions = require('../lib/options');
7 |
8 | const options = getOptions({ logLevel: 'silent' });
9 |
10 | const createServer = (port, host, callback) =>
11 | new Promise((resolve) => {
12 | const server = http.createServer();
13 | server.on('close', () => {
14 | resolve();
15 | });
16 | server.listen(port, host, callback.bind(null, server));
17 | });
18 |
19 | const getSocket = (port, host) => new WebSocket(`ws://${host}:${port}`);
20 |
21 | describe('socket server', () => {
22 | test('getServer', (done) => {
23 | const server = getServer(options);
24 | const { broadcast, close, send } = server;
25 |
26 | expect(broadcast).toBeDefined();
27 | expect(close).toBeDefined();
28 | expect(send).toBeDefined();
29 |
30 | server.on('listening', () => {
31 | const { host, port } = server;
32 | expect(host).toBe('127.0.0.1');
33 | expect(port).toBeGreaterThan(0);
34 |
35 | close(done);
36 | });
37 | });
38 |
39 | test('getServer: { server }', () =>
40 | createServer(1337, '127.0.0.1', (server) => {
41 | const opts = getOptions({ server });
42 | const { close, host, port } = getServer(opts);
43 |
44 | expect(host).toBe('127.0.0.1');
45 | expect(port).toBe(1337);
46 |
47 | server.close(close);
48 | }));
49 |
50 | test('payload', () => {
51 | expect(payload('test', { batman: 'superman ' })).toMatchSnapshot();
52 | });
53 |
54 | test('broadcast', (done) => {
55 | const server = getServer(options);
56 | const { broadcast, close } = server;
57 |
58 | server.on('listening', () => {
59 | const { host, port } = server;
60 | const catcher = getSocket(port, host);
61 |
62 | catcher.on('message', (data) => {
63 | expect(data).toMatchSnapshot();
64 | close(done);
65 | });
66 |
67 | catcher.on('open', () => {
68 | broadcast(payload('broadcast', { received: true }));
69 | });
70 | });
71 | });
72 |
73 | test('socket broadcast', (done) => {
74 | const server = getServer(options);
75 | const { close } = server;
76 |
77 | server.on('listening', () => {
78 | const { host, port } = server;
79 | const pitcher = getSocket(port, host);
80 | const catcher = getSocket(port, host);
81 |
82 | catcher.on('message', (data) => {
83 | expect(data).toMatchSnapshot();
84 | close(done);
85 | });
86 |
87 | pitcher.on('open', () => {
88 | pitcher.send(payload('broadcast', { received: true }));
89 | });
90 | });
91 | });
92 |
93 | test('send via stats', (done) => {
94 | const stats = {
95 | context: process.cwd(),
96 | toJson: () => {
97 | return { hash: '111111', warnings: [] };
98 | },
99 | };
100 | const opts = getOptions({ logLevel: 'silent', stats });
101 | const server = getServer(opts);
102 | const { close } = server;
103 |
104 | server.on('listening', () => {
105 | const { host, port } = server;
106 | const catcher = getSocket(port, host);
107 |
108 | catcher.on('message', (raw) => {
109 | const data = JSON.parse(raw);
110 | if (data.type === 'ok') {
111 | expect(data).toMatchSnapshot();
112 | close(done);
113 | }
114 | });
115 | });
116 | });
117 |
118 | test('errors', (done) => {
119 | const server = getServer(options);
120 | const { close } = server;
121 |
122 | server.on('listening', () => {
123 | const { host, port, send } = server;
124 | const catcher = getSocket(port, host);
125 |
126 | catcher.on('message', (raw) => {
127 | const data = JSON.parse(raw);
128 | if (data.type === 'errors') {
129 | expect(data).toMatchSnapshot();
130 | close(done);
131 | }
132 | });
133 |
134 | catcher.on('open', () => {
135 | send({
136 | errors: ['test error'],
137 | });
138 | });
139 | });
140 | });
141 |
142 | test('hash-ok', (done) => {
143 | const server = getServer(options);
144 | const { close } = server;
145 |
146 | server.on('listening', () => {
147 | const { host, port, send } = server;
148 | const catcher = getSocket(port, host);
149 |
150 | catcher.on('message', (raw) => {
151 | const data = JSON.parse(raw);
152 | if (data.type === 'ok') {
153 | expect(data).toMatchSnapshot();
154 | close(done);
155 | }
156 | });
157 |
158 | catcher.on('open', () => {
159 | send({
160 | hash: '000000',
161 | warnings: [],
162 | });
163 | });
164 | });
165 | });
166 |
167 | test('no-change', (done) => {
168 | const server = getServer(options);
169 | const { close } = server;
170 |
171 | server.on('listening', () => {
172 | const { host, port, send } = server;
173 | const catcher = getSocket(port, host);
174 |
175 | catcher.on('message', (data) => {
176 | expect(data).toMatchSnapshot();
177 | close(done);
178 | });
179 |
180 | catcher.on('open', () => {
181 | send({
182 | assets: [{ emitted: false }],
183 | });
184 | });
185 | });
186 | });
187 |
188 | test('hash-ok', (done) => {
189 | const server = getServer(options);
190 | const { close } = server;
191 |
192 | server.on('listening', () => {
193 | const { host, port, send } = server;
194 | const catcher = getSocket(port, host);
195 |
196 | catcher.on('message', (raw) => {
197 | const data = JSON.parse(raw);
198 | if (data.type === 'warnings') {
199 | expect(data).toMatchSnapshot();
200 | close(done);
201 | }
202 | });
203 |
204 | catcher.on('open', () => {
205 | send({
206 | hash: '000000',
207 | warnings: ['test warning'],
208 | });
209 | });
210 | });
211 | });
212 | });
213 |
--------------------------------------------------------------------------------
/test/watch.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | const { readFileSync: read, writeFileSync: write } = require('fs');
3 | const { resolve } = require('path');
4 |
5 | const webpack = require('webpack');
6 | const WebSocket = require('ws');
7 |
8 | const client = require('../lib');
9 |
10 | const logLevel = 'silent';
11 | const options = { logLevel };
12 |
13 | describe('watch', () => {
14 | const appPath = resolve(__dirname, 'fixtures/app.js');
15 | const clean = read(appPath, 'utf-8');
16 |
17 | test('invalidate', (done) => {
18 | const config = require('./fixtures/webpack.config-watch.js');
19 | const compiler = webpack(config);
20 | const { server } = client(compiler, options);
21 | let watcher;
22 | let dirty = false;
23 |
24 | server.on('listening', () => {
25 | const { host, port } = server;
26 | const socket = new WebSocket(`ws://${host}:${port}`);
27 |
28 | socket.on('message', (raw) => {
29 | const data = JSON.parse(raw);
30 |
31 | if (data.type === 'invalid') {
32 | watcher.close(() => {
33 | write(appPath, clean, 'utf-8');
34 | server.close(done);
35 | });
36 | }
37 |
38 | if (data.type === 'ok' && !dirty) {
39 | setTimeout(() => {
40 | dirty = true;
41 | write(appPath, `${clean}\nconsole.log('dirty');`, 'utf-8');
42 | }, 500);
43 | }
44 | });
45 |
46 | socket.on('open', () => {
47 | watcher = compiler.watch({}, () => {});
48 | });
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------