Recently in learning vue- CLI source code, benefit a lot. In order to make myself understand more deeply, I decided to build a wheel modeled after it and try to achieve as many original functions as possible.

I divided this wheel into three versions:

  1. Implement a minimalist version of scaffolding with as little code as possible.
  2. Add some accessibility to 1, such as select package manager, NPM source, and so on.
  3. Realize plug-in, can be free to extend. Add functionality without affecting the internal source code.

For those of you who don’t know what scaffolding is. As I understand it, scaffolding is the scaffolding that helps you put up the foundation of your project. Examples include project dependencies, templates, build tools, and so on. It allows you to develop your business as quickly as possible without having to configure a project from scratch.

It is recommended that when reading this article, it can be used together with the project source code for better results. This is the project address mini-cli. Each branch in the project has a version, for example, the first version of git is v1. So when reading the source code, remember to switch to the appropriate branch.

The first version is v1

The first version of the function is relatively simple, roughly:

  1. The user enters a command ready to create the project.
  2. Scaffolding parses user commands and pops up interactive statements asking the user what functions are needed to create the project.
  3. Users choose the features they want.
  4. Scaffolding is created based on the user’s choicepackage.jsonFile and add the corresponding dependencies.
  5. The scaffold renders the project template based on the user’s selection, generating files (e.gindex.html,main.js,App.vueEtc.).
  6. performnpm installCommand to install dependencies.

Project directory tree:

├ ─. Vscode ├ ─ bin │ ├ ─ MVC. Js# MVC global command├ ─ lib │ ├ ─ the generatorTemplates for each function│ │ ├ ─ Babel# Babel template│ │ ├ ─ linter# eslint template│ │ ├ ─ the router# vue - the router template│ │ ├ ─ vue# vue template│ │ ├ ─ vuex# vuex template│ │ └ ─ webpack# webpack template│ ├ ─ promptModules# Interactive prompts for each module│ └ ─ utils# A series of utility functions│ ├ ─ the create. Js# Create command handler│ ├ ─ Creator. Js# Handle interactive prompts│ ├ ─ the Generator. Js# Render templates│ ├ ─ PromptModuleAPI. Js# Inject the prompts of each function into Creator└ ─ scripts# commit Message: verify that the script is project-independent
Copy the code

Processing user commands

The scaffold’s first function is to process the user’s commands, which requires the use of commander.js. The function of the library is to parse the user’s commands and extract the user’s input for the scaffold. Take this code for example:

#! /usr/bin/env node
const program = require('commander')
const create = require('.. /lib/create')

program
.version('0.1.0 from')
.command('create <name>')
.description('create a new project')
.action(name= > { 
    create(name)
})

program.parse()
Copy the code

It registers a create command with Commander and sets the version and description of the scaffold. I saved this code in the bin directory under the project and named it mVC.js. Then add this code to the package.json file:

"bin": {
  "mvc": "./bin/mvc.js"
},
Copy the code

You can register MVC as a global command by executing NPM Link. So you can use MVC commands anywhere on your computer. Instead of executing node. /bin/mvc.js, use the MVC command.

Suppose the user enters MVC create demo on the command line (actually executing node. /bin/mvc.js create demo), commander parses to the command create and the argument demo. The scaffold can then take the parameter name (value demo) in the action callback.

Interacting with users

Once you get demo, the name of the project you want to create, you can pop up an interactive option that asks the user what functions are required for the project you want to create. To do this, use Inquirer. Js. What Inquirer. Js does is pop up a question and some options for the user to choose. And options can be specified to be multiple, single, and so on.

For example, the following code:

const prompts = [
    {
        "name": "features".// Option name
        "message": "Check the features needed for your project:".// Option prompt
        "pageSize": 10."type": "checkbox".// Other options include confirm list
        "choices": [ // Specific options
            {
                "name": "Babel"."value": "babel"."short": "Babel"."description": "Transpile modern JavaScript to older versions (for compatibility)"."link": "https://babeljs.io/"."checked": true
            },
            {
                "name": "Router"."value": "router"."description": "Structure the app with dynamic pages"."link": "https://router.vuejs.org/"
            },
        ]
    }
]

inquirer.prompt(prompts)
Copy the code

Questions and options pop up as follows:

Problem type” type”: “checkbox” is checkbox indicates multiple selections. If both options are selected, the value returned is:

{ features: ['babel'.'router']}Copy the code

Where features is the name attribute in the above question. The values in the features array are the values in each option.

Inquirer. Js can also provide questions that are relevant, where the previous question selects the specified option until the next question is displayed. For example, the following code:

{
    name: 'Router'.value: 'router'.description: 'Structure the app with dynamic pages'.link: 'https://router.vuejs.org/'}, {name: 'historyMode'.when: answers= > answers.features.includes('router'),
    type: 'confirm'.message: `Use history mode for router? ${chalk.yellow(`(Requires proper server setup for index fallback in production)`)}`.description: `By using the HTML5 History API, the URLs don't need the '#' character anymore.`.link: 'https://router.vuejs.org/guide/essentials/history-mode.html',},Copy the code

The second problem is an attribute of the when, its value is a function answers = > answers. The features. Includes (‘ the router). The second problem is only displayed when the function executes true. If you selected Router in the previous question, it will return true. The second question pops up and asks you if the routing mode is history mode.

Once you have a general understanding of Inquirer. Js, you can see what we need to do in this step. The main function is to support the scaffolding with the corresponding problems, optional values on the console display, for users to choose. The templates and dependencies are rendered after the user-specific option values are obtained.

What are the functions?

Here’s a look at what the first version supports:

  • vue
  • vue-router
  • vuex
  • babel
  • webpack
  • linter(eslint)

Since this is a VUE related scaffolding, vUE is provided by default and does not require user selection. In addition, the build tool WebPack, which provides development environment and packaging capabilities, is also required without user choice. So there are only four features for users to choose from:

  • vue-router
  • vuex
  • babel
  • linter

Now let’s take a look at the interactive prompt related files for these four functions. They are all in the lib/promptModules directory:

-babel.js
-linter.js
-router.js
-vuex.js
Copy the code

Each file contains all the interactive issues associated with it. For example, the previous example shows that there are two router-related problems. Let’s look at the babel.js code again:

module.exports = (api) = > {
    api.injectFeature({
        name: 'Babel'.value: 'babel'.short: 'Babel'.description: 'Transpile modern JavaScript to older versions (for compatibility)'.link: 'https://babeljs.io/'.checked: true})},Copy the code

The only question is to ask the user if they want Babel, which defaults to Checked: true, which means yes.

Injection problem

After the user uses the create command, the scaffold needs to aggregate interactive prompts for all functions:

// craete.js
const creator = new Creator()
// Get interactive prompts for each module
const promptModules = getPromptModules()
const promptAPI = new PromptModuleAPI(creator)
promptModules.forEach(m= > m(promptAPI))
// Clear the console
clearConsole()

// The interactive prompt is displayed and the user's selection is obtained
const answers = await inquirer.prompt(creator.getFinalPrompts())
    
function getPromptModules() {
    return [
        'babel'.'router'.'vuex'.'linter',
    ].map(file= > require(`./promptModules/${file}`))}// Creator.js
class Creator {
    constructor() {
        this.featurePrompt = {
            name: 'features'.message: 'Check the features needed for your project:'.pageSize: 10.type: 'checkbox'.choices: [],}this.injectedPrompts = []
    }

    getFinalPrompts() {
        this.injectedPrompts.forEach(prompt= > {
            const originalWhen = prompt.when || (() = > true)
            prompt.when = answers= > originalWhen(answers)
        })
    
        const prompts = [
            this.featurePrompt, ... this.injectedPrompts, ]return prompts
    }
}

module.exports = Creator


// PromptModuleAPI.js
module.exports = class PromptModuleAPI {
    constructor(creator) {
        this.creator = creator
    }

    injectFeature(feature) {
        this.creator.featurePrompt.choices.push(feature)
    }

    injectPrompt(prompt) {
        this.creator.injectedPrompts.push(prompt)
    }
}
Copy the code

The logic of the above code is as follows:

  1. createcreatorobject
  2. callgetPromptModules()Gets interactive prompts for all features
  3. Call againPromptModuleAPIInject all interactive prompts intocreatorobject
  4. throughconst answers = await inquirer.prompt(creator.getFinalPrompts())The interactive statement pops up in the console and assigns the result of the user selection toanswersThe variable.

If all functions are selected, the value of answers is:

{
  features: [ 'vue'.'webpack'.'babel'.'router'.'vuex'.'linter'].// The functions of the project
  historyMode: true.// Whether to use the history mode for the route
  eslintConfig: 'airbnb'.// Default rules for esilnt validation code can be overridden
  lintOn: [ 'save' ] // Verify the code when it is saved
}
Copy the code

Project template

Once you get the user’s options, it’s time to render the template and generate the package.json file. First look at how to generate a package.json file:

// The contents of package.json file
const pkg = {
    name,
    version: '0.1.0 from'.dependencies: {},
    devDependencies: {},}Copy the code

Start by defining a PKG variable to represent the package.json file and setting some defaults.

All project templates are in the lib/generator directory:

├ ─ lib │ ├ ─ the generatorTemplates for each function│ │ ├ ─ Babel# Babel template│ │ ├ ─ linter# eslint template│ │ ├ ─ the router# vue - the router template│ │ ├ ─ vue# vue template│ │ ├ ─ vuex# vuex template│ │ └ ─ webpack# webpack template
Copy the code

Each template does the same thing:

  1. topkgVariable injection dependencies
  2. Providing template files

dependent

Here’s the code for Babel:

module.exports = (generator) = > {
    generator.extendPackage({
        babel: {
            presets: ['@babel/preset-env'],},dependencies: {
            'core-js': '^ 3.8.3',},devDependencies: {
            '@babel/core': '^ 7.12.13'.'@babel/preset-env': '^ 7.12.13'.'babel-loader': '^ 8.2.2',}})}Copy the code

As you can see, the template calls the extendPackage() method of the Generator object to inject all babel-related dependencies into the PKG variable.

extendPackage(fields) {
    const pkg = this.pkg
    for (const key in fields) {
        const value = fields[key]
        const existing = pkg[key]
        if (isObject(value) && (key === 'dependencies' || key === 'devDependencies' || key === 'scripts')) {
            pkg[key] = Object.assign(existing || {}, value)
        } else {
            pkg[key] = value
        }
    }
}
Copy the code

The process of injecting a dependency is to iterate through all the templates that the user has selected and call extendPackage() to inject the dependency.

Apply colours to a drawing template

How does the scaffold render the template? Using vuex as an example, let’s first look at its code:

module.exports = (generator) = > {
	// Inject code into 'SRC /main.js' import store from './store'
    generator.injectImports(generator.entryFile, `import store from './store'`)
	
    // Inject the store option into the new Vue() of the entry file 'SRC /main.js'
    generator.injectRootOptions(generator.entryFile, `store`)
	
    // Inject dependencies
    generator.extendPackage({
        dependencies: {
            vuex: '^ 3.6.2',}})// Render the template
    generator.render('./template'}, {})Copy the code

You can see that the code for rendering is generator.render(‘./template’, {}). ./template is the path to the template directory:

While all the template code is stored in the template directory, vuex will generate a store folder with an index.js file in the SRC directory under the user created directory. Its contents are:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {},mutations: {},actions: {},modules: {},})Copy the code

Here is a brief description of the generator.render() process.

First, use globby to read all files in the template directory:

const _files = await globby(['* * / *'] and {cwd: source, dot: true })
Copy the code

The second step is to iterate over all the files read. If the file is binary, no processing is done and the file is generated directly during rendering. Otherwise read the contents of the file and call EJS again to render:

// Return the contents of the file
const template = fs.readFileSync(name, 'utf-8')
return ejs.render(template, data, ejsOptions)
Copy the code

The advantage of using EJS is that you can combine variables to decide whether or not to render certain code. For example, the WebPack template contains the following code:

module: {
      rules: [< % _if (hasBabel) { _%>
          {
              test: /\.js$/,
              loader: 'babel-loader'.exclude: /node_modules/}, <%_} _%>],},Copy the code

Ejs can decide whether to render this code based on whether the user has Babel selected. If hasBabel is false, this code:

{
    test: /\.js$/,
    loader: 'babel-loader'.exclude: /node_modules/,},Copy the code

Will not be rendered. The value of hasBabel is passed as an argument to render() :

generator.render('./template', {
    hasBabel: options.features.includes('babel'),
    lintOnSave: options.lintOn.includes('save'})),Copy the code

Step three, inject specific code. Think back to vuex:

// Inject code into 'SRC /main.js' import store from './store'
generator.injectImports(generator.entryFile, `import store from './store'`)

// Inject the store option into the new Vue() of the entry file 'SRC /main.js'
generator.injectRootOptions(generator.entryFile, `store`)
Copy the code

These two lines of code are used to inject specific code into the project entry file SRC /main.js.

Vuex is a state management library of VUE, belonging to the vUE family bucket. If the project is created without vuex and Vue-Router selected. The code for SRC /main.js is:

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
    render: (h) = > h(App),
}).$mount('#app')
Copy the code

If vuex is selected, it will inject the above two lines of code. Now the SRC /main.js code becomes:

import Vue from 'vue'
import store from './store' // The injected code
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  store, // The injected code
  render: (h) = > h(App),
}).$mount('#app')
Copy the code

Here is a brief description of the code injection process:

  1. Use Vue-Codemod to parse the code into a syntax abstract tree AST.
  2. The code to be inserted then becomes an AST node inserted into the AST described above.
  3. Finally, the new AST is rerendered into code.

extractpackage.jsonPart of the options

Some third-party library configuration items can be placed in a package.json file, or generated in a separate file. For example, Babel in package.json is configured as:

babel: {
    presets: ['@babel/preset-env'],
}
Copy the code

We can call the generator. ExtractConfigFiles () to pick up the content and generate Babel. Config. Js files:

module.exports = {
    presets: ['@babel/preset-env'],}Copy the code

Generate the file

The rendered template file and package.json file are still in memory, not actually created on hard disk. The file can be generated by calling writeFileTree() :

const fs = require('fs-extra')
const path = require('path')

module.exports = async function writeFileTree(dir, files) {
    Object.keys(files).forEach((name) = > {
        const filePath = path.join(dir, name)
        fs.ensureDirSync(path.dirname(filePath))
        fs.writeFileSync(filePath, files[name])
    })
}
Copy the code

The logic of this code is as follows:

  1. Iterate over all rendered files and generate them one by one.
  2. When a file is generated, verify that its parent directory is present. If not, it becomes the parent directory.
  3. Write to a file.

For example, now a file path is SRC /test.js, the first time write, because there is no SRC directory. So the file will be generated into the SRC directory and generated into test.js.

webpack

Webpack needs to provide services such as hot loading and compilation in the development environment, and also needs to provide packaging services. Webpack currently has less code and simpler functionality. And in the generated project, the WebPack configuration code is exposed. This will be left to v3.

Add new features

To add a new feature, you need to add code in two places: Lib /promptModules and Lib /generator. Added to lib/promptModules are interactive prompts related to this feature. Added to the lib/generator are the dependencies and template code associated with this functionality.

However, not all functions need to add template code, such as Babel does not. When new functionality is added, it is possible to impact existing template code. For example, I now need project support for TS. In addition to adding ts dependencies, you have to modify the original template code in webpack vue Vue-Router vuex Linter.

For example, in vue-Router, if ts is supported, this code:

const routes = [ // ... ]
Copy the code

It needs to be modified as follows:

The < % _if (hasTypeScript) { _%>
const routes: Array<RouteConfig> = [ // ... ]The < % _}else{_ % >const routes = [ // ... ]The < % _} _ % >Copy the code

Because the value of TS has a type.

In short, the more new features you add, the more template code you will have for each feature. It also needs to consider the impact of the various functions.

Download the dependent

Downloading dependencies requires the use of EXECA, which calls the child process to execute commands.

const execa = require('execa')

module.exports = function executeCommand(command, cwd) {
    return new Promise((resolve, reject) = > {
        const child = execa(command, [], {
            cwd,
            stdio: ['inherit'.'pipe'.'inherit'],
        })

        child.stdout.on('data'.buffer= > {
            process.stdout.write(buffer)
        })

        child.on('close'.code= > {
            if(code ! = =0) {
                reject(new Error(`command failed: ${command}`))
                return
            }

            resolve()
        })
    })
}

/ / create. Js file
console.log('\n downloading dependencies... \n')
// Download dependencies
await executeCommand('npm install', path.join(process.cwd(), name))
console.log('\n depends on the download to complete! Execute the following command to start development: \n')
console.log(`cd ${name}`)
console.log(`npm run dev`)
Copy the code

Call executeCommand() to start downloading dependencies, taking NPM install and the user-created project path. In order for the user to see the dependency download process, we need to pass the output of the child process to the main process, i.e. to the console, using the following code:

child.stdout.on('data'.buffer= > {
    process.stdout.write(buffer)
})
Copy the code

Let me show you how to create version V1 using a GIF:

Screenshot of the successfully created project:

Version 2 V2

The second version adds some accessibility features on the basis of V1:

  1. When creating a project, determine whether the project already exists. Override and merge creation are supported.
  2. The default configuration mode and manual selection mode are available.
  3. If both YARN and NPM exist in the user’s environment, the system prompts the user which package manager to use.
  4. If the default source speed of NPM is slow, the user will be prompted whether to switch to taobao source.
  5. If the user manually selects the function, the user will be asked if he wants to save this selection as the default configuration.

Overlay and merge

When creating a project, check in advance to see if the project exists:

const targetDir = path.join(process.cwd(), name)
// If the target directory already exists, ask whether to overwrite or merge
if (fs.existsSync(targetDir)) {
    // Clear the console
    clearConsole()

    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'},],},])if (action === 'overwrite') {
        console.log(`\nRemoving ${chalk.cyan(targetDir)}. `)
        await fs.remove(targetDir)
    }
}
Copy the code

If overwrite is selected, fs.remove(targetDir) is removed.

Default configuration and manual mode

Write the default configuration code in advance:

exports.defaultPreset = {
    features: ['babel'.'linter'].historyMode: false.eslintConfig: 'airbnb'.lintOn: ['save'],}Copy the code

This configuration uses Babel and ESLint by default.

When the interactive prompt is then generated, the getDefaultPrompts() method is first called to get the default configuration.

getDefaultPrompts() {
    const presets = this.getPresets()
    const presetChoices = Object.entries(presets).map(([name, preset]) = > {
        let displayName = name

        return {
            name: `${displayName} (${preset.features}) `.value: name,
        }
    })

    const presetPrompt = {
        name: 'preset'.type: 'list'.message: `Please pick a preset:`.choices: [
            // Default configuration. presetChoices,// This is the manual mode prompt
            {
                name: 'Manually select features'.value: '__manual__',},],}const featurePrompt = {
        name: 'features'.when: isManualMode,
        type: 'checkbox'.message: 'Check the features needed for your project:'.choices: [].pageSize: 10,}return {
        presetPrompt,
        featurePrompt,
    }
}
Copy the code

After this configuration, a message like this will pop up before the user selects the function:

A package manager

When a project is created in vue-cli, a.vuerc file is generated, which records some configuration information about the project. For example, which package manager to use, whether NPM source uses Taobao source, and so on. To avoid conflicts with vue-CLI, the configuration file generated by the scaffold is.mvcrc.

The.mvcrc file is stored in the user’s home directory (which varies by operating system). My is Win10 operating system, save directory as C:\Users\bin. The user’s home directory can be obtained by:

const os = require('os')
os.homedir()
Copy the code

The.mvcrc file also saves the configuration of the user-created project, so that when the user recreates the project, he or she can directly select the previously created configuration without having to step through the project selection function.

The.mvcrc file does not exist when the project is first created. If yarn is also installed, the scaffold prompts the user which package manager to use:

// Read the '.mvcrc 'file
const savedOptions = loadOptions()
// If no package manager is specified and YARN exists
if(! savedOptions.packageManager && hasYarn) {const packageManagerChoices = []

    if (hasYarn()) {
        packageManagerChoices.push({
            name: 'Use Yarn'.value: 'yarn'.short: 'Yarn',
        })
    }

    packageManagerChoices.push({
        name: 'Use NPM'.value: 'npm'.short: 'NPM',
    })

    otherPrompts.push({
        name: 'packageManager'.type: 'list'.message: 'Pick the package manager to use when installing dependencies:'.choices: packageManagerChoices,
    })
}
Copy the code

After the user selects YARN, the downloaded dependent command changes to YARN. If NPM is selected, the download command is NPM install:

const PACKAGE_MANAGER_CONFIG = {
    npm: {
        install: ['install'],},yarn: {
        install: [].}},await executeCommand(
    this.bin, // 'yarn' or 'npm'
    [
        ...PACKAGE_MANAGER_CONFIG[this.bin][command], ... (args || []), ],this.context,
)
Copy the code

Switch the NPM source

After the user selects the project function, it will first call shouldUseTaobao() method to determine whether to switch taobao source:

const execa = require('execa')
const chalk = require('chalk')
const request = require('./request')
const { hasYarn } = require('./env')
const inquirer = require('inquirer')
const registries = require('./registries')
const { loadOptions, saveOptions } = require('./options')
  
async function ping(registry) {
    await request.get(`${registry}/vue-cli-version-marker/latest`)
    return registry
}
  
function removeSlash(url) {
    return url.replace(/ / / $/.' ')}let checked
let result
  
module.exports = async function shouldUseTaobao(command) {
    if(! command) { command = hasYarn() ?'yarn' : 'npm'
    }
  
    // ensure this only gets called once.
    if (checked) return result
    checked = true
  
    // previously saved preference
    const saved = loadOptions().useTaobaoRegistry
    if (typeof saved === 'boolean') {
        return (result = saved)
    }
  
    const save = val= > {
        result = val
        saveOptions({ useTaobaoRegistry: val })
        return val
    }
  
    let userCurrent
    try {
        userCurrent = (await execa(command, ['config'.'get'.'registry'])).stdout
    } catch (registryError) {
        try {
        // Yarn 2 uses `npmRegistryServer` instead of `registry`
            userCurrent = (await execa(command, ['config'.'get'.'npmRegistryServer'])).stdout
        } catch (npmRegistryServerError) {
            return save(false)}}const defaultRegistry = registries[command]
    if(removeSlash(userCurrent) ! == removeSlash(defaultRegistry)) {// user has configured custom registry, respect that
        return save(false)}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)}if (process.env.VUE_CLI_API_MODE) {
        return save(true)}// ask and save preference
    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? `,)},])// Register taobao source
    if (useTaobaoRegistry) {
        await execa(command, ['config'.'set'.'registry', registries.taobao])
    }

    return save(useTaobaoRegistry)
}
Copy the code

The logic of the above code is:

  1. Determine the default configuration file first.mvcrcIs there auseTaobaoRegistryOptions. If yes, return the result without judgment.
  2. Send one to NPM default source and one to Taobao sourcegetRequest, passPromise.race()To invoke. This way, the faster request will be returned first, so you know whether the default source or the Taobao source is faster.
  3. If the taobao source is faster, prompt the user whether to switch to the Taobao source.
  4. If the user chooses taobao source, then callawait execa(command, ['config', 'set', 'registry', registries.taobao])Change the current NPM source to Taobao source, i.enpm config set registry https://registry.npm.taobao.org. If yarn is used, the command isyarn config set registry https://registry.npm.taobao.org.

A little doubt

Vue-cli does not have this code:

// Register taobao source
if (useTaobaoRegistry) {
    await execa(command, ['config'.'set'.'registry', registries.taobao])
}
Copy the code

I added it myself. The main thing is that I didn’t find the code to explicitly register taobao source in vue-CLI, it just reads out whether to use Taobao source from the configuration file, or writes the option of whether to use Taobao source to the configuration file. In addition, NPM configuration file. NPMRC can change the default source, if in the.npmrc file directly write taobao mirror address, then NPM will use Taobao source download dependency. But NPM will certainly not read the configuration of.vuerc to decide whether to use Taobao source.

For this POINT I did not understand, so after the user chose taobao source, manually call the command to register again.

Save the project functionality as the default configuration

If the user selects manual mode when creating the project, after selecting a series of functions, the following prompt will pop up:

Ask the user whether to save the project selection as the default configuration, if the user selects yes, the next prompt will be displayed:

Let the user enter a name to save the configuration.

The code associated with these two prompts is:

const otherPrompts = [
    {
        name: 'save'.when: isManualMode,
        type: 'confirm'.message: 'Save this as a preset for future projects? '.default: false}, {name: 'saveName'.when: answers= > answers.save,
        type: 'input'.message: 'Save preset as:',},]Copy the code

The code to save the configuration is:

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 above code saves the user’s configuration directly into the.mvcrc file. Here is the contents of.mvCRC on my computer:

{
  "packageManager": "npm"."presets": {
    "test": {
      "features": [
        "babel"."linter"]."eslintConfig": "airbnb"."lintOn": [
        "save"]},"demo": {
      "features": [
        "babel"."linter"]."eslintConfig": "airbnb"."lintOn": [
        "save"]}},"useTaobaoRegistry": true
}
Copy the code

The next time the project is created, the scaffold will read the contents of the profile and let the user decide whether to create the project with the existing configuration.

At this point, the v2 version of the content is introduced.

summary

Since I haven’t read all of vue-CLI’s source code for the plug-in, this article will cover only the first two versions. The V3 version will be filled after I read the source code of VUe-CLI. It is expected to be completed in early March.

If you want to learn more about front-end engineering, check out my introduction to Front-end Engineering. Here is the full list:

  1. Technology selection: How to do technology selection?
  2. Uniform specifications: How are specifications developed and tools used to ensure that they are strictly enforced?
  3. Front-end componentization: What is modularization and componentization?
  4. Testing: How do I write unit tests and E2E (end-to-end) tests?
  5. Build tools: What are the build tools? What are the features and advantages?
  6. Automated deployment: How can Jenkins, Github Actions automate deployment projects?
  7. Front-end monitoring: explain the principle of front-end monitoring and how to use Sentry to monitor projects.
  8. Performance optimization (a) : How to test the performance of the website? What are some useful performance tuning rules?
  9. Performance optimization (2) : How to detect website performance? What are some useful performance tuning rules?
  10. Refactoring: Why do refactoring? What are the techniques of refactoring?
  11. Microservices: What is a microservice? How to build a micro service project?
  12. Severless: What is Severless? How to use Severless?

Article 2:

  • How to write a scaffold (part 2)

The resources

  • vue-cli