Skip to content

Instantly share code, notes, and snippets.

@int128
Last active Jul 16, 2022
Embed
What would you like to do?
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
});
}
@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

piyush1104 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.

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

pascallapradebrite4 commented Nov 4, 2020

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 commented Jan 8, 2021

@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

grumpyTofu commented Oct 20, 2021

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

SgtPooki commented Jul 16, 2022

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

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