Skip to content

Instantly share code, notes, and snippets.

@int128
Last active January 21, 2024 14:52
Show Gist options
  • Star 92 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • 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
});
}
@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