Author: Yue Yong

introduce

In scaffolding starter benefits our awareness of the role of scaffolding and develop a scaffold of basic routines, so in the scaffold premium I will together with you to explore the vue official scaffolding vue – cli to realize the core of the main logic, so that we in the premium version of scaffolding to achieve mastery through a comprehensive study to realize our own want. Ok, let’s first look at the scaffold advanced directory tree.

Advanced project directory tree

Coo - cli2 ├ ─ README. Md ├ ─ index, js// Import file├ ─ package. Json ├ ─ lib | ├ ─ utils | | ├ ─ executeCommand. Js | | ├ ─ normalizeFilePaths. Js// Slash escape| | ├ ─ waitFnLoading. Js | | ├ ─ writeFileTree. Js// Write to the file| | ├ ─ codemods/ / AST injection| | | ├ ─ injectImports. Js | | | └ injectOptions. Js | ├ ─ promptModules// Wizard prompts for each module| | ├ ─ Babel. Js | | ├ ─ linter. Js | | ├ ─ the router. The js | | └ vuex. Js | ├ ─ linkConfig | | └ vue - repo. Js | ├ ─ the generator// The functional template for the project is currently being generated| | ├ ─ webpack | | ├ ─ vuex | | ├ ─ vue | | ├ ─ the router | | ├ ─ linter | | ├ ─ Babel | ├ ─ core | | ├ ─ Creator. Js// Create an interactive prompt class function| | ├ ─ the Generator. Js// The project generates the constructor| | ├ ─ PromptModuleAPI. Js// Interactive prompts to inject class functions| | ├ ─ the create. Js | | ├ ─ help. Js | | ├ ─ actions | | | ├ ─ the createProject. Js | | | ├ ─ index. The js | | | └ initTemplate. Js// Initial template script
Copy the code

Coo init my-project command (advanced version)

  • Interactive prompts for multiple functional modules
  • Inject dependencies as needed
  • Template file rendering
  • AST parsing generation
  • Download the dependent
  • Start the project

Let’s take a look at each feature in the order in which it is implemented. It is recommended to read this article together with the project source code, the effect is better. Cli-learn, current project branch v2

registeredinitThe command

const program = require('commander')
const { createProjectAction, initTemplateAction } = require('./actions')

program
  .command('init 
      
        [others...] '
      )
  .description('copy a mini cli just like vue-cli')
  .action(initTemplateAction)
Copy the code

The init command will be registered in /lib/core/create.js. For details on how to implement command line parsing, see scaffolding Primer. In the above code, we can see that the main behavior logic implemented by the init command is in the initTemplateAction function, so we’ll focus on what this function does.

Function module interaction

In scaffolding Starter we also implemented a simple user interaction that provided the user with a selection wizard for the package management tool (YARN or NPM), and we learned that this interaction was mainly implemented using the Inquirer library. So in the scaffolding advanced version of learning, we still use this library to provide users with interactive wizard of functional modules. Different from the simple interaction implemented in the scaffolding starter version, the Scaffolding Advanced version provides vuex, Router, Linter and Babel function modules for users to choose from. Therefore, the interactive prompts of each module are grouped into the /lib/promptModules folder as follows:

| ├ ─ promptModules// Wizard prompts for each module| | ├ ─ Babel. Js | | ├ ─ linter. Js | | ├ ─ the router. The js | | └ vuex. JsCopy the code

This part focuses on how to display multiple interactive prompts in /lib/promptModules for users to select. Location: / lib/core/actions/initTemplate. Js

const path = require('path')
const inquirer = require('inquirer')
// Create the interactive prompt constructor class
const Creator = require('.. /Creator')
// Interactive prompts to inject the constructor class
const PromptModuleAPI = require('.. /PromptModuleAPI')
const waitFnLoading = require('.. /.. /utils/waitFnLoading')

// Initialize the vue template
const initTemplate = async (project) => {
  const creator = new Creator()
  const promptModules = getPromptModules()
  const promptAPI = new PromptModuleAPI(creator)
  promptModules.forEach(m= > m(promptAPI))

  // console.dir(creator. GetFinalPrompts (), 'have taken all interactive prompts of the module ');

  // Provide a question wizard for the user
  const answers = await inquirer.prompt(creator.getFinalPrompts())
}

// Import modules in batches
function getPromptModules () {
  return [
    'babel'.'router'.'vuex'.'linter',
  ].map(file= > require(`.. /.. /promptModules/${file}`))}module.exports = initTemplate
Copy the code

For the code above, we’ll focus on the Creator class, the PromptModuleAPI class, and the various files in the /lib/promptModules directory.

Creatorclass

Location: / lib/core/Creator. Js

class Creator {
  constructor() {
    this.featurePrompt = {
      name: 'features'.message: 'Check the features needed for your project:'.pageSize: 10.type: 'checkbox'.choices: [],}this.injectedPrompts = []
  }

  // Get all interactive prompts
  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
Copy the code

For a brief introduction to the implementation of this class, initialize the featurePrompt object, which is the interaction parameter to be passed to the iquirer. Prompt method in the future. Type is checkbox, indicating that this is a check item. Prompt injectedPrompts. Before I tell you what this array can do, I should add that one other feature of the Inquirer library is that it can also provide a relevant question. An 🌰 :

  {
    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

One of the second question object when attribute, the attribute value is a function answers = > answers. The features. Includes (‘ the router). The second question pops up if the result of the function’s execution is true. If you select the Router in the previous question, the result will be true. The second question pops up: Use history mode for router? . . Ok, now that you know that, you can use the empty array injectedPrompts to take a few prompts that have a relevant question (with the WHEN attribute). The last thing the Creator class does is traverse the array injectedPrompts using the getFinalPrompts method, which returns a merge of the relevant and general prompts that have been hit.

PromptModuleAPIclass

Location: / lib/core/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 PromptModuleAPI class is implemented simply by receiving an assignment to the Creator parameter, which in this case is an instantiation of the Creator class we just mentioned. This class internally registers two methods: injectFeature for pushing common prompts and injectPrompt for pushing dependent prompts.

/lib/promptModulesThe files under the

As mentioned above, the files under /lib/promptModules are mainly interactive prompts of functional modules, as follows:

| ├ ─ promptModules// Wizard prompts for each module| | ├ ─ Babel. Js | | ├ ─ linter. Js// eslint| | ├ ─ the router. Js | | └ vuex. JsCopy the code

Here’s an example of a router.js that has both normal and dependent prompts:

const chalk = require('chalk')

module.exports = (api) = > {
  api.injectFeature({
    name: 'Router'.value: 'router'.description: 'Structure the app with dynamic pages'.link: 'https://router.vuejs.org/',
  })

  api.injectPrompt({
    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

Here we export a function, return the array function wrapped in the array’s map method in the getPromptModules method, and iterate over it. Each of the iterated items, m, is the exported function, which takes an instantiation of the PromptModuleAPI class, The export function parameters of the API for new PromptModuleAPI (), the core code is as follows: location: / lib/core/actions/initTemplate. Js

// Create the interactive prompt constructor class
const Creator = require('.. /Creator')
// Interactive prompts to inject the constructor class
const PromptModuleAPI = require('.. /PromptModuleAPI')

// Initialize the vue template
const initTemplate = async (project) => {
  const creator = new Creator()
  const promptModules = getPromptModules()
  const promptAPI = new PromptModuleAPI(creator)
  promptModules.forEach(m= > m(promptAPI))
}

// Import modules in batches
function getPromptModules () {
  return [
    'babel'.'router'.'vuex'.'linter',
  ].map(file= > require(`.. /.. /promptModules/${file}`))}module.exports = initTemplate
Copy the code

For the above code logic, use the flowchart to summarize:

Here,creator.getFinalPromptsThe method can be/lib/promptModulesAll module prompts under and returned as an array, asinquirer.promptMethod input parameter.

// Provide a question wizard for the user
const answers = await inquirer.prompt(creator.getFinalPrompts())
Copy the code

The effect is as follows:

All functions are selected. Answers has the value:

Dependency injection

In scaffolding Starter, we use the download-git-repo library to pull projects directly from the remote to the local as a remote URL. There is no need for on-demand dependency injection. In scaffolding advanced we also do the same with vue-CLI to inject corresponding templates and dependencies according to the user’s choice. Ok, let’s start learning how to implement template dependency injection into your project.

Dependency injection is essentially associating all of the user-selected functionality with a preset template in our project, assigning values to properties in package.json.

So first we need to define a variable PKG to represent the package.json file and set some default values.

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

The preset templates in the project are placed in /lib/generator

| ├ ─ the generator// The functional template for the project is currently being generated| | ├ ─ webpack/ / webpack template| | ├ ─ vuex/ / vuex template| | ├ ─ vue/ / the vue template| | ├ ─ the router/ / the vue - the router template| | ├ ─ linter/ / eslint template| | ├ ─ Babel/ / the Babel template
Copy the code
  • The dependency option is associated with the template

In the previous section, we learned which dependencies the user selected so that we could get the corresponding template by iterating through the dependency array Features. The following code: location: / lib/core/actions/initTemplate. Js

const Generator = require('.. /Generator')
const generator = new Generator(pkg, path.join(process.cwd(), project))

// Since this is a VUE related scaffold, the VUE template should be provided by default and does not require user selection.
// In addition, the build tool Webpack provides the development environment and packaging capabilities, which are also required and do not require user choice.
// So we need to manually add these two default dependencies to the dependency array before iterating.
answers.features.unshift('vue'.'webpack')

// Load the corresponding module according to the options selected by the user and write the corresponding dependencies in package.json
answers.features.forEach(feature= > {
  require(`.. /.. /generator/${feature}`)(generator, answers)
})
Copy the code

So we have the template association.

  • topackage.jsonAttribute assignment in

This step is done primarily by requiring (.. /.. /generator/${feature})(generator, answers) to import a function and execute it to implement attribute assignment. Take the Babel template code under /lib/generator as an example:

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, I’m exporting a function, The extendPackage() method within the template that calls the passed new Generator(PKG, path.join(process.cwd(), project)) instance injects all babel-related dependencies into the PKG variable.

// Inject all template-related dependencies into PKG variables
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

Template rendering

In this section we will focus on how scaffolding interprets render templates. The so-called templates, as mentioned above, are mainly in /lib/generator. The main logic for parsing the render template is in /lib/core/ generator.js. Take router as an example in the template. Assume that the user selected vue-Router and selected the history mode. Inject code as follows:

module.exports = (generator, options = {}) = > {
  // Import router from './router'
  generator.injectImports(generator.entryFile, `import router from './router'`)

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

  generator.extendPackage({
    dependencies: {
      'vue-router': '^ 3.5.1 track of',
    },
  })

  generator.render('./template', {
    historyMode: options.historyMode,
    hasTypeScript: false.plugins: [],})}Copy the code

As you can see, the template calls the injectImports method of the Generator object to inject code import router from ‘./router’ into the entry file SRC /main.js, Call injectRootOptions method to file the SRC/main entrance, js new Vue () into the options for the router, due to the nature of these two methods to do all the same, is to add an object attribute values, so we take injectImports method is simple and have a look at here. The following code:

/** * Add import statements to a file. * * injectImports(file: `src/main.js`, imports: `import store from './store'`) * * imports: { * './src/main.js': Set{0: `import store from './store'`} * } */
injectImports (file, imports) {
  const _imports = (
    this.imports[file]
    || (this.imports[file] = new Set())); (Array.isArray(imports) ? imports : [imports]).forEach(imp= > {
    _imports.add(imp)
  })
}
Copy the code

Let’s look at what generator.render(‘./template’, {}) does:

  1. useglobbyParse read template render path./templateAll files under
const _files = await globby(['* * / *'] and {cwd: source, dot: true })
/** Router template * _files: [* 'SRC/app.vue ', *' SRC /router/index.js', 'SRC /views/ about.vue ',' SRC /views/ home.png '*] */
Copy the code
  1. Iterate over all read files. If it is a binary file, read the file content and return. Otherwise, read the contents of the file first and call it laterejsTo render
for (const rawPath of _files) {
  const sourcePath = path.resolve(source, rawPath)
  / / sourcePath: / Users/Erwin/Desktop/scaffold/coo - cli2 / lib/generator/router/template / *
  // Parse the file contents
  const content = this.renderFile(sourcePath, data, ejsOptions)
  // only set file if it's not all whitespace, or is a Buffer (binary files)
  if (Buffer.isBuffer(content) || /[^\s]/.test(content)) {
    files[rawPath] = content
  }
}

renderFile (pathName, data, ejsOptions) {
  // If it is a binary file, the result will be read directly back
  if (isBinaryFileSync(pathName)) {
    return fs.readFileSync(pathName) // return buffer
  }

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

Ejs allows you to use variables to determine whether or not to render code, such as index.js in the router template:

const router = new VueRouter({
  <%_ if (historyMode) { _%>
  mode: 'history',
  <%_ } _%>
  routes
})
Copy the code

Ejs then decides whether to render mode: ‘history’ based on whether the user chooses history mode. Here the historyMode is passed in by calling the Render method

generator.render('./template', {
  historyMode: options.historyMode,
  hasTypeScript: false.plugins: [],})Copy the code

The template is then rendered by calling data, the second parameter of the ejs.render(template, data, ejsOptions) method.

renderFile (pathName, data, ejsOptions) {
  // If it is a binary file, the result will be read directly back
  if (isBinaryFileSync(pathName)) {
    return fs.readFileSync(pathName) // return buffer
  }

  // Return the file contents
  const template = fs.readFileSync(pathName, 'utf-8')
  return ejs.render(template, data, ejsOptions)
}
Copy the code
  1. willejsRender good content asvalueAssigned tofilesThe object’skey
// File rendering
const content = this.renderFile(sourcePath, data, ejsOptions)
// only set file if it's not all whitespace, or is a Buffer (binary files)
if (Buffer.isBuffer(content) || /[^\s]/.test(content)) {
  files[rawPath] = content
}
Copy the code

ASTParsing generated

await generator.generate()

// Generate template file
async generate () {
  // Parse the file contents
  await this.resolveFiles()

  this.files['package.json'] = JSON.stringify(this.pkg, null.2) + '\n'
  // Write all files to the directory that the user wants to create
  // context: /Users/ Erwin /Desktop/ scaffolding /my-project
  await writeFileTree(this.context, this.files)
}
Copy the code

/ SRC /main.js/vue/webpack/Babel/linter/router/vuex / SRC /main.js; / SRC /main.js; / SRC /main.js The code is as follows:

// vue-codemod library, parse the code to get AST, and inject the import statement and the root option
// Handle the import of import statements
Object.keys(files).forEach(file= > {
  let imports = this.imports[file] SRC /main.js in the vue template
  imports = imports instanceof Set ? Array.from(imports) : imports
  if (imports && imports.length > 0) {
    // Inject the import statement into the SRC /main.js entry file
    files[file] = runTransformation(
      { path: file, source: files[file] }, // source: SRC /main.js
      require('.. /utils/codemods/injectImports'),
      { imports }, // imports: [`import router from './router'`])}}Copy the code

The rendered template files and package.json files are written locally using the writeFileTree method. Location: / lib/core/utils/writeFileTree. Js

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. Walk through all the rendered files, generating one by one.
  2. When generating a file, verify that its parent directory is present. If not, make it the parent directory.

For example, the path of a file is SRC /main.js. When the file is written for the first time, the SRC directory is changed to the SRC directory and the main.js file is generated. 3. Write files.

Download the dependencies and start the project

// Download dependencies
await waitFnLoading(commandSpawn, 'installing packages... ') ('npm'['install'] and {cwd: `. /${project}` })

// Start the project
await commandSpawn('npm'['run'.'dev'] and {cwd: `. /${project}` })
Copy the code

This function has been realized in the scaffolding introduction version. For details, please move to the scaffolding introduction version, which is not repeated here.

Project operation Effect:

Screenshot of successful project directory:

conclusion

Scaffolding advanced version compared to the entry version, although the content is much more, but in fact the overall architecture is not very difficult, this is the vuE-CLI scaffolding implementation of the core of the main part, I hope to help you explore their work in the most commonly used VUE scaffolding implementation and development ideas. Next, in the scaffolding advanced version, I will continue to take you with vuE-CLI implementation ideas to develop some practical work in the foot finger order, to help you improve the efficiency of development ~

References:

  • Vue – cli: github.com/vuejs/vue-c…
  • Commander. Js: github.com/tj/commande…
  • NPM link: docs.npmjs.com/cli/v7/comm…
  • Inquirer. Js: github.com/SBoudrias/I…