├── client
└── .gitkeep
├── .gitattributes
├── test
├── fixtures
│ ├── foo.js
│ ├── multi
│ │ ├── client.js
│ │ ├── server.js
│ │ └── webpack.config.js
│ ├── test-cert.pfx
│ ├── sub
│ │ └── resource.html
│ ├── component.js
│ ├── index.html
│ ├── app-clean.js
│ ├── webpack.config-watch.js
│ ├── webpack.config-array.js
│ ├── webpack.config-invalid.js
│ ├── webpack.config-object.js
│ ├── webpack.config-function.js
│ ├── webpack.config-function-invalid.js
│ ├── webpack.config-invalid-object.js
│ ├── webpack.config-allentries.js
│ ├── webpack.config-invalid-plugin.js
│ └── app.js
├── __snapshots__
│ ├── compiler.test.js.snap
│ ├── socket-server.test.js.snap
│ ├── index.test.js.snap
│ └── options.test.js.snap
├── watch.test.js
├── options.test.js
├── compiler.test.js
├── index.test.js
└── socket-server.test.js
├── .prettierrc
├── lib
├── HotClientError.js
├── client
│ ├── .eslintrc
│ ├── .babelrc
│ ├── log.js
│ ├── socket.js
│ ├── index.js
│ └── hot.js
├── index.js
├── options.js
├── socket-server.js
└── compiler.js
├── .eslintignore
├── codecov.yml
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── SUPPORT.md
│ ├── FEATURE.md
│ ├── DOCS.md
│ ├── MODIFICATION.md
│ └── BUG.md
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
├── CONTRIBUTING-OLD.md
└── CONTRIBUTING.md
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── commitlint.config.js
├── docs
├── WEBSOCKETS.md
└── REMOTE.md
├── LICENSE
├── schemas
└── options.json
├── package.json
├── .circleci
└── config.yml
└── README.md
/client/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | package-lock.json -diff
2 | * text=auto
3 | bin/* eol=lf
4 |
--------------------------------------------------------------------------------
/test/fixtures/foo.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line
2 | console.log('foo');
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5",
4 | "arrowParens": "always"
5 | }
--------------------------------------------------------------------------------
/test/fixtures/test-cert.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/webpack-contrib/webpack-hot-client/HEAD/test/fixtures/test-cert.pfx
--------------------------------------------------------------------------------
/test/fixtures/sub/resource.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | subdirectory
7 |
8 |
9 |
--------------------------------------------------------------------------------
/test/fixtures/component.js:
--------------------------------------------------------------------------------
1 | /* global document */
2 |
3 | const target = document.querySelector('#target');
4 | target.innerHTML = 'component';
5 |
--------------------------------------------------------------------------------
/lib/HotClientError.js:
--------------------------------------------------------------------------------
1 | module.exports = class HotClientError extends Error {
2 | constructor(message) {
3 | super(`webpack-hot-client: ${message}`);
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/lib/client/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "WebSocket": true,
4 | "window": true,
5 | "__hotClientOptions__": true,
6 | "__webpack_hash__": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/lib/client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", {
4 | "targets": {
5 | "browsers": ["last 2 versions"]
6 | }
7 | }]
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/test/fixtures/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | yay
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 | };
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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-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-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-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-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-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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/.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/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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`._
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |