Skip to content

Instantly share code, notes, and snippets.

@int128
Last active December 18, 2024 08:48
Show Gist options
  • Save int128/e0cdec598c5b3db728ff35758abdbafd to your computer and use it in GitHub Desktop.
Save int128/e0cdec598c5b3db728ff35758abdbafd to your computer and use it in GitHub Desktop.
Watching build mode on Create React App

Create React App does not provide watching build mode oficially (#1070).

This script provides watching build mode for an external tool such as Chrome Extensions or Firebase app.

How to Use

Create a React app.

Put the script into scripts/watch.js.

Add watch task into the scripts block in package.json as follows:

  "scripts": {
    "start": "react-scripts start",
    // Add next line
    "watch": "node scripts/watch.js",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }

Run the watch task.

npm run watch

Change source code and check build output.

Directory structure may be following:

  • app/
    • src/
    • public/
    • scripts/
      • watch.js (need to add)
    • package.json (need to modify)
    • build/ (output)
process.env.NODE_ENV = 'development';
const fs = require('fs-extra');
const paths = require('react-scripts/config/paths');
const webpack = require('webpack');
const config = require('react-scripts/config/webpack.config.dev.js');
// removes react-dev-utils/webpackHotDevClient.js at first in the array
config.entry.shift();
webpack(config).watch({}, (err, stats) => {
if (err) {
console.error(err);
} else {
copyPublicFolder();
}
console.error(stats.toString({
chunks: false,
colors: true
}));
});
function copyPublicFolder() {
fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true,
filter: file => file !== paths.appHtml
});
}
@piyush1104
Copy link

Note: still working in 2020 ;) I just added those lines to speed up (drastically) rebuild time.

config.mode = 'development';
config.devtool = 'eval-cheap-module-source-map';
delete config.optimization;

If you are building a browser extension, then using eval-cheap module will give the error. Better stick to default one.

@piyush1104
Copy link

this is a cool script but I notice that css is not updated...I'm the only one with this problem?for css I use scss and these 2 scripts:

    "build-css": "node-sass-chokidar src/ -o src/",
    "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive",

I'm running watch-css in paralles

You can actually do that, because you kind of get the whole config. I tried to that but it requires a lot of changes which is not worthy of the time. Rather I did npm run eject, and passed a variable to check if I am using the watch option. This is the fastest way to keep everything running.

@AdelinaUwU
Copy link

What need to do?

No way. Do everything in one folder and then in production mode, move to the desired

@AdelinaUwU
Copy link

AdelinaUwU commented Jun 15, 2020

I used the script for quite some time. There he is:

process.env.NODE_ENV =  development';

const fs = require('fs-extra');
const paths = require('react-scripts/config/paths');
const webpack = require('webpack');
const webpackconfig = require('react-scripts/config/webpack.config.js');
const config = webpackconfig('development');
config.entry.shift();

webpack(config).watch({}, (err, stats) => {
    if (err) {
        console.error(err);
    } else {
        copyPublicFolder();
    }
    console.error(stats.toString({
        chunks: false,
        colors: true
    }));
});

function copyPublicFolder() {
    fs.copySync(paths.appPublic, paths.appBuild, {
        dereference: true,
        filter: file => file !== paths.appHtml
    });
}

Now I have a project of 73 files and 3000 lines of code. I save files, but the code only works on some files, the rest of the more attached files are ignored or not always compiled. Something is wrong with this script.

@piyush1104
Copy link

I used the script for quite some time. There he is:

process.env.NODE_ENV =  development';

const fs = require('fs-extra');
const paths = require('react-scripts/config/paths');
const webpack = require('webpack');
const webpackconfig = require('react-scripts/config/webpack.config.js');
const config = webpackconfig('development');
config.entry.shift();

webpack(config).watch({}, (err, stats) => {
    if (err) {
        console.error(err);
    } else {
        copyPublicFolder();
    }
    console.error(stats.toString({
        chunks: false,
        colors: true
    }));
});

function copyPublicFolder() {
    fs.copySync(paths.appPublic, paths.appBuild, {
        dereference: true,
        filter: file => file !== paths.appHtml
    });
}

Now I have a project of 73 files and 3000 lines of code. I save files, but the code only works on some files, the rest of the more attached files are ignored or not always compiled. Something is wrong with this script.

It will not compile your stylesheets, but other js files work fine for me. This is my watch.js

process.env.BABEL_ENV = 'development'
process.env.NODE_ENV = 'development'
process.env.INLINE_RUNTIME_CHUNK = 'false'

const fs = require('fs-extra')
const paths = require('../config/paths')
const webpack = require('webpack')
const webpackconfig = require('../config/webpack.config')
const config = webpackconfig('development', true)
const pkg = require('../package.json')

delete config.optimization

config.watch = true
config.watchOptions = {
	poll: false,
	ignored: /node_modules/,
	aggregateTimeout: 1000,
}

webpack(config).watch({}, (err, stats) => {
	if (err) {
		console.error(err)
	} else {
		// this just exists to copy the remaining thing from the public folder to build folder ( see build.js)
		copyPublicFolder()
	}
	console.error(
		stats.toString({
			chunks: false,
			colors: true,
		})
	)
})

// copy favicon.ico and robots.txt from public to build folder
function copyPublicFolder() {
	fs.copySync(paths.appPublic, paths.appBuild, {
		dereference: true,
		filter: file => file !== paths.appHtml,
	})
}

@davidmroth
Copy link

davidmroth commented Jul 8, 2020

var entry = config.entry;
var plugins = config.plugins;

entry = entry.filter(fileName => !fileName.match(/webpackHotDevClient/));
plugins = plugins.filter(plugin => !(plugin instanceof webpack.HotModuleReplacementPlugin));

Thanks @BalavigneshJ! Worked perfectly!

@pascallapradebrite4
Copy link

The script no longer works with CRA 4.0.0. This did the trick for us:

Replace config.entry.shift(); with:

// ...other requires...
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

// ...other content...

// Remove 'react-refresh' from the loaders.
for (const rule of conf.module.rules) {
  if (!rule.oneOf) continue

  for (const one of rule.oneOf) {
    if (
      one.loader &&
      one.loader.includes('babel-loader') &&
      one.options &&
      one.options.plugins
    ) {
      one.options.plugins = one
        .options
        .plugins
        .filter(
          plugin =>
            typeof plugin !== 'string' ||
            !plugin.includes('react-refresh'),
        )
    }
  }
}

// Remove 'react-refresh' and HMR plugins.
conf.plugins = conf
  .plugins
  .filter(
    plugin =>
      !(plugin instanceof webpack.HotModuleReplacementPlugin) &&
      !(plugin instanceof ReactRefreshPlugin),
  )

// ...other content...

This removes both the HMR plugin as previously, as well as the ReactRefreshPlugin which seems to have been added in the new version (which causes errors to be printed in the browser's console if left there).

@firedev
Copy link

firedev commented Jan 8, 2021

@pascallapradebrite4 Could you please paste the whole file?

@pascallapradebrite4
Copy link

@pascallapradebrite4 Could you please paste the whole file?

Yeah sure! It's pretty much a mix between what I put above and the original gist (please excuse the lack of consistency between the styles):

// Source: https://gist.github.com/int128/e0cdec598c5b3db728ff35758abdbafd

process.env.NODE_ENV = 'development';

const fs = require('fs-extra');
const paths = require('react-scripts/config/paths');
const webpack = require('webpack');
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const config = require('react-scripts/config/webpack.config.js');
const path = require('path');

const conf = config('development');

for (const rule of conf.module.rules) {
  if (!rule.oneOf) continue

  for (const one of rule.oneOf) {
    if (
      one.loader &&
      one.loader.includes('babel-loader') &&
      one.options &&
      one.options.plugins
    ) {
      one.options.plugins = one
        .options
        .plugins
        .filter(plugin =>
          typeof plugin !== 'string' ||
          !plugin.includes('react-refresh')
        )
    }
  }
}

conf.plugins = conf
  .plugins
  .filter(plugin =>
    !(plugin instanceof webpack.HotModuleReplacementPlugin) &&
    !(plugin instanceof ReactRefreshPlugin)
  )

// We needed to output to a specific folder for cross-framework interop.
// Make sure to change the output path or to remove this line if the behavior
// of the original gist is sufficient for your needs!
conf.output.path = path.join(process.cwd(), './path/to/output');

webpack(conf).watch({}, (err, stats) => {
  if (err) {
    console.error(err);
  } else {
    copyPublicFolder();
  }
  console.error(stats.toString({
    chunks: false,
    colors: true
  }));
});

function copyPublicFolder() {
  fs.copySync(paths.appPublic, paths.appBuild, {
    dereference: true,
    filter: file => file !== paths.appHtml
  });
}

We only used it with react-scripts@4.0.0 so far, so I don't know if further changes are required with 4.0.1.

@grumpyTofu
Copy link

Updated for the latest cra (4.0.3):

process.env.NODE_ENV = 'development';

const fs = require('fs-extra');
const paths = require('../config/paths');
const webpack = require('webpack');
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const config = require('../config/webpack.config.js');
const path = require('path');

const conf = config('development');

for (const rule of conf.module.rules) {
  if (!rule.oneOf) continue

  for (const one of rule.oneOf) {
    if (
      one.loader &&
      one.loader.includes('babel-loader') &&
      one.options &&
      one.options.plugins
    ) {
      one.options.plugins = one
        .options
        .plugins
        .filter(plugin =>
          typeof plugin !== 'string' ||
          !plugin.includes('react-refresh')
        )
    }
  }
}

conf.plugins = conf
  .plugins
  .filter(plugin =>
    !(plugin instanceof webpack.HotModuleReplacementPlugin) &&
    !(plugin instanceof ReactRefreshPlugin)
  )

// We needed to output to a specific folder for cross-framework interop.
// Make sure to change the output path or to remove this line if the behavior
// of the original gist is sufficient for your needs!
conf.output.path = path.join(process.cwd(), './build');

webpack(conf).watch({}, (err, stats) => {
  if (err) {
    console.error(err);
  } else {
    copyPublicFolder();
  }
  console.error(stats.toString({
    chunks: false,
    colors: true
  }));
});

function copyPublicFolder() {
  fs.copySync(paths.appPublic, paths.appBuild, {
    dereference: true,
    filter: file => file !== paths.appHtml
  });
}

@Ark-kun
Copy link

Ark-kun commented Apr 6, 2022

Looks like this script does not respond to PUBLIC_URL="./" env variable. (Very useful when you need to have relative URLs like with VSCode plugins )
I know nothing, so I just added conf.output.publicPath = process.env.PUBLIC_URL. It worked But why would it not propagate?

conf.output.path = path.join(process.cwd(), './path/to/output')

I'm not sure this is needed - you can always use the BUILD_PATH env variable.
For example: cross-env-shell BUILD_PATH=$INIT_CWD/build npm run watch

@Ark-kun
Copy link

Ark-kun commented Jul 6, 2022

I just set FAST_REFRESH=false instead of the complicated plugin filtering logic.

process.env.FAST_REFRESH = false;

or

cross-env-shell FAST_REFRESH=false node scripts/watch.js

@SgtPooki
Copy link

with "react-scripts": "^4.0.3",

> node scripts/watch.js

INF | Serving assets from frontend DevServer URL: http://localhost:3000
DEB | [DevWebServer] Waiting for frontend DevServer 'http://localhost:3000' to be ready
node:internal/modules/cjs/loader:936
  throw err;
  ^

Error: Cannot find module 'react-scripts/config/webpack.config.dev.js'
Require stack:
- /Users/sgtpooki/code/work/protocol.ai/ipfs/ipfs-desktop-wails/frontend/scripts/watch.js
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:15)
    at Function.Module._load (node:internal/modules/cjs/loader:778:27)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/Users/sgtpooki/code/work/protocol.ai/ipfs/ipfs-desktop-wails/frontend/scripts/watch.js:6:16)
    at Module._compile (node:internal/modules/cjs/loader:1101:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    '/Users/sgtpooki/code/work/protocol.ai/ipfs/ipfs-desktop-wails/frontend/scripts/watch.js'
  ]
}
Dev command exited!

Use 'react-scripts/config/webpackDevServer.config.js' instead

@rulyotano
Copy link

I just wanted that the start command keep alive to be able to test it on local development process. In my case what I just did was add && && ping -i 100 localhost at the end of the start command:

"start": "react-scripts start && ping -i 100 localhost",

Maybe this helps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment