1. Preparation

1.1 monorepo

  • monoRepo: all modules are managed in a single trunk branch.
  • multiRepo: Split the project into modules and create a separate Repo for each module to manage.

1.2 Lerna

  • LernaIs one manage multiplenpmModule tools, optimize the maintenance of multi-package workflow, to solve the problem of multiple packages depend on each other, and release the need to manually maintain multiple packages

1.2.1 installation

npm i lerna -g
Copy the code

1.2.2 initialization

lerna init
Copy the code
The command function
lerna bootstrap Install dependencies
lerna clean Delete node_modules for each package
lerna init Create a new Lerna library
lerna list View the list of local packages
lerna changed Displays packages that have changed since the last release tag, with the option pass list
lerna diff Git diff to display the difference in packages that have changed since the last release tag
lerna exec Execute arbitrary commands in each package directory
lerna run Execute the script commands in each package.json package
lerna add Add a package version to the dependencies of each package
lerna import The introduction of package
lerna link Link libraries that reference each other
lerna create The new package
lerna publish release

1.2.3 file

1.2.3.1 package. Json
{
  "name": "root"."private": true."devDependencies": {
    "lerna": "^ 4.0.0"}}Copy the code
1.2.3.2 lerna. Json
{
  "packages": [
    "packages/*"]."version": "0.0.0"
}
Copy the code
1.2.3.3 .gitignore
node_modules
.DS_Store
design
*.log
packages/test
dist
temp
.vuerc
.version
.versions
.changelog
Copy the code

1. Yarn workspace

  • yarn workspaceAllow us to usemonorepoTo manage the project
  • In the installationnode_modulesIt will not be installed in every subprojectnode_modulesInstead, it is installed directly under the root directory, so that each subproject can read the root directorynode_modules
  • There is only one copy of the entire project under the root directoryyarn.lock File. Subprojects will also belinknode_modulesInside, which allows us to use it directlyimportImport the corresponding project
  • yarn.lockThe files are generated automatically and completelyYarnTo deal withyarn.lockLocking the version of each dependency you install ensures that you don’t accidentally get bad dependencies
1.2.4.1 package. Json

package.json

{
  "name": "root",
  "private": true,
+ "workspaces": [
+ "packages/*"
+],"DevDependencies ": {"devDependencies": "^4.0.0"}}Copy the code
1.2.4.2 lerna. Json

lerna.json

{" packages ": [" packages / *"], "version" : "1.0.0",+ "useWorkspaces": true,
+ "npmClient": "yarn"
}
Copy the code
1.2.4.3 Adding dependencies
  • yarnpkg
  • lerna

Setting accelerated Mirroring

yarn config set registry http://registry.npm.taobao.org
npm config set registry https://registry.npm.taobao.org
Copy the code
role The command
View workspace information yarn workspaces info
Add dependencies to the root space yarn add chalk cross-spawn fs-extra –ignore-workspace-root-check
Add dependencies to a project yarn workspace create-react-app3 add commander
Delete all node_modules Lerna clean equals yarn workspaces Run clean
Install and link Yarn install equals lerna bootstrap –npm-client yarn –use-workspaces
Retrieve all node_modules yarn install –force
View the cache directory yarn cache dir
Clearing the local cache yarn cache clean

1.2.5 Creating subprojects

lerna create james-cli
lerna create james-cli-shared-utils
Copy the code
1.2.5.1 James – cli
1.2.5.1.1 package. Json

packages\james-cli\bin\package.json

{
  "name": "james-cli"."version": "0.0.0"."description": "james-cli"."keywords": [
    "james-cli"]."author": "james <[email protected]>"."homepage": "https://github.com/GolderBrother/lerna-demo#readme"."license": "MIT"."main": "bin/vue.js"."directories": {
    "lib": "lib"."test": "__tests__"
  },
  "files": [
    "lib"]."publishConfig": {
    "registry": "https://registry.npm.taobao.org/"
  },
  "repository": {
    "type": "git"."url": "git+https://github.com/GolderBrother/lerna-demo.git"
  },
  "scripts": {
    "test": "echo \"Error: run tests from root\" && exit 1"
  },
  "bugs": {
    "url": "https://github.com/GolderBrother/lerna-demo/issues"}}Copy the code
1.2.5.1.2 vue. Js

packages\james-cli\bin\vue.js

#! /usr/bin/env node
console.log('vue cli');
Copy the code
1.2.5.2 James – cli – Shared – utils
1.2.5.2.1 package. Json

packages\james-cli-shared-utils\package.json

{
  "name": "james-cli-shared-utils"."version": "0.0.0"."description": " james-cli-shared-utils"."keywords": [
    "james-cli-shared-utils"]."author": "james <[email protected]>"."homepage": "https://github.com/GolderBrother/james-cli-shared-utils#readme"."license": "MIT"."main": "index.js"."directories": {
    "lib": "lib"."test": "__tests__"
  },
  "files": [
    "lib"]."publishConfig": {
    "registry": "https://registry.npm.taobao.org/"
  },
  "repository": {
    "type": "git"."url": "git+https://github.com/GolderBrother/james-cli-shared-utils.git"
  },
  "scripts": {
    "test": "echo \"Error: run tests from root\" && exit 1"
  },
  "bugs": {
    "url": "https://github.com/GolderBrother/james-cli-shared-utils/issues"}}Copy the code
1.2.5.2.2 index. Js

packages\james-cli-shared-utils\index.js

console.log('james-cli-shared-utils');
Copy the code

1.2.6 Creating soft Links

yarn
cd packages/james-cli
npm link
npm root -g
james-cli
Copy the code

1.2.7 createThe command

{" name ":" root ", "private" : true, "workspaces:" [" packages / * "], "devDependencies" : {" lerna ":" ^ 4.0.0 "}, "scripts" : {+ "create": "node ./packages/james-cli/bin/vue.js create hello1"}}Copy the code

1.2.8 Debugging Commands

Create a debugger using vscode

.vscode/launch.json

{
    "version": "0.2.0"."configurations": [{"type": "node"."request": "launch"."name": "vue-cli"."cwd":"${workspaceFolder}"."runtimeExecutable": "npm"."runtimeArgs": [
                "run"."create"]."port":9229."autoAttachChildProcesses": true."stopOnEntry": true."skipFiles": [
                "<node_internals>/**"]]}}Copy the code

1.3 Installation Dependencies

Installing dependencies in both packages will automatically install node_modules in the root directory

npm config set registry=https://registry.npm.taobao.org
yarn config set registry https://registry.npm.taobao.org

cd packages/james-cli-shared-utils
yarn workspace james-cli-shared-utils add  chalk execa

cd packages/james-cli
yarn workspace james-cli add  james-cli-shared-utils commander inquirer execa chalk ejs globby  lodash.clonedeep fs-extra ora isbinaryfile
Copy the code

1.4 lerna vs yarn

  • Many functions are equivalent
  • yarnTo deal with dependencies,lernaUsed for initialization and publishing

1.5 commander. Js

  • Commander is a powerful command line framework that provides users with command line input and parameter parsing

The installation

npm install commander -D
Copy the code
#! /usr/bin/env node
const program = require('commander');
program
    .version(` ` James - cli 0.0.0})
    .usage('<command> [options]')

program
    .command('create <app-name>')
    .description('create a new project powered by vue-cli-service')
    .action((name) = > {
        console.log(name);
    })

program.parse(process.argv)
Copy the code
james-cli                             
Usage: james-cli <command> [options]

Options:
  -V, --version      output the version number
  -h, --help         display help for command

Commands:
  create <app-name>  create a new project powered by vue-cli-service
  help [command]     display help for commandNode 1.2.com mander. Js create helloCopy the code

1.6 Inquirer. Js

  • Inquirer is an interactive command-line tool
const inquirer = require('inquirer')
const isManualMode = answers= > answers.preset === '__manual__';
const defaultPreset = {
    useConfigFiles: false.cssPreprocessor: undefined.plugins: {
        '@vue/cli-plugin-babel': {},
        '@vue/cli-plugin-eslint': {
            config: 'base'.lintOn: ['save']}}}const presets = {
    'default': Object.assign({ vueVersion: '2' }, defaultPreset),
    '__default_vue_3__': Object.assign({ vueVersion: '3' }, defaultPreset)
}
const presetChoices = Object.entries(presets).map(([name, preset]) = > {
    let displayName = name
    if (name === 'default') {
        displayName = 'Default'
    } else if (name === '__default_vue_3__') {
        displayName = 'Default (Vue 3)'
    }
    return {
        name: `${displayName}`.value: name
    }
})
const presetPrompt = {
    name: 'preset'.type: 'list'.message: `Please pick a preset:`.choices: [
        ...presetChoices,
        {
            name: 'Manually select features'.value: '__manual__'}}]let features = [
    'vueVersion'.'babel'.'typescript'.'pwa'.'router'.'vuex'.'cssPreprocessors'.'linter'.'unit'.'e2e'
];
const featurePrompt = {
    name: 'features'.when: isManualMode,
    type: 'checkbox'.message: 'Check the features needed for your project:'.choices: features,
    pageSize: 10
}
constprompts = [ presetPrompt, featurePrompt ] ; (async function(){
 let result = await inquirer.prompt(prompts);
 console.log(result); }) ();Copy the code

1.7 execa

  • Execa is capable of calling shell and local external programs
  • It starts the child process, rightchild_process.execThe encapsulation
const execa = require('execa');

(async() = > {const {stdout} = await execa('echo'['hello']);
    console.log(stdout); }) ();Copy the code

1.8 —

  • Chalk can change the style of the console string, including font style, color, and background color
const chalk = require('chalk');
console.log(chalk.blue('Hello world! '));
Copy the code

1.9 ejs

  • Ejs is an efficient embedded JavaScript template engine
  • slashwillWindowsThe backslash path of the system is converted to a slash path, such asfoo\\barfoo/bar
  • Globby is used to pattern match directory files

1.9.1 main. Js

template/main.js

<%_ if (rootOptions.vueVersion === '3') { _%> import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app') <%_ } else { _%> import Vue from 'vue' import App from './App.vue' Vue.config.productionTip  = false new Vue({ render: h => h(App), }).$mount('#app') <%_ } _%>Copy the code

1.9.2 components

doc\template\components

<template>
  <h1>HelloWorld</h1>
</template>

<script>
export default {
  name: 'HelloWorld'
}
</script>
Copy the code

1.9.3 ejs. Js

Doc / 1.7 ejs. Js

const path = require('path');
const fs = require('fs');
const ejs = require('ejs');
const globby = require('globby')
const slash = require('slash')
let source = path.join(__dirname, 'template'); ; (async function () {
    const _files = await globby(['* * / *'] and {cwd: source })
    let files = {};
    for (const rawPath of _files) {
        const sourcePath = slash(path.resolve(source, rawPath))
        const template = fs.readFileSync(sourcePath, 'utf8')
        const content = ejs.render(template, {
            rootOptions: { vueVersion: '2' }
        })
        files[sourcePath] = content;
    }
    console.log(files); }) ();Copy the code

1.10 isbinaryfile

  • Isbinaryfile checks whether a file is a binaryfile
const path = require('path');
const { isBinaryFileSync } = require('isbinaryfile');
let logo = path.join(__dirname,'template/assets/logo.png');
let isBinary = isBinaryFileSync(logo);
console.log(isBinary);
let main = path.join(__dirname,'template/main.js');
isBinary = isBinaryFileSync(main);
console.log(isBinary);
Copy the code

1.11 ora

  • oraIt is mainly used to implement the Node.js command line environmentloadingEffects, and ICONS that display various states, etc
const ora = require('ora')
const spinner = ora()

exports.logWithSpinner = (msg) = > {
    spinner.text = msg
    spinner.start();
}

exports.stopSpinner = () = > {
    spinner.stop();
}

exports.logWithSpinner('npm install');
setTimeout(() = >{
    exports.stopSpinner();
},3000);
Copy the code

2. Core concepts

  • @vue/ CLI is a complete system for rapid development based on vue.js

2.1 plug-in

  • The plug-in
  • The Vue CLI uses a plug-in based architecture. If you look at a newly created projectpackage.json, you will find that dependencies are based on@vue/cli-plugin- At the beginning of. Plugins can be modifiedwebpackInternal configuration is also available tovue-cli-serviceInjection command. During project creation, most of the features listed were implemented through plug-ins
  • Each CLI plug-in contains a generator (for creating files) and a generator (for tuningwebpackRuntime plug-in for core configuration and injection commands
  • Official plug-in format@vue/cli-plugin-eslint, community pluginvue-cli-plugin-apollo, the specified scope uses a third-party plug-in@foo/vue-cli-plugin-bar

2.2 the preset

  • aVue CLI presetIs a JSON object that contains predefined options and plug-ins needed to create a new project, eliminating the need for users to select them in a command prompt
  • invue createStored in the processpresetIt’s gonna be on yourhomeDirectory in a configuration file (~/.vuerc). You can adjust, add, or delete saved files by editing them directlypreset
  • PresetThe data is used by the plug-in generator to generate the corresponding project file
exports.defaultPreset = {
  useConfigFiles: false.cssPreprocessor: undefined.plugins: {
    '@vue/cli-plugin-babel': {},
    '@vue/cli-plugin-eslint': {
      config: 'base'.lintOn: ['save']}}}Copy the code

2.3 features

  • In manual mode, we are free to choose from the following features
    • vueVersion
    • babel
    • typescript
    • pwa
    • router
    • vuex
    • cssPreprocessors
    • linter
    • unit
    • e2e
  • Selecting different features adds different plug-ins, which generate different files and modify the configuration of the project

2.4 the create

Let’s take a look at the create process

3. Parameter analysis

3.1 the vue. Js

packages/james-cli/bin/vue.js

#! /usr/bin/env node
const program = require('commander');
program
    .version(`@vue/james-cli The ${require('.. /package').version}`)
    .usage('<command> [options]')

program
    .command('create <app-name>')
    .description('create a new project powered by vue-cli-service')
    .action((name) = > {
        require('.. /lib/create')(name)
    })

program.parse(process.argv)
Copy the code

3.2 create. Js

packages\james-cli\lib\create.js

const path = require('path');
async function create(projectName, options) {
    const cwd = process.cwd();
    const name = projectName;
    const targetDir = path.resolve(cwd, projectName);
    console.log(name);
    console.log(targetDir);
}

module.exports = (. args) = > {
    returncreate(... args).catch(err= > console.log(err));
}
Copy the code

4. Get the preset

4.1 create. Js

packages/james-cli/lib/create.js

const path = require('path');
+const Creator = require('./Creator');
+const { getPromptModules } = require('./util/createTools')
async function create(projectName) {
  const cwd = process.cwd();
  const name = projectName;
  const targetDir = path.resolve(cwd, projectName);
+ const promptModules = getPromptModules();
+ const creator = new Creator(name, targetDir,promptModules);
+ await creator.create();} module.exports = (... args) => { return create(... args).catch(err => console.log(err)); }Copy the code

4.2 the options. Js

packages/james-cli/lib/options.js

exports.defaultPreset = {
  useConfigFiles: false.cssPreprocessor: undefined.plugins: {
    '@vue/cli-plugin-babel': {},
    '@vue/cli-plugin-eslint': {
      config: 'base'.lintOn: ['save']}}}exports.defaults = {
  presets: {
    'default': Object.assign({ vueVersion: '2' }, exports.defaultPreset),
    '__default_vue_3__': Object.assign({ vueVersion: '3' }, exports.defaultPreset)
  }
}
Copy the code

4.3 PromptModuleAPI. Js

packages/james-cli/lib/PromptModuleAPI.js

class PromptModuleAPI {
  constructor(creator) {
    this.creator = creator;
  }
  injectFeature(feature) {
    this.creator.featurePrompt.choices.push(feature);
  }

  injectPrompt(prompt) {
    this.creator.injectedPrompts.push(prompt);
  }

  onPromptComplete(cb) {
    this.creator.promptCompleteCbs.push(cb); }}module.exports = PromptModuleAPI;

Copy the code

4.4 createTools. Js

packages/james-cli/lib/util/createTools.js

const getPromptModules = () = > {
  const files = ['vueVersion'];
  return files.map((file) = > require(`.. /promptModules/${file}`));
};
module.exports = {
  getPromptModules,
};

Copy the code

4.5 vueVersion. Js

packages/james-cli/lib/promptModules/vueVersion.js

module.exports = (cli) = > {
  InjectFeature is a featurePrompt that initializes projects using Babel, typescript, pwa, etc
  cli.injectFeature({
    name: 'Choose Vue version'.value: 'vueVersion'.description: 'Choose a version of Vue.js that you want to start the project with'.checked: true});InjectPrompt is based on feature prompts selected and then injected with corresponding prompts. When unit is selected, the following prompt will follow. Select Mocha + Chai or Jest
  cli.injectPrompt({
    name: 'vueVersion'.when: (answers) = > answers.features.includes('vueVersion'),
    message: 'Choose a version of Vue.js that you want to start the project with'.type: 'list'.choices: [{name: '2.x'.value: '2'}, {name: '3.x'.value: '3'],},default: '2'});If mocha is selected, @vue/cli-plugin-unit-mocha is added. //cli.onPromptComplete is a callback
  cli.onPromptComplete((answers, options) = > {
    if(answers.vueVersion) { options.vueVersion = answers.vueVersion; }}); };Copy the code

4.6 Creator. Js

packages/james-cli/lib/Creator.js

const { defaults } = require('./options');
const PromptModuleAPI = require('./PromptModuleAPI');
const inquirer = require('inquirer');
const isManualMode = (answers) = > answers.preset === '__manual__';
class Creator {
  constructor(name, context, promptModules) {
    this.name = name;
    this.context = process.env.VUE_CLI_CONTEXT = context;
    const { presetPrompt, featurePrompt } = this.resolveIntroPrompts();
    this.presetPrompt = presetPrompt;
    this.featurePrompt = featurePrompt;
    this.injectedPrompts = [];
    this.promptCompleteCbs = [];
    const promptAPI = new PromptModuleAPI(this);
    promptModules.forEach((m) = > m(promptAPI));
  }
  async create() {
    let preset = await this.promptAndResolvePreset();
    console.log('preset', preset);
  }
  resolveFinalPrompts() {
    this.injectedPrompts.forEach((prompt) = > {
      const originalWhen = prompt.when || (() = > true);
      prompt.when = (answers) = > {
        return isManualMode(answers) && originalWhen(answers);
      };
    });
    const prompts = [this.presetPrompt, this.featurePrompt, ... this.injectedPrompts];return prompts;
  }
  async promptAndResolvePreset(answers = null) {
    if(! answers) { answers =await inquirer.prompt(this.resolveFinalPrompts());
    }
    let preset;
    if(answers.preset && answers.preset ! = ='__manual__') {
      preset = await this.resolvePreset(answers.preset);
    } else {
      preset = {
        plugins: {}}; answers.features = answers.features || [];this.promptCompleteCbs.forEach((cb) = > cb(answers, preset));
    }
    return preset;
  }
  async resolvePreset(name) {
    const savedPresets = this.getPresets();
    return savedPresets[name];
  }
  getPresets() {
    return Object.assign({}, defaults.presets);
  }
  resolveIntroPrompts() {
    const presets = this.getPresets();
    const presetChoices = Object.entries(presets).map(([name]) = > {
      let displayName = name;
      if (name === 'default') {
        displayName = 'Default';
      } else if (name === '__default_vue_3__') {
        displayName = 'Default (Vue 3)';
      }
      return {
        name: `${displayName}`.value: name,
      };
    });
    const presetPrompt = {
      name: 'preset'.type: 'list'.message: `Please pick a preset:`.choices: [
        ...presetChoices,
        {
          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, }; }}module.exports = Creator;
Copy the code

5. Write package. Json

5.1 cli – Shared – utils \ index. Js

packages/james-cli-shared-utils/index.js

exports.chalk = require('chalk')
Copy the code

5.2 Creator. Js

packages/james-cli/lib/Creator.js

const { defaults } = require('./options');
const PromptModuleAPI = require('./PromptModuleAPI');
const inquirer = require('inquirer')
+const cloneDeep = require('lodash.clonedeep')
+const writeFileTree = require('./util/writeFileTree')
+const { chalk } = require('james-cli-shared-utils')
const isManualMode = answers => answers.preset === '__manual__'
class Creator {
    constructor(name, context, promptModules) {
        this.name = name;
        this.context = process.env.VUE_CLI_CONTEXT = context;
        const { presetPrompt, featurePrompt } = this.resolveIntroPrompts();
        this.presetPrompt = presetPrompt;
        this.featurePrompt = featurePrompt;
        this.injectedPrompts = []
        this.promptCompleteCbs = []
        const promptAPI = new PromptModuleAPI(this)
        promptModules.forEach(m => m(promptAPI))
    }
    async create() {
+ const {name,context} = this;
        let preset = await this.promptAndResolvePreset()
        console.log('preset', preset);
+ preset = cloneDeep(preset);
+ preset.plugins['@vue/cli-service'] = Object.assign({projectName: name}, preset);
+ console.log(' ✨ Creating project in ${chalk. Yellow (context)}. ')
+ const pkg = {
+ name,
+ version: '0.1.0 from',
+ private: true,
+ devDependencies: {}
+}
+ const deps = Object.keys(preset.plugins)
+ deps.forEach(dep => {
+ pkg.devDependencies[dep] = 'latest';
+})
+ await writeFileTree(context, {
+ 'package.json': JSON.stringify(pkg, null, 2)
+})} resolveFinalPrompts() { this.injectedPrompts.forEach(prompt => { const originalWhen = prompt.when || (() => true) prompt.when = answers => { return isManualMode(answers) && originalWhen(answers) } }) const prompts = [ this.presetPrompt, this.featurePrompt, ...this.injectedPrompts, ] return prompts } async promptAndResolvePreset(answers = null) { if (! answers) { answers = await inquirer.prompt(this.resolveFinalPrompts()) } let preset; if (answers.preset && answers.preset ! == '__manual__') { preset = await this.resolvePreset(answers.preset) } else { preset = { plugins: {} } answers.features = answers.features || [] this.promptCompleteCbs.forEach(cb => cb(answers, preset)) } return preset } async resolvePreset (name) { const savedPresets = this.getPresets() return savedPresets[name]; } getPresets() { return Object.assign({}, defaults.presets) } resolveIntroPrompts() { const presets = this.getPresets() const presetChoices = Object.entries(presets).map(([name]) => { let displayName = name if (name=== 'default') {
                displayName = 'Default'
            } else if (name === '__default_vue_3__') {
                displayName = 'Default (Vue 3)'
            }
            return {
                name: `${displayName}`,
                value: name
            }
        })
        const presetPrompt = {
            name: 'preset',
            type: 'list',
            message: `Please pick a preset:`,
            choices: [
                ...presetChoices,
                {
                    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
        }
    }
}


module.exports = Creator;
Copy the code

5.3 writeFileTree. Js

packages\james-cli\lib\util\writeFileTree.js

const fs = require('fs-extra');
const path = require('path');
async function writeFileTree(dir, files) {
  Object.entries(files).forEach(([filename, value]) = > {
    const filePath = path.join(dir, filename);
    // Make sure the directory exists. If a directory structure does not exist, create one
    fs.ensureDirSync(path.dirname(filePath));
    fs.writeFileSync(filePath, value);
  });
}
module.exports = writeFileTree;
Copy the code

6. Install dependencies

6.1 Creator. Js

packages/james-cli/lib/Creator.js

const { defaults } = require('./options');
const PromptModuleAPI = require('./PromptModuleAPI');
const inquirer = require('inquirer')
const cloneDeep = require('lodash.clonedeep')
const writeFileTree = require('./util/writeFileTree')
+const { chalk, execa } = require('james-cli-shared-utils')
const isManualMode = answers => answers.preset === '__manual__'
class Creator {
    constructor(name, context, promptModules) {
        this.name = name;
        this.context = process.env.VUE_CLI_CONTEXT = context;
        const { presetPrompt, featurePrompt } = this.resolveIntroPrompts();
        this.presetPrompt = presetPrompt;
        this.featurePrompt = featurePrompt;
        this.injectedPrompts = []
        this.promptCompleteCbs = []
+ this.run = this.run.bind(this)// Run the function
        const promptAPI = new PromptModuleAPI(this)
        promptModules.forEach(m => m(promptAPI))
    }
+ run(command, args) {
+ return execa(command, args, { cwd: this.context })
+}
    async create() {
+ const {name,context,run} = this;let preset = await this.promptAndResolvePreset() console.log('preset', preset); preset = cloneDeep(preset); preset.plugins['@vue/cli-service'] = Object.assign({projectName: name}, preset); Console. log(' ✨ Creating project in ${chalk. Yellow (context)}. ') const PKG = {name, version: '0.1.0', private: true, devDependencies: {} } const deps = Object.keys(preset.plugins) deps.forEach(dep => { pkg.devDependencies[dep] = 'latest'; }) await writeFileTree(context, { 'package.json': JSON.stringify(pkg, null, 2) })+ console.log(' 🗃 Initializing Git repository... `)
+ await run('git init');
+ console.log(' ⚙\u{fe0f} Installing CLI plugins. This might take a while... `)
+ await run('npm install');} resolveFinalPrompts() { this.injectedPrompts.forEach(prompt => { const originalWhen = prompt.when || (() => true) prompt.when = answers => { return isManualMode(answers) && originalWhen(answers) } }) const prompts = [ this.presetPrompt, this.featurePrompt, ...this.injectedPrompts, ] return prompts } async promptAndResolvePreset(answers = null) { if (! answers) { answers = await inquirer.prompt(this.resolveFinalPrompts()) } let preset; if (answers.preset && answers.preset ! == '__manual__') { preset = await this.resolvePreset(answers.preset) } else { preset = { plugins: {} } answers.features = answers.features || [] this.promptCompleteCbs.forEach(cb => cb(answers, preset)) } return preset } async resolvePreset (name) { const savedPresets = this.getPresets() return savedPresets[name]; } getPresets() { return Object.assign({}, defaults.presets) } resolveIntroPrompts() { const presets = this.getPresets() const presetChoices = Object.entries(presets).map(([name]) => { let displayName = name if (name=== 'default') {
                displayName = 'Default'
            } else if (name === '__default_vue_3__') {
                displayName = 'Default (Vue 3)'
            }
            return {
                name: `${displayName}`,
                value: name
            }
        })
        const presetPrompt = {
            name: 'preset',
            type: 'list',
            message: `Please pick a preset:`,
            choices: [
                ...presetChoices,
                {
                    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
        }
    }
}

module.exports = Creator;
Copy the code

7. Implement plug-in mechanism

packages/james-cli/lib/Creator.js

7.1 Creator. Js

packages/james-cli/lib/Creator.js

const { defaults } = require('./options');
const PromptModuleAPI = require('./PromptModuleAPI');
const inquirer = require('inquirer')
const cloneDeep = require('lodash.clonedeep')
const writeFileTree = require('./util/writeFileTree')
+const { chalk, execa,loadModule } = require('james-cli-shared-utils')
+const Generator = require('./Generator')
const isManualMode = answers => answers.preset === '__manual__'class Creator { constructor(name, context, promptModules) { this.name = name; this.context = process.env.VUE_CLI_CONTEXT = context; const { presetPrompt, featurePrompt } = this.resolveIntroPrompts(); this.presetPrompt = presetPrompt; this.featurePrompt = featurePrompt; PromptCompleteCbs = [] this.run = this.run.bind(this)// run the function const promptAPI = new PromptModuleAPI(this) promptModules.forEach(m => m(promptAPI)) } run(command, args) { return execa(command, args, { cwd: this.context }) } async create() { const {name,context,run} = this; let preset = await this.promptAndResolvePreset() console.log('preset', preset); preset = cloneDeep(preset); preset.plugins['@vue/cli-service'] = Object.assign({projectName: name}, preset); Console. log(' ✨ Creating project in ${chalk. Yellow (context)}. ') const PKG = {name, version: '0.1.0', private: true, devDependencies: {} } const deps = Object.keys(preset.plugins) deps.forEach(dep => { pkg.devDependencies[dep] = 'latest'; }) await writeFileTree(context, { 'package.json': Json.stringify (PKG, NULL, 2)}) console.log(' 🗃 Initializing Git repository... `) await run('git init'); Console. log(' ⚙\u{fe0f} Installing CLI plugins. This might take a while... `) await run('npm install');+ the console. The log (` 🚀 Invoking generators... `)
+ const plugins = await this.resolvePlugins(preset.plugins)
+ const generator = new Generator(context, {pkg,plugins})
+ await generator.generate();
    }
+ async resolvePlugins(rawPlugins) {
+ const plugins = []
+ for (const id of Object.keys(rawPlugins)) {
+ try{
+ const apply = loadModule(`${id}/generator`, this.context) || (() => {})
+ let options = rawPlugins[id] || {}
+ plugins.push({ id, apply, options })
+ }catch(error){
+ console.log(error);
+}
+}
+ return plugins
+}// Iterate over the plugin's generator. The plugin adds dependencies or fields to package.json via the GeneratorAPI. And through the render to add files resolveFinalPrompts () {this. InjectedPrompts. ForEach (prompt = > {const originalWhen = prompt. The when | | (()  => true) prompt.when = answers => { return isManualMode(answers) && originalWhen(answers) } }) const prompts = [ this.presetPrompt, this.featurePrompt, ...this.injectedPrompts, ] return prompts } async promptAndResolvePreset(answers = null) { if (! answers) { answers = await inquirer.prompt(this.resolveFinalPrompts()) } let preset; if (answers.preset && answers.preset ! == '__manual__') { preset = await this.resolvePreset(answers.preset) } else { preset = { plugins: {} } answers.features = answers.features || [] this.promptCompleteCbs.forEach(cb => cb(answers, preset)) } return preset } async resolvePreset (name) { const savedPresets = this.getPresets() return savedPresets[name]; } getPresets() { return Object.assign({}, defaults.presets) } resolveIntroPrompts() { const presets = this.getPresets() const presetChoices = Object.entries(presets).map(([name]) => { let displayName = name if (name=== 'default') {
                displayName = 'Default'
            } else if (name === '__default_vue_3__') {
                displayName = 'Default (Vue 3)'
            }
            return {
                name: `${displayName}`,
                value: name
            }
        })
        const presetPrompt = {
            name: 'preset',
            type: 'list',
            message: `Please pick a preset:`,
            choices: [
                ...presetChoices,
                {
                    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
        }
    }
}
module.exports = Creator;
Copy the code

7.2 cli – Shared – utils \ index. Js

packages\james-cli-shared-utils\index.js

+['pluginResolution','module'].forEach(module => {
+ Object.assign(exports, require(`./lib/${module}`))
+})
exports.chalk = require('chalk')
exports.execa = require('execa')
Copy the code

7.3 the module. Js

packages/james-cli-shared-utils/lib/module.js

const Module = require('module');
const path = require('path');
function loadModule(request, context) {
  // Load the CommonJS module
  return Module.createRequire(path.resolve(context, 'package.json'))(request);
}
module.exports = {
  loadModule,
};
Copy the code

7.4 pluginResolution. Js

packages/james-cli-shared-utils/lib/pluginResolution.js

const pluginRE = /^@vue\/cli-plugin-/;
@vue/cli-plugin-babel => Babel
const toShortPluginId = (id = ' ') = > id.replace(pluginRE, ' ');
const isPlugin = (id = ' ') = > pluginRE.test(id);
const matchesPluginId = (input, full) = > input === full;
module.exports = {
  toShortPluginId,
  isPlugin,
  matchesPluginId,
};
Copy the code

7.5 mergeDeps. Js

packages/james-cli/lib/util/mergeDeps.js

function mergeDeps(sourceDeps, depsToInject = {}) {
  const result = Object.assign({}, sourceDeps);
  Object.entries(depsToInject).forEach((depName, dep) = > {
    result[depName] = dep;
  });
  return result;
}

module.exports = mergeDeps;
Copy the code

7.6 normalizeFilePaths. Js

packages/james-cli/lib/util/normalizeFilePaths.js

const slash = require('slash');
// Convert a Windows backslash path to a slash path, such as foo\\bar specific foo/bar
function normalizeFilePaths(files = {}) {
  Object.entries(files).forEach(([filePath, file]) = > {
    const normalized = slash(filePath);
    Backslash path is converted to slash path
    if(filePath ! == normalized) { files[normalized] = file;deletefiles[filePath]; }});return files;
}

module.exports = normalizeFilePaths;
Copy the code

7.7 GeneratorAPI. Js

packages/james-cli/lib/GeneratorAPI.js

const { toShortPluginId } = require('james-cli-shared-utils');
const mergeDeps = require('./util/mergeDeps');
const { isBinaryFileSync } = require('isbinaryfile');
const isString = (val) = > typeof val === 'string';
const isObject = (val) = > val && typeof val === 'object';
const path = require('path');
const fs = require('fs');
const ejs = require('ejs');
class GeneratorAPI {
  constructor(id, generator, options, rootOptions) {
    this.id = id;
    this.generator = generator;
    this.options = options;
    this.rootOptions = rootOptions;
    this.pluginsData = generator.plugins
      .filter(({ id }) = >id ! = =`@vue/cli-service`)
      .map(({ id }) = > ({ name: toShortPluginId(id) }));
  }
  hasPlugin(id) {
    return this.generator.hasPlugin(id);
  }
  extendPackage(fields) {
    const pkg = this.generator.pkg;
    const toMerge = fields;
    for (const key in toMerge) {
      const value = toMerge[key];
			const existing = pkg[key];
      if (isObject(value) && ['dependencies'.'devDependencies'].includes(key)) {
        pkg[key] = mergeDeps(existing || {}, value);
      } else{ pkg[key] = value; }}}injectFileMiddleware(middleware) {
    this.generator.fileMiddlewares.push(middleware);
  }
  resolveData(additionalData) {
    return Object.assign(
      {
        options: this.options,
        rootOptions: this.rootOptions,
        plugins: this.pluginsData,
      },
      additionalData,
    );
  }
  render(source, additionalData) {
    const baseDir = extractCallDir();
    if (isString(source)) {
      source = path.resolve(baseDir, source);
      this.injectFileMiddleware(async (files) => {
        const data = this.resolveData(additionalData);
        const globby = require('globby');
        const _files = await globby(['* * / *'] and {cwd: source });
        for (const rawPath of _files) {
          const targetPath = rawPath
            .split('/')
            .map((filename) = > {
              if (filename.charAt(0) = = ='_' && filename.charAt(1)! = ='_') {
                return `.${filename.slice(1)}`;
              }
              return filename;
            })
            .join('/');
          const sourcePath = path.resolve(source, rawPath);
          constcontent = renderFile(sourcePath, data); files[targetPath] = content; }}); }}}function extractCallDir() {
  const obj = {};
  Error.captureStackTrace(obj);
  const callSite = obj.stack.split('\n') [3];
  const namedStackRegExp = /\s\((.*):\d+:\d+\)$/;
  let matchResult = callSite.match(namedStackRegExp);
  const fileName = matchResult[1];
  return path.dirname(fileName);
}
function renderFile(name, data) {
  if (isBinaryFileSync(name)) {
    return fs.readFileSync(name);
  }
  const template = fs.readFileSync(name, 'utf8');
  return ejs.render(template, data);
}
module.exports = GeneratorAPI;
Copy the code

7.8 the Generator. Js

packages/james-cli/lib/Generator.js

const { isPlugin, matchesPluginId } = require('james-cli-shared-utils');
const ejs = require('ejs');
const GeneratorAPI = require('./GeneratorAPI');
const writeFileTree = require('./util/writeFileTree');
class Generator {
  constructor(context, { pkg = {}, plugins = [] } = {}) {
    this.context = context;
    this.plugins = plugins;
    this.pkg = pkg;
    this.files = {};
    this.fileMiddleWares = [];
    const allPluginIds = [
      ...Object.keys(this.pkg.dependencies || {}), ... Object.keys(this.pkg.devDependencies || {}),
    ].filter(isPlugin);
    this.allPluginIds = allPluginIds;
    const cliService = plugins.find((p) = > p.id === '@vue/cli-service');
    this.rootOptions = cliService.options;
  }
  async generate() {
    await this.initPlugins();
    // Extract some configuration information from package.json into a separate file, such as postcss.config.js babel.config.js
    this.extractConfigFiles();
    // Iterate through fileMiddleware, write files to files, and insert import and rootOptions
    await this.resolveFiles();
    // console.log(this.files);
    this.sortPkg();
    this.files['package.json'] = JSON.stringify(this.pkg, null.2) + '\n';
    // Write files from memory to disk
    await writeFileTree(this.context, this.files);
  }
  sortPkg() {
    console.log('ensure package.json keys has readable order');
  }
  extractConfigFiles() {
    console.log('extractConfigFiles');
  }
  async initPlugins() {
    const { rootOptions, plugins = [] } = this;
    for (const plugin of plugins) {
      const { id, apply, options } = plugin;
      const api = new GeneratorAPI(id, apply, options, rootOptions);
      awaitapply(api, options, rootOptions); }}// Parse the file
  async resolveFiles() {
    const files = this.files;
    for (const fileMiddleWare of this.fileMiddleWares) {
      await fileMiddleWare(files, ejs.render);
    }
    normalizeFilePaths(files);
  }
  hasPlugin(id) {
    const pluginIds = [...this.plugins.map((plugin) = >plugin.id), ... this.allPluginIds];return pluginIds.some((_id) = > matchesPluginId(id, _id));
  }
  printExitLogs() {
    console.log('printExitLogs'); }}module.exports = Generator;
Copy the code

8. Run the create command

8.1 Creator. Js

packages/james-cli/lib/Creator.js

const { defaults } = require('./options');
const PromptModuleAPI = require('./PromptModuleAPI');
const inquirer = require('inquirer')
const cloneDeep = require('lodash.clonedeep')
const writeFileTree = require('./util/writeFileTree')
const { chalk, execa,loadModule } = require('james-cli-shared-utils')
const Generator = require('./Generator')
const isManualMode = answers => answers.preset === '__manual__'class Creator { constructor(name, context, promptModules) { this.name = name; this.context = process.env.VUE_CLI_CONTEXT = context; const { presetPrompt, featurePrompt } = this.resolveIntroPrompts(); this.presetPrompt = presetPrompt; this.featurePrompt = featurePrompt; PromptCompleteCbs = [] this.run = this.run.bind(this)// run the function const promptAPI = new PromptModuleAPI(this) promptModules.forEach(m => m(promptAPI)) } run(command, args) { return execa(command, args, { cwd: this.context }) } async create() { const {name,context,run} = this; let preset = await this.promptAndResolvePreset() console.log('preset', preset); preset = cloneDeep(preset); preset.plugins['@vue/cli-service'] = Object.assign({projectName: name}, preset); Console. log(' ✨ Creating project in ${chalk. Yellow (context)}. ') const PKG = {name, version: '0.1.0', private: true, devDependencies: {} } const deps = Object.keys(preset.plugins) deps.forEach(dep => { pkg.devDependencies[dep] = 'latest'; }) await writeFileTree(context, { 'package.json': Json.stringify (PKG, NULL, 2)}) console.log(' 🗃 Initializing Git repository... `) await run('git init'); Console. log(' ⚙\u{fe0f} Installing CLI plugins. This might take a while... `) await run('npm install'); The console. The log (` 🚀 Invoking generators... `) const plugins = await this.resolvePlugins(preset.plugins) const generator = new Generator(context, {pkg,plugins}) await generator.generate();+ console.log(' 📦 Installing Additional dependencies... `)
+ await run('npm install');
+ the console. The log (' 📄 Generating README. Md... ');
+ await writeFileTree(context, {
+ 'README.md': `cd ${name}\n npm run serve`
+});
+ await run('git', ['add', '-A']);
+ await run('git', ['commit', '-m', 'created', '--no-verify']);
+ console.log(' 🎉 ${chalk. Green ('Successfully created project')} ${chalk. Yellow (name)} ');
+ console.log(
+ '👉 Get started with the following commands:\n\n' +
+ (chalk.cyan(`cd ${name}\n`)) +
+ (chalk.cyan(`npm run serve`))
+);
+ generator.printExitLogs();} // Iterate over the generator of the plug-in, which adds dependencies or fields to package.json via the GeneratorAPI, Async resolvePlugins(rawPlugins) {const plugins = [] for (const id of object.keys (rawPlugins)) {try{ const apply = loadModule(`${id}/generator`, this.context) || (() => {}) let options = rawPlugins[id] || {} plugins.push({ id, apply, options }) }catch(error){ console.log(error); } } return plugins } resolveFinalPrompts() { this.injectedPrompts.forEach(prompt => { const originalWhen = prompt.when || (() => true) prompt.when = answers => { return isManualMode(answers) && originalWhen(answers) } }) const prompts = [ this.presetPrompt, this.featurePrompt, ...this.injectedPrompts, ] return prompts } async promptAndResolvePreset(answers = null) { if (! answers) { answers = await inquirer.prompt(this.resolveFinalPrompts()) } let preset; if (answers.preset && answers.preset ! == '__manual__') { preset = await this.resolvePreset(answers.preset) } else { preset = { plugins: {} } answers.features = answers.features || [] this.promptCompleteCbs.forEach(cb => cb(answers, preset)) } return preset } async resolvePreset (name) { const savedPresets = this.getPresets() return savedPresets[name]; } getPresets() { return Object.assign({}, defaults.presets) } resolveIntroPrompts() { const presets = this.getPresets() const presetChoices = Object.entries(presets).map(([name]) => { let displayName = name if (name=== 'default') {
                displayName = 'Default'
            } else if (name === '__default_vue_3__') {
                displayName = 'Default (Vue 3)'
            }
            return {
                name: `${displayName}`,
                value: name
            }
        })
        const presetPrompt = {
            name: 'preset',
            type: 'list',
            message: `Please pick a preset:`,
            choices: [
                ...presetChoices,
                {
                    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
        }
    }
}


module.exports = Creator;
Copy the code

8.2 the Generator. Js

packages/james-cli/lib/Generator.js

const { isPlugin,matchesPluginId } = require('james-cli-shared-utils')
const GeneratorAPI = require('./GeneratorAPI')
const normalizeFilePaths = require('./util/normalizeFilePaths')
const writeFileTree = require('./util/writeFileTree')
const ejs = require('ejs')
class Generator {
    constructor(context, { pkg = {}, plugins = [] } = {}) {
    this.context = context;
    this.plugins = plugins;
    this.pkg = pkg;
    this.files = {};
    this.fileMiddleWares = [];
    const allPluginIds = [
      ...Object.keys(this.pkg.dependencies || {}),
      ...Object.keys(this.pkg.devDependencies || {}),
    ].filter(isPlugin);
    this.allPluginIds = allPluginIds;
    const cliService = plugins.find((p) => p.id === '@vue/cli-service');this.rootOptions = cliService.options; } async generate() { await this.initPlugins(); // Extract some configuration information from package.json into a separate file, such as postcss.config.js babel.config.js this.ExtractConfigfiles (); // Iterate over fileMiddleware, write files to files, and insert import and rootOptions await this.resolveFiles(); // console.log(this.files); this.sortPkg(); this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'; // Write the file in memory to disk await writeFileTree(this.context, this.files); } sortPkg() { console.log('ensure package.json keys has readable order'); } extractConfigFiles() { console.log('extractConfigFiles'); } async initPlugins() { const { rootOptions, plugins = [] } = this; for (const plugin of plugins) { const { id, apply, options } = plugin; const api = new GeneratorAPI(id, apply, options, rootOptions); await apply(api, options, rootOptions); } // async resolveFiles() {const files = this.files; for (const fileMiddleWare of this.fileMiddleWares) { await fileMiddleWare(files, ejs.render); } normalizeFilePaths(files); } hasPlugin(id) { const pluginIds = [...this.plugins.map((plugin) => plugin.id), ...this.allPluginIds]; return pluginIds.some((_id) => matchesPluginId(id, _id)); }+ printExitLogs(){
+ console.log('printExitLogs');
+}
}

module.exports = Generator;
Copy the code