diff --git a/docs/angular/api-web/executors/dev-server.md b/docs/angular/api-web/executors/dev-server.md index e53943a850..c6c7bdbf24 100644 --- a/docs/angular/api-web/executors/dev-server.md +++ b/docs/angular/api-web/executors/dev-server.md @@ -26,6 +26,14 @@ Type: `string` Target which builds the application +### hmr + +Default: `false` + +Type: `boolean` + +Enable hot module replacement. + ### host Default: `localhost` diff --git a/docs/angular/cli/serve.md b/docs/angular/cli/serve.md index 0cbdfd6aa3..10baa8942f 100644 --- a/docs/angular/cli/serve.md +++ b/docs/angular/cli/serve.md @@ -34,6 +34,10 @@ The options below are common to the `serve` command used within an Nx workspace. This option allows you to whitelist services that are allowed to access the dev server. +### hmr + +Enable hot module replacement. + ### host Host to listen on. @@ -140,10 +144,6 @@ Don't verify connected clients are part of allowed hosts. Output in-file eval sourcemaps. -### hmr - -Enable hot module replacement. - ### hmr-warning Show a warning when the `--hmr` option is enabled. diff --git a/docs/node/api-web/executors/dev-server.md b/docs/node/api-web/executors/dev-server.md index 96ae80a5b6..13d567267b 100644 --- a/docs/node/api-web/executors/dev-server.md +++ b/docs/node/api-web/executors/dev-server.md @@ -27,6 +27,14 @@ Type: `string` Target which builds the application +### hmr + +Default: `false` + +Type: `boolean` + +Enable hot module replacement. + ### host Default: `localhost` diff --git a/docs/node/cli/serve.md b/docs/node/cli/serve.md index 0cbdfd6aa3..10baa8942f 100644 --- a/docs/node/cli/serve.md +++ b/docs/node/cli/serve.md @@ -34,6 +34,10 @@ The options below are common to the `serve` command used within an Nx workspace. This option allows you to whitelist services that are allowed to access the dev server. +### hmr + +Enable hot module replacement. + ### host Host to listen on. @@ -140,10 +144,6 @@ Don't verify connected clients are part of allowed hosts. Output in-file eval sourcemaps. -### hmr - -Enable hot module replacement. - ### hmr-warning Show a warning when the `--hmr` option is enabled. diff --git a/docs/react/api-web/executors/dev-server.md b/docs/react/api-web/executors/dev-server.md index 882b04de2f..9901f05083 100644 --- a/docs/react/api-web/executors/dev-server.md +++ b/docs/react/api-web/executors/dev-server.md @@ -27,6 +27,14 @@ Type: `string` Target which builds the application +### hmr + +Default: `false` + +Type: `boolean` + +Enable hot module replacement. + ### host Default: `localhost` diff --git a/docs/react/cli/serve.md b/docs/react/cli/serve.md index 0cbdfd6aa3..10baa8942f 100644 --- a/docs/react/cli/serve.md +++ b/docs/react/cli/serve.md @@ -34,6 +34,10 @@ The options below are common to the `serve` command used within an Nx workspace. This option allows you to whitelist services that are allowed to access the dev server. +### hmr + +Enable hot module replacement. + ### host Host to listen on. @@ -140,10 +144,6 @@ Don't verify connected clients are part of allowed hosts. Output in-file eval sourcemaps. -### hmr - -Enable hot module replacement. - ### hmr-warning Show a warning when the `--hmr` option is enabled. diff --git a/docs/react/tutorial/06-proxy.md b/docs/react/tutorial/06-proxy.md index bd49864b6c..a4aeb22e04 100644 --- a/docs/react/tutorial/06-proxy.md +++ b/docs/react/tutorial/06-proxy.md @@ -64,6 +64,7 @@ Options: --sslCert SSL certificate to use for serving HTTPS. --watch Watches for changes and rebuilds application (default: true) --liveReload Whether to reload the page on change, using live-reload. (default: true) + --hmr Enable hot module replacement. --publicHost Public URL where the application will be served --open Open the application in the browser. --allowedHosts This option allows you to whitelist services that are allowed to access the dev server. diff --git a/docs/shared/cli/serve.md b/docs/shared/cli/serve.md index 0cbdfd6aa3..10baa8942f 100644 --- a/docs/shared/cli/serve.md +++ b/docs/shared/cli/serve.md @@ -34,6 +34,10 @@ The options below are common to the `serve` command used within an Nx workspace. This option allows you to whitelist services that are allowed to access the dev server. +### hmr + +Enable hot module replacement. + ### host Host to listen on. @@ -140,10 +144,6 @@ Don't verify connected clients are part of allowed hosts. Output in-file eval sourcemaps. -### hmr - -Enable hot module replacement. - ### hmr-warning Show a warning when the `--hmr` option is enabled. diff --git a/package.json b/package.json index 44a191b909..016a53c736 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@nrwl/tao": "12.3.0", "@nrwl/web": "12.3.0", "@nrwl/workspace": "12.3.0", + "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", "@popperjs/core": "^2.9.2", "@reduxjs/toolkit": "1.5.0", "@rollup/plugin-babel": "5.0.2", @@ -189,6 +190,7 @@ "protractor": "5.4.3", "raw-loader": "3.1.0", "react-redux": "7.2.3", + "react-refresh": "^0.9.0", "react-router-dom": "5.1.2", "regenerator-runtime": "0.13.7", "release-it": "^7.4.0", diff --git a/packages/react/package.json b/packages/react/package.json index 2851d12440..07959e3e1f 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -37,11 +37,13 @@ "@nrwl/storybook": "*", "@nrwl/web": "*", "@nrwl/workspace": "*", + "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", "@svgr/webpack": "^5.4.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-react": "^7.23.1", "eslint-plugin-react-hooks": "^4.2.0", + "react-refresh": "^0.9.0", "url-loader": "^3.0.0" } } diff --git a/packages/react/plugins/webpack.ts b/packages/react/plugins/webpack.ts index f808c592dc..5dcfcbba81 100644 --- a/packages/react/plugins/webpack.ts +++ b/packages/react/plugins/webpack.ts @@ -1,4 +1,5 @@ import { Configuration } from 'webpack'; +import * as ReactRefreshPlugin from '@pmmmwh/react-refresh-webpack-plugin'; // Add React-specific configuration function getWebpackConfig(config: Configuration) { @@ -54,6 +55,27 @@ function getWebpackConfig(config: Configuration) { } ); + if (config.mode === 'development' && config['devServer']?.hot) { + // add `react-refresh/babel` to babel loader plugin + const babelLoader = config.module.rules.find((rule) => + rule.loader.toString().includes('babel-loader') + ); + if (babelLoader) { + babelLoader.options['plugins'] = [ + ...(babelLoader.options['plugins'] || []), + [ + require.resolve('react-refresh/babel'), + { + skipEnvCheck: true, + }, + ], + ]; + } + + // add https://github.com/pmmmwh/react-refresh-webpack-plugin to webpack plugin + config.plugins.push(new ReactRefreshPlugin()); + } + return config; } diff --git a/packages/react/src/generators/application/application.spec.ts b/packages/react/src/generators/application/application.spec.ts index 43692b0f03..0839e46a2b 100644 --- a/packages/react/src/generators/application/application.spec.ts +++ b/packages/react/src/generators/application/application.spec.ts @@ -278,9 +278,11 @@ describe('app', () => { expect(targetConfig.serve.executor).toEqual('@nrwl/web:dev-server'); expect(targetConfig.serve.options).toEqual({ buildTarget: 'my-app:build', + hmr: true, }); expect(targetConfig.serve.configurations.production).toEqual({ buildTarget: 'my-app:build:production', + hmr: false, }); }); diff --git a/packages/react/src/generators/application/lib/add-project.ts b/packages/react/src/generators/application/lib/add-project.ts index 29cd09e897..66fe1587f7 100644 --- a/packages/react/src/generators/application/lib/add-project.ts +++ b/packages/react/src/generators/application/lib/add-project.ts @@ -112,10 +112,12 @@ function createServeTarget(options: NormalizedSchema): TargetConfiguration { executor: '@nrwl/web:dev-server', options: { buildTarget: `${options.projectName}:build`, + hmr: true, }, configurations: { production: { buildTarget: `${options.projectName}:build:production`, + hmr: false, }, }, }; diff --git a/packages/web/src/builders/dev-server/dev-server.impl.ts b/packages/web/src/builders/dev-server/dev-server.impl.ts index dc9d391435..802e8825b5 100644 --- a/packages/web/src/builders/dev-server/dev-server.impl.ts +++ b/packages/web/src/builders/dev-server/dev-server.impl.ts @@ -35,6 +35,7 @@ export interface WebDevServerOptions { buildTarget: string; open: boolean; liveReload: boolean; + hmr: boolean; watch: boolean; allowedHosts: string; maxWorkers?: number; diff --git a/packages/web/src/builders/dev-server/schema.json b/packages/web/src/builders/dev-server/schema.json index 4f3fb9086f..28127ab4c3 100644 --- a/packages/web/src/builders/dev-server/schema.json +++ b/packages/web/src/builders/dev-server/schema.json @@ -41,6 +41,11 @@ "description": "Whether to reload the page on change, using live-reload.", "default": true }, + "hmr": { + "type": "boolean", + "description": "Enable hot module replacement.", + "default": false + }, "publicHost": { "type": "string", "description": "Public URL where the application will be served" diff --git a/packages/web/src/utils/devserver.config.spec.ts b/packages/web/src/utils/devserver.config.spec.ts index 3fe10ddf30..e4017675ba 100644 --- a/packages/web/src/utils/devserver.config.spec.ts +++ b/packages/web/src/utils/devserver.config.spec.ts @@ -2,6 +2,7 @@ import { getDevServerConfig } from './devserver.config'; import TsConfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; import * as ts from 'typescript'; import * as fs from 'fs'; +import { HotModuleReplacementPlugin } from 'webpack'; import { WebBuildBuilderOptions } from '../builders/build/build.impl'; import { WebDevServerOptions } from '../builders/dev-server/dev-server.impl'; import { join } from 'path'; @@ -54,6 +55,7 @@ describe('getDevServerConfig', () => { buildTarget: 'webapp:build', ssl: false, liveReload: true, + hmr: true, open: false, watch: true, allowedHosts: null, @@ -328,7 +330,7 @@ describe('getDevServerConfig', () => { root, sourceRoot, buildInput, - serveInput + { ...serveInput, hmr: false } ); expect(result.liveReload).toEqual(true); @@ -339,13 +341,49 @@ describe('getDevServerConfig', () => { root, sourceRoot, buildInput, - { ...serveInput, liveReload: false } + { ...serveInput, hmr: false, liveReload: false } ); expect(result.liveReload).toEqual(false); }); }); + describe('hmr option', () => { + it('should set the correct value', () => { + const { devServer: result } = getDevServerConfig( + root, + sourceRoot, + buildInput, + { ...serveInput, hmr: false } + ); + + expect(result.hot).toEqual(false); + }); + + it('should set the correct if true and disable live reload', () => { + const { devServer: result } = getDevServerConfig( + root, + sourceRoot, + buildInput, + serveInput + ); + + expect(result.liveReload).toEqual(false); + expect(result.hot).toEqual(true); + }); + + it('should add hot module replacement plugin', () => { + const { plugins } = getDevServerConfig( + root, + sourceRoot, + buildInput, + serveInput + ); + + expect(plugins).toContainEqual(new HotModuleReplacementPlugin()); + }); + }); + describe('ssl option', () => { it('should set https to false if not on', () => { const { devServer: result } = getDevServerConfig( diff --git a/packages/web/src/utils/devserver.config.ts b/packages/web/src/utils/devserver.config.ts index 5807ad49a3..d006b29c48 100644 --- a/packages/web/src/utils/devserver.config.ts +++ b/packages/web/src/utils/devserver.config.ts @@ -7,7 +7,7 @@ import { readFileSync } from 'fs'; import * as path from 'path'; import { getWebConfig } from './web.config'; -import { Configuration } from 'webpack'; +import { Configuration, HotModuleReplacementPlugin } from 'webpack'; import { WebBuildBuilderOptions } from '../builders/build/build.impl'; import { WebDevServerOptions } from '../builders/dev-server/dev-server.impl'; import { buildServePath } from './serve-path'; @@ -31,6 +31,10 @@ export function getDevServerConfig( serveOptions, buildOptions ); + webpackConfig.plugins = [ + ...(webpackConfig.plugins || []), + getHmrPlugin(serveOptions), + ].filter(Boolean); return webpackConfig; } @@ -85,7 +89,8 @@ function getDevServerPartial( publicPath: servePath, contentBase: false, allowedHosts: [], - liveReload: options.liveReload, + liveReload: options.hmr ? false : options.liveReload, // disable liveReload if hmr is enabled + hot: options.hmr, }; if (options.ssl && options.sslKey && options.sslCert) { @@ -114,3 +119,7 @@ function getProxyConfig(root: string, options: WebDevServerOptions) { const proxyPath = path.resolve(root, options.proxyConfig as string); return require(proxyPath); } + +function getHmrPlugin(options: WebDevServerOptions) { + return options.hmr && new HotModuleReplacementPlugin(); +} diff --git a/yarn.lock b/yarn.lock index d1d680226a..43f20e8917 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20519,6 +20519,11 @@ react-refresh@0.8.3, react-refresh@^0.8.3: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg== +react-refresh@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf" + integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ== + react-router-dom@5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18"