From 793c475ab3e506d678697d6bc6b03a2bf8b1b1f5 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Tue, 27 Jul 2021 11:25:33 -0400 Subject: [PATCH] feat(react): add fast refresh support for webpack 5 --- .../migrate-to-webpack-5.spec.ts | 50 +++++++++++++++++ .../migrate-to-webpack-5.ts | 53 +++++++++++++++---- packages/react/package.json | 1 + packages/react/plugins/webpack.ts | 10 +--- packages/react/src/webpack/bundle4.ts | 9 ++++ packages/react/src/webpack/bundle5.ts | 38 +++++++++++++ .../react/src/webpack/delete-in-nx-13.txt | 1 + packages/react/src/webpack/entry.ts | 21 ++++++++ packages/react/src/webpack/require-shim.ts | 10 ++++ .../migrate-to-webpack-5.spec.ts | 34 ++++++++++++ .../migrate-to-webpack-5.ts | 39 +++++++++----- packages/web/src/utils/config.ts | 2 + .../plugins/index-html-webpack-plugin.ts | 17 +++--- 13 files changed, 248 insertions(+), 37 deletions(-) create mode 100644 packages/node/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.spec.ts create mode 100644 packages/react/src/webpack/bundle4.ts create mode 100644 packages/react/src/webpack/bundle5.ts create mode 100644 packages/react/src/webpack/delete-in-nx-13.txt create mode 100644 packages/react/src/webpack/entry.ts create mode 100644 packages/react/src/webpack/require-shim.ts create mode 100644 packages/web/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.spec.ts diff --git a/packages/node/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.spec.ts b/packages/node/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.spec.ts new file mode 100644 index 00000000000000..a17d5402622993 --- /dev/null +++ b/packages/node/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.spec.ts @@ -0,0 +1,50 @@ +import { readJson, Tree, updateJson } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { nodeMigrateToWebpack5Generator } from './migrate-to-webpack-5'; + +describe('nodeMigrateToWebpack5Generator', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + updateJson(tree, 'package.json', (json) => { + json.devDependencies = { + '@nrwl/cli': '100.0.0', + '@nrwl/jest': '100.0.0', + '@nrwl/node': '100.0.0', + '@nrwl/workspace': '100.0.0', + }; + return json; + }); + }); + + it('should add packages needed by Node', async () => { + await nodeMigrateToWebpack5Generator(tree, {}); + + const json = readJson(tree, '/package.json'); + + expect(json.devDependencies['webpack']).toMatch(/\^5/); + }); + + it('should add packages needed by Web if used', async () => { + updateJson(tree, 'package.json', (json) => { + json.devDependencies = { + '@nrwl/cli': '100.0.0', + '@nrwl/jest': '100.0.0', + '@nrwl/node': '100.0.0', + '@nrwl/react': '100.0.0', + '@nrwl/web': '100.0.0', + '@nrwl/workspace': '100.0.0', + }; + return json; + }); + + await nodeMigrateToWebpack5Generator(tree, {}); + + const json = readJson(tree, '/package.json'); + + expect( + json.devDependencies['@pmmmwh/react-refresh-webpack-plugin'] + ).toMatch(/^0\.5/); + }); +}); diff --git a/packages/node/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.ts b/packages/node/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.ts index 8b2ceb2d3ad998..92c04ed735953f 100644 --- a/packages/node/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.ts +++ b/packages/node/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.ts @@ -1,25 +1,58 @@ import { addDependenciesToPackageJson, convertNxGenerator, + GeneratorCallback, logger, + readJson, + removeDependenciesFromPackageJson, Tree, } from '@nrwl/devkit'; +import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; -const webpack5Packages = { - 'copy-webpack-plugin': '^9.0.0', - 'mini-css-extract-plugin': '^1.6.0', - 'source-map-loader': '^2.0.1', +const basePackages = { + 'copy-webpack-plugin': '^9.0.1', + webpack: '^5.47.0', + 'webpack-merge': '^5.8.0', + 'webpack-node-externals': '^3.0.0', +}; + +const webPackages = { + 'mini-css-extract-plugin': '^2.1.0', + 'source-map-loader': '^3.0.0', 'terser-webpack-plugin': '^5.1.1', - webpack: '^5.39.1', - 'webpack-dev-server': '^3.11.2', - 'webpack-merge': '^5.7.3', - 'webpack-node-externals': '^2.5.2', - 'webpack-sources': '^2.2.0', + 'webpack-dev-server': '4.0.0-rc.0', + 'webpack-sources': '^3.0.2', + 'react-refresh': '^0.10.0', + '@pmmmwh/react-refresh-webpack-plugin': '0.5.0-rc.2', }; export async function nodeMigrateToWebpack5Generator(tree: Tree, schema: {}) { + let packages = basePackages; + const tasks: GeneratorCallback[] = []; + + const packageJson = readJson(tree, 'package.json'); + const deps = [ + ...Object.keys(packageJson.dependencies), // just in case someone installed it here + ...Object.keys(packageJson.devDependencies), + ]; + + if (deps.includes('@nrwl/web')) { + packages = { + ...packages, + ...webPackages, + }; + } + logger.info(`NX Adding webpack 5 to workspace.`); - return addDependenciesToPackageJson(tree, {}, webpack5Packages); + + // Removing the packages ensures that the versions will be updated when adding them after + tasks.push( + removeDependenciesFromPackageJson(tree, [], Object.keys(packages)) + ); + + tasks.push(addDependenciesToPackageJson(tree, {}, packages)); + + return runTasksInSerial(...tasks); } export default nodeMigrateToWebpack5Generator; diff --git a/packages/react/package.json b/packages/react/package.json index 2e42572c5ac9e5..c143dd58eca7d6 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -39,6 +39,7 @@ "@nrwl/workspace": "*", "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", "@svgr/webpack": "^5.5.0", + "chalk": "4.1.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-react": "^7.23.1", diff --git a/packages/react/plugins/webpack.ts b/packages/react/plugins/webpack.ts index 5a282139ec3465..0dae10fb303094 100644 --- a/packages/react/plugins/webpack.ts +++ b/packages/react/plugins/webpack.ts @@ -1,10 +1,9 @@ import type { Configuration } from 'webpack'; -import * as ReactRefreshPlugin from '@pmmmwh/react-refresh-webpack-plugin'; // Add React-specific configuration function getWebpackConfig(config: Configuration) { // TODO(jack): Remove in Nx 13 - const { isWebpack5 } = require('@nrwl/web/src/webpack/entry'); + const { ReactRefreshPlugin, isWebpack5 } = require('../src/webpack/entry'); config.module.rules.push( { test: /\.(png|jpe?g|gif|webp)$/, @@ -55,12 +54,7 @@ function getWebpackConfig(config: Configuration) { } ); - // TODO(jack): support webpack 5 - if ( - !isWebpack5 && - config.mode === 'development' && - config['devServer']?.hot - ) { + 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') diff --git a/packages/react/src/webpack/bundle4.ts b/packages/react/src/webpack/bundle4.ts new file mode 100644 index 00000000000000..c352228663c580 --- /dev/null +++ b/packages/react/src/webpack/bundle4.ts @@ -0,0 +1,9 @@ +module.exports = function (useShim = true) { + const webpack = require('webpack'); + webpack.webpack = webpack; + + return { + ReactRefreshPlugin: require('@pmmmwh/react-refresh-webpack-plugin'), + webpack, + }; +}; diff --git a/packages/react/src/webpack/bundle5.ts b/packages/react/src/webpack/bundle5.ts new file mode 100644 index 00000000000000..a41a393b231332 --- /dev/null +++ b/packages/react/src/webpack/bundle5.ts @@ -0,0 +1,38 @@ +import { logger, stripIndents } from '@nrwl/devkit'; +import chalk = require('chalk'); + +import { requireShim } from './require-shim'; +import packageJson = require('../../package.json'); + +function validateVersion(path) { + if ( + packageJson.dependencies[path] === + requireShim(`${path}/package.json`).version + ) { + logger.warn(`Found an outdated version of ${chalk.bold(path)}\n`); + + logger.info(stripIndents` + If you want to use webpack 5, try installing compatible versions of the plugins. + See: https://nx.dev/guides/webpack-5 + `); + + throw new Error('Incompatible version'); + } +} + +module.exports = function (onFallback) { + try { + validateVersion('@pmmmwh/react-refresh-webpack-plugin'); + } catch { + logger.info( + `NX Falling back to webpack 4 due to incompatible plugin versions` + ); + onFallback(); + return require('./bundle4')(); + } + + return { + ReactRefreshPlugin: requireShim('@pmmmwh/react-refresh-webpack-plugin'), + webpack: requireShim('webpack'), + }; +}; diff --git a/packages/react/src/webpack/delete-in-nx-13.txt b/packages/react/src/webpack/delete-in-nx-13.txt new file mode 100644 index 00000000000000..7d73560485a72c --- /dev/null +++ b/packages/react/src/webpack/delete-in-nx-13.txt @@ -0,0 +1 @@ +TODO(jack): Delete for Nx 13 diff --git a/packages/react/src/webpack/entry.ts b/packages/react/src/webpack/entry.ts new file mode 100644 index 00000000000000..8065e839161e8c --- /dev/null +++ b/packages/react/src/webpack/entry.ts @@ -0,0 +1,21 @@ +import { requireShim } from './require-shim'; + +const result = requireShim('webpack/package.json'); +const version = result?.version; + +exports.default = undefined; + +const forceWebpack4 = process.env.NX_FORCE_WEBPACK_4; + +exports.isWebpack5 = !forceWebpack4 && /^5\./.test(version); + +if (exports.isWebpack5) { + Object.assign( + exports, + require('./bundle5')(() => { + exports.isWebpack5 = false; + }) + ); +} else { + Object.assign(exports, require('./bundle4')()); +} diff --git a/packages/react/src/webpack/require-shim.ts b/packages/react/src/webpack/require-shim.ts new file mode 100644 index 00000000000000..a20e3b1f73f6c9 --- /dev/null +++ b/packages/react/src/webpack/require-shim.ts @@ -0,0 +1,10 @@ +import { appRootPath } from '@nrwl/tao/src/utils/app-root'; +import { join } from 'path'; + +export function requireShim(path: string) { + try { + return require(join(appRootPath, 'node_modules', path)); + } catch { + return require(path); + } +} diff --git a/packages/web/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.spec.ts b/packages/web/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.spec.ts new file mode 100644 index 00000000000000..b4c4152580db96 --- /dev/null +++ b/packages/web/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.spec.ts @@ -0,0 +1,34 @@ +import { readJson, Tree, updateJson } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { webMigrateToWebpack5Generator } from './migrate-to-webpack-5'; + +describe('webMigrateToWebpack5Generator', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should add packages needed by Web ', async () => { + updateJson(tree, 'package.json', (json) => { + json.devDependencies = { + '@nrwl/cli': '100.0.0', + '@nrwl/jest': '100.0.0', + '@nrwl/node': '100.0.0', + '@nrwl/react': '100.0.0', + '@nrwl/web': '100.0.0', + '@nrwl/workspace': '100.0.0', + }; + return json; + }); + + await webMigrateToWebpack5Generator(tree, {}); + + const json = readJson(tree, '/package.json'); + + expect(json.devDependencies['webpack']).toMatch(/\^5/); + expect( + json.devDependencies['@pmmmwh/react-refresh-webpack-plugin'] + ).toMatch(/^0\.5/); + }); +}); diff --git a/packages/web/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.ts b/packages/web/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.ts index 8247ed7cd5fdd2..273ca00457a055 100644 --- a/packages/web/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.ts +++ b/packages/web/src/generators/migrate-to-webpack-5/migrate-to-webpack-5.ts @@ -1,28 +1,43 @@ -import type { Tree } from '@nrwl/devkit'; +import type { GeneratorCallback, Tree } from '@nrwl/devkit'; import { addDependenciesToPackageJson, convertNxGenerator, logger, removeDependenciesFromPackageJson, } from '@nrwl/devkit'; +import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; -const webpack5Packages = { - 'copy-webpack-plugin': '^9.0.0', - 'mini-css-extract-plugin': '^1.6.0', - 'source-map-loader': '^2.0.1', +const basePackages = { + 'copy-webpack-plugin': '^9.0.1', + webpack: '^5.47.0', + 'webpack-merge': '^5.8.0', + 'webpack-node-externals': '^3.0.0', +}; + +const webPackages = { + 'mini-css-extract-plugin': '^2.1.0', + 'source-map-loader': '^3.0.0', 'terser-webpack-plugin': '^5.1.1', - webpack: '^5.39.1', - 'webpack-dev-server': '^3.11.2', - 'webpack-merge': '^5.7.3', - 'webpack-node-externals': '^2.5.2', - 'webpack-sources': '^2.2.0', + 'webpack-dev-server': '4.0.0-rc.0', + 'webpack-sources': '^3.0.2', + 'react-refresh': '^0.10.0', + '@pmmmwh/react-refresh-webpack-plugin': '0.5.0-rc.2', }; export async function webMigrateToWebpack5Generator(tree: Tree, schema: {}) { + const packages = { ...basePackages, ...webPackages }; + const tasks: GeneratorCallback[] = []; + logger.info(`NX Adding webpack 5 to workspace.`); + // Removing the packages ensures that the versions will be updated when adding them after - removeDependenciesFromPackageJson(tree, [], Object.keys(webpack5Packages)); - return addDependenciesToPackageJson(tree, {}, webpack5Packages); + tasks.push( + removeDependenciesFromPackageJson(tree, [], Object.keys(packages)) + ); + + tasks.push(addDependenciesToPackageJson(tree, {}, packages)); + + return runTasksInSerial(...tasks); } export default webMigrateToWebpack5Generator; diff --git a/packages/web/src/utils/config.ts b/packages/web/src/utils/config.ts index 09f2126c6c57b4..0287ed8206547a 100644 --- a/packages/web/src/utils/config.ts +++ b/packages/web/src/utils/config.ts @@ -40,6 +40,8 @@ export function getBaseWebpackPartial( const mode = isScriptOptimizeOn ? 'production' : 'development'; const webpackConfig: Configuration = { + target: 'web', // webpack defaults to 'browserslist' which breaks Fast Refresh + entry: { main: [options.main], }, diff --git a/packages/web/src/utils/third-party/cli-files/plugins/index-html-webpack-plugin.ts b/packages/web/src/utils/third-party/cli-files/plugins/index-html-webpack-plugin.ts index 4e9ffe08d31c35..8853c52b404711 100644 --- a/packages/web/src/utils/third-party/cli-files/plugins/index-html-webpack-plugin.ts +++ b/packages/web/src/utils/third-party/cli-files/plugins/index-html-webpack-plugin.ts @@ -74,13 +74,16 @@ export class IndexHtmlWebpackPlugin extends IndexHtmlGenerator { try { for (const [entryName, entrypoint] of this.compilation.entrypoints) { - const entryFiles: FileInfo[] = entrypoint?.getFiles()?.map( - (f: string): FileInfo => ({ - name: entryName, - file: f, - extension: extname(f), - }) - ); + const entryFiles: FileInfo[] = entrypoint + ?.getFiles() + ?.filter((f) => !f.endsWith('.hot-update.js')) + ?.map( + (f: string): FileInfo => ({ + name: entryName, + file: f, + extension: extname(f), + }) + ); if (!entryFiles) { continue;