As the company’s business lines increased, the basic scaffolding could not meet the needs, so I started to develop the scaffolding of the business line. Based on the Vue CLI source code and my own business practice, I made a set of project scaffolding about React by absorbing the development advantages of the VUE-CLI plug-in model and combining the business.

Writing in the front

This is a long-term update of the React scaffolding practice to take advantage of the Vue Cli scaffolding experience and build our React business scaffolding with the plugin-presets we’re used to. It may not be the best scaffolding development practice, but it’s certainly the most complete scaffolding development practice.

The full set of practice we will be through the existing VUE CLI source code one by one interpretation of the way, on the one hand is to be familiar with the mature scaffolding code implementation, on the other hand is to improve their own code and practice understanding, so that everyone in their own development scaffolding or learning process, can have a deeper understanding.

Article navigation:

  1. The React business scaffolding practice (part 1) — Scaffolding infrastructure construction
  2. The React Business scaffolding Practice (PART 2) builds and services (Create)

Scaffolding construction

First of all, let’s give the scaffold a cool name. When we were doing it, we suddenly saw a cool picture, as follows: a cat smoking

Then it broke out that Yang Chaoyue’s photos had cigarette news. At that time, she explained that her cat was smoking. At that time, I thought that was it. Then call it cat-smoker!

After the name was chosen, we had to sort out the architecture ideas and flow charts. I sorted out the scaffolding thinking logic mapping based on VUe-CLI by myself as follows:

At the beginning, there are two main structures: one is CLI and the other is CLI-service. One mainly provides the ability to construct projects based on plug-ins, and the other provides basic services (serve, build, etc.) for the constructed projects. This is also the basic Vue CLI infrastructure, CRA is too basic. Many configurations are not enough for our daily use, so we copied the MODE of Vue CLI and set it up under the scaffolding of React project.

The advantage of this architecture is that it is more targeted and scalable for the customized development of internal business of our company. To be honest, I am not used to react script. We can save some common business Settings by writing presets on local disks. Combine our project architecture through vue-CLI-like select mode and extend our architecture through external plug-in apis.

Cli-service provides black box operation for us. We can do webpack setup, optimization configuration, packaging (third-party, filtering, CDN setup and other services) configuration, Runtime, project routing, webpackChain chain, project template, Configuration, such as business architecture, etc., has been configured centrally by throwing a Cs.config.js from the black box to achieve normalization. This is a basic Cat-Smoker architecture.

Scaffolding package Management Scheme (LERNA + YARN)

Because we have two architectural modules that are independent but interdependent at the same time, and the whole project is a warehouse, to solve the warehouse design mode in this scenario, we can adopt the mode of Monorepo warehouse, which can well manage the dependency relationship between multiple packages. Instead of the multiple Repo model, in which modules need to be managed separately (a single REPO repository), Lerna was developed to solve this problem. For example, if you need a single cli-serive for your project, in Multi mode you need several repositories to manage the CLI and cli-service, which is easier to solve in Monorepo mode.

The advantages and disadvantages of Monorepo mode and Multi mode are not discussed here, otherwise it will be too long. You can see the link below, or you can search for related knowledge by yourself:

  1. Monorepo new wave | introduce lerna
  2. Monorepos: both Please don ‘t!

As for why scaffolding adopts this mode, it is through the practice of CRA, VUE-CLI and other libraries. Based on the experience of predecessors and the characteristics of close multi-module dependence and subcontracting development under our framework, we also adopt this mode for package management. Because the yarn workspace mode, you can use the Lerna +yarn workspace feature to manage packages in the working area. Considering that some partners do not install YARN, YARN is still very needed after my practice. Yarn Workspace + Lerna mode development is very smooth!

Yarn Workspace + Lerna

Yarn workspace: Yarn workspace. Some yarn directives can be mapped to lerna directives. Declare useWorkspaces in the lerna

{
  "useWorkspaces": true."npmClient": "yarn"."packages": []."version": 0.0.8 - alpha. "0"
}
Copy the code

You can then happily use Yarn,

  1. Install dependencies
$ yarn install # equivalent to lerna bootstrap -- nPm-client yarn --use-workspaces
Copy the code
  1. Cleaning up package dependencies
$ lerna clean Clean up all node_modules
$ yarn workspaces run clean Clean all packages
Copy the code
  1. Add (delete) dependencies
$ yarn workspace packageB add(remove) packageA Add packageA dependencies to packageB
$ yarn workspaces add(remove) lodash Add dependencies to all packages
$ yarn add(remove) -W -D loadash # root adds dependencies
Copy the code

Publish and test other commands, you can look at the Monorepo workflow based on Lerna and Yarn Workspace

Begin to build

Global installation first:

npm install --global lerna
Copy the code

Then go into our CD cat-smoker project and init it

lerna init
Copy the code

The initial project structure is as follows:

├ ─ ─ the cat - smoker / ├ ─ ─ packages / │ ├ ─ ─ package - child1 /// Different packages│ ├─ ├─ Package. json ├─ ├.json// Manage json files
Copy the code

To create a package belonging to our project, look at the lerna.json file in our root directory. To unify the package name and management, Insert packages/@cat-smoker/cli* into the packages field to indicate that there is a package starting with @cat-smoker/cli- under compatible packages.

{
  "useWorkspaces": true."packages": [
    "packages/@cat-smoker/cli*"]."version": "0.0.1"
}

Copy the code

Then we create our own package by lerna create . From the path above we build:

lerna create cli 0
Copy the code

0 indicates the array subscript 0 that matches the above packages positions. We can see that there is a cli file under @cat-smoker/, which is the CLI body package of our above flow chart. NPM install can be used to add dependencies within lerna packages. Use lerna add –scope to add dependencies, or yarn workspace XX add XX.

Lerna create cli-service 0,lerna create cli-shared-utils. The project structure is as follows:

├ ─ ─ the cat - smoker / ├ ─ ─ packages / │ ├ ─ ─ @ cat - smoker /// Different packages│ │ └ ─ ─ cli // / main package│ │ └ ─ ─ cli - service // / service pack│ │ └ ─ ─ cli - Shared - utils /// The utility class package that is common between packages├ ─ ─ package. Json ├ ─ ─ lerna. Json// Manage json files
Copy the code

Once the infrastructure is built, you can see that each package has its own separate package.json. Note the following fields:

{/ /... "bin": { "cat-smoker": "bin/react-build-cli.js" } // ... }Copy the code

This is the main program bin entry of the project, which is also the command cat-Smokershell entry. We need to define a command cat-smoker like Vue, and write in the header of the file of this entry:

#! /usr/bin/env node

/ / code
Copy the code

What does this code mean? Represents a shell command that executes the script, the language interpreter is Node, and the node path is found along the text values

When you use NPM link, you can link this command to the global. The advantage is that after the link, we can locally develop real-time changes, which is convenient for our development. Then, a logical operation is conducted via cat-smoker, and the entry is confirmed. For local debugging, remember NPM link or YARN Link

Technical stack preparation

Nodejs makes it possible for front-end engineering scaffolding to operate Terminal terminals and IO reads. The above entry to the bin file provides the possibility to operate the shell, so a Terminal parser is needed. First, the Terminal operation under Node requires the following libraries:

First we specify our terminal entry in package.json as above:

{/ /... "bin": { "cat-smoker": "bin/react-build-cli.js" } // ... }Copy the code

Declare a #! File in the bin/react-build-cli.js directory. The /usr/bin/env node header is then ready to begin our pleasant tour of terminal commands:

Terminal Operation (COMMANDER)

Commander: This is a complete nodeJS terminal solution. For example, your enforcement command vue create is parsed by the library. Similarly, we can define some commands here.

#! /usr/bin/env node

const program = require('commander');
// Declare the version
program.version(require('.. /package').version);

program
  .version(`@cat-smoker/cli The ${require('.. /package').version}`)
  // What about a cat-smoker? // What about a smoker
  .usage('<command> [options]')

program
  .command('create <app-name>') // Define the directive create
  .description('create a new react project') // Command description
	.option('-g, --git [message]'.'Force git initialization with initial commit message'.true) // -g indicates cat-smoker CREate-g
	.action((name, cmd) = > {
  	// CMD is the command line parameter configuration item, and name is the parameter of vue create 
      
  	// This responds to the create command
  	// See the following logic
	})

// Remember to parse the command line arguments (process.argv)
program.parse(process.argv);
Copy the code

The above is the entrance of the cat-smoker create command. We can implement the create logic in the action. We need the Minimist library to parse our command-line parameters, which is like a process of formatting parameters.

In addition, vue-CLI provides a cleanArgs function, which can be used directly. This function extracts the CMD options as an options object and holds the values of the parameters. For example, -g will declare a {git: True} and so on.

The advantage of this formatting is that the data mapping relationship of the command line => object is visually expressed, which is convenient for the subsequent code to call the command line parameters.

const minimist = require('minimist');
/ /... Omit the above code
// .action
program.action((name, cmd) = > {
  // Parse command line arguments
  if (minimist(process.argv.slice(3))._.length > 1) {
    log('Your command line argument is more than 1, not the correct command, please refer to the help.');
  }
  // Format the parameters into objects
  const options = cleanArgs(cmd)
  // Call lib's create method, which is the main logic of our create method
  require('.. /lib/create')(name, options);
})

// This step is to change the - delimiter to hump
function camelize (str) {
  return str.replace(/-(\w)/g, (_, c) => c ? c.toUpperCase() : ' ')}// Parse the configuration. Throws options to provide configuration objects for subsequent development
function cleanArgs (cmd) {
  const args = {}
  cmd.options.forEach(o= > {
    const key = camelize(o.long.replace(/ ^ - /.' '))
    if (typeofcmd[key] ! = ='function' && typeofcmd[key] ! = ='undefined') {
      args[key] = cmd[key]
    }
  })
  return args
}
Copy the code

As you can see from the above terminal operation, the final logic goes to the js file in lib/create, so we create it in the corresponding directory to write the logic of the create command. Similarly, you can declare commands like add and delete, which correspond to the vuE-CLI source code, portal, and some details of the logic, such as checking node versions, etc., which are not expanded here, you can read the source code directly if you are interested.

Terminal command logic service (cat-smoker- CLI)

First, let’s take a look at the specific logical business. Referring to the vue-CLI model, I sorted out a general cat-smoker black-box business structure as follows:

In the main line of business (create.js), we need to implement two functions of project structure construction and project dependency package management through Creator and PackageManage respectively.

There is a key Generator constructor in Creator, which is the main logic for constructing our project. The constructor internally throws an Interface, which in VUe-CLI we call the “GeneratorAPI”, which provides all the methods available to the plug-in for business extension.

Identify some basic utility functions

Let’s create a cli-shared-utils package: lerna create cli-shared-utils 0.

Default package.json file entry address:

{/ /... main: 'index.js' }Copy the code

index.js

Identify some necessary libraries, which may not be complete and will be added later:

  1. Chalk: A terminal style library that provides cool colors and other displays to make your logs look cool.
  2. execa: Runs the necessary library of terminal commands, similar to enabling subtaskschild_processAn enhanced version of.
  3. Semver: version semantic parsing library. Utility classes are involved in the management of various versions, such as Node version, package version, so this library is definitely needed.

There are also some of our own tool classes, like the following:

  1. Log: console log output function, can control different types of log printing, simplified as follows, error, WARN, INFO, success and other types of functions
const logInfo = console.log;
const logError = console.error;
const logWarn = console.warn;

const prefix = 'cat-smoker-';

module.exports.info = msg= > logInfo(`${chalk.hex('# 3333').bgBlue(`${prefix}INFO `)} ${msg}`);

module.exports.error = msg= > logError(`${chalk.hex('# 3333').bgRed(`${prefix}ERROR `)} ${msg}`);

module.exports.warn = msg= > logWarn(`${chalk.hex('# 3333').bgYellow(`${prefix}WARN `)} ${msg}`);

module.exports.success = msg= >
  logInfo(`${chalk.hex('# 3333').bgGreen(`${prefix}SUCCESS `)} ${msg}`);

module.exports.log = (msg = ' ') = > {
  console.log(msg);
};

// Clear terminal contents
module.exports.clearConsole = title= > {
  // Determine whether it is in terminal context based on isTTY
  // This is a copy of vue-cli judgment, although I think this is not meaningful judgment
  // The purpose of this judgment is to distinguish between the general terminal environment and other file environments
  if (process.stdout.isTTY) {
    const blank = '\n'.repeat(process.stdout.rows);
    console.log(blank);
    readline.cursorTo(process.stdout, 0.0);
    readline.clearScreenDown(process.stdout);
    if (title) {
      console.log(title); }}};Copy the code
  1. Module load function, directly copied vue-CLI module file, this module is more important, see the following analysis
const Module = require('module')
const path = require('path')
const semver = require('semver')
// Request is the relative path of the request. Context is the absolute path of the context

// Compatible with different node versions of module loading mode, return require function
const createRequire = Module.createRequire || Module.createRequireFromPath || function (filename) {
  const mod = new Module(filename, null)
  mod.filename = filename
  mod.paths = Module._nodeModulePaths(path.dirname(filename))

  mod._compile(`module.exports = require; `, filename)

  return mod.exports
}
function resolveFallback (request, options) {
  // polyfill the following resolution file resolve compatibility
  // ...
}

const resolve = semver.satisfies(process.version, '> = 10.0.0')?require.resolve
  : resolveFallback

// Returns the path to load the module
exports.resolveModule = function (request, context) {
  let resolvedPath
  try {
    try {
      // Start parsing from the specified context absolute path, and return a path resolved by require.resolve
      resolvedPath = createRequire(path.resolve(context, 'package.json')).resolve(request)
    } catch (e) {
      // Call the resolve method if there is an error
      resolvedPath = resolve(request, { paths: [context] })
    }
  } catch (e) {
    console.log(e);
  }
  return resolvedPath
}
// Load the module logic
exports.loadModule = function (request, context, force = false) {
  try {
    // Return the require dependency for the target file context
    return createRequire(path.resolve(context, 'package.json'))(request)
  } catch (e) {
    const resolvedPath = exports.resolveModule(request, context)
    if (resolvedPath) {
      // force deletes the cache and loads it again
      if (force) {
        clearRequireCache(resolvedPath)
      }
      return require(resolvedPath)
    }
  }
}
// Clear the module cache
exports.clearModule = function (request, context) {
  const resolvedPath = exports.resolveModule(request, context)
  if (resolvedPath) {
    clearRequireCache(resolvedPath)
  }
}

// We know that the commonJS require has a cache, this is to clear the require cache
function clearRequireCache (id, map = new Map()) {
  / /... See source: https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-shared-utils/lib/module.js
}

Copy the code
  1. Env.js: Environment variables and environment functions, such as those that contain Git checks and return git repository information
  2. Js: Schema data structure mapping, we recommend @hapi/joi, because our scaffolding function must throw an options configuration to provide user modification, so how to manage options data type and data mapping, etc., we need schema.
  3. Spinner. Js: That thing you usually see spinning around in the terminal…

These are the packages we need for the time being, and we’ll add them later if needed, and we’ll throw them inside index for external use:

['env'.'logger'.'spinner'.'module'.'schema'].forEach(key= > {
  // exports is directly assigned
  Object.assign(exports, require(`./lib/${key}`));
});

exports.chalk = require('chalk');
exports.execa = require('execa');
exports.semver = require('semver');
Copy the code

After declaring the necessary utility functions, the excitement begins: write the logic for the specific business!

Create.js

We declare a create.js:

const create = async (projectName, options) => {
  // projectName is the projectName
  / / the options are
}

module.exports = function (. args) {
  try {
    returncreate(... args); }catch (e) {
    console.log(e); }};Copy the code

After declaring the project name and context, we write the CREATE logic. First, the basic business flow chart of the CREATE logic is as follows, the incomplete version, for reference only:

The validate-npm-package-name library is used to determine whether your project complies with the NPM package specification. This library is used to determine whether your project complies with the NPM package specification. Resolve issues such as whether to publish, whether to repeat with others, etc.

const { log } = require('@cat-smoker/cli-shared-utils');
const validateNpmPackageName = require('validate-npm-package-name');

const isValidate = validateNpmPackageName(projectName);

// If not conforming to the specification
if(! isValidate.validForNewPackages) {// There is an error in the output
  log(`Invalid project name: "${projectName}", causes of this:`.'red');
  if (isValidate.errors && isValidate.errors.length > 0) {
    log(`reason: ${isValidate.errors.join(' ')}`.'red');
  }
  if (isValidate.warnings && isValidate.warnings.length > 0) {
    log(`reason: ${isValidate.warnings.join(' ')}`.'yellow');
  }
  process.exit(1);
}
Copy the code

Ps: One thing we notice here is that in @cat-smoker/cli we introduced a package of @cat-smoker/cli-shared-utils, that is, there is a mutual dependence between our internal packages.

After NPM link, we can support local packages for local development. At this time, we do not need to install our dependencies through NPM install, because lerna management package practice, so we directly:

$ lerna add @cat-smoker/cli-shared-utils --scope=@cat-smoker/cli
# or
$ yarn workspace @cat-smoker/cli add @cat-smoker/[email protected]
# (the version number must be inserted at the end, otherwise the remote dependencies will be loaded)
Copy the code

Using this command, you can use scope to make sure that the CLI can rely on cli-shared-utils alone without polluting other packages.

Next determine if there is a folder with the same name

// For fs, we use the fs-extra library, which is better than the native nodejs method
const fs = require('fs-extra');

// The current working destination is the directory where the command is executed
const cwd = options.cwd || process.cwd();
// This is the folder path of the target project
const destDir = path.resolve(cwd, projectName);
// Determine if the project has a folder with the same name
const isProjectExist = fs.existsSync(destDir);
Copy the code

If there is a folder with the same name, should we let the user choose it? Git repository inquirer git repository inquirer git repository inquirer git repository

When we use vue-CLI, do you often have options pop up for you to choose? Checkboxes, for example, confirm, as shown below, is what this library does.

Continuing with our logic:

// If the folder exists
if (isProjectExist) {
  const { ok } = await inquirer.prompt([
    {
      name: 'ok'.type: 'confirm'.message: 'Warning: Your project${chalk.cyan(projectName)}Already exists, your action will result in the current item${projectName}Covered, sure? `,}]);if(! ok) {return;
  }
	
  // If the user selects OK, delete all files of the target address directly
  console.log(`\nWill removing ${chalk.cyan(destDir)}. `);
  await fs.remove(destDir);
}
Copy the code

So far, looking at the above flow chart, we can see that the basic business has been solved and the logic of Creator can be entered:

const creator = new Creator(projectName, destDir);
creator.create(options)
Copy the code

This specific create business logic will be explained in the next section.

Basic logic code

The overall logic code is as follows:

bin/react-build-cli.js:

#! /usr/bin/env node

const program = require('commander');
const minimist = require('minimist');
const { log } = require('@cat-smoker/cli-shared-utils');

program.version(require('.. /package').version);

program
  .version(`@cat-smoker/cli The ${require('.. /package').version}`)
  .usage('<command> [options]')

program
  .command('create <app-name>')
  .description('create a new react project')
  .option('-g, --git [message]'.'Force git initialization with initial commit message'.true)
  .option('-n, --no-git'.'Skip git initialization')
  .action((name, cmd) = > {
    if (minimist(process.argv.slice(3))._.length > 1) {
      log(
        'Info: You provided more than one argument. The first one will be used as the appName, check it by cat-smoker --help.'
      );
    }

    const options = cleanArgs(cmd)
    if (process.argv.includes('-g') || process.argv.includes('--git')) {
      options.forceGit = true
    }
    
    require('.. /lib/create')(name, options);
  });
Copy the code

create.js

const path = require('path');
const fs = require('fs-extra');
const inquirer = require('inquirer');
const validateNpmPackageName = require('validate-npm-package-name');
const { log, chalk } = require('@cat-smoker/cli-shared-utils');
const Creator = require('./generator/Creator');

const create = async (projectName, options) => {
  const cwd = options.cwd || process.cwd();
  const destDir = path.resolve(cwd, projectName);

  const isValidate = validateNpmPackageName(projectName);

  // Determine whether the project name conforms to the specification
  if(! isValidate.validForNewPackages) { log(`Invalid project name: "${projectName}", causes of this:`.'red');
    if (isValidate.errors && isValidate.errors.length > 0) {
      log(`reason: ${isValidate.errors.join(' ')}`.'red');
    }
    if (isValidate.warnings && isValidate.warnings.length > 0) {
      log(`reason: ${isValidate.warnings.join(' ')}`.'yellow');
    }
    process.exit(1);
  }

  // Determine if the project has a folder with the same name
  const isProjectExist = fs.existsSync(destDir);

  if (isProjectExist) {
    const { ok } = await inquirer.prompt([
      {
        name: 'ok'.type: 'confirm'.message: 'Warning: Your project${chalk.cyan( projectName )}Already exists, your action will result in the current item${projectName}Covered, sure? `,}]);if(! ok) {return;
    }

    console.log(`\nWill removing ${chalk.cyan(destDir)}. `);
    await fs.remove(destDir);
  }
  const creator = new Creator(projectName, destDir);
  creator.create(options)
};

module.exports = function (. args) {
  try {
    returncreate(... args); }catch (e) {
    console.log(e); }};Copy the code

conclusion

The GitHub warehouse address of this project is here. The overall progress will be a little faster than that of the article, because the article constructed the cat-smoker project while writing, so don’t worry, it will be a long-term project.

Based on this project, we will analyze vuE-CLI source code a little bit, introduce vue-CLI scaffolding architecture and some plug-in design ideas, important: Of course, our project is not a simple vuE-CLI copy project, but on the basis of borrowing vue-CLI architecture, abandon CRA(creation-React-app) scaffolding, and add plugin plugin is not some configuration plug-in, but our business structure plug-in

The purpose of this project is to solve the problem that CRA cannot meet some requirements of React users in multi-service scenarios, so the vue-CLI business scaffolding is made. The open-source part may have some shortcomings, and we hope we can improve it together.

Outlook for the next chapter

The React Business scaffolding practice (II) — Project Build and Service (CREATE) highlights the following issues:

  1. How do I declare the template (EJS) project structure?
  2. The complete process of the CREATE project?
  3. How to bridge the plug-in API and business?
  4. How do I distinguish between a Debug development environment and a formal environment?

And so on a series of questions, the article will be launched as soon as possible, please look forward to, thank you for paying attention to Thanks – (· ω ·) Blue.