cra-build-watch
Version:
A script for create-react-app that writes development builds to the disk
210 lines (175 loc) • 6.32 kB
JavaScript
;
process.env.NODE_ENV = 'development'; // eslint-disable-line no-process-env
process.env.FAST_REFRESH = false;
const importCwd = require('import-cwd');
const fs = require('fs-extra');
const path = require('path');
const ora = require('ora');
const assert = require('assert');
const exec = require('child_process').exec;
const {
flags: {
buildPath,
publicPath,
reactScriptsVersion,
verbose,
disableChunks,
outputFilename,
chunkFilename,
afterInitialBuildHook,
afterRebuildHook,
},
} = require('../utils/cliHandler');
const { getReactScriptsVersion, isEjected } = require('../utils');
const { major, concatenatedVersion } = getReactScriptsVersion(reactScriptsVersion);
const paths = isEjected ? importCwd('./config/paths') : importCwd('react-scripts/config/paths');
const webpack = importCwd('webpack');
const config =
concatenatedVersion >= 212
? (isEjected
? importCwd('./config/webpack.config')
: importCwd('react-scripts/config/webpack.config'))('development')
: isEjected
? importCwd('./config/webpack.config.dev')
: importCwd('react-scripts/config/webpack.config.dev');
const HtmlWebpackPlugin = importCwd('html-webpack-plugin');
const InterpolateHtmlPlugin = importCwd('react-dev-utils/InterpolateHtmlPlugin');
const getClientEnvironment = isEjected
? importCwd('./config/env')
: importCwd('react-scripts/config/env');
console.log();
const spinner = ora('Update webpack configuration').start();
// we need to set the public_url ourselves because in dev mode
// it is supposed to always be an empty string as they are using
// the in-memory development server to serve the content
const env = getClientEnvironment(process.env.PUBLIC_URL || ''); // eslint-disable-line no-process-env
/**
* We need to update the webpack dev config in order to remove the use of webpack devserver
*/
if (major < 4) {
config.entry = config.entry.filter(fileName => !fileName.match(/webpackHotDevClient/));
}
config.plugins = config.plugins.filter(
plugin => !(plugin instanceof webpack.HotModuleReplacementPlugin)
);
/**
* We also need to update the path where the different files get generated.
*/
const resolvedBuildPath = buildPath ? handleBuildPath(buildPath) : paths.appBuild; // resolve the build path
// update the paths in config
config.output.path = resolvedBuildPath;
config.output.publicPath = publicPath || '';
// Grab output names from cli args, otherwise use some default naming.
const fileNameToUse = outputFilename || `js/bundle.js`;
const chunkNameToUse = chunkFilename || `js/[name].chunk.js`;
// If cli user adds .js, respect that, otherwise we add it ourself
config.output.filename = fileNameToUse.slice(-3) !== '.js' ? `${fileNameToUse}.js` : fileNameToUse;
config.output.chunkFilename =
chunkNameToUse.slice(-3) !== '.js' ? `${chunkNameToUse}.js` : chunkNameToUse;
if (disableChunks) {
assert(major >= 2, 'Split chunks optimization is only available in react-scripts >= 2.0.0');
// disable code-splitting/chunks
config.optimization.runtimeChunk = false;
config.optimization.splitChunks = {
cacheGroups: {
default: false,
},
};
}
// update media path destination
if (major >= 4) {
const oneOfIndex = 1;
config.module.rules[oneOfIndex].oneOf[0].options.name = `media/[name].[hash:8].[ext]`;
config.module.rules[oneOfIndex].oneOf[1].options.name = `media/[name].[hash:8].[ext]`;
config.module.rules[oneOfIndex].oneOf[8].options.name = `media/[name].[hash:8].[ext]`;
} else if (major >= 2) {
// 2.0.0 => 2
// 2.0.1 => 3
// 2.0.2 => 3
// 2.0.3 => 3
// 2.0.4 to 3.0.0 => 2
const oneOfIndex = concatenatedVersion === 200 || concatenatedVersion >= 204 ? 2 : 3;
config.module.rules[oneOfIndex].oneOf[0].options.name = `media/[name].[hash:8].[ext]`;
config.module.rules[oneOfIndex].oneOf[7].options.name = `media/[name].[hash:8].[ext]`;
} else {
config.module.rules[1].oneOf[0].options.name = `media/[name].[hash:8].[ext]`;
config.module.rules[1].oneOf[3].options.name = `media/[name].[hash:8].[ext]`;
}
let htmlPluginIndex = 1;
let interpolateHtmlPluginIndex = 0;
if (major >= 2) {
htmlPluginIndex = 0;
interpolateHtmlPluginIndex = 1;
}
// we need to override the InterpolateHtmlPlugin because in dev mod
// they don't provide it the PUBLIC_URL env
config.plugins[interpolateHtmlPluginIndex] = new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw);
config.plugins[htmlPluginIndex] = new HtmlWebpackPlugin({
inject: true,
template: paths.appHtml,
filename: 'index.html',
});
spinner.succeed();
spinner.start('Clear destination folder');
let inProgress = false;
fs.emptyDir(paths.appBuild)
.then(() => {
spinner.succeed();
return new Promise((resolve, reject) => {
const webpackCompiler = webpack(config);
new webpack.ProgressPlugin(() => {
if (!inProgress) {
spinner.start('Start webpack watch');
inProgress = true;
}
}).apply(webpackCompiler);
webpackCompiler.watch({}, (err, stats) => {
if (err) {
return reject(err);
}
spinner.succeed();
runHook('after rebuild hook', spinner, afterRebuildHook);
inProgress = false;
if (verbose) {
console.log();
console.log(
stats.toString({
chunks: false,
colors: true,
})
);
console.log();
}
return resolve();
});
});
})
.then(() => copyPublicFolder())
.then(() => runHook('after initial build hook', spinner, afterInitialBuildHook));
function copyPublicFolder() {
return fs.copy(paths.appPublic, resolvedBuildPath, {
dereference: true,
filter: file => file !== paths.appHtml,
});
}
function runHook(label, spinner, hook) {
if (!hook || typeof hook !== 'string') {
return;
}
spinner.start(label);
exec(hook, (error, stdout, stderr) => {
if (error) {
spinner.fail(`${label}: exec error: ${error}`);
} else if (stderr) {
spinner.warn(`${label}: ${stderr}`);
} else {
spinner.succeed(`${label}: ${stdout}`);
}
});
}
function handleBuildPath(userBuildPath) {
if (path.isAbsolute(userBuildPath)) {
return userBuildPath;
}
return path.join(process.cwd(), userBuildPath);
}