Skip to content

Instantly share code, notes, and snippets.

@int128
Last active January 21, 2024 14:52
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
});
}
@francescovenica
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

@rachelthecodesmith
Copy link

Recent update to react-scripts, there's now only one file, 'react-scripts/config/webpack.config.js' you can give it the env as a parameter.

e.g.

const webpackconfig = require('react-scripts/config/webpack.config.js');

const config = webpackconfig('development');

@BalavigneshJ
Copy link

BalavigneshJ commented May 22, 2019

This watch.js worked for me.

process.env.NODE_ENV = 'development';

const fs = require('fs-extra');
const paths = require('react-scripts/config/paths');
const webpack = require('webpack');
const importCwd = require('import-cwd');
const config = importCwd('react-scripts/config/webpack.config')('production')

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

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

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

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

@brymon68
Copy link

brymon68 commented Jun 4, 2019

Has anyone been successful in changing paths.appBuild to a directory outside of the project root? For instance, I need to build to something like /opt/app/files/ on the host machine.

@cikandin
Copy link

cikandin commented Jul 24, 2019

If you use this script with customize-cra, you can add this line to script

const overrides = require('./config-overrides') // correct this line to your config-overrides path
overrides.webpack(config, process.env.NODE_ENV)

@vaas-montenegro
Copy link

overrides.webpack(config, process.env.NODE_ENV)

Can you give me a full example
This is my config-overrides file
`const { override, fixBabelImports, addLessLoader } = require('customize-cra');

module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
addLessLoader({
javascriptEnabled: true,
modifyVars: {
'@primary-color': '#2e5bff' ,
'@text-color' : '#2e384d'
},
}),
);`

@mojtabaasadi
Copy link

my problem is that it can not find modules and looks for them in the src folder

ERROR in ./src/App.js
Module not found: Error: Can't resolve 'react' in '/var/www/pricewatch/src'
 @ ./src/App.js 2:0-26 7:9-14 14:5-10 21:5-10 30:6-11 36:14-19 42:45-50
 @ ./src/index.js
 @ multi ./src/index.js

@naholyr
Copy link

naholyr commented Feb 13, 2020

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;

@thomas-avada
Copy link

thomas-avada commented Mar 14, 2020

I'm also running fireabase app. Here is the script that work for me

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');

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

Copy link

ghost commented Mar 17, 2020

I need to change the dist folder to my name and put it in the path below.
I found the starting point

// paths.js
// 17 line changed to this
const appDirectory = path.resolve(fs.realpathSync(process.cwd()), '../dash');

I take an error

> [email protected] watch /home/user/project/dash-dev
> node scripts/watch.js

internal/modules/cjs/loader.js:583
    throw err;
    ^

Error: Cannot find module '/home/user/project/dash/package.json'
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:581:15)
    at Function.Module._load (internal/modules/cjs/loader.js:507:25)
    at Module.require (internal/modules/cjs/loader.js:637:17)
    at require (internal/modules/cjs/helpers.js:22:18)
    at Object.<anonymous> (/home/user/project/dash-dev/node_modules/react-scripts/config/paths.js:30:3)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! [email protected] watch: `node scripts/watch.js`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the [email protected] watch script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/user/.npm/_logs/2020-03-17T10_16_12_894Z-debug.log

What need to do?

@baurine
Copy link

baurine commented Apr 1, 2020

Big thanks to you guys, this absolutely works for me!

This is my full code:

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')
const pkg = require('../package.json')

// work with react-app-rewire and customize-cra
const overrides = require('../config-overrides')
overrides(config, process.env.NODE_ENV)

// removes react-dev-utils/webpackHotDevClient.js at first in the array
// config.entry.shift()
config.entry = config.entry.filter(
  (fileName) => !fileName.match(/webpackHotDevClient/)
)
config.plugins = config.plugins.filter(
  (plugin) => !(plugin instanceof webpack.HotModuleReplacementPlugin)
)

// to speed up rebuild time
config.mode = 'development'
config.devtool = 'eval-cheap-module-source-map'
delete config.optimization

// fix publicPath and output path
config.output.publicPath = pkg.homepage
config.output.path = paths.appBuild // else it will put the outputs in the dist folder

webpack(config).watch({}, (err, stats) => {
  if (err) {
    console.error(err)
  } else {
    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,
  })
}

The config-overrides.js:

const { override } = require('customize-cra')
const addYaml = require('react-app-rewire-yaml')

module.exports = override(addYaml)

@baurine
Copy link

baurine commented Apr 1, 2020

overrides.webpack(config, process.env.NODE_ENV)

Can you give me a full example
This is my config-overrides file
`const { override, fixBabelImports, addLessLoader } = require('customize-cra');

module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
addLessLoader({
javascriptEnabled: true,
modifyVars: {
'@primary-color': '#2e5bff' ,
'@text-color' : '#2e384d'
},
}),
);`

hi @vaas-montenegro, for your config-overrides.js, you can just use overrides(config, process.env.NODE_ENV) instead of overrides.webpack(...).

for @cikandin 's example, I think his config-overrides.js likes this:

module.exports = {
  webpack: function(config, env) {...},
  ...
}

@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 [email protected] 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