【 Front-end Engineering Basics – CLI 】 series of articles, constantly updated:

  • 【 Front-end Engineering basics – CLI chapter 】Vue HOW to implement the CLI
  • How to implement Creact React App

Concern about the public number to play camera programmers, the first time to read the latest article.

Original link axuebin.com/articles/fe… Please contact for reprint.


Vue CLI is a complete system for rapid development based on Vue. Js. It provides terminal command line tools, zero configuration scaffolding, plug-in system, graphical management interface, etc. For the moment, this article will focus on the project initialization part, which is the implementation of the terminal command line tool.

Use 0.

Usage is simple, and each CLI is pretty much the same:

npm install -g @vue/cli
vue create vue-cli-test
Copy the code

Currently, the Vue CLI supports the creation of both Vue 2 and Vue 3 projects (default configuration).

The above is the default configuration provided by the Vue CLI to quickly create a project. In addition, you can customize the project engineering configuration according to your own project requirements (whether you use Babel, whether you use TS, etc.), which gives you more flexibility.

After the selection is complete, press Enter to start executing commands such as installing dependencies and copying templates…

If you see Successfully, the project is initialized Successfully.

The vue create command supports several parameters. You can obtain detailed documentation from vue create –help:

Create [options] <app-name> -p, --preset <presetName> ignores the prompt and uses the saved or remote preset option -d, --default ignores the prompt and uses the default preset option -i, -- Inlineemanager < JSON > Ignores prompts and uses inline JSON string preset options -m, --packageManager <command> Use the specified NPM client -r, --registry <url> Use the specified NPM registry -g, --git [message] force/skip git initialization, And optionally specify initialization commit information -n, --no-git skips git initialization -f, --force overrides possible configurations in the target directory -c, --cloneUse the gitcloneGet the remote default option -x, --proxy creates the project using the specified proxy -b, --bare creates the project omitting the newbie guidance information in the default component -h, --helpOutput help informationCopy the code

The specific usage we are interested in can try, here will not expand, the subsequent source code analysis will have the corresponding part mentioned.

1. Import files

The Vue CLI version in this article is 4.5.9. If there is a break change when reading this article, you may need to understand it for yourself

Following normal logic, we found the entry file in package.json:

{
  "bin": {
    "vue": "bin/vue.js"}}Copy the code

The create/add/UI command is registered on vue. This article will analyze the create part of the code (delete the code that is not related to the main process) :

// Check the node version
checkNodeVersion(requiredVersion, '@vue/cli');

// Mount the create command
program.command('create <app-name>').action((name, cmd) = > {
  // Get extra parameters
  const options = cleanArgs(cmd);
  // Execute the create method
  require('.. /lib/create')(name, options);
});
Copy the code

CleanArgs is the argument passed in by – after getting vue Create, and a list of executed arguments can be obtained with vue Create –help.

So once you get the parameters, you’re going to execute the actual CREATE method, and so on.

It has to be said that the Vue CLI manages code modules very carefully, and each module is basically a single function module that can be assembled and used arbitrarily. The number of lines of code in each file is also small, making it very comfortable to read.

2. The input command is incorrect, and the user intention is presumed

One interesting aspect of the Vue CLI is what happens if the user types Vue creat XXX instead of Vue create XXX in the terminal? It should be an error in theory.

If it’s just an error, I won’t mention it. Check out the results:

Did you mean create? Vue CLI seems to know that the user wants to use create, but the hand speed is too fast to type the wrong word.

How does this work? We looked for the answer in the source code:

const leven = require('leven');

// If the command is not currently mounted, the user's intent will be guessed
program.arguments('<command>').action(cmd= > {
  suggestCommands(cmd);
});

// Guess the user's intent
function suggestCommands(unknownCommand) {
  const availableCommands = program.commands.map(cmd= > cmd._name);

  let suggestion;

  availableCommands.forEach(cmd= > {
    const isBestMatch =
      leven(cmd, unknownCommand) < leven(suggestion || ' ', unknownCommand);
    if (leven(cmd, unknownCommand) < 3&& isBestMatch) { suggestion = cmd; }});if (suggestion) {
    console.log(` ` + chalk.red(`Did you mean ${chalk.yellow(suggestion)}? `)); }}Copy the code

The code used leven this package, which is used to calculate the string editing distance algorithm JS implementation, Vue CLI here used this package, respectively, to calculate the input command and currently mounted all command editing examples, so as to guess the user actually want to input the command is which.

A small but beautiful function, user experience greatly improved.

3. Check the Node version

3.1 Expected Node version

Similar to create-react-app, the Vue CLI checks whether the current Node version meets the requirements:

  • Current Node version:process.version
  • Expected Node version:require(".. /package.json").engines.node

For example, I am currently using Node V10.20.1 and @vue/ CLI 4.5.9 requires Node version >=8.9, so it meets the requirements.

3.2 Node LTS versions are recommended

In bin/vue.js there is this code, which also looks like checking the Node version:

const EOL_NODE_MAJORS = ['8.x'.'9.x'.'11.x'.'13.x'];
for (const major of EOL_NODE_MAJORS) {
  if (semver.satisfies(process.version, major)) {
    console.log(
      chalk.red(
        `You are using Node ${process.version}.\n` +
          `Node.js ${major} has already reached end-of-life and will not be supported in future major releases.\n` +
          `It's strongly recommended to use an active LTS version instead.`)); }}Copy the code

Not everyone knows what it does, so here’s a little bit of a primer.

To put it simply, there are odd-numbered and even-numbered major versions of Node. Each release lasts for six months, after which the odd-numbered version becomes EOL and the even-numbered version becomes Active LTS and long term support. When using Node in production, try to use the LTS version rather than the EOL version.

EOL version: an end-of-life version Of Node LTS version: A long-term supported version Of Node

This is a common situation with the current version of Node:

Explain the following states:

  • CURRENT: Fixes bugs, adds new features and continues to improve
  • ACTIVE: indicates a long-term stable version
  • MAINTENANCE: only fixes bugs, no new features will be added
  • EOL: When the progress bar runs out, this version is no longer maintained and supported

Node 8.x will be EOL in 2020, and Node 12.x will be in MAINTENANCE in 2021. Node 10.x will become EOL in April-May 2021.

The Vue CLI checks the current Node version and recommends LTS if you are using EOL. That said, there will be an extra 10.x in the near future, so go ahead and give Vue CLI a PR.

4. Check whether it is in the current path

Error: Missing required argument

when executing vue create, you must specify an app-name.

What if the user has already created a directory and wants to create a project in the current empty directory? Of course, Vue CLI is also supported, execute Vue create. With respect to OK.

There is code in lib/create.js that handles this logic.

async function create(projectName, options) {
  // Check whether the projectName passed in is.
  const inCurrent = projectName === '. ';
  // path.relative returns the relative path from the first parameter to the second parameter
  // This is the directory name used to get the current directory
  const name = inCurrent ? path.relative('.. / ', cwd) : projectName;
  // Finally initialize the project path
  const targetDir = path.resolve(cwd, projectName || '. ');
}
Copy the code

This logic is handy if you need to implement a CLI.

5. Check the application name

The Vue CLI checks that the entered projectName conforms to the specification using the validate-npm-package-name package.

const result = validateProjectName(name);
if(! result.validForNewPackages) {console.error(chalk.red(`Invalid project name: "${name}"`));
  exit(1);
}
Copy the code

Naming Rules can be found in the corresponding NPM Naming convention

6. If the destination folder already exists, determine whether to overwrite it

Check whether the target directory exists and ask the user if the target directory exists.

// Whether to vue create-m
if(fs.existsSync(targetDir) && ! options.merge) {// Whether to vue create-f
  if (options.force) {
    await fs.remove(targetDir);
  } else {
    await clearConsole();
    // If the initialization is in the current path, just check whether the creation is in the current directory
    if (inCurrent) {
      const { ok } = await inquirer.prompt([
        {
          name: 'ok'.type: 'confirm'.message: `Generate project in current directory? `,}]);if(! ok) {return; }}else {
      // If there is a target directory, ask what to do: Overwrite/Merge/Cancel
      const { action } = await inquirer.prompt([
        {
          name: 'action'.type: 'list'.message: `Target directory ${chalk.cyan( targetDir )} already exists. Pick an action:`.choices: [{name: 'Overwrite'.value: 'overwrite' },
            { name: 'Merge'.value: 'merge' },
            { name: 'Cancel'.value: false},],},]);// Abort directly if Cancel is selected
      // If you select Overwrite, delete the original directory first
      // If Merge is selected, no preprocessing is required
      if(! action) {return;
      } else if (action === 'overwrite') {
        console.log(`\nRemoving ${chalk.cyan(targetDir)}. `);
        awaitfs.remove(targetDir); }}}}Copy the code

7. Overall error capture

At the outermost layer of the create method, a catch method is placed to catch any errors thrown internally, stopping the current spinner state and exiting the process.

module.exports = (. args) = > {
  returncreate(... args).catch(err= > {
    stopSpinner(false); // do not persist
    error(err);
    if(! process.env.VUE_CLI_TEST) { process.exit(1); }}); };Copy the code

8. Creator classes

At the end of the lib/create.js method, two lines of code are executed:

const creator = new Creator(name, targetDir, getPromptModules());
await creator.create(options);
Copy the code

It seems that the most important code is in the Creator class.

Open the Creator. Js file, good boy, 500+ lines of code, and introduce 12 modules. Of course, this article won’t go through all 500 lines of code and 12 modules, so it’s not necessary. You can read it yourself if you’re interested.

This article also reviews the main process and some interesting features.

8.1 Constructor constructor

Let’s take a look at the Creator class constructor:

module.exports = class Creator extends EventEmitter {
  constructor(name, context, promptModules) {
    super(a);this.name = name;
    this.context = process.env.VUE_CLI_CONTEXT = context;
    // Retrievals an interactive selection list of preset and feature, available for selection during vue Create
    const { presetPrompt, featurePrompt } = this.resolveIntroPrompts();
    this.presetPrompt = presetPrompt;
    this.featurePrompt = featurePrompt;

    // Interactive selection list: whether to output some files
    this.outroPrompts = this.resolveOutroPrompts();

    this.injectedPrompts = [];
    this.promptCompleteCbs = [];
    this.afterInvokeCbs = [];
    this.afterAnyInvokeCbs = [];

    this.run = this.run.bind(this);

    const promptAPI = new PromptModuleAPI(this);
    // Inject some default configuration into the interaction list
    promptModules.forEach(m= >m(promptAPI)); }};Copy the code

Constructors, for example, basically initialize some variables. The logic is encapsulated in the methods resolveIntroPrompts/resolveOutroPrompts and PromptModuleAPI.

Take a look at what the PromptModuleAPI class does.

module.exports = class PromptModuleAPI {
  constructor(creator) {
    this.creator = creator;
  }
  // Used in promptModules
  injectFeature(feature) {
    this.creator.featurePrompt.choices.push(feature);
  }
  // Used in promptModules
  injectPrompt(prompt) {
    this.creator.injectedPrompts.push(prompt);
  }
  // Used in promptModules
  injectOptionForPrompt(name, option) {
    this.creator.injectedPrompts
      .find(f= > {
        return f.name === name;
      })
      .choices.push(option);
  }
  // Used in promptModules
  onPromptComplete(cb) {
    this.creator.promptCompleteCbs.push(cb); }};Copy the code

Just to be brief, promptModules returns all modules used for terminal interaction, injectFeature and injectPrompt are called to insert the interaction configuration, and a callback is registered via onPromptComplete.

OnPromptComplete registers the callback in the form of pushing the method passed in to the array promptCompleteCbs, and you can guess that the callback should be invoked in the following form after all interactions are complete:

this.promptCompleteCbs.forEach(cb= > cb(answers, preset));
Copy the code

Go back to this code:

module.exports = class Creator extends EventEmitter {
  constructor(name, context, promptModules) {
    const promptAPI = new PromptModuleAPI(this);
    promptModules.forEach(m= >m(promptAPI)); }};Copy the code

In the Creator constructor, a promptAPI object is instantiated and passed to promptModules by iterating through prmptModules, indicating that all interaction configurations are registered when Creator is instantiated.

Prompt. There are four prompt types that you notice in the constructor: presetPrompt, featurePrompt, InjectedPrompt, and outroprompt. What are the differences? There are more details below.

8.2 EventEmitter event module

First, the Creator class is an EventEmitter class that inherits from Node.js. As we all know, Events is the most important module in Node.js, and EventEmitter class is its foundation, which is the encapsulation of functions such as event triggering and event monitoring in Node.js.

Creator is derived from EventEmitter, which is supposed to emit some events during the create process. We have compiled the following 8 events:

this.emit('creation', { event: 'creating' }); / / create
this.emit('creation', { event: 'git-init' }); // Initialize git
this.emit('creation', { event: 'plugins-install' }); // Install the plug-in
this.emit('creation', { event: 'invoking-generators' }); / / call the generator
this.emit('creation', { event: 'deps-install' }); // Install additional dependencies
this.emit('creation', { event: 'completion-hooks' }); // Complete the callback
this.emit('creation', { event: 'done' }); // Create the process ends
this.emit('creation', { event: 'fetch-remote-preset' }); // Pull remote preset
Copy the code

We know that event emit must have an on somewhere. Where is it? It is in @vue/cli-ui package, that is, in the case of terminal command line tools, does not trigger these events, here is a brief overview:

const creator = new Creator(' ', cwd.get(), getPromptModules());
onCreationEvent = ({ event }) = > {
  progress.set({ id: PROGRESS_ID, status: event, info: null }, context);
};
creator.on('creation', onCreationEvent);
Copy the code

To put it simply, when a vue UI starts a graphical interface to initialize a project, a server side will be started, and there is communication between the terminal. The Server side mounts events that are triggered from methods in the CLI at each stage of create.

9. Preset.

The instance method create of the Creator class takes two arguments:

  • CliOptions: parameters passed in from the terminal command line
  • Preset: Preset for Vue CLI

9.1 What is Preset?

What is Preset? The official explanation is a JSON object that contains predefined options and plug-ins needed to create a new project, eliminating the need for users to select them in a command prompt. Such as:

{
  "useConfigFiles": true."cssPreprocessor": "sass"."plugins": {
    "@vue/cli-plugin-babel": {},
    "@vue/cli-plugin-eslint": {
      "config": "airbnb"."lintOn": ["save"."commit"]}},"configs": {
    "vue": {... },"postcss": {... },"eslintConfig": {... },"jest": {... }}}Copy the code

Local preset and remote preset are allowed in the CLI.

9.2 prompt

Those of you who have used inquirer will be familiar with the word prompt. It has types such as input and checkbox, and is an interaction between the user and the terminal.

Let’s go back to the getPromptModules method in Creator. This method literally gets some modules for interaction.

exports.getPromptModules = () = > {
  return [
    'vueVersion'.'babel'.'typescript'.'pwa'.'router'.'vuex'.'cssPreprocessors'.'linter'.'unit'.'e2e',
  ].map(file= > require(`.. /promptModules/${file}`));
};
Copy the code

It looks like it took a series of modules and returned an array. I have a look at several modules listed here, the code format is basically the same:

module.exports = cli= > {
  cli.injectFeature({
    name: ' '.value: ' '.short: ' '.description: ' '.link: ' '.checked: true}); cli.injectPrompt({name: ' '.when: answers= > answers.features.includes(' '),
    message: ' '.type: 'list'.choices: [].default: '2'}); cli.onPromptComplete((answers, options) = > {});
};
Copy the code

InjectFeature and injectPrompt objects look just like inquirer objects. Yes, they are configuration options for user interaction. What is the difference between Feature and Prompt?

Feature: Vue CLI top-level options when selecting a custom configuration:

Prompt: Select secondary options corresponding to specific features, such as Choose Vue version Feature, and ask the user to select 2.x or 3.x:

OnPromptComplete registers a callback method that is executed after the interaction is complete.

GetPromptModules getPromptModules getPromptModules getPromptModules getPromptModules getPromptModules getPromptModules getPromptModules getPromptModules

  • Babel: Select whether to use Babel
  • CssPreprocessors: Select CSS preprocessors (Sass, Less, Stylus)
  • .

That said, all of the prompt used by the Vue CLI will be expanded later in the section on custom configuration loading.

9.3 Obtaining a Preset

Let’s look specifically at the logic associated with getting presets. This code is in the create instance method:

// Creator.js
module.exports = class Creator extends EventEmitter {
  async create(cliOptions = {}, preset = null) {
    const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG;
    const { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this;

    if(! preset) {if (cliOptions.preset) {
        // vue create foo --preset bar
        preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone);
      } else if (cliOptions.default) {
        // vue create foo --default
        preset = defaults.presets.default;
      } else if (cliOptions.inlinePreset) {
        // vue create foo --inlinePreset {... }
        try {
          preset = JSON.parse(cliOptions.inlinePreset);
        } catch (e) {
          error(
            `CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`
          );
          exit(1); }}else {
        preset = await this.promptAndResolvePreset(); }}}};Copy the code

As you can see, several cases are handled in the code:

  • The CLI parameter is set to –preset
  • The CLI parameter is set to –default
  • The CLI parameter is set to –inlinePreset
  • Cli parameters are not configured and are obtained by defaultPresetThe behavior of the

The first three cases are forgotten, and let’s look at the fourth case, which is Preset logic obtained by default via interactive prompt, the promptAndResolvePreset method.

Let’s see what it looks like in practice:

We can assume that this is const answers = await inquirer. Prompt ([]) code.

 async promptAndResolvePreset(answers = null) {
    // prompt
    if(! answers) {await clearConsole(true);
      answers = await inquirer.prompt(this.resolveFinalPrompts());
    }
    debug("vue-cli:answers")(answers);
 }

 resolveFinalPrompts() {
    this.injectedPrompts.forEach((prompt) = > {
      const originalWhen = prompt.when || (() = > true);
      prompt.when = (answers) = > {
        return isManualMode(answers) && originalWhen(answers);
      };
    });

    const prompts = [
      this.presetPrompt,
      this.featurePrompt, ... this.injectedPrompts, ... this.outroPrompts, ]; debug("vue-cli:prompts")(prompts);
    return prompts;
 }
Copy the code

Yes, we guessed right. The wayway. resolveFinalPrompts method is simply a reminder taken together that was initialized in the Creator’s constructor. These four types of Prompt have also been mentioned above and will be introduced in the next section. 支那

9.4 Saving a Preset

At the end of the Vue CLI, the user is asked to save this as a preset for future? If the user selects Yes, the logic is executed to save the result of the interaction. This part of logic is also in promptAndResolvePreset.

async promptAndResolvePreset(answers = null)  {
  if (
    answers.save &&
    answers.saveName &&
    savePreset(answers.saveName, preset)
  ) {
    log();
    log(
      ` 🎉 Preset${chalk.yellow(answers.saveName)} saved in ${chalk.yellow( rcPath )}`); }}Copy the code

The preset will be parsed, verified, and so on before savePreset is called. Instead, let’s look at the savePreset method:

exports.saveOptions = toSave= > {
  const options = Object.assign(cloneDeep(exports.loadOptions()), toSave);
  for (const key in options) {
    if(! (keyin exports.defaults)) {
      delete options[key];
    }
  }
  cachedOptions = options;
  try {
    fs.writeFileSync(rcPath, JSON.stringify(options, null.2));
    return true;
  } catch (e) {
    error(
      `Error saving preferences: ` +
        `make sure you have write access to ${rcPath}.\n` +
        ` (${e.message}) `); }};exports.savePreset = (name, preset) = > {
  const presets = cloneDeep(exports.loadOptions().presets || {});
  presets[name] = preset;
  return exports.saveOptions({ presets });
};
Copy the code

The code is simple, Preset (clonedeep of Lodash is used here) and writeFileSync to the.vuerc file mentioned above after a few merge operations.

10. Load custom configurations

These four types of Prompt correspond to default option, custom feature selection, specific feature option and other options respectively, which are interrelated and progressive. Combined with these four prompt types, the Vue CLI presents all interactions in front of the user, including the loading of custom configurations.

10.1 presetPrompt: Default options

Vue2 or Vue3 or custom feature:

If Vue2 or Vue3 is selected, all subsequent prompts about preset cease.

10.2 featurePrompt: Customize feature options

* * if thepresetPromptSelect theManually, will continue to selectfeature:

FeaturePrompt is the list stored, and the corresponding code looks like this:

const isManualMode = answers= > answers.preset === '__manual__';

const featurePrompt = {
  name: 'features'.when: isManualMode,
  type: 'checkbox'.message: 'Check the features needed for your project:'.choices: [].pageSize: 10};Copy the code

As you can see in the code, this interaction only pops up during isManualMode.

InjectedPrompts: Specific feature option

FeaturePrompt simply provides a first-level list of new interactions that pop up when the user selects options such as Vue Version/Babel/TypeScript, such as Choose Vue Version:

InjectedPrompts are a list of those specific options that are stored, namely those prompt modules that are retrieved from the promptModules directory using the getPromptModules method mentioned above:

The corresponding code can be reviewed again:

cli.injectPrompt({
  name: 'vueVersion'.when: answers= > answers.features.includes('vueVersion'),
  message: 'Choose a version of Vue.js that you want to start the project with'.type: 'list'.choices: [{name: '2.x'.value: '2'}, {name: '3.x (Preview)'.value: '3'],},default: '2'});Copy the code

As you can see, in the answers = > answers. The features. Includes (‘ vueVersion ‘), If vueVersion is included in the result of a featurePrompt interaction, a specific Vue Version selection interaction will pop up.

10.4 outroPrompts: Other options

** The options stored here are some of the options that are not included in the above three categories, currently including three:

**Where do you prefer placing config for Babel, ESLint, etc.? ** How are configuration files such as Babel, ESLint stored?

  • In dedicated config files. Save them in their own configuration files.
  • In the package. The json. Uniformly stored in package.json.

**Save this as a preset for future projects? ** Whether to save this Preset for immediate use later.

If you choose Yes, another interaction will come out: Save Preset as Enter the name of preset.

10.5 Conclusion: Vue CLI Interaction process

Here’s a summary of the overall interaction of the Vue CLI, which is the implementation of Prompt.

In addition to the default configuration, the Vue CLI also supports custom configurations (Babel, TS, etc.), and this interactive flow is realized at the beginning of this article.

The Vue CLI classifies all interactions into four broad categories:

From preset options to specific feature options, they are a progressive relationship, and different opportunities and choices will trigger different interactions.

The design of Vue CLI code architecture is worth learning. Each interaction is maintained in different modules. When Creator instance is initialized, a unified prmoptAPI instance is inserted into different Prompt and its respective callback functions are registered. This design is completely decoupled from prompts, and deleting a prompt has negligible impact on the context.

Well, this is the basic analysis of presets and prompts, and the rest of the details are forgotten.

Here involved in the relevant source file, we can take a look:

  • Creator.js
  • PromptModuleAPI.js
  • utils/createTools.js
  • promptModules
  • .

11. Initialize the basic project files

Once the user has selected all the interactions, the CLI’s next responsibility is to generate the corresponding code based on the user’s choices, which is one of the core functions of the CLI.

11.1 Initializing the Package. json File

Depending on the user’s options, the relevant vue-cli-plugin is mounted and then used to generate a dependency devDependencies for package.json. For example, @vue/cli-service / @vue/cli-plugin-babel / @vue/cli-plugin-eslint, etc.

Vue CLI will now create a directory to write a basic package.json:

{
  "name": "a"."version": "0.1.0 from"."private": true."devDependencies": {
    "@vue/cli-plugin-babel": "~ 4.5.0." "."@vue/cli-plugin-eslint": "~ 4.5.0." "."@vue/cli-service": "~ 4.5.0." "}}Copy the code

11.2 Initializing Git

Depending on the parameters passed in and a series of judgments, the Git environment is initialized in the target directory. This simply means doing Git init:

await run('git init');
Copy the code

Whether to initialize Git environment is as follows:

shouldInitGit(cliOptions) {
  // If Git is not installed globally, it will not be initialized
  if(! hasGit()) {return false;
  }
  // If the CLI passes a --git argument, initialize it
  if (cliOptions.forceGit) {
    return true;
  }
  // If the CLI passes --no-git, it will not be initialized
  if (cliOptions.git === false || cliOptions.git === "false") {
    return false;
  }
  // If you already have a Git environment in your current directory, do not initialize it
  return! hasProjectGit(this.context);
}
Copy the code

11.3 Initializing readme. md

The project’s readme.md is dynamically generated based on the context, rather than a dead document:

function generateReadme(pkg, packageManager) {
  return [
    ` #${pkg.name}\n`.'## Project setup'.'` ` `'.`${packageManager} install`.'` ` `',
    printScripts(pkg, packageManager),
    '### Customize configuration'.'See [Configuration Reference](https://cli.vuejs.org/config/).'.' ',
  ].join('\n');
}
Copy the code

Vue CLI-created readme. md tells the user how to use the project. In addition to NPM install, it dynamically generates usage documents based on the scripts parameter in package.json, such as how to develop, build, and test:

const descriptions = {
  build: 'Compiles and minifies for production'.serve: 'Compiles and hot-reloads for development'.lint: 'Lints and fixes files'.'test:e2e': 'Run your end-to-end tests'.'test:unit': 'Run your unit tests'};function printScripts(pkg, packageManager) {
  return Object.keys(pkg.scripts || {})
    .map(key= > {
      if(! descriptions[key])return ' ';
      return [
        `\n### ${descriptions[key]}`.'` ` `'.`${packageManager} ${packageManager ! = ='yarn' ? 'run ' : ' '}${key}`.'` ` `'.' ',
      ].join('\n');
    })
    .join(' ');
}
Copy the code

Why not just copy the readme. md file?

  • First, Vue CLI supports different package management, corresponding installation, startup and build scripts are different, this needs to be dynamically generated;
  • Second, dynamic generation provides more freedom and allows you to generate documents according to the user’s preferences rather than the same for everyone.

11.4 Installing Dependencies

Call ProjectManage’s Install method to install dependencies, and the code is not complex:

 async install () {
   if (this.needsNpmInstallFix) {
     / / read package. Json
     const pkg = resolvePkg(this.context)
     / / install dependencies
     if (pkg.dependencies) {
       const deps = Object.entries(pkg.dependencies).map(([dep, range]) = > `${dep}@${range}`)
       await this.runCommand('install', deps)
     }
     / / install devDependencies
     if (pkg.devDependencies) {
       const devDeps = Object.entries(pkg.devDependencies).map(([dep, range]) = > `${dep}@${range}`)
       await this.runCommand('install', [...devDeps, '--save-dev'])}/ / optionalDependencies installation
     if (pkg.optionalDependencies) {
       const devDeps = Object.entries(pkg.devDependencies).map(([dep, range]) = > `${dep}@${range}`)
       await this.runCommand('install', [...devDeps, '--save-optional'])}return
   }
   return await this.runCommand('install'.this.needsPeerDepsFix ? ['--legacy-peer-deps'] : [])}Copy the code

Simply read package.json and install the different NPM dependencies separately.

The logic here is still very complicated, I did not look at it carefully, I will not expand to say…

11.4.1 Automatically Identifying NPM Sources

One interesting point here is the NPM repository source used when installing dependencies. If the user does not specify the installation source, Vue CLI will automatically determine whether to use taobao’s NPM installation source. Guess how to achieve this?

function shouldUseTaobao() {
  let faster
  try {
    faster = await Promise.race([
      ping(defaultRegistry),
      ping(registries.taobao)
    ])
  } catch (e) {
    return save(false)}if(faster ! == registries.taobao) {// default is already faster
    return save(false)}const { useTaobaoRegistry } = await inquirer.prompt([
    {
      name: 'useTaobaoRegistry'.type: 'confirm'.message: chalk.yellow(
        ` Your connection to the default ${command} registry seems to be slow.\n` +
          `   Use ${chalk.cyan(registries.taobao)}for faster installation? `)})return save(useTaobaoRegistry);
}
Copy the code

The Vue CLI will request the default installation source and taobao installation source via promise. race: **

  • If the first return is taobao installation source, will let the user confirm once, whether to use Taobao installation source
  • If the default installation source is returned first, the default installation source is used directly

In general, you are sure to use the default installation source, but consider domestic users. Ahem.. Give this design a thumbs up.

12. Generator generates code

After Creator, the second most important class in the entire Vue CLI is Generator, which is responsible for generating project code to see what’s going on.

12.1 Initializing plug-ins

The first method executed in the generate method is an initPlugins method with the following code:

async initPlugins () {
  for (const id of this.allPluginIds) {
    const api = new GeneratorAPI(id, this, {}, rootOptions)
    const pluginGenerator = loadModule(`${id}/generator`.this.context)

    if (pluginGenerator && pluginGenerator.hooks) {
      await pluginGenerator.hooks(api, {}, rootOptions, pluginIds)
    }
  }
}
Copy the code

Here we initialize a GeneratorAPI instance for each plug-in in package.json, pass the instance to the corresponding plug-in’s generator method and execute it, such as @vue/cli-plugin-babel/generator.js.

12.2 GeneratorAPI class

The Vue CLI uses a plug-in based architecture. If you look at package.json for a newly created project, you’ll see that the dependencies start with @vue/cli-plugin-. Plug-ins can modify the internal configuration of Webpack or inject commands into vue-cli-service. During project creation, most of the features listed were implemented through plug-ins.

As mentioned, an instance of the GeneratorAPI is passed to the generator for each plug-in to see what the class provides.

12.2.1 Example: @vue/cli-plugin-babel

To be less abstract, let’s start with @vue/cli-plugin-babel. This plugin is relatively simple:

module.exports = api= > {
  delete api.generator.files['babel.config.js'];

  api.extendPackage({
    babel: {
      presets: ['@vue/cli-plugin-babel/preset'],},dependencies: {
      'core-js': '^ 3.6.5',}}); };Copy the code

Here the API is an instance of the GeneratorAPI, using an extendPackage method:

// GeneratorAPI.js
// Delete some code for @vue/cli-plugin-babel analysis only
extendPackage (fields, options = {}) {
  const pkg = this.generator.pkg
  const toMerge = isFunction(fields) ? fields(pkg) : fields
  // Iterate over the parameters passed in. Here are the Babel and dependencies objects
  for (const key in toMerge) {
    const value = toMerge[key]
    const existing = pkg[key]
    // If the key name is dependencies and devDependencies
    // Merge dependencies into package.json using the mergeDeps method
    if (isObject(value) && (key === 'dependencies' || key === 'devDependencies')) {
      pkg[key] = mergeDeps(
        this.id,
        existing || {},
        value,
        this.generator.depSources,
        extendOptions
      )
    } else if(! extendOptions.merge || ! (keyin pkg)) {
      pkg[key] = value
    }
  }
}
Copy the code

In this case, the default package.json becomes:

{
  "babel": {
    "presets": ["@vue/cli-plugin-babel/preset"]},"dependencies": {
    "core-js": "^ 3.6.5." "
  },
  "devDependencies": {},
  "name": "test"."private": true."version": "0.1.0 from"
}
Copy the code

Now that you have some idea of what an instance of the GeneratorAPI does, let’s look at an instance of this class in detail.

12.2.2 Important sample methods

I’ll introduce some of the GeneratorAPI’s most important instance methods, but I’ll leave out the functionality, the code, and so on.

  • ExtendPackage: Extends package.json configuration
  • Render: Render the template file via EJS
  • OnCreateComplete: Callback after the registration file has been written to disk
  • GenJSConfig: Outputs json files as JS files
  • InjectImports: Adds an import to a file
  • .

12.3 @ vue/cli – service

The @vue/cli-plugin-babel plugin has already been seen. Do you have any feelings about the plugin architecture of vue CLI? You also learned about a more important GeneratorAPI class where some of the configuration modification functions in the plug-in are instance methods.

Create react app react-scripts is a plugin for the vue command-line interface (CLI). We should have a better understanding of how the GeneratorAPI and plug-in architecture of the Vue CLI are implemented.

Take a look at the generator/index.js file in the @vue/cli-service package. For analysis purposes, the source code is broken up into multiple sections, which call different methods of the GeneratorAPI instance:

12.3.1 rendering the template

api.render('./template', {
  doesCompile: api.hasPlugin('babel') || api.hasPlugin('typescript')});Copy the code

Render files in the template directory into memory using EJS as the template rendering engine.

12.3.2 write package. Json

Write Vue dependencies to pacakge.json via extendPackage:

if (options.vueVersion === '3') {
  api.extendPackage({
    dependencies: {
      vue: '^ 3.0.0',},devDependencies: {
      '@vue/compiler-sfc': '^ 3.0.0',}}); }else {
  api.extendPackage({
    dependencies: {
      vue: '^ 2.6.11',},devDependencies: {
      'vue-template-compiler': '^ 2.6.11',}}); }Copy the code

Write scripts to pacakge.json via extendPackage:

api.extendPackage({
  scripts: {
    serve: 'vue-cli-service serve'.build: 'vue-cli-service build',},browserslist: ['> 1%'.'last 2 versions'.'not dead']});Copy the code

Write CSS preprocessing parameters to pacakge.json via extendPackage:

if (options.cssPreprocessor) {
  const deps = {
    sass: {
      sass: '^ 1.26.5'.'sass-loader': '^ 8.0.2',},'node-sass': {
      'node-sass': '^ 4.12.0'.'sass-loader': '^ 8.0.2',},'dart-sass': {
      sass: '^ 1.26.5'.'sass-loader': '^ 8.0.2',},less: {
      less: '^ 3.0.4'.'less-loader': '^ 5.0.0',},stylus: {
      stylus: '^ 0.54.7'.'stylus-loader': '^ 3.0.2',}}; api.extendPackage({devDependencies: deps[options.cssPreprocessor],
  });
}
Copy the code

12.3.3 Invoking the Router Plug-in and Vuex Plug-in

// for v3 compatibility
if(options.router && ! api.hasPlugin('router')) {
  require('./router')(api, options, options);
}

// for v3 compatibility
if(options.vuex && ! api.hasPlugin('vuex')) {
  require('./vuex')(api, options, options);
}
Copy the code

Isn’t it easy to modify and customize projects in plug-ins with instance methods provided by the GeneratorAPI?

13. Extract an individual configuration file

As mentioned earlier, write some configuration back to package.json via extendPackage. Where do you prefer placing config for Babel, ESLint, etc.? That is, the configuration is extracted into a separate file. The extractConfigFiles method in Generate implements this logic.

extractConfigFiles(extractAll, checkExisting) {
  const configTransforms = Object.assign(
    {},
    defaultConfigTransforms,
    this.configTransforms,
    reservedConfigTransforms
  );
  const extract = (key) = > {
    if (
      configTransforms[key] &&
      this.pkg[key] &&
      !this.originalPkg[key]
    ) {
      const value = this.pkg[key];
      const configTransform = configTransforms[key];
      const res = configTransform.transform(
        value,
        checkExisting,
        this.files,
        this.context
      );
      const { content, filename } = res;
      this.files[filename] = ensureEOL(content);
      delete this.pkg[key]; }};if (extractAll) {
    for (const key in this.pkg) { extract(key); }}else {
    extract("babel"); }}Copy the code

The configTransforms here are some of the configurations that will be extracted:

If the extractAll is true, which is why Yes was selected in the interaction above, all key configTransforms in package.json are compared, and if they all exist, the configuration is extracted into a separate file.

14. Output files in the memory to disks

As mentioned above, api.render renders the template file into a string in memory via EJS. After executing all the logic for generate, you now have various files in memory that you need to output in this. Files. The final step in Generate is to call writeFileTree to write all the files in memory to hard disk.

At this point, the logic of generate is basically done, and so is the section of Vue CLI generation code.

15. To summarize

On the whole, Vue CLI code is still relatively complex, the overall architecture is still relatively clear, among which there are two deepest impressions:

First, mount the overall interaction flow. The interaction logic of each module is maintained through an instance of a class, and execution timing and successful callbacks are also well designed.

Second, plug-in mechanics are important. Plug-in mechanisms decouple functionality from scaffolding.

It seems that both create-React-app and Vue CLI are designed with plug-in mechanism in mind, opening up capabilities and integrating functions. Both the core functions of Vue CLI and community developers are open and extensible enough.

Looking at the overall code, the most important concepts are two:

  • Preset: Preset, including the overall interaction flow (Prompt)
  • Plugin: The overall Plugin system

Around these two concepts, these classes in the code: Creator, PromptModuleAPI, Generator, GeneratorAPI are the core.

A quick summary of the process:

  1. performvue create
  2. Initialize theCreatorThe instancecreatorTo mount all interaction configurations
  3. callcreatorInstance method ofcreate
  4. Ask users to customize configurations
  5. Initialize theGeneratorThe instancegenerator
  6. Initialize various plug-ins
  7. Execute plug-ingeneratorLogic, writepackage.json, rendering templates, etc
  8. Writes files to hard disk

The CLI life cycle is complete, and the project has been initialized.

Attached: Vue CLI can be directly used in the tool method

Read the Vue CLI source code, in addition to sigh this complex design, also found a lot of tools, in our implementation of their OWN CLI, are available to use, summarized here.

Obtaining CLI Parameters

Parse the arguments passed through the CLI through –.

const program = require('commander');

function camelize(str) {
  return str.replace(/-(\w)/g.(_, c) = > (c ? c.toUpperCase() : ' '));
}

function cleanArgs(cmd) {
  const args = {};
  cmd.options.forEach(o= > {
    const key = camelize(o.long.replace(/ ^ - /.' '));
    // if an option is not present and Command has a method with the same name
    // it should not be copied
    if (typeofcmd[key] ! = ='function' && typeofcmd[key] ! = ='undefined') { args[key] = cmd[key]; }});return args;
}
Copy the code

Checking the Node Version

Compare two Node versions by semver.distribution:

  • Process. version: Node version of the current running environment
  • Wanted: package.json Node version configured
const requiredVersion = require('.. /package.json').engines.node;

function checkNodeVersion(wanted, id) {
  if(! semver.satisfies(process.version, wanted, {includePrerelease: true{}))console.log(
      chalk.red(
        'You are using Node ' +
          process.version +
          ', but this version of ' +
          id +
          ' requires Node ' +
          wanted +
          '.\nPlease upgrade your Node version.')); process.exit(1);
  }
}

checkNodeVersion(requiredVersion, '@vue/cli');
Copy the code

Read the package. The json

const fs = require('fs');
const path = require('path');

function getPackageJson(cwd) {
  const packagePath = path.join(cwd, 'package.json');

  let packageJson;
  try {
    packageJson = fs.readFileSync(packagePath, 'utf-8');
  } catch (err) {
    throw new Error(`The package.json file at '${packagePath}' does not exist`);
  }

  try {
    packageJson = JSON.parse(packageJson);
  } catch (err) {
    throw new Error('The package.json is malformed');
  }

  return packageJson;
}
Copy the code

Object sorting

The e output of package.json can be sorted first.

module.exports = function sortObject(obj, keyOrder, dontSortByUnicode) {
  if(! obj)return;
  const res = {};

  if (keyOrder) {
    keyOrder.forEach(key= > {
      if (obj.hasOwnProperty(key)) {
        res[key] = obj[key];
        deleteobj[key]; }}); }const keys = Object.keys(obj); ! dontSortByUnicode && keys.sort(); keys.forEach(key= > {
    res[key] = obj[key];
  });

  return res;
};
Copy the code

Output files to hard disk

This is actually nothing, just three steps:

  • Fs. unlink Deletes a file
  • Fs.ensuredirsync Creates a directory
  • Fs. WriteFileSync writing files
const fs = require('fs-extra');
const path = require('path');

// Delete existing files
function deleteRemovedFiles(directory, newFiles, previousFiles) {
  // get all files that are not in the new filesystem and are still existing
  const filesToDelete = Object.keys(previousFiles).filter(
    filename= >! newFiles[filename] );// delete each of these files
  return Promise.all(
    filesToDelete.map(filename= > {
      returnfs.unlink(path.join(directory, filename)); })); }// Output files to hard disk
module.exports = async function writeFileTree(dir, files, previousFiles) {
  if (previousFiles) {
    await deleteRemovedFiles(dir, files, previousFiles);
  }
  // Mainly here
  Object.keys(files).forEach(name= > {
    const filePath = path.join(dir, name);
    fs.ensureDirSync(path.dirname(filePath));
    fs.writeFileSync(filePath, files[name]);
  });
};
Copy the code

Check whether the project has initialized Git

Git status > git status > git status

const hasProjectGit = cwd= > {
  let result;
  try {
    execSync('git status', { stdio: 'ignore', cwd });
    result = true;
  } catch (e) {
    result = false;
  }
  return result;
};
Copy the code

The get method of the

I can use Lodash, now I can just use A, right? .b? . C

function get(target, path) {
  const fields = path.split('. ');
  let obj = target;
  const l = fields.length;
  for (let i = 0; i < l - 1; i++) {
    const key = fields[i];
    if(! obj[key]) {return undefined;
    }
    obj = obj[key];
  }
  return obj[fields[l - 1]];
}
Copy the code

Personal original technical articles will be posted on the public account of programmers playing with cameras, using the keyboard and camera to record life.