Skip to content
This repository was archived by the owner on Mar 25, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions packages/react-dev-utils/FileSizeReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}

Expand Down
85 changes: 53 additions & 32 deletions packages/react-dev-utils/WebpackDevServerUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -128,47 +128,60 @@ 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();
}
console.log('Compiling...');
});

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.
compiler.hooks.done.tap('done', async stats => {
masterCompiler.hooks.done.tap('done', async stats => {
if (isInteractive) {
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.
Expand All @@ -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);
Expand Down Expand Up @@ -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 {
Expand All @@ -283,7 +304,7 @@ function createCompiler({
});
}

return compiler;
return masterCompiler;
}

function resolveLoopback(proxy) {
Expand Down
4 changes: 2 additions & 2 deletions packages/react-dev-utils/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-dev-utils",
"version": "11.0.3",
"name": "@ouihelp/react-dev-utils",
"version": "11000003.0.2",
"description": "webpack utilities used by Create React App",
"repository": {
"type": "git",
Expand Down
6 changes: 6 additions & 0 deletions packages/react-scripts/config/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -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'),
Expand Down Expand Up @@ -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`),
Expand Down
118 changes: 103 additions & 15 deletions packages/react-scripts/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -51,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';
Expand Down Expand Up @@ -87,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) {
Expand Down Expand Up @@ -168,7 +209,7 @@ module.exports = function (webpackEnv) {
return loaders;
};

return {
const originalConfig = {
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
// Stop compilation early in production
bail: isEnvProduction,
Expand Down Expand Up @@ -359,6 +400,7 @@ module.exports = function (webpackEnv) {
reactRefreshOverlayEntry,
]),
],
symlinks: false,
},
resolveLoader: {
plugins: [
Expand Down Expand Up @@ -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,
Expand All @@ -709,19 +754,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({
Expand Down Expand Up @@ -795,4 +827,60 @@ 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,
}),
new InjectMainEntrypointManifestPlugin(),
].filter(Boolean),
};

return [originalConfig, serviceWorkerConfig];
};
7 changes: 4 additions & 3 deletions packages/react-scripts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-scripts",
"version": "4.0.3",
"name": "@ouihelp/react-scripts",
"version": "4000003.0.4",
"description": "Configuration and scripts for Create React App.",
"repository": {
"type": "git",
Expand Down Expand Up @@ -29,6 +29,7 @@
"types": "./lib/react-app.d.ts",
"dependencies": {
"@babel/core": "7.12.3",
"@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",
Expand Down Expand Up @@ -85,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",
Expand Down
Loading