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
});
}
@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