diff --git a/packages/react-scripts/config/distNaming.js b/packages/react-scripts/config/distNaming.js new file mode 100644 index 00000000000..745e8f21d91 --- /dev/null +++ b/packages/react-scripts/config/distNaming.js @@ -0,0 +1,95 @@ +'use strict'; +const chalk = require('react-dev-utils/chalk'); + +/** + * @typedef {Object} DistNaming + * @property {OutputStructure} output Naming patterns for webpack's config output section + * @property {FileNaming} media Naming pattern for images `webpackConfig.module.rules..oneOf` + * @property {FileNaming} files Naming patterns for latest in `oneOf` rule – `file-loader` + * @property {FileNaming} css Naming patterns for MiniCassExtractPlugin + */ + +/** + * @typedef {Object} OutputStructure + * @property {FileNaming} development + * @property {FileNaming} production + */ + +/** + * @typedef {Object} FileNaming + * @property {string} filename + * @property {string} [chunkFilename] + */ + +const env = process.env; +const npmPropertyRegex = /^npm_package_distNaming_(.+)$/; + +/** + * @type DistNaming + */ +const defaultNaming = { + output: { + development: { + filename: 'static/js/bundle.js', + chunkFilename: 'static/js/[name].chunk.js', + }, + production: { + filename: 'static/js/[name].[contenthash:8].js', + chunkFilename: 'static/js/[name].[contenthash:8].chunk.js', + }, + }, + + media: { + filename: 'static/media/[name].[hash:8].[ext]', + }, + + files: { + filename: 'static/media/[name].[hash:8].[ext]', + }, + + css: { + filename: 'static/css/[name].[contenthash:8].css', + chunkFilename: 'static/css/[name].[contenthash:8].chunk.css', + }, +}; + +/** + * @type DistNaming + */ +const structure = Object.entries(env).reduce(setCustomNaming, defaultNaming); + +module.exports = structure; + +function setCustomNaming(naming, envEntry) { + const [key, value] = envEntry; + + if (npmPropertyRegex.test(key)) { + const [, name] = npmPropertyRegex.exec(key); + const path = name.split('_'); + + try { + return update(naming, path, value); + } catch (e) { + throw new Error(e.message + chalk.red(` in ${path.join('.')}`)); + } + } + + return naming; +} + +function update(obj, path, value) { + const [head, ...tail] = path; + + if (head) { + if (!obj.hasOwnProperty(head)) { + throw new Error(chalk.red('Unexpected property ') + chalk.red.bold(head)); + } + + return { + ...obj, + [head]: update(obj[head], tail, value), + }; + } else { + return value; + } +} diff --git a/packages/react-scripts/config/webpack.config.js b/packages/react-scripts/config/webpack.config.js index 771b06c94d1..592b6268ff4 100644 --- a/packages/react-scripts/config/webpack.config.js +++ b/packages/react-scripts/config/webpack.config.js @@ -37,6 +37,7 @@ const typescriptFormatter = require('react-dev-utils/typescriptFormatter'); const getCacheIdentifier = require('react-dev-utils/getCacheIdentifier'); // @remove-on-eject-end const postcssNormalize = require('postcss-normalize'); +const distNaming = require('./distNaming'); // Source maps are resource heavy and can cause out of memory issue for large source files. const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false'; @@ -169,14 +170,14 @@ module.exports = function(webpackEnv) { // There will be one main bundle, and one file per asynchronous chunk. // In development, it does not produce real files. filename: isEnvProduction - ? 'static/js/[name].[contenthash:8].js' - : isEnvDevelopment && 'static/js/bundle.js', + ? distNaming.output.production.filename + : isEnvDevelopment && distNaming.output.development.filename, // TODO: remove this when upgrading to webpack 5 futureEmitAssets: true, // There are also additional JS chunk files if you use code splitting. chunkFilename: isEnvProduction - ? 'static/js/[name].[contenthash:8].chunk.js' - : isEnvDevelopment && 'static/js/[name].chunk.js', + ? distNaming.output.production.chunkFilename + : isEnvDevelopment && distNaming.output.development.chunkFilename, // We inferred the "public path" (such as / or /my-project) from homepage. // We use "/" in development. publicPath: publicPath, @@ -348,7 +349,7 @@ module.exports = function(webpackEnv) { loader: require.resolve('url-loader'), options: { limit: imageInlineSizeLimit, - name: 'static/media/[name].[hash:8].[ext]', + name: distNaming.media.filename, }, }, // Process application JS with Babel. @@ -517,7 +518,7 @@ module.exports = function(webpackEnv) { // by webpacks internal loaders. exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/], options: { - name: 'static/media/[name].[hash:8].[ext]', + name: distNaming.files.filename, }, }, // ** STOP ** Are you adding a new loader? @@ -590,8 +591,8 @@ module.exports = function(webpackEnv) { new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // both options are optional - filename: 'static/css/[name].[contenthash:8].css', - chunkFilename: 'static/css/[name].[contenthash:8].chunk.css', + filename: distNaming.css.filename, + chunkFilename: distNaming.css.chunkFilename, }), // Generate a manifest file which contains a mapping of all asset filenames // to their corresponding output file so that tools can pick it up without