Extending create-react-app to make your own app generator

Chris Patty
8 min readAug 29, 2017

--

Edit January 2019: This article still gets attention, however since the time I wrote it a better, officially recommended, option for creating custom CRA generators has been created here: Customizing create-react-app: How to Make Your Own Template. The instructions below may still be helpful for extending other types of app generators.

Like you, I’m a huge fan of Facebook’s create-react-app. Like you, the amount of time I spend setting up new React projects has gone from a few agonizing hours to a few blissful minutes. But also like you, I find that I’m still spending a lot of time adding extra packages and boilerplate for state management and routing. Lucky for both you and me it’s extremely easy to extend create-react-app to create our own quick-start app generators.

Note: The code from this tutorial has been published as an npm package here, and on Github here.

The Goal

In creating our new app generator there’s a few guidelines we’ll try to adhere to:

  • The generator shouldn’t require create-react-app to be ejected. That way apps generated with your tool can be easily upgraded to new versions of react-scripts.
  • The template should follow create-react-app's convention:
    create-[react-generator-name] [app-name] to make it feel immediately familiar to people who already use CRA.
  • And, in the spirit of create-react-app, our tool should require zero configuration. (You can stretch this one if you must)

The tool we’re going to create will carry out 3 simple tasks.

  1. Trigger create-react-app to create a new app with a provided app name.
  2. Download and install a list of additional packages.
  3. Replace the default template files with new files as needed.

1. Initialize a new npm package

You know the drill. Open up your terminal, point to a new directory and run:

npm init

Then step through the prompts to create a new package.json file.

2. Create an index.js file

Create a new file called index.js, and open it up in your editor. This file will be the entry point for our new command line interface.

We’re going to need shelljs (for executing shell commands) and colors (for changing the color of our console output) to make it easier to work with the command line, so let’s install them by running:

npm install --save shelljs colors 

Next, we need to add a shebang to the top of our index.js file to let our command line know that it needs to run the script we’re going to write inside of node.

#!/usr/bin/env node

Last, let’s require the packages we’ll need to make our script work.

let shell = require('shelljs')
let colors = require('colors')
let fs = require('fs') //fs already comes included with node.

3. Add global variables and the main function

We’re first going to need a couple global variables. One will pull the provided app name out of the parameters of our shell command, and the second will store the path to the directory that create-react-app is going to create.

Define the following variables after your require statements in index.js:

let appName = process.argv[2]
let appDirectory = `${process.cwd()}/${appName}`

Then create and call a function called run

const run = async () => {
let success = await createReactApp()
if(!success){
console.log('Something went wrong while trying to create a new React app using create-react-app'.red)
return false;
}
await cdIntoNewApp()
await installPackages()
await updateTemplates()
console.log("All done")
}
run()

Stepping through the above code you can see we’re first running create-react-app, then, if it was successful, we cd into the new directory, install our extra packages, update some of the files with our own templates and voila! We’re done!

Also, as you can see we’re going to be using the new async/await syntax included with node 8+ because we’re cool and hip and we pronounce gif correctly.

Note: Make sure to call your run() function somewhere in index.js or your code will never run.

4. Execute create-react-app

Next let’s define the createReactApp() function from above.

const createReactApp = () => {
return new Promise(resolve=>{
if(appName){
shell.exec(`create-react-app ${appName}`, () => {
console.log("Created react app")
resolve(true)
})
}else{
console.log("\nNo app name was provided.".red)
console.log("\nProvide an app name in the following format: ")
console.log("\ncreate-react-redux-router-app ", "app-name\n".cyan)
resolve(false)
}
})
}

In this function we return a new promise which first checks that the user provided an app name, and then executes the create-react-app command, passing in appName as the first parameter. After the command completes we resolve the promise to true so our script can continue.

5. CD into the new directory created by create-react-app

Next let’s define the cdIntoNewApp function:

const cdIntoNewApp = () => {
return new Promise(resolve=>{
shell.exec(`cd ${appName}`, ()=>{resolve()})
})
}

Once again, we return a new promise which cd's down into the directory of the newly created app and then resolves to true

6. Install any additional packages

This is where things really get interesting. For this tutorial I’m going to be adding a few popular packages that I end up using in most of my React apps.

These are some of my favorite libraries, but there’s definitely other options out there. Decide which packages you want to include and then define the installPackages() function.

const installPackages = () => {
return new Promise(resolve=>{
console.log("\nInstalling redux, react-router, react-router-dom, react-redux, and redux-thunk\n".cyan)
shell.exec(`npm install --save redux react-router react-redux redux-thunk react-router-dom`, () => {
console.log("\nFinished installing packages\n".green)
resolve()
})
})
}

This function first prints a message to the console to let the user know the package installation has started, executes an npm install, prints a success message, and then resolves.

7. Create boilerplate templates

If all you wanted to do was install some packages then congrats, your all done! But we haven’t really saved ourselves more than a few precious keystrokes at this point. The real time-saver comes from replacing create-react-app's template files with your own boilerplate code.

Create a directory named templates. For this tutorial I’m going to be replacing App.js, and index.js, and then adding a new file called store.js.

App.js

Create a file called App.js with the following code.

module.exports = `import React, { Component } from 'react'
import logo from './logo.svg'
import './App.css'
import { connect } from 'react-redux'
class App extends Component {
render() {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
export default connect(
state=>({
}),
{}
)(App)
`

If you’re familiar with create-react-app already then this will look familiar. We’re exporting a string containing the original App.js code but with a few key differences. First, we’ve imported connect from react-redux, and then connected our App component to our eventual Redux store.

store.js

Create a file called store.js with this code:

module.exports = `import { createStore, combineReducers, applyMiddleware } from 'redux'
import ReduxThunk from 'redux-thunk'
const initialState = {}const AppReducer = (state=initialState, action) => {
switch (action.type) {
case 'ACTION':
return state
default:
return state
}
}
export default createStore(
combineReducers({
app: AppReducer
}),
applyMiddleware(
ReduxThunk
)
)
`

If you’re familiar with Redux already, this file contains the basic boilerplate for a new Redux store.

index.js

Create a file called index.js with this code:

module.exports = `import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import store from './store.js'
import { Provider } from 'react-redux'
import { BrowserRouter as Router } from 'react-router-dom'
ReactDOM.render(
<Router>
<Provider store={store}>
<App />
</Provider>
</Router>
, document.getElementById('root'));
registerServiceWorker();
`

Once again, very similar to the original, but here we’ve imported the store that we just created in store.js along with the Provider component from react-redux which will provide our store to the entire React tree. We’ve also wrapped <App/> in the Router component provided by react-router-dom.

templates.js

Lastly we’ll tie these new files together in a new file called templates.js which imports and then immediately exports our templates.

module.exports = {
'App.js': require('./App.js'),
'index.js': require('./index.js'),
'store.js': require('./store.js')
}

Note: it’s very important that the keys in this object correspond perfectly to the names of the files being imported as they will eventually be used to create and overwrite create-react-app’s template files.

8. Overwrite create-react-app’s files with our own

Back in the index.js file at the root of our project let’s require the templates.js file we just created.

let templates = require('./templates/templates.js')

Then let’s define our last function, updateTemplates(), which loops through our templates object, and uses the node file system package to overwrite or create our new boilerplate code, resolving to true when it all the files are finished.

const updateTemplates = () => {
return new Promise(resolve=>{
let promises = []
Object.keys(templates).forEach((fileName, i)=>{
promises[i] = new Promise(res=>{
fs.writeFile(`${appDirectory}/src/${fileName}`, templates[fileName], function(err) {
if(err) { return console.log(err) }
res()
})
})
})
Promise.all(promises).then(()=>{resolve()})
})
}

9. Define the shell command to execute our script

Now that our script is done, the last think we need to do is define what command will trigger it to run in our terminal.

Open up your package.json file and add a new property called bin like so:

...
"bin": {
"create-react-redux-router-app": "index.js"
},
...

Here we’ve defined an obnoxiously long, but descriptive command create-react-redux-router-app and told it to run index.js when we call it.

Last we need to let our operating system know that this new command exists by running the following command in your terminal:

npm link

If everything went well our new command should be available to use!

Note: Some command line programs like command prompt on Windows may require a quick restart before they’ll recognize newly-linked commands. Also, if you plan to publish your hot new app generator to npm when you’re done (which you totally should), your command will be automatically linked when users download and install it using npm install -g my-app-generator.

To test your hot new app generator, just open up a terminal and run:

create-react-redux-router-app my-new-app

If all went well you should have a fresh new folder in your current directory called my-new-app with all your custom code baked right in.

10. 🎉 Revel in your success 🎉

Now that you’re not spending precious time configuring new React apps, commit to spend that time:

  • ❤️ Contributing to an open-source project that you use and love.
  • 💀🔥 Building a babel preset to rename all variables to various Nicolas Cage puns.
  • 🐦 Following me on Twitter @chrisjpatty.

And last, I’ve published the code from this tutorial as an NPM package, which can be easily installed with npm install -g create-react-redux-router-app. You can also find the full source code on my Github.

If you had any trouble with this tutorial, or have an idea to improve it, comment below or @ me on Twitter!

--

--