Teach you how to write a scaffold (2)


After three months, I finally had time to write the second article in the scaffolding series. Working in Beijing is indeed much busier than in Tianjin, and I don't have time to fish. If you haven't read the first article in this series, you to write a scaffolding , it is recommended to read this article first, and the effect will be better.

mini-cli project github address: https://github.com/woai3c/mini-cli

The v3 version of the code is in the v3 branch, and the v4 version of the code is in the v4 branch.

The third version v3

The third version mainly adds two functions:

  1. How to split the project into monorepo.
  2. Added the add command, you can add plug-ins mvc add xxx


First, let's take a brief look at monorepo and multirepo. They are all a way of project management. Multirepo means to maintain different projects in different git repositories, while monorepo maintains multiple projects in the same git repository. In the v3 version, I will transform mini-cli into a monorepo method, and maintain different plug-ins as independent projects.

After transforming the project into monorepo, the directory is as follows:

│  ├─@mvc
│  │  ├─cli # 核心插件
│  │  ├─cli-plugin-babel # babel 插件
│  │  ├─cli-plugin-linter # linter 插件
│  │  ├─cli-plugin-router # router 插件
│  │  ├─cli-plugin-vue # vue 插件
│  │  ├─cli-plugin-vuex # vuex 插件
│  │  └─cli-plugin-webpack # webpack 插件
└─scripts # commit message 验证脚本 和项目无关 不需关注

Monorepo transformation process

Install lerna globally

npm install -g lerna

Create project

git init mini-cli


cd mini-cli
lerna init

Create package

lerna create xxx

Since cli is the core scaffolding code, other plugins need to be called here, because other plugins need to be added to the dependencies of @mvc/cli

# 如果是添加到 devDependencies,则需要在后面加上 --dev
# 下载第三方依赖也是同样的命令
lerna add @mvc/cli-plugin-babel --scope=@mvc/cli

The scaffolding function after transformation into monorepo-repo is no different from the second version, except that the code related to the plug-in is separated into a separate repo, and the plug-in can be published separately to npm in the future.

Advantages of using monorepo

  1. If you use multirepo to develop, if you need to call other plug-ins during local debugging, you need to npm i before you can use it. Using monorepo, there is no such trouble, you can directly call other plug-ins in the packages directory to facilitate development and debugging.
  2. If multiple plug-ins have been modified, the modified plug-ins can be released at the same time when lerna publish

add command

The purpose of transforming the project into monorepo-repo is to facilitate subsequent expansion. For example, the generated project does not support router, and suddenly want to add router function in the middle, you can execute command mvc add router add vue-router dependency and related template code.

Let's take a look at the code of the add command first:

const path = require('path')
const inquirer = require('inquirer')
const Generator = require('./Generator')
const clearConsole = require('./utils/clearConsole')
const PackageManager = require('./PackageManager')
const getPackage = require('./utils/getPackage')
const readFiles = require('./utils/readFiles')

async function add(name) {
    const targetDir = process.cwd()
    const pkg = getPackage(targetDir)
    // 清空控制台

    let answers = {}
    try {
        const pluginPrompts = require(`@mvc/cli-plugin-${name}/prompts`)
        answers = await inquirer.prompt(pluginPrompts)
    } catch (error) {

    const generator = new Generator(pkg, targetDir, await readFiles(targetDir))
    const pm = new PackageManager(targetDir, answers.packageManager)
    require(`@mvc/cli-plugin-${name}/generator`)(generator, answers)

    await generator.generate()
    // 下载依赖
    await pm.install()

module.exports = add

Since the v3 version is still developed locally, the relevant plug-ins are not published on npm, because the plug-ins can be directly referenced without the need to perform npm i installation. When you execute the create command to create a project in the v2 version, all interactive prompts are placed under the cli plugin, but the add command adds a single plugin, so you also need to add a prompts.js file under each plugin (if not needed, You don’t need to add), there are some statements that interact with the user. For example, when you use the add command to add a router plug-in, you will be asked whether to select the history mode.

const chalk = require('chalk')

module.exports = [
        name: 'historyMode',
        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.`,

It can be seen from the code logic of the add command that if the newly added plug-in has a prompts.js file, it will read the code and pop up the interactive statement. Otherwise, skip and download directly.

Fourth version v4

The v4 version mainly makes webpack's dev and build functions dynamic. The original scaffolding project has a build directory, which contains some configuration codes of webpack. The project generated by the v4 version of the scaffold does not have a build directory.

This function is implemented by the newly added mvc-cli-service plug-in. The generated project will have the following two script commands:

scripts: {
    serve: 'mvc-cli-service serve',
    build: 'mvc-cli-service build',

When running npm run serve , the command mvc-cli-service serve will be executed. The code for this piece is as follows:

#!/usr/bin/env node
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const devConfig = require('../lib/dev.config')
const buildConfig = require('../lib/pro.config')

const args = process.argv.slice(2)
if (args[0] === 'serve') {
    const compiler = webpack(devConfig)
    const server = new WebpackDevServer(compiler)

    server.listen(8080, '', err => {
} else if (args[0] === 'build') {
    webpack(buildConfig, (err, stats) => {
        if (err) console.log(err)
        if (stats.hasErrors()) {
            console.log(new Error('Build failed with errors.'))
} else {
    console.log('error command')

The principle is as follows ( npm scripts user guide ):

The principle of npm scripts is very simple. Whenever npm run is executed, a new Shell will be created automatically, and the specified script commands will be executed in this Shell. Therefore, as long as it is a command that can be run by Shell (usually Bash), it can be written in an npm script.

What's more special is that the shell created by npm run will add the node_modules/.bin subdirectory of the current directory to the PATH variable. After the execution is over, the PATH variable will be restored to its original shape.

The above code judges the executed command, if it is serve , a new WebpackDevServer start the development environment. If it is build , use webpack for packaging.

The webpack configuration of vue-cli is dynamic, and chainwebpack is used to dynamically add different configurations. My demo was written to death, mainly because I didn't have time, so I didn't further study it.

After publishing to npm

Download mini-cli scaffolding, in fact, only the core plug-in mvc-cli is downloaded. If this plug-in needs to reference other plug-ins, you need to install it before calling it. Therefore, some modifications need to be made create add Let's take a look at the changes to the create

answers.features.forEach(feature => {
    if (feature !== 'service') {
        pkg.devDependencies[`mvc-cli-plugin-${feature}`] = '~1.0.0'
    } else {
        pkg.devDependencies['mvc-cli-service'] = '~1.0.0'

await writeFileTree(targetDir, {
    'package.json': JSON.stringify(pkg, null, 2),

await pm.install()

// 根据用户选择的选项加载相应的模块,在 package.json 写入对应的依赖项
// 并且将对应的 template 模块渲染
answers.features.forEach(feature => {
    if (feature !== 'service') {
        require(`mvc-cli-plugin-${feature}/generator`)(generator, answers)
    } else {
        require(`mvc-cli-service/generator`)(generator, answers)

await generator.generate()

// 下载依赖
await pm.install()

The above code is the new logic. After the user selects the required plug-ins, write these plug-ins to the pkg object, then generate the package.json file, and then execute npm install install the dependencies. After installing the plug-in, read the generator directory/file code of each plug-in to generate a template or add different dependencies again. Then perform the installation again.

Publish the pits encountered

The v3 version of the plug-in has a prefix of @mvc . Since the npm package with the @ prefix will be a private package by default, it has encountered some pitfalls. It took a long time, and then I was too lazy to make it. I simply changed the prefix of all the plug-ins to the prefix mvc .


阅读 1.8k
6.4k 声望
12.5k 粉丝
0 条评论
6.4k 声望
12.5k 粉丝