Skip to content
This repository was archived by the owner on Apr 24, 2024. It is now read-only.

Commit 7d2a516

Browse files
committed
feat: rewrite to take advantage of webpackDevServer writeToDisk setting
CRA has gone a long way since I wrote the first cra-build-watch version. Their webpack configuration changed a lot and often broke this tool in subtle ways. It has proven to be hard to maintain while being brittle and providing only a subset of the features CRA offers out of the box. Since then webpack-dev-server provided a new feature called writeToDisk which does what cra-build-watch first intended to provide. This rewrite intends on making a clean break from the past and embrace the future 🚀. BREAKING CHANGE: Remove most of the flags/features of cra-build-watch. It is not anymore a CLI replacement for react-scripts but rather something you call one-time after an `npm install` and then forget about it. It keeps CRA webpack configuration intact so you can enjoy all of CRA's wonderful features.
1 parent 234545b commit 7d2a516

File tree

3 files changed

+61
-213
lines changed

3 files changed

+61
-213
lines changed

README.md

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,27 @@ As of now (20/04/2018), `create-react-app` (more precisely `react-scripts`) does
3535
- Incorporating your React application into an existing application.
3636
- Serving your React app with a dedicated backend.
3737

38+
# Notes for version `v4.0.0` beta testers
39+
40+
This is just the bare version for the new behavior of the tool. I doesn't not handle any of the previous options and perhaps it shall never again. Most issues on this repo asked for features that CRA already provided out of the box so I expect most of them to be dealt with in this version.
41+
42+
It does not handle ejected projects yet either but it will in subsequent beta versions.
43+
44+
## Usage
45+
46+
Call at the project root:
47+
48+
```bash
49+
npx cra-build-watch
50+
51+
```
52+
53+
You don't need to use `cra-build-watch` as an executable in an npm script anymore. You just need to call it every time you upgrade `react-scripts`. It works on bare CRA project. You should be able to enjoy what CRA has to offer. This tool now only monkey-patches the webpack configuration to leverage webpack-dev-server `writeToDisk` property.
54+
55+
<details>
56+
57+
<summary>Old versions</summary>
58+
3859
# Prerequisites
3960

4061
Supports `react-scripts >= 1.0.x`, hence it supports the newest version `4.x.x`.
@@ -61,9 +82,9 @@ Add a new script into your `package.json`:
6182

6283
```json
6384
{
64-
"scripts": {
65-
"watch": "cra-build-watch"
66-
}
85+
"scripts": {
86+
"watch": "cra-build-watch"
87+
}
6788
}
6889
```
6990

@@ -85,20 +106,21 @@ By default the script will generate everything into `build/` at your project roo
85106

86107
If those defaults do not work for you, the script accepts some arguments:
87108

88-
- `--after-initial-build-hook`: accepts a string of shell code that will be run only once after the initial build in the same process as the `cra-build-watch`.
89-
- `--after-rebuild-hook`: accepts a string of shell code that will be run every time webpack rebuilds your project after a filesystem change. It runs in the same process as `cra-build-watch`.
90-
- `-b|--build-path`: expects either an absolute or relative path. If a relative path is given it will be prefixed by your project root path.
91-
- default: `yourProjectRoot/build`.
92-
- `--chunk-filename`: Set the naming you want to use for non-entry chunk files. Accepts webpack placeholders such as `[id]`, `[name]`, `[hash]`. Directories can be supplied.
93-
- default: `js/bundle.js`.
94-
- `--disable-chunks`: disable code-splitting / chunks so that only a single bundle.js file is generated. It only works with `react-scripts` >= `2.0.0`.
95-
- `-o|--output-filename`: Set the name to be used for the output bundle. Accepts webpack placeholders such as `[id]`, `[name]`, `[hash]`. Directories can be supplied.
96-
- default: `js/[name].chunk.js`
97-
- `--react-scripts-version`: expects the `react-scripts` version you are using in your project i.e `2.0.3`. If not given it will be implied from your `node_modules` and if it cannot be implied the version `2.1.2` will be the default. Consider setting it if you **ejected** and are not using the latest `react-scripts` version.
98-
- `-p|--public-path`: expects a relative URL where `/` is the root. If you serve your files using an external webserver this argument is to match with your web server configuration. More information can be found in [webpack configuration guide](https://webpack.js.org/configuration/output/#output-publicpath).
99-
- default: "".
100-
- `-v|--verbose`: display webpack build output.
109+
- `--after-initial-build-hook`: accepts a string of shell code that will be run only once after the initial build in the same process as the `cra-build-watch`.
110+
- `--after-rebuild-hook`: accepts a string of shell code that will be run every time webpack rebuilds your project after a filesystem change. It runs in the same process as `cra-build-watch`.
111+
- `-b|--build-path`: expects either an absolute or relative path. If a relative path is given it will be prefixed by your project root path.
112+
- default: `yourProjectRoot/build`.
113+
- `--chunk-filename`: Set the naming you want to use for non-entry chunk files. Accepts webpack placeholders such as `[id]`, `[name]`, `[hash]`. Directories can be supplied.
114+
- default: `js/bundle.js`.
115+
- `--disable-chunks`: disable code-splitting / chunks so that only a single bundle.js file is generated. It only works with `react-scripts` >= `2.0.0`.
116+
- `-o|--output-filename`: Set the name to be used for the output bundle. Accepts webpack placeholders such as `[id]`, `[name]`, `[hash]`. Directories can be supplied.
117+
- default: `js/[name].chunk.js`
118+
- `--react-scripts-version`: expects the `react-scripts` version you are using in your project i.e `2.0.3`. If not given it will be implied from your `node_modules` and if it cannot be implied the version `2.1.2` will be the default. Consider setting it if you **ejected** and are not using the latest `react-scripts` version.
119+
- `-p|--public-path`: expects a relative URL where `/` is the root. If you serve your files using an external webserver this argument is to match with your web server configuration. More information can be found in [webpack configuration guide](https://webpack.js.org/configuration/output/#output-publicpath).
120+
- default: "".
121+
- `-v|--verbose`: display webpack build output.
101122

102123
# Contributions
103124

104125
All contributions are welcomed.
126+
</details>

scripts/index.js

Lines changed: 11 additions & 197 deletions
Original file line numberDiff line numberDiff line change
@@ -1,209 +1,23 @@
11
'use strict';
22

33
process.env.NODE_ENV = 'development'; // eslint-disable-line no-process-env
4-
process.env.FAST_REFRESH = false;
54

6-
const importCwd = require('import-cwd');
75
const fs = require('fs-extra');
86
const path = require('path');
97
const ora = require('ora');
10-
const assert = require('assert');
11-
const exec = require('child_process').exec;
128

13-
const {
14-
flags: {
15-
buildPath,
16-
publicPath,
17-
reactScriptsVersion,
18-
verbose,
19-
disableChunks,
20-
outputFilename,
21-
chunkFilename,
22-
afterInitialBuildHook,
23-
afterRebuildHook,
24-
},
25-
} = require('../utils/cliHandler');
26-
const { getReactScriptsVersion, isEjected } = require('../utils');
27-
28-
const { major, concatenatedVersion } = getReactScriptsVersion(reactScriptsVersion);
29-
30-
const paths = isEjected ? importCwd('./config/paths') : importCwd('react-scripts/config/paths');
31-
const webpack = importCwd('webpack');
32-
33-
const config =
34-
concatenatedVersion >= 212
35-
? (isEjected
36-
? importCwd('./config/webpack.config')
37-
: importCwd('react-scripts/config/webpack.config'))('development')
38-
: isEjected
39-
? importCwd('./config/webpack.config.dev')
40-
: importCwd('react-scripts/config/webpack.config.dev');
41-
42-
const HtmlWebpackPlugin = importCwd('html-webpack-plugin');
43-
const InterpolateHtmlPlugin = importCwd('react-dev-utils/InterpolateHtmlPlugin');
44-
const getClientEnvironment = isEjected
45-
? importCwd('./config/env')
46-
: importCwd('react-scripts/config/env');
47-
48-
console.log();
49-
const spinner = ora('Update webpack configuration').start();
50-
51-
// we need to set the public_url ourselves because in dev mode
52-
// it is supposed to always be an empty string as they are using
53-
// the in-memory development server to serve the content
54-
const env = getClientEnvironment(process.env.PUBLIC_URL || ''); // eslint-disable-line no-process-env
55-
56-
/**
57-
* We need to update the webpack dev config in order to remove the use of webpack devserver
58-
*/
59-
if (major < 4) {
60-
config.entry = config.entry.filter(fileName => !fileName.match(/webpackHotDevClient/));
61-
}
62-
config.plugins = config.plugins.filter(
63-
plugin => !(plugin instanceof webpack.HotModuleReplacementPlugin)
64-
);
65-
66-
/**
67-
* We also need to update the path where the different files get generated.
68-
*/
69-
const resolvedBuildPath = buildPath ? handleBuildPath(buildPath) : paths.appBuild; // resolve the build path
70-
71-
// update the paths in config
72-
config.output.path = resolvedBuildPath;
73-
config.output.publicPath = publicPath || '';
74-
75-
// Grab output names from cli args, otherwise use some default naming.
76-
const fileNameToUse = outputFilename || `js/bundle.js`;
77-
const chunkNameToUse = chunkFilename || `js/[name].chunk.js`;
78-
// If cli user adds .js, respect that, otherwise we add it ourself
79-
config.output.filename = fileNameToUse.slice(-3) !== '.js' ? `${fileNameToUse}.js` : fileNameToUse;
80-
config.output.chunkFilename =
81-
chunkNameToUse.slice(-3) !== '.js' ? `${chunkNameToUse}.js` : chunkNameToUse;
82-
83-
if (disableChunks) {
84-
assert(major >= 2, 'Split chunks optimization is only available in react-scripts >= 2.0.0');
85-
// disable code-splitting/chunks
86-
config.optimization.runtimeChunk = false;
87-
88-
config.optimization.splitChunks = {
89-
cacheGroups: {
90-
default: false,
91-
},
92-
};
93-
}
94-
95-
// update media path destination
96-
if (major >= 4) {
97-
const oneOfIndex = 1;
98-
config.module.rules[oneOfIndex].oneOf[0].options.name = `media/[name].[hash:8].[ext]`;
99-
config.module.rules[oneOfIndex].oneOf[1].options.name = `media/[name].[hash:8].[ext]`;
100-
config.module.rules[oneOfIndex].oneOf[8].options.name = `media/[name].[hash:8].[ext]`;
101-
} else if (major >= 2) {
102-
// 2.0.0 => 2
103-
// 2.0.1 => 3
104-
// 2.0.2 => 3
105-
// 2.0.3 => 3
106-
// 2.0.4 to 3.0.0 => 2
107-
const oneOfIndex = concatenatedVersion === 200 || concatenatedVersion >= 204 ? 2 : 3;
108-
config.module.rules[oneOfIndex].oneOf[0].options.name = `media/[name].[hash:8].[ext]`;
109-
config.module.rules[oneOfIndex].oneOf[7].options.name = `media/[name].[hash:8].[ext]`;
110-
} else {
111-
config.module.rules[1].oneOf[0].options.name = `media/[name].[hash:8].[ext]`;
112-
config.module.rules[1].oneOf[3].options.name = `media/[name].[hash:8].[ext]`;
113-
}
114-
115-
let htmlPluginIndex = 1;
116-
let interpolateHtmlPluginIndex = 0;
117-
if (major >= 2) {
118-
htmlPluginIndex = 0;
119-
interpolateHtmlPluginIndex = 1;
120-
}
121-
122-
// we need to override the InterpolateHtmlPlugin because in dev mod
123-
// they don't provide it the PUBLIC_URL env
124-
config.plugins[interpolateHtmlPluginIndex] = new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw);
125-
config.plugins[htmlPluginIndex] = new HtmlWebpackPlugin({
126-
inject: true,
127-
template: paths.appHtml,
128-
filename: 'index.html',
129-
});
9+
const rootCraPath = path.join(process.cwd(), 'node_modules', 'react-scripts');
10+
const configPath = path.join(rootCraPath, 'config');
11+
const configSource = path.join(configPath, 'webpackDevServer.config.js');
12+
const destSource = path.join(configPath, 'webpackDevServer.config.ori.js');
13013

14+
const spinner = ora('💾 Create webpackDevServer.config.js backup').start();
15+
fs.moveSync(configSource, destSource);
13116
spinner.succeed();
132-
spinner.start('Clear destination folder');
13317

134-
let inProgress = false;
135-
136-
fs.emptyDir(paths.appBuild)
137-
.then(() => {
138-
spinner.succeed();
139-
140-
return new Promise((resolve, reject) => {
141-
const webpackCompiler = webpack(config);
142-
new webpack.ProgressPlugin(() => {
143-
if (!inProgress) {
144-
spinner.start('Start webpack watch');
145-
inProgress = true;
146-
}
147-
}).apply(webpackCompiler);
148-
149-
webpackCompiler.watch({}, (err, stats) => {
150-
if (err) {
151-
return reject(err);
152-
}
153-
154-
spinner.succeed();
155-
156-
runHook('after rebuild hook', spinner, afterRebuildHook);
157-
158-
inProgress = false;
159-
160-
if (verbose) {
161-
console.log();
162-
console.log(
163-
stats.toString({
164-
chunks: false,
165-
colors: true,
166-
})
167-
);
168-
console.log();
169-
}
170-
171-
return resolve();
172-
});
173-
});
174-
})
175-
.then(() => copyPublicFolder())
176-
.then(() => runHook('after initial build hook', spinner, afterInitialBuildHook));
177-
178-
function copyPublicFolder() {
179-
return fs.copy(paths.appPublic, resolvedBuildPath, {
180-
dereference: true,
181-
filter: file => file !== paths.appHtml,
182-
});
183-
}
184-
185-
function runHook(label, spinner, hook) {
186-
if (!hook || typeof hook !== 'string') {
187-
return;
188-
}
189-
190-
spinner.start(label);
191-
192-
exec(hook, (error, stdout, stderr) => {
193-
if (error) {
194-
spinner.fail(`${label}: exec error: ${error}`);
195-
} else if (stderr) {
196-
spinner.warn(`${label}: ${stderr}`);
197-
} else {
198-
spinner.succeed(`${label}: ${stdout}`);
199-
}
200-
});
201-
}
202-
203-
function handleBuildPath(userBuildPath) {
204-
if (path.isAbsolute(userBuildPath)) {
205-
return userBuildPath;
206-
}
18+
spinner.start('🔧 Add webpackDevServer.config.js wrapper');
19+
fs.copySync(path.join(__dirname, '../utils/configWrapper.js'), configSource);
20+
spinner.succeed();
20721

208-
return path.join(process.cwd(), userBuildPath);
209-
}
22+
console.log();
23+
console.log(`You're all done. 🧰 Enjoy!`);

utils/configWrapper.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use strict';
2+
3+
const createDevServerConfig = require('./webpackDevServer.config.ori');
4+
5+
module.exports = function (...args) {
6+
const initialConfig = createDevServerConfig(...args);
7+
8+
return {
9+
...initialConfig,
10+
writeToDisk: true,
11+
};
12+
};

0 commit comments

Comments
 (0)