[dry goods] teach you to write a scaffold hand in hand

I wonder if you have thought about how to implement Vue cli when you use it? Why do we have different options to inject custom statements under a large code framework? It can also automatically generate dependent files and select overwrite / merge after initialization. This paper explains and practices this, which is very strong.
Link: juejin.cn/post/6932610749906812935...
Author: Tan Guangzhi

I've been studying recently vue-cli Source code, benefit a lot. In order to make myself understand more deeply, I decided to imitate it to build a wheel and strive to realize the original functions as much as possible.

I divide this wheel into three versions:

  1. Implement a simplest version of scaffolding with as little code as possible.
  2. Add some auxiliary functions on the basis of 1, such as selecting package manager, npm source and so on.
  3. Realize plug-in, which can be extended freely. Add functions without affecting the internal source code.

Some people may not understand what scaffolding is. According to my understanding, scaffolding is to help you put up the basic framework of the project. For example, project dependencies, templates, build tools, and so on. So that you don't have to configure a project from scratch and carry out business development as soon as possible.

It is suggested that when reading this article, it can be used together with the project source code, and the effect is better. This is the project address mini-cli . Each branch in the project corresponds to a version. For example, the git branch corresponding to the first version is v1. So when reading the source code, remember to switch to the corresponding branch.

First version v1

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

  1. The user enters a command and is ready to create the project.
  2. The scaffold parses the user's commands and pops up interactive statements to ask the user what functions are required to create the project.
  3. Users choose the functions they need.
  4. The scaffold creates a package according to the user's choice JSON file and add the corresponding dependencies.
  5. The scaffold renders the project template according to the user's choice and generates files (such as index.html, main.js, App.vue, etc.).
  6. Execute the npm install command to install the dependency.

Project tree:

├─.vscode
├─bin 
│  ├─mvc.js # mvc global command
├─lib
│  ├─generator # Templates for each function
│  │  ├─babel # babel template
│  │  ├─linter # eslint template
│  │  ├─router # Vue router template
│  │  ├─vue # vue template
│  │  ├─vuex # vuex template
│  │  └─webpack # webpack template
│  ├─promptModules # Interactive prompts of each module
│  └─utils # A series of tool functions
│  ├─create.js # create command processing function
│  ├─Creator.js # Handling interactive prompts
│  ├─Generator.js # Render template
│  ├─PromptModuleAPI.js # Inject the prompt of each function into Creator
└─scripts # The commit message validation script has nothing to do with the project and needs no attention
 Copy code

Processing user commands

The first function of the scaffold is to process the user's commands, which needs to be used commander.js . The function of this library is to parse the user's commands, extract the user's input and give it to the scaffold. For example, this Code:

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

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

program.parse()
Copy code

It registers a create command with the 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 in package Add this code to the JSON file:

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

Re execution npm link , you can register MVC as a global command. In this way, MVC commands can be used anywhere on the computer. In fact, the MVC command is used instead of executing node/ bin/mvc. js.

Suppose the user enters mvc create demo on the command line (actually node. / bin / mvc.js create demo is executed), and the commander resolves to the command create and parameter demo. Then you can get the parameter name (the value is demo) in the action callback.

Interact with users

After you get the demo of the name of the project you want to create, you can pop up an interactive option to ask the user what functions the project you want to create needs. This requires Inquirer.js . Inquirer. The function of JS is to pop up a question and some options for users to choose. And the options can be multi-choice, single choice, etc.

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", // Option types include confirm list, etc
        "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 code

The pop-up questions and options are as follows:

The type of question is "type": "checkbox" is checkbox, and the description is multiple choice. If both options are selected, the returned value is:

{ features: ['babel', 'router'] }
Copy code

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

Inquirer.js can also provide relevant questions, that is, if the specified option is selected for the previous question, the next question will be 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 code

In the second question, there is an attribute when whose value is a function answers = > answers Features includes('router'). When the execution result of the function is true, the second problem will be displayed. If you selected router in the previous question, its result will become true. The second question pops up: ask you whether the routing mode chooses the history mode.

Get a general idea of inquirer JS, we can understand what we need to do in this step. It is mainly to display the functions supported by the scaffold with the corresponding problems and optional values on the console for users to choose. After obtaining the user's specific option value, render the template and dependency.

What are the functions

Let's take a look at the functions supported in the first version:

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

Because this is a vue related scaffold, vue is provided by default and does not need to be selected by the user. In addition, the build tool webpack provides the functions of development environment and packaging, which is also necessary without users' choice. Therefore, there are only four functions for users to choose from:

  • vue-router
  • vuex
  • babel
  • linter

Now let's take a look at the files related to the interactive prompts corresponding to these four functions. They are all placed in the lib/promptModules Directory:

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

Each file contains all interactive questions related to it. For example, the example just now shows that there are two problems related to router. Now let's take a look at Babel JS code:

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 code

There is only one question, that is, whether the user needs the babel function. The default is checked: true, that is, yes.

Injection problem

After the user uses the create command, the scaffold needs to aggregate the interactive prompt statements of all functions:

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

// Pop up the interactive prompt and get the user's selection
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 code

The logic of the above code is as follows:

  1. Create creator object
  2. Call getPromptModules() to get interactive prompts for all functions
  3. Then call the promptmodule API to inject all interactive prompts into the creator object
  4. Through const answers = await inquirer Prompt (creator. Getfinalprompts()) pops up an interactive statement on the console and assigns the user selection result to the answers variable.

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

{
  features: [ 'vue', 'webpack', 'babel', 'router', 'vuex', 'linter' ], // Functions of the project
  historyMode: true, // Does the route use history mode
  eslintConfig: 'airbnb', // The default rule of esilnt verification code can be overridden
  lintOn: [ 'save' ] // Check when saving code
}
Copy code

Project template

After getting the user's options, it's time to render the template and generate the package JSON file. Let's take a look at how to generate a package JSON file:

// package.json file content
const pkg = {
    name,
    version: '0.1.0',
    dependencies: {},
    devDependencies: {},
}
Copy code

First define a pkg variable to represent package JSON file and set some default values.

All project templates are placed in the lib/generator Directory:

├─lib
│  ├─generator # Templates for each function
│  │  ├─babel # babel template
│  │  ├─linter # eslint template
│  │  ├─router # Vue router template
│  │  ├─vue # vue template
│  │  ├─vuex # vuex template
│  │  └─webpack # webpack template
 Copy code

Each template has similar functions:

  1. Inject dependencies into pkg variables
  2. Provide template file

Injection dependency

The following is the code related to 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 code

You can see that the template calls the extendPackage() method of the generator object and injects 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 code

The process of injecting dependency is to traverse all the templates selected by the user and call extendPackage() to inject dependency.

Render template

How does the scaffold render the template? Take vuex for example. Let's take a look at its code first:

module.exports = (generator) => {
    // Add entry file ` Src / main JS ` import store from '/ store'
    generator.injectImports(generator.entryFile, `import store from './store'`)

    // Add entry file ` Src / main JS ` new Vue() injection option store
    generator.injectRootOptions(generator.entryFile, `store`)

    // Injection dependency
    generator.extendPackage({
        dependencies: {
            vuex: '^3.6.2',
        },
    })

    // Render template
    generator.render('./template', {})
}
Copy code

You can see that the rendered code is generator render('./template', {}). ./ Template is the path to the template directory:

All template codes are placed in the template directory. vuex will generate a store folder in the src directory under the user created directory, which has an index JS file. Its contents are:

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

Vue.use(Vuex)

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

Here is a brief description of the generator Render().

The first step is to use globby Read all files in the template directory:

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

The second step is to traverse all the read files. If the file is a binary file, it will not be processed, and the file will be generated directly during rendering. Otherwise, read the contents of the file and call ejs Render:

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

The advantage of using ejs is that you can combine variables to decide whether to render some code. For example, there is a piece of code in the template of webpack:

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

ejs can decide whether to render this code according to whether the user selects babel. If hasBabel is false, this Code:

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

Will not be rendered. The value of hasBabel is passed by the parameter when calling render():

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

The third step is to inject specific code. Think back to vuex:

// Add entry file ` Src / main JS ` import store from '/ store'
generator.injectImports(generator.entryFile, `import store from './store'`)

// Add entry file ` Src / main JS ` new Vue() injection option store
generator.injectRootOptions(generator.entryFile, `store`)
Copy code

The function of these two lines of code is: in the project entry file Src / main JS.

Vuex is a state management library of vue, which belongs to the whole family of vue. If you create a project without selecting vuex and vue router. Then Src / main JS code is:

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

Vue.config.productionTip = false

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

If vuex is selected, it will inject the two lines of code mentioned above. Now Src / main The JS code changes to:

import Vue from 'vue'
import store from './store' // Injected code
import App from './App.vue'

Vue.config.productionTip = false

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

Here is a brief description of the code injection process:

  1. use vue-codemod Parse the code into the idiom method abstract tree AST.
  2. Then change the code to be inserted into the AST node and insert it into the AST mentioned above.
  3. Finally, the new AST is re rendered into code.

Extract package Some options for JSON

Some configuration items of third-party libraries can be placed in package JSON file, or you can generate a file independently. For example, babel is in package The configuration injected in JSON is:

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

We can call generator Extractconfigfiles() extracts the content and generates Babel config. JS file:

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

Generate file

Rendered template file and package The file is not created on the hard disk. At this time, you can call writeFileTree() to generate the file:

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 code

The logic of this code is as follows:

  1. Traverse all rendered files and generate them one by one.
  2. When generating a file, confirm whether its parent directory is present. If not, it will become the parent directory.
  3. Write file.

For example, now a file path is src / test JS, because there is no src directory when it is written for the first time. So Mr. Hui will create the src directory and then generate test JS file.

webpack

Webpack needs to provide hot loading, compilation and other services in the development environment, as well as packaging services. At present, the code of webpack is relatively few and the function is relatively simple. Moreover, the webpack configuration code is exposed in the generated project. This version remains to be improved v3.

Add new features

To add a new function, you need to add code in two places: lib/promptModules and lib/generator. The interactive prompt related to this function is added to lib/promptModules. The dependency and template code related to this function are added to lib/generator.

However, not all functions need to add template code, such as babel. When adding new functions, it may affect the existing template code. For example, I now need project support ts. In addition to adding TS related dependencies, you have to modify the original template code in functions such as webpack Vue router vuex linter.

For example, in Vue router, if ts is supported, this Code:

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

It needs to be modified as:

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

Because the value of ts has type.

In short, the more new functions are added, the more template codes for each function will be added. It also needs to consider the impact between various functions.

Download dependency

Download dependency requires execa , it can call child processes 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 dependency
await executeCommand('npm install', path.join(process.cwd(), name))
console.log('\n Dependent download complete! Execute the following command to start development:\n')
console.log(`cd ${name}`)
console.log(`npm run dev`)
Copy code

Call executeCommand() to start downloading dependencies. The parameters are npm install and the project path created by the user. In order to let users see the process of downloading dependencies, we need to use the following code to transmit the output of the sub process to the main process, that is, to the console:

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

Let me use the motion chart to demonstrate the creation process of v1 version:

Screenshot of successfully created project:

Second version v2

The second version adds some auxiliary functions on the basis of v1:

  1. When creating a project, judge whether the project already exists, and support overwrite and merge creation.
  2. When selecting a function, it provides two modes: default configuration and manual selection.
  3. If both yarn and npm exist in the user's environment, the user will be prompted which package manager to use.
  4. If the default source speed of npm is relatively slow, the user will be prompted whether to switch to Taobao source.
  5. If the user selects the function manually, the user will be asked whether to save this selection as the default configuration.

Overwrite and merge

When creating a project, first judge whether the project exists in advance:

const targetDir = path.join(process.cwd(), name)
// If the target directory already exists, ask whether to overwrite or merge
if (fs.existsSync(targetDir)) {
    // Empty 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 code

If overwrite is selected, remove FS remove(targetDir).

Default configuration and manual mode

Write the default configuration code in advance in the code:

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

This configuration uses babel and eslint by default.

Then, when generating interactive prompts, first call the getDefaultPrompts() method to obtain 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 code

After this configuration, the following prompt will pop up before the user selects the function:

Package manager

When you Vue cli create a project, a is generated vuerc file, which will record some configuration information about the project. For example, which package manager to use, whether the npm source uses Taobao source, and so on. In order to avoid conflict with Vue cli, the configuration file generated by this scaffold is mvcrc.

This mvcrc files are saved in the user's home directory (different operating system directories). My is win10 operating system, and the saving directory is C:\Users\bin. The user's home directory can be obtained through the following code:

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

The. mvcrc file also saves the configuration of the project created by the user, so that when the user re creates the project, he can directly select the previously created configuration without selecting the project function step by step.

When you first create a project The mvcrc file does not exist. If the user also installs yarn at this time, the scaffold will prompt the user which package manager to use:

// Read ` mvcrc ` file
const savedOptions = loadOptions()
// If no package manager is specified and there is a yarn
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 code

When the user selects yarn, the download dependent command will become 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 code

Switch npm source

When the user selects the project function, he will first call shouldUseTaobao() method to determine whether to switch Taobao sources:

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 code

The logic of the above code is:

  1. First determine the default configuration file Whether mvcrc has the use Taobao registry option. If yes, the result will be returned directly without judgment.
  2. Send a get request to npm default source and Taobao source respectively through promise Race(). In this way, the faster request will be returned first, so as to know whether the default source or Taobao source is faster.
  3. If Taobao source is faster, the user will be prompted whether to switch to Taobao source.
  4. If the user selects Taobao source, call await execa (command, ['config ',' set ',' registry ', registers. Taobao]) to change the current NPM source to Taobao source, that is, npm config set registry https://registry.npm.taobao.org . If it is yarn, the command is yarn config set registry https://registry.npm.taobao.org .

A little doubt

In fact, Vue cli does not have this Code:

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

I added it myself. The main reason is that I didn't find the code to explicitly register Taobao source in Vue cli. It just reads whether to use Taobao source from the configuration file, or writes the option whether to use Taobao source to the configuration file. In addition, the configuration file of npm Npmrc can change the default source, if in If the npmrc file is written directly to the image address of Taobao, npm will use the Taobao source download dependency. But npm certainly won't read it The configuration of vuerc determines whether to use Taobao source.

I don't understand this, so after the user selects Taobao source, manually call the command to register again.

Save project features as default configuration

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

Ask the user whether to save the project selection as the default configuration. If the user selects yes, the next prompt will pop up:

Let the user enter a name to save the configuration.

The relevant codes of these two sentences are:

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 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 (!(key in 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 code

The above code directly saves the user's configuration to Mvcrc file. The following is on my computer Content of mvcrc:

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

The next time you create a project, the scaffold will first read the contents of the configuration file and let the user decide whether to use the existing configuration to create the project.

So far, the content of v2 version has been introduced.

Summary

Because I haven't finished reading the source code of Vue cli plug-in, this article only explains the source code of the first two versions. The v3 version will be completed in early March after I finish reading the source code of Vue cli.

If you want to know more about front-end engineering, you can take a look at what I wrote Take you to front end Engineering . Here is the full-text catalog:

  1. Technology selection: how to carry out technology selection?
  2. Unified norms: how to formulate norms and use tools to ensure that norms are strictly implemented?
  3. What is modularization and front-end component?
  4. Testing: how to write unit tests and E2E (end-to-end) tests?
  5. Build tools: what are the build tools? What are the functions and advantages?
  6. Automated Deployment: how to use Jenkins and Github Actions to automate deployment projects?
  7. Front end monitoring: explain the principle of front-end monitoring and how to use sentry to monitor the project.
  8. Performance optimization (I): how to detect website performance? What are the practical performance optimization rules?
  9. Performance optimization (II): how to detect website performance? What are the practical performance optimization rules?
  10. Refactoring: why refactoring? What are the techniques of refactoring?
  11. Microservices: what are microservices? How to build a micro service project?
  12. Severless: what is severless? How to use severless?

Tags: cli

Posted by TVining on Thu, 14 Apr 2022 12:09:27 +0930