From edfb31e1ba24e12ac883dab96042ab18dce3fd8f Mon Sep 17 00:00:00 2001 From: Bastien Gandouet Date: Thu, 18 Nov 2021 15:34:13 +0100 Subject: [PATCH 1/5] Empty commit to start PR From c29001e9cda08bf05fb888ddf20f3dc94b76d0a4 Mon Sep 17 00:00:00 2001 From: Bastien Gandouet Date: Thu, 18 Nov 2021 15:49:54 +0100 Subject: [PATCH 2/5] First update try --- packages/react-dev-utils/FileSizeReporter.js | 4 +- .../react-dev-utils/WebpackDevServerUtils.js | 83 ++++++++++++------- packages/react-dev-utils/package.json | 4 +- packages/react-scripts/config/paths.js | 6 ++ .../react-scripts/config/webpack.config.js | 71 ++++++++++++---- packages/react-scripts/package.json | 5 +- packages/react-scripts/scripts/build.js | 4 +- packages/react-scripts/scripts/start.js | 2 +- .../scripts/utils/verifyTypeScriptSetup.js | 4 +- 9 files changed, 125 insertions(+), 58 deletions(-) diff --git a/packages/react-dev-utils/FileSizeReporter.js b/packages/react-dev-utils/FileSizeReporter.js index b2b4cc6904d..9e2d52f9b5e 100644 --- a/packages/react-dev-utils/FileSizeReporter.js +++ b/packages/react-dev-utils/FileSizeReporter.js @@ -17,9 +17,7 @@ var gzipSize = require('gzip-size').sync; function canReadAsset(asset) { return ( - /\.(js|css)$/.test(asset) && - !/service-worker\.js/.test(asset) && - !/precache-manifest\.[0-9a-f]+\.js/.test(asset) + /\.(js|css)$/.test(asset) ); } diff --git a/packages/react-dev-utils/WebpackDevServerUtils.js b/packages/react-dev-utils/WebpackDevServerUtils.js index 6a56f666aa1..cdf9fa12919 100644 --- a/packages/react-dev-utils/WebpackDevServerUtils.js +++ b/packages/react-dev-utils/WebpackDevServerUtils.js @@ -113,9 +113,9 @@ function createCompiler({ }) { // "Compiler" is a low-level interface to webpack. // It lets us listen to some events and provide our own custom messages. - let compiler; + let masterCompiler; try { - compiler = webpack(config); + masterCompiler = webpack(config); } catch (err) { console.log(chalk.red('Failed to compile.')); console.log(); @@ -128,7 +128,7 @@ function createCompiler({ // recompiling a bundle. WebpackDevServer takes care to pause serving the // bundle, so if you refresh, it'll wait instead of serving the old one. // "invalid" is short for "bundle invalidated", it doesn't imply any errors. - compiler.hooks.invalid.tap('invalid', () => { + masterCompiler.hooks.invalid.tap('invalid', () => { if (isInteractive) { clearConsole(); } @@ -136,31 +136,33 @@ function createCompiler({ }); let isFirstCompile = true; - let tsMessagesPromise; - let tsMessagesResolver; - - if (useTypeScript) { - compiler.hooks.beforeCompile.tap('beforeCompile', () => { - tsMessagesPromise = new Promise(resolve => { - tsMessagesResolver = msgs => resolve(msgs); + let tsMessagesPromises = []; + + masterCompiler.compilers.forEach((compiler, compilerIndex) => { + let tsMessagesResolver; + if (useTypeScript) { + compiler.hooks.beforeCompile.tap('beforeCompile', () => { + tsMessagesPromises[compilerIndex] = new Promise(resolve => { + tsMessagesResolver = msgs => resolve(msgs); + }); }); - }); - forkTsCheckerWebpackPlugin - .getCompilerHooks(compiler) - .receive.tap('afterTypeScriptCheck', (diagnostics, lints) => { - const allMsgs = [...diagnostics, ...lints]; - const format = message => - `${message.file}\n${typescriptFormatter(message, true)}`; - - tsMessagesResolver({ - errors: allMsgs.filter(msg => msg.severity === 'error').map(format), - warnings: allMsgs - .filter(msg => msg.severity === 'warning') - .map(format), + forkTsCheckerWebpackPlugin + .getCompilerHooks(compiler) + .receive.tap('afterTypeScriptCheck', (diagnostics, lints) => { + const allMsgs = [...diagnostics, ...lints]; + const format = message => + `${message.file}\n${typescriptFormatter(message, true)}`; + + tsMessagesResolver({ + errors: allMsgs.filter(msg => msg.severity === 'error').map(format), + warnings: allMsgs + .filter(msg => msg.severity === 'warning') + .map(format), + }); }); - }); - } + } + }); // "done" event fires when webpack has finished recompiling the bundle. // Whether or not you have warnings or errors, you will get this event. @@ -169,6 +171,17 @@ function createCompiler({ clearConsole(); } + stats.compilation = { + errors: stats.stats.reduce( + (previousErrors, s) => [...previousErrors, ...s.compilation.errors], + [] + ), + warnings: stats.stats.reduce( + (previousErrors, s) => [...previousErrors, ...s.compilation.warnings], + [] + ), + }; + // We have switched off the default webpack output in WebpackDevServer // options so we are going to "massage" the warnings and errors and present // them in a readable focused way. @@ -189,7 +202,15 @@ function createCompiler({ ); }, 100); - const messages = await tsMessagesPromise; + const masterMessages = await Promise.all(tsMessagesPromises); + const messages = masterMessages.reduce( + (previousMessages, currentMessages) => ({ + errors: [...previousMessages.errors, ...currentMessages.errors], + warnings: [...previousMessages.warnings, ...currentMessages.warnings], + }), + { errors: [], warnings: [] } + ); + clearTimeout(delayedMsg); if (tscCompileOnError) { statsData.warnings.push(...messages.errors); @@ -269,12 +290,12 @@ function createCompiler({ arg => arg.indexOf('--smoke-test') > -1 ); if (isSmokeTest) { - compiler.hooks.failed.tap('smokeTest', async () => { - await tsMessagesPromise; + masterCompiler.hooks.failed.tap('smokeTest', async () => { + await Promise.all(tsMessagesPromises); process.exit(1); }); - compiler.hooks.done.tap('smokeTest', async stats => { - await tsMessagesPromise; + masterCompiler.hooks.done.tap('smokeTest', async stats => { + await Promise.all(tsMessagesPromises); if (stats.hasErrors() || stats.hasWarnings()) { process.exit(1); } else { @@ -283,7 +304,7 @@ function createCompiler({ }); } - return compiler; + return masterCompiler; } function resolveLoopback(proxy) { diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 77a41ebe588..65dd1312bb3 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -1,6 +1,6 @@ { - "name": "react-dev-utils", - "version": "11.0.3", + "name": "@ouihelp/react-dev-utils", + "version": "11000003.0.1", "description": "webpack utilities used by Create React App", "repository": { "type": "git", diff --git a/packages/react-scripts/config/paths.js b/packages/react-scripts/config/paths.js index 67ba927fc80..e805c41953d 100644 --- a/packages/react-scripts/config/paths.js +++ b/packages/react-scripts/config/paths.js @@ -66,6 +66,7 @@ module.exports = { appPublic: resolveApp('public'), appHtml: resolveApp('public/index.html'), appIndexJs: resolveModule(resolveApp, 'src/index'), + appServiceWorkerJs: resolveModule(resolveApp, 'src/service-worker.entry'), appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), appTsConfig: resolveApp('tsconfig.json'), @@ -89,6 +90,7 @@ module.exports = { appPublic: resolveApp('public'), appHtml: resolveApp('public/index.html'), appIndexJs: resolveModule(resolveApp, 'src/index'), + appServiceWorkerJs: resolveModule(resolveApp, 'src/service-worker.entry'), appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), appTsConfig: resolveApp('tsconfig.json'), @@ -125,6 +127,10 @@ if ( appPublic: resolveOwn(`${templatePath}/public`), appHtml: resolveOwn(`${templatePath}/public/index.html`), appIndexJs: resolveModule(resolveOwn, `${templatePath}/src/index`), + appServiceWorkerJs: resolveModule( + resolveOwn, + `${templatePath}/src/service-worker.entry` + ), appPackageJson: resolveOwn('package.json'), appSrc: resolveOwn(`${templatePath}/src`), appTsConfig: resolveOwn(`${templatePath}/tsconfig.json`), diff --git a/packages/react-scripts/config/webpack.config.js b/packages/react-scripts/config/webpack.config.js index 26c2a65aad0..c724dc3de27 100644 --- a/packages/react-scripts/config/webpack.config.js +++ b/packages/react-scripts/config/webpack.config.js @@ -22,7 +22,6 @@ const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const safePostCssParser = require('postcss-safe-parser'); const ManifestPlugin = require('webpack-manifest-plugin'); const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); -const WorkboxWebpackPlugin = require('workbox-webpack-plugin'); const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent'); @@ -168,7 +167,7 @@ module.exports = function (webpackEnv) { return loaders; }; - return { + const originalConfig = { mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development', // Stop compilation early in production bail: isEnvProduction, @@ -709,19 +708,6 @@ module.exports = function (webpackEnv) { // https://github.com/jmblog/how-to-optimize-momentjs-with-webpack // You can remove this if you don't use Moment.js: new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), - // Generate a service worker script that will precache, and keep up to date, - // the HTML & assets that are part of the webpack build. - isEnvProduction && - fs.existsSync(swSrc) && - new WorkboxWebpackPlugin.InjectManifest({ - swSrc, - dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./, - exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/], - // Bump up the default maximum size (2mb) that's precached, - // to make lazy-loading failure scenarios less likely. - // See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270 - maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, - }), // TypeScript type checking useTypeScript && new ForkTsCheckerWebpackPlugin({ @@ -795,4 +781,59 @@ module.exports = function (webpackEnv) { // our own hints via the FileSizeReporter performance: false, }; + + const serviceWorkerConfig = { + target: 'webworker', + mode: originalConfig.mode, + bail: originalConfig.bail, + devtool: originalConfig.devtool, + entry: { 'service-worker': paths.appServiceWorkerJs }, + output: { + path: originalConfig.output.path, + pathinfo: originalConfig.output.pathinfo, + filename: 'sw.js', + publicPath: originalConfig.output.publicPath, + devtoolModuleFilenameTemplate: + originalConfig.output.devtoolModuleFilenameTemplate, + }, + optimization: { + minimize: originalConfig.optimization.minimize, + minimizer: originalConfig.optimization.minimizer, + }, + resolve: originalConfig.resolve, + resolveLoader: originalConfig.resolveLoader, + module: originalConfig.module, + plugins: [ + new webpack.DefinePlugin(env.stringified), + useTypeScript && + new ForkTsCheckerWebpackPlugin({ + typescript: resolve.sync('typescript', { + basedir: paths.appNodeModules, + }), + async: isEnvDevelopment, + useTypescriptIncrementalApi: true, + checkSyntacticErrors: true, + resolveModuleNameModule: process.versions.pnp + ? `${__dirname}/pnpTs.js` + : undefined, + resolveTypeReferenceDirectiveModule: process.versions.pnp + ? `${__dirname}/pnpTs.js` + : undefined, + tsconfig: paths.appTsConfig, + reportFiles: [ + '**', + '!**/__tests__/**', + '!**/?(*.)(spec|test).*', + '!**/src/setupProxy.*', + '!**/src/setupTests.*', + ], + watch: paths.appSrc, + silent: true, + // The formatter is invoked directly in WebpackDevServerUtils during development + formatter: isEnvProduction ? typescriptFormatter : undefined, + }), + ].filter(Boolean), + }; + + return [originalConfig, serviceWorkerConfig]; }; diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index ffae0fa304b..a8025526a42 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -1,6 +1,6 @@ { - "name": "react-scripts", - "version": "4.0.3", + "name": "@ouihelp/react-scripts", + "version": "4000003.0.1", "description": "Configuration and scripts for Create React App.", "repository": { "type": "git", @@ -29,6 +29,7 @@ "types": "./lib/react-app.d.ts", "dependencies": { "@babel/core": "7.12.3", + "@ouihelp/react-dev-utils": "11000003.0.1", "@pmmmwh/react-refresh-webpack-plugin": "0.4.3", "@svgr/webpack": "5.5.0", "@typescript-eslint/eslint-plugin": "^4.5.0", diff --git a/packages/react-scripts/scripts/build.js b/packages/react-scripts/scripts/build.js index de2ff505eb3..8bdde3054d7 100644 --- a/packages/react-scripts/scripts/build.js +++ b/packages/react-scripts/scripts/build.js @@ -41,7 +41,7 @@ const paths = require('../config/paths'); const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); -const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); +const FileSizeReporter = require('@ouihelp/react-dev-utils/FileSizeReporter'); const printBuildError = require('react-dev-utils/printBuildError'); const measureFileSizesBeforeBuild = @@ -115,7 +115,7 @@ checkBrowsers(paths.appPath, isInteractive) const appPackage = require(paths.appPackageJson); const publicUrl = paths.publicUrlOrPath; - const publicPath = config.output.publicPath; + const publicPath = config[0].output.publicPath; const buildFolder = path.relative(process.cwd(), paths.appBuild); printHostingInstructions( appPackage, diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js index ffbb15d1204..8f2da8f1188 100644 --- a/packages/react-scripts/scripts/start.js +++ b/packages/react-scripts/scripts/start.js @@ -42,7 +42,7 @@ const { createCompiler, prepareProxy, prepareUrls, -} = require('react-dev-utils/WebpackDevServerUtils'); +} = require('@ouihelp/react-dev-utils/WebpackDevServerUtils'); const openBrowser = require('react-dev-utils/openBrowser'); const semver = require('semver'); const paths = require('../config/paths'); diff --git a/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js b/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js index 949f34ab7d2..3b6cc1c995a 100644 --- a/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js +++ b/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js @@ -218,7 +218,7 @@ function verifyTypeScriptSetup() { if (appTsConfig.compilerOptions == null) { appTsConfig.compilerOptions = {}; firstTimeSetup = true; - } + } for (const option of Object.keys(compilerOptions)) { const { parsedValue, value, suggested, reason } = compilerOptions[option]; @@ -290,7 +290,7 @@ function verifyTypeScriptSetup() { if (!fs.existsSync(paths.appTypeDeclarations)) { fs.writeFileSync( paths.appTypeDeclarations, - `/// ${os.EOL}` + `/// ${os.EOL}` ); } } From e0f738586703d6b715e0bf01897366d9bb68c2ba Mon Sep 17 00:00:00 2001 From: Bastien Gandouet Date: Thu, 18 Nov 2021 15:56:42 +0100 Subject: [PATCH 3/5] Some typos --- packages/react-dev-utils/WebpackDevServerUtils.js | 2 +- packages/react-dev-utils/package.json | 2 +- packages/react-scripts/package.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-dev-utils/WebpackDevServerUtils.js b/packages/react-dev-utils/WebpackDevServerUtils.js index cdf9fa12919..499195e8d2c 100644 --- a/packages/react-dev-utils/WebpackDevServerUtils.js +++ b/packages/react-dev-utils/WebpackDevServerUtils.js @@ -166,7 +166,7 @@ function createCompiler({ // "done" event fires when webpack has finished recompiling the bundle. // Whether or not you have warnings or errors, you will get this event. - compiler.hooks.done.tap('done', async stats => { + masterCompiler.hooks.done.tap('done', async stats => { if (isInteractive) { clearConsole(); } diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 65dd1312bb3..93f3e42613e 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@ouihelp/react-dev-utils", - "version": "11000003.0.1", + "version": "11000003.0.2", "description": "webpack utilities used by Create React App", "repository": { "type": "git", diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index a8025526a42..23d9cb28e27 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@ouihelp/react-scripts", - "version": "4000003.0.1", + "version": "4000003.0.2", "description": "Configuration and scripts for Create React App.", "repository": { "type": "git", @@ -29,7 +29,7 @@ "types": "./lib/react-app.d.ts", "dependencies": { "@babel/core": "7.12.3", - "@ouihelp/react-dev-utils": "11000003.0.1", + "@ouihelp/react-dev-utils": "11000003.0.2", "@pmmmwh/react-refresh-webpack-plugin": "0.4.3", "@svgr/webpack": "5.5.0", "@typescript-eslint/eslint-plugin": "^4.5.0", From 9c74c2f56444e53410e9f9ab35320b9f400085db Mon Sep 17 00:00:00 2001 From: Bastien Gandouet Date: Thu, 18 Nov 2021 16:18:10 +0100 Subject: [PATCH 4/5] Try wo symlinks --- packages/react-scripts/config/webpack.config.js | 1 + packages/react-scripts/package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-scripts/config/webpack.config.js b/packages/react-scripts/config/webpack.config.js index c724dc3de27..6f304e6abbb 100644 --- a/packages/react-scripts/config/webpack.config.js +++ b/packages/react-scripts/config/webpack.config.js @@ -358,6 +358,7 @@ module.exports = function (webpackEnv) { reactRefreshOverlayEntry, ]), ], + symlinks: false, }, resolveLoader: { plugins: [ diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 23d9cb28e27..03abaf702c2 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@ouihelp/react-scripts", - "version": "4000003.0.2", + "version": "4000003.0.3", "description": "Configuration and scripts for Create React App.", "repository": { "type": "git", From a89fe1e3b53abd049a4264b3463624361046c56a Mon Sep 17 00:00:00 2001 From: Bastien Gandouet Date: Thu, 25 Nov 2021 18:29:56 +0100 Subject: [PATCH 5/5] Add InjectMainEntrypointManifestPlugin --- .../react-scripts/config/webpack.config.js | 46 +++++++++++++++++++ packages/react-scripts/package.json | 4 +- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/react-scripts/config/webpack.config.js b/packages/react-scripts/config/webpack.config.js index 6f304e6abbb..13605308a44 100644 --- a/packages/react-scripts/config/webpack.config.js +++ b/packages/react-scripts/config/webpack.config.js @@ -50,6 +50,10 @@ const reactRefreshOverlayEntry = require.resolve( 'react-dev-utils/refreshOverlayInterop' ); +// These two requirements are for our custom `InjectMainEntrypointManifestPlugin`. +const { RawSource } = require('webpack-sources'); +const replaceAndUpdateSourceMap = require('workbox-build/build/lib/replace-and-update-source-map'); + // Some apps do not need the benefits of saving a web request, so not inlining the chunk // makes for a smoother build process. const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false'; @@ -86,6 +90,44 @@ const hasJsxRuntime = (() => { } })(); + +// Custom Webpack plugin to stamp the main entrypoint list of files +// into our service worker. +// Inspired by workbox's `InjectManifestPlugin`. +let mainEntrypointFilesPromiseResolver; +const mainEntrypointFilesPromise = new Promise((resolve) => {mainEntrypointFilesPromiseResolver = resolve}); + +class InjectMainEntrypointManifestPlugin { + apply(compiler) { + compiler.hooks.emit.tapPromise( + this.constructor.name, + (compilation) => this.handleEmit(compilation).catch( + (error) => compilation.errors.push(error)), + ); + } + + async handleEmit(compilation) { + const mainEntryPointFiles = await mainEntrypointFilesPromise; + + const swFileName = "sw.js"; + const swSrcmapFileName = "sw.js.map"; + + const swAsset = compilation.assets[swFileName]; + const initialSWAssetString = swAsset.source(); + const sourcemapAsset = compilation.assets[swSrcmapFileName]; + const {source, map} = await replaceAndUpdateSourceMap({ + jsFilename: swFileName, + originalMap: JSON.parse(sourcemapAsset.source()), + originalSource: initialSWAssetString, + replaceString: JSON.stringify(Object.values(mainEntryPointFiles)), + searchString: "__MAIN_ENTRYPOINT_FILES", + }); + + compilation.assets[swSrcmapFileName] = new RawSource(map); + compilation.assets[swFileName] = new RawSource(source); + } +} + // This is the production and development configuration. // It is focused on developer experience, fast rebuilds, and a minimal bundle. module.exports = function (webpackEnv) { @@ -697,6 +739,9 @@ module.exports = function (webpackEnv) { fileName => !fileName.endsWith('.map') ); + // Give all files to our resolver. + mainEntrypointFilesPromiseResolver(manifestFiles); + return { files: manifestFiles, entrypoints: entrypointFiles, @@ -833,6 +878,7 @@ module.exports = function (webpackEnv) { // The formatter is invoked directly in WebpackDevServerUtils during development formatter: isEnvProduction ? typescriptFormatter : undefined, }), + new InjectMainEntrypointManifestPlugin(), ].filter(Boolean), }; diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 03abaf702c2..ad4fd85e683 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@ouihelp/react-scripts", - "version": "4000003.0.3", + "version": "4000003.0.4", "description": "Configuration and scripts for Create React App.", "repository": { "type": "git", @@ -86,7 +86,7 @@ "webpack": "4.44.2", "webpack-dev-server": "3.11.1", "webpack-manifest-plugin": "2.2.0", - "workbox-webpack-plugin": "5.1.4" + "workbox-build": "5.1.4" }, "devDependencies": { "react": "^17.0.1",