preface

Create-react-app works quite well as facebook’s official React scaffolding. The main design principle is to merge configuration such as Webpack, Babel, ESLint into the react-Scripts NPM package, which users can use out of the box. A lot of developers build on that. Note that react-scripts is the core configuration code for the create-react-app scaffolding.

Currently, if you want to customize the configuration yourself, you have two options. One is Eject, and the idea is to take React-Scripts away and expose the configuration to the top layer of the application so that users can configure it themselves. The other is react-app-rewired, where the user adds and modifies the configuration through config-overrides. Both have their advantages. Eject exposure is self-configurable, but the downside is that react-Scripts is disbanded and cannot be upgraded with the official configuration. React-scripts does the dirty work of the most basic configuration and maintains it all the time, such as BUG fixes and packaging optimizations and speed optimizations. With the rapid development of the front end, these basic configurations can change as the infrastructure is upgraded. I think there’s a risk of maintenance costs after eject. My philosophy is to leave professional things to professional people to do, we should enjoy the infrastructure convenience brought by the bottom of the pyramid to create value, there is no need to repeat the wheel, let alone spend too much maintenance cost on the wheel.

My idea is to recommend using config-overrides. Js to customize the configuration and reduce maintenance costs. React-scripts can be updated seamlessly in the future to improve speed or fix bugs you haven’t noticed. However, create-React-app only provides the most basic infrastructure. The most commonly used framework configuration needs to be customized by ourselves, and every time we create a project, we have to write the custom code again, which is quite annoying. Hence today’s topic: create-React-app scaffolding, or more specifically, React-Scripts scaffolding.

So there should be two main topics in this article

  • How to make a CLI tool
  • How to according toreact-scriptsTo write about scaffolding

The core code of the project is on github :(github.com/LinYouYuan/…) There are also instructions on the use of this link, you can click on it first to better understand the use and requirements.

Project core requirements

Our requirements are:

  1. Ensure base dependencies and official synchronization;
  2. [Fixed] Add common frame selection when creating
  3. Configuration items can be customized after creating a project;

First, we needed to introduce react-scripts and react-app-rewired to keep things officially synchronized and customizable.

Second, I’ve sorted out the framework options we commonly use:

type Optional frame name
language JavaScript / TypeScript
State management library Redux / Mobx
CSS preprocessor SCSS / LESS / styled-components
UI components Antd / Ant-mobile
Code specification Airbnb
HTTP library Axios
routing react-router

Thirdly, we can pre-configure the project through config-overrides. Js file after the project is created, and then users can continue to configure and modify the project through this file.

Making CLI Tools

Introducing common toolkits

Start by creating the NodeJS project. To make common Cli tools, you need to install the following five toolkits :(run NPM install or other tools to install them)

  • Commander: Receives input command parameters and processes events.
  • Execa: For executing operational commands, a better onechild_process;
  • Inquirer: This is the primary tool for creating a CLI that produces a very nice command line interface;
  • Chalk: can change font color;
  • Fs-extra: fs that works better than native FS;

Create a global use

The first step is to create a command that can be executed globally like creact-react-app.

  1. We create folders and files in the root directorylib/index.jsThis is actually the entry execution file. Among them#! /usr/bin/env nodeBe sure to fill it out.

lib/index.js

#! /usr/bin/env node
console.log('hello world')
Copy the code
  1. Then, inpackage.jsonAdd the code below, wherereact-cliIs the name of the command to be used globally,lib/index.jsIs the address of the file to be executed above.

package.json

"bin": {
    "react-cli": "lib/index.js"
}
Copy the code
  1. Execute NPM link. After executing, we can mount the command globally, just like after NPM install -g, we can enter the command globally. Link’s main purpose is for me to develop debugging. Now type React-cli directly on the console and you’ll see hello World printed.

  2. Once the development is complete, you can try publishing to the NPM package, but I recommend waiting until the development is complete, so you don’t want to give it a try. Release need to be performed before the NPM login, the login NPM account password, pay attention to you if it is a source of taobao you need through the NPM config set registry https://registry.npmjs.org/ temporarily cut back to official sources. Next, execute NPM publish. Also note that the name in your package.json (project name) is not the same as someone else’s. Once published you can install your package globally via NPM I

    -g.

Order management

In lib/index.js, we enter the following

const program = require('commander');
const chalk = require("chalk");

program
  .version(require('.. /package').version)
  .usage('<command> [options]');

program
  .command('create <app-name>')
  .description('create a new project powered by react-cli')
  .action(name= > {
    // Handle logic here
    console.log(chalk.blue(`React CLI vThe ${require('.. /package').version}`));
    // const create = require('./cli/create');
    // create(name);
  });
Copy the code

Here, the commander command is used to configure the processing of different commands. The main idea here is to take the create

parameter and process the logic of the input command. Chalk is color processing.

Then proceed to handle no input and input error when pop-up help is shown below

program
  .arguments('<command>')
  .action((cmd) = > {
    program.outputHelp()
    console.log(` ` + chalk.red(`Unknown command ${chalk.yellow(cmd)}. `))
    console.log()
  })

program.parse(process.argv);

if(! program.args.length) { program.outputHelp(); }Copy the code

interface

After receiving the user’s input, we need to render the interface, so we use the inquirer tool which is very useful. See inquirer’s NPM website for details on how many types of interaction can be implemented. Here I mainly use list and confirm functions, that is, list selection and query functions. For example, let the user choose which framework to use:

function selectManually(appName) {
  inquirer
    .prompt([
      {
        type: 'list'.name: 'language'.message: 'pick a language:'.choices: [
          'JavaScript'.'TypeScript',]}, {type: 'list'.name: 'stateManagement'.message: 'Pick a state management:'.choices: [
          'Mobx'.'Redux',]}, {type: 'list'.name: 'cssPre'.message: 'Pick a CSS pre-processor:'.choices: [
          'LESS'.'SCSS/SASS'.'styled-components',]}, {type: 'list'.name: 'design'.message: 'Pick a UI Design:'.choices: [
          'Ant Design'.'Ant Design Mobile',
        ]
      },
    ])
    .then(answers= > {
      const creator = newCreator(appName, answers); creator.create(); })}Copy the code

Create a project

Create a Creator class, which is mainly used to create projects, and initialize it to take two parameters, the name of the project and the frame selected by the user. Templates in my project are stored in lib/ Packages /common-default. Json, babelrc, config-overrides. Js files, and then copy them.

const chalk = require("chalk");

const fs = require("fs-extra");

const path = require("path");

const inquirer = module.require('inquirer');

const {
  getPackageJson,
  writeJsonToApp,
  copyFiles,
  setNewPackageVersion,
  installPackge,
  setUserConfig,
} = require('.. /packages/common');

class Creator {
  constructor(appName, answers) {
    this.appName = appName;
    this.answers = answers;
    this.appDir = path.resolve(process.cwd(), this.appName);
    this.package = getPackageJson('cli-switch');
    this.babelrc = {
      plugins: [["import",
          {
            libraryName: "antd".style: true,}]]}}async testExistDir() {
    if (fs.existsSync(this.appDir)) {
      const { override } = await inquirer.prompt([
        {
          type: "confirm".name: "override".message: chalk.red(`directory The ${this.appName}exist,override it? `)}]);if (override) {
        console.log(chalk.green("removing..."));
        fs.removeSync(this.appDir);
        return true;
      } else {
        process.exit(1);
        return false; }}return true;
  }

  async create() {
    const { stateManagement, cssPre, design } = this.answers;

    console.log();

    console.log(`you pick: ${chalk.yellow(`${stateManagement}.${cssPre}.${design}, Router, ESLint`)}`);

    console.log();

    const isOk = await this.testExistDir(this.appDir, this.appName);

    if(! isOk) {return;
    }

    console.log(` 🚀 Invoking generators... `);

    console.log();

    let { dependencies, devDependencies } = this.package;

    switch (stateManagement) {
      case 'Mobx':
        dependencies['mobx'] = ' ';
        dependencies['mobx-react'] = ' ';
        break;
      case 'Redux':
        devDependencies['redux-devtools'] = ' ';
        dependencies['redux'] = ' ';
        dependencies['react-redux'] = ' ';
        break;
    }

    switch (design) {
      case 'Ant Design':
        let myTd = this.babelrc.plugins[0] [1];
        myTd.libraryDirectory = 'es';
        dependencies['antd'] = ' ';
        break;
      case 'Ant Design Mobile':
        let myTdw = this.babelrc.plugins[0] [1];
        myTdw.libraryName = 'antd-mobile';
        myTdw.style = 'css';
        dependencies['antd-mobile'] = ' ';
        break;
    }

    switch (cssPre) {
      case 'LESS':
        dependencies['less-loader'] = ' ';
        devDependencies['react-app-rewire-less-modules'] = ' ';
        break;
      case 'SCSS/SASS':
        dependencies['node-sass'] = ' ';
        break;
      case 'styled-components':
        dependencies['styled-components'] = ' ';
        devDependencies['babel-plugin-styled-components'] = ' ';
        this.babelrc.plugins.push("babel-plugin-styled-components");
        break;
    }

    fs.mkdirSync(this.appDir);

    this.beginCopy(cssPre === 'LESS');

    writeJsonToApp(this.appDir, '.babelrc'.this.babelrc);

    console.log(` 📦 Installing additional dependencies... `);

    installPackge(this.appDir);

    setUserConfig({ hasConfig: true.config: this.answers });

    console.log(` 🎉 Successfully created the project${chalk.yellow(this.appName)}. `)

    process.exit(1);
  }

  async beginCopy(isLess = false) {
    setNewPackageVersion(this.package.dependencies);
    setNewPackageVersion(this.package.devDependencies);

    this.package.name = this.appName;

    copyFiles(path.join(__filename, '.. /.. /packages/common-default'), this.appDir);

    writeJsonToApp(this.appDir, 'package.json'.this.package);

    if(! isLess) { fs.copySync(path.join(__filename,'.. /.. /packages/cli-switch/config-overrides.js'), this.appDir + '/config-overrides.js'); }}}module.exports = Creator;
Copy the code

Configure after the project is created

To create the project, simply configure Webpack devServer jest in config-overrides. Here you can add custom config configuration to add and modify loader, Plugin, optimization configuration. WebpackMerge adds config using mixin.

config-overrides.js

const path = require('path');
const webpackMerge = require('@/webpack-merge');

const appSrc = path.join(__dirname, 'src');

SKIP_PREFLIGHT_CHECK = true

const {
  override, addLessLoader, addWebpackAlias, useBabelRc, addDecoratorsLegacy,
} = require('@/customize-cra');

// Package analysis
const BundleAnalyzerPlugin = require('@/webpack-bundle-analyzer').BundleAnalyzerPlugin;

// You can change Host or Port directly
// process.env.HOST = 'localhost.xxxx.com';
// process.env.PORT = 3006;

// Whether to pack Source Map in production environment
process.env.GENERATE_SOURCEMAP = false;

module.exports = {
  / / configuration devServer
  devServer: configFunction= > (proxy, allowedHost) => {
    proxy = {
      '/mock': {
        // Configure the proxy service address here
        target: 'http://localhost:3000'.changeOrigin: true.pathRewrite: { '^/mock': ' '}},}// allowedHost: add additional addresses
    const config = configFunction(proxy, allowedHost);
    return config;
  },

  / / configuration webpack
  webpack: (config, env) = > {
    // Development environment
    const isEnvDevelopment = env === 'development';
    // Production environment
    const isEnvProduction = env === 'production';

    // overridden by the customize-cra plug-in
    config = override(
      // Configure the path alias
      addWebpackAlias({ The '@': appSrc }),
      Support for Decorators
      addDecoratorsLegacy(),
      useBabelRc(),
    )(config, env);

    return webpackMerge(config, {
      // User can add custom config configuration here to add modify loader, plugin, optimization
      plugins: [
        // new BundleAnalyzerPlugin(),].optimization: {
        splitChunks: {
          cacheGroups: {
            vendors: { // Basic framework
              chunks: 'all'.test: /(react|react-dom|react-dom-router|babel-polyfill|mobx|antd)/.priority: 100.name: 'vendors',},asyncCommons: { // Load the rest asynchronously
              chunks: 'async'.minChunks: 2.name: 'async-commons'.priority: 90,},commons: { // Synchronously load the rest of the package
              chunks: 'all'.minChunks: 2.name: 'commons'.priority: 80,},// echartsVendor: {// load the echarts package asynchronously
            // test: /echarts/,
            // priority: 100, // Higher than async-Commons priority
            // name: 'echartsVendor',
            // chunks: 'async'
            // },}},}})},// Configure tests
  jest: config= > {
    config.moduleNameMapper = {
      // Configure the alias as webpack does
      '@ / (. *) $': '<rootDir>/src/$1',}returnconfig; }},Copy the code

Using a simple demo

The first user creation has two options

  • default (JavaScript, Redux, Antd, Less, Router, ESLint)The default configuration
  • Manually select featuresSelect the configuration

The second creation will add a config option that the user selected last time, as shown below.