Customizing Create React App — Done Right

John Tucker
codeburst
Published in
6 min readDec 7, 2018

--

Example of customizing Create React App by forking react-scripts.

A newer (new to me) approach to customizing Create React App (CRA) is to fork react-scripts:

Ejecting lets you customize anything, but from that point on you have to maintain the configuration and scripts yourself. This can be daunting if you have many similar projects. In such cases instead of ejecting we recommend to fork react-scripts and any other packages you need.

Create React App — Alternatives to Ejecting

Through example, we will explore customizing CRA using this approach.

If you wish to follow along, you will need:

  • Node.js; The latest LTS version; used 10.14.0
  • Yarn; While I switched back to npm on most other Node.js projects, CRA appears to prefer Yarn (playing it safe)
  • GitHub Account
  • npm Accoount

The final outcome of this article is available for download.

In some sense, configuring CRA is actually more complicated than configuring webpack itself (under-the-hood of CRA). For background on this, the series, React: Bake from Scratch or Box (JavaScript Version): Part 1, walks through building a fairly complete custom built-build solution using webpack.

Hello World

We will start with a simple “hello-world” example by first forking the Create React App repository; described in the GitHub documentation Fork a Repo.

The Create React App repository is a monorepo, i.e., a repository that consists of multiple packages, with one of the packages being react-scripts.

In the root of the project, we install the dependencies:

yarn install

Observations:

  • In addition to installing dependencies into node_modules, this command also installs each of the package’s dependencies
  • It also does a number of other things, e.g., it creates several GIT hooks; one of which prevents the project’s yarn.lock file from being committed (took me awhile to figure this one out)

In preparation of publishing our customized react-scripts to npm, we need to update the package name among other things; updating packages/react-scripts/package.json:

{
"name": "hello-larkintuckerllc-react-scripts",
"version": "0.1.0",
"description": "(2.1.1) Hello Larkin & Tucker LLC Configuration and scripts for Create React App.",
"repository": "larkintuckerllc/hello-create-react-app",
...
"bugs": {
"url": "https://github.com/larkintuckerllc/hello-create-react-app/issues"
},
...
}

Observations:

  • The name needs to globally unique
  • We create our own semantic version
  • The description is completely arbitrary; for a lack of a better location we can keep track of the upstream version of react-scripts package here
  • We update repository and bugs url with our repository

Now we can begin to customize react-script, e.g., updating packages/react-scripts/scripts/init.js:

...
// hello-larkintuckerllc-react-scripts start
function helloLarkinTuckerLLCDirections() {
console.log(chalk.yellow('Hello Larkin & Tucker LLC Directions'));
console.log('Hello World');
}
// hello-larkintuckerllc-react-scripts end
...
module.exports = function(
...
) {
...
// hello-larkintuckerllc-react-scripts start
helloLarkinTuckerLLCDirections();
// hello-larkintuckerllc-react-scripts end
};
...

Observations:

  • Because these files are long, we wrap our updates with easy to identify comments
  • Because we anticipating regularly synching our fork, as described in the GitHub documentation on Synching a Fork, we want to isolate our changes to minimize merge conflicts

Update 12/14/18: Found a bug that is specific to TypeScript in this example (corrected now). There is a TypeScript definition file that needs be updated because we are creating a different flavor of react-scripts.

packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js

...
// Reference `react-scripts` types
if (!fs.existsSync(paths.appTypeDeclarations)) {
fs.writeFileSync(
paths.appTypeDeclarations,
// hello-larkintuckerllc-react-scripts start
`/// <reference types="hello-larkintuckerllc-react-scripts" />${os.EOL}`
// hello-larkintuckerllc-react-scripts end
);
}
...

With these changes in place, we can publish to npm.

npm login
npm publish

Hello World Test

With our published customized react-scripts, we can now create a new React project using it:

npx create-react-app test-app --scripts-version hello-larkintuckerllc-react-scripts

Notice the output:

Less

As CRA does not support Less, let us update our customized react-scripts to support it for a more complicated example.

Knowing that the Less configuration is going to be similar to the existing SASS configuration, we can search the react-scripts folder for sass to find the files we need to consider to update.

We start by updating webpack.config.js:

...
// hello-larkintuckerllc-react-scripts start
const lessRegex = /\.less$/;
const lessModuleRegex = /\.module\.less$/;
// hello-larkintuckerllc-react-scripts end
...
return {
...
module: {
...
rules: [
...
{
..
oneOf: [
...
// hello-larkintuckerllc-react-scripts start
// Opt-in support for Less (using .less extensions).
// By default we support Less Modules with the
// extensions .module.less
{
test: lessRegex,
exclude: lessModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
},
'less-loader'
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules, but using Less
// using the extension .module.less
{
test: lessModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
},
'less-loader'
),
},
// hello-larkintuckerllc-react-scripts end
...
],
},
],
},
...
].filter(Boolean),
...
};
};

Because we used less-loader, we install it from the react-scripts folder (so that it updates packages/react-scripts/package.json)

yarn add less-loader

From what I can tell, the files in the packages/react-scripts/fixtures/kitchensink folder are for the CRA “kitchen sink” example; we don’t need to update any of these.

We need to update the TypeScript type definitions file for Less module files; packages/react-scripts/react-app.d.ts:

...
// hello-larkintuckerllc-react-scripts start
declare module '*.module.less' {
const classes: { [key: string]: string };
export default classes;
}
// hello-larkintuckerllc-react-scripts end

We update our directions for our new Less configuration; packages/react-scripts/scripts/init.js:

...
// hello-larkintuckerllc-react-scripts start
function helloLarkinTuckerLLCDirections() {
console.log(chalk.yellow('Hello Larkin & Tucker LLC Directions'));
console.log('Inside that directory, you can run several commands:');
console.log();
console.log(chalk.cyan(` yarn add --dev less`));
console.log(' Adds support for Less.');
}
// hello-larkintuckerllc-react-scripts end
...

Because our testing tool, Jest, needs to know how to deal with Less modules, we update packages/react-scripts/scripts/utils/createJestConfig.js:

...
module.exports = (resolve, rootDir, isEjecting) => {
...
setupTestFrameworkScriptFile: setupTestsFile,
...
transformIgnorePatterns: [
...
// hello-larkintuckerllc-react-scripts start
'^.+\\.module\\.less$',
// hello-larkintuckerllc-react-scripts end
],
moduleNameMapper: {
...
// hello-larkintuckerllc-react-scripts start
'^.+\\.module\\.less$': 'identity-obj-proxy',
// hello-larkintuckerllc-react-scripts end
},
...
};
...
};

Because we added non-breaking functionality to our package, we update the minor version number in packages/react-scripts/package.json:

{
...
"version": "0.2.0",
...
}

We finally publish our changes:

npm publish

Less Test

With our updated published customized react-scripts, we can now create a new React project using it:

npx create-react-app test-app2 --scripts-version hello-larkintuckerllc-react-scripts

Notice the updated output:

With these changes, and following to instructions to install less, we can now use .less and .module.less files as expected.

Upgrading to New Releases

We can follow the same directions in the CRA documentation, Updating to New Releases, to upgrade our custom react-scripts.

note: To play it safe, I always remove the node_modules folder before re-installing project dependencies.

Wrap Up

Hope you found this useful.

--

--