Extending create-react-app to make your own React CLI Scaffolding Tool
At RateSetter, we've recently spun up a bunch of new React projects. Our React projects use create-react-app
with specific config for the app to be deployed and hosted by our platform.
To spin up a new frontend project, teams would often clone preexisting React projects, delete irrelevant files and update files with the new name of the app. This resulted in problems such as git history not being cleaned from the cloned project, teams wasting time cleaning up the project and errors arising from when config wasn't correctly updated for the new project.
I saw a real need for a frontend project scaffolding tool. The tool needed to run create-react-app
and then augment it with all the configuration and templates necessary to get a frontend app up and running at RateSetter.
I've never made a CLI tool before this, so it was an excellent opportunity to learn how to do so. This article focuses on how I built it, and what I learned from building it.
Building the Scaffolding Tool
I've created a GitHub repository for the scaffolding tool. It might help to have this open while following this article:
Feel free to clone, fork, star or suggest improvements for it :).
Overview
I designed the CLI tool to have the following flow:
- Ask the user for the app name (must follow
create-react-app
's naming convention) - Present the user with a list of available front-end libraries/frameworks
- Present a list of options that the user can choose from to augment their frontend application with
- Run
create-react-app
followed by: - Installing additional NPM dependencies
- Adding/modifying
package.json
entries - Adding templates
This article will focus on creating the CLI tool by augmenting create-react-app
- a very popular React scaffolding too. Since we are pragmatic engineers, we will be structuring our CLI in an extensible manner to allow future support of other frontend libraries/frameworks (Angular and React).
For the React side of the tool, we're going to provide the user with three options they can augment their React app with:
- Adding Redux and Redux Saga
- Adding a Mock API server. This will be a simple Node/Express server that can run alongside the app to provide mock API responses
- Adding static code analysis tools (Prettier, TsLint, StyleLint)
Scaffold Dependencies
I've uncreatively called the tool: create-frontend-app
. Create that directory and initialise Git (git init
) and NPM (npm init
). Then, add the following project structure:
.gitignore
package.json
index.js
react/
config/
mockApi/
templates/
...
index.js
redux/
templates/
...
index.js
staticCodeAnalysis/
index.js
reactApp.js
We will need to install three Node packages:
- inquirer - provide our users with an interactive CLI interface
- ora - loading spinner to display while our CLI is running commands
- fs-extra - extension utils to
fs
that helps make our CLI code more concise - colors - to provide our users with a colourful CLI experience :)
In frontend-scaffold-cli
run:
npm install --save inquirer ora fs-extra
Option Config Setup
As previously mentioned, we're going to allow the user to choose to add up to three optional augmentation options:
- Adding Redux/Redux Saga
- Adding static code analysis tools (Prettier/StyleLint/EsLint)
- Adding a mock API server
My aim for this tool was to have one config file for each augmentation option that that drives what each will provide the app. I've created the following (TypeScript) schema of how I thought I could best handle the requirement of:
- Installation of additional NPM dependencies
- Additional/modified
package.json
entries - Additional/updated project files
interface IConfig {
name: string;
description?: string;
dependencies: string[],
devDependencies: string[],
packageEntries: Array<{
key: string;
value: string
}>,
templates: Array<{
path: string;
file: IFile;
}>
}
name
is the name of the config.description
is a description of what selecting this augmentation will provide to the user. It will be output to the user and asked as a question.dependencies
/devDependencies
are a list of NPM packages that will be installed into the projectpackageEntries
is a key value pair list which will be added to the project'spackage.json
templates
are a list ofpath/file
entries. The CLI will iterate through each, create a new file using the specified template at the path location.
The config I've created for the Mock API option looks like:
You'll notice that we're importing apiIndex
from "./templates/apiIndex"
. This is a template file that should be added into the project and looks like:
It simply exports the contents of the template as a string. This string content will be used to construct template files in projects created by the tool. All the other template files have similar formats, but I won't display them here because there are too many!
You can see all the config files for the other two options in my GitHub repo:
- Redux/Redux Saga: https://github.com/HarveyD/create-frontend-app/tree/master/react/config/redux
- Static Code Analysis: https://github.com/HarveyD/create-frontend-app/tree/master/react/config/staticCodeAnalysis
We then need to create an index.js
file in root of /config
to import and export the three config files:
The CLI Entry-point
With our config all sorted, we now have to construct the base of our CLI. This entry point needs to extract two pieces of information from our user: the desired app name and framework/library. In our entry point (index.js
) add the following:
Here we're using inquirer
to prompt users to:
- Enter an app name
- Select an app type from 3 options: React, Angular and Vue
It'll find the next part of the CLI tool using the app type selected, which it'll pass the current directory and app name.
As you can see, appDict
only has an entry for React at the moment as we're only focusing on React in this article.
React CLI
If React is selected,index.js
will run the function in react/reactApp.js
with the name and directory of our app. Let's build it out.
react/reactApp.js
is a relatively big file, so we'll be breaking it down. First let's add the dependencies required:
Now, we create the default function to be exported by reactApp.js
:
This function is performing a number of steps. Each step is separated out into a function and returns a promise (hence why we're await
ing them). We'll flesh out the functions one at a time.
askQuestions
The first thing we need to do is to iterate through our list of augmentation options and ask the user if they want to include it within their app.
Here we're transforming our list of configs into a list of inquirer
questions. We then ask the user each question sequentially. We then return a list of options that were selected as yes
. This list is passed to subsequent steps in the CLI.
createReactApp
The most essential function of our CLI tool is this function! We need to run create-react-app
to have something we can augment!
We're utilising ora
to show the user a spinner while create-react-app
is doing it's thing.
We utilise shelljs
to execute npx
, which simply runs create-react-app
with the app name provided to the CLI tool in index.js
. This is wrapped in a Promise due to shelljs
being asynchronous, but only providing a callback to indicate its completion.
Once it's done we need to change the current directory to this freshly created React app, stop the spinner and resolve our Promise.
installPackages
This function installs all the dependencies and dev dependencies our augmentations require.
Here, for each selected augmentation provided from askQuestions
, we're grabbing the list of NPM dependencies it requires and installing them! We use shelljs
to run the NPM command to install the dependency! We perform the same thing for dev dependencies.
updatePackageDotJson
This function updates package.json
by adding entries, or modifying them.
First we grab all the entry modifications we need to perform from our selected config list. Then we use fse
to grab the existing contents of package.json
and parse it. We loop over the list of entries, and use lodash
set to output the key value pair in package.json
. We then use fse
to overwrite the old package.json
with the new, mutated package.json
.
addTemplates
This function adds the template files associated with the augmentation option to our new project.
Similar to the updatePackageDotJson
function, we merge the templates we need to add from all the selected augmentation configs. For each, we utilise fse
to output the contents of the template at the desired path.
commitGit
create-react-app
intialises a GIT repository. Since we added and modified a number of files we will have a number of untracked changes. We want our user to have a fresh experience, free of these untracked files. So we simply add all the files and commit them!
That's it! View the file in it's entirety at: https://github.com/HarveyD/create-frontend-app/blob/master/react/reactApp.js
Running
Let's test out our new CLI tool. Run node index.js
or npm run start
(if main
in package.json
points to index.js
) and you should see:
Followed by our three options:
It'll then run all the steps:
If everything went okay, you should see test-app
within your CLI tool:
It should have all the dependencies, templates and package.json
entries we specified!
Publishing and Consuming
Before publishing, make sure to add the following to your package.json
:
This allows the user to execute the script globally once the CLI tool is installed globally via NPM!
Now all there's left to do is publish your new CLI tool to NPM! To find out how to do this, follow: https://blog.harveydelaney.com/setting-up-a-private-npm-registry-publishing-ci-cd-pipeline/
After publishing your library to an NPM registry, you should be able to install it by running: npm i -g create-frontend-app
.
Then by running create-frontend-app
, you should be presented with the CLI's questions! After answering the questions and letting the tool run, your new project should be created in the directory you rancreate-frontend-app
.