手把手實現新版@vue/cli腳手架及其插件系統

1.準備工做

1.1 monorepo

  • monoRepo: 是將全部的模塊統一的放在一個主幹分支之中管理。
  • multiRepo: 將項目分化成爲多個模塊,並針對每個模塊單獨的開闢一個Repo來進行管理。

1.jpg

1.2 Lerna

  • Lerna是一個管理多個 npm 模塊的工具,優化維護多包的工做流,解決多個包互相依賴,且發佈須要手動維護多個包的問題

1.2.1 安裝

npm i lerna -g
複製代碼

1.2.2 初始化

lerna init
複製代碼
命令 功能
lerna bootstrap 安裝依賴
lerna clean 刪除各個包下的node_modules
lerna init 建立新的lerna庫
lerna list 查看本地包列表
lerna changed 顯示自上次release tag以來有修改的包, 選項通 list
lerna diff 顯示自上次release tag以來有修改的包的差別, 執行 git diff
lerna exec 在每一個包目錄下執行任意命令
lerna run 執行每一個包package.json中的腳本命令
lerna add 添加一個包的版本爲各個包的依賴
lerna import 引入package
lerna link 連接互相引用的庫
lerna create 新建package
lerna publish 發佈

1.2.3 文件

1.2.3.1 package.json
{
  "name": "root",
  "private": true,
  "devDependencies": {
    "lerna": "^4.0.0"
  }
}
複製代碼
1.2.3.2 lerna.json
{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0"
}
複製代碼
1.2.3.3 .gitignore
node_modules
.DS_Store
design
*.log
packages/test
dist
temp
.vuerc
.version
.versions
.changelog
複製代碼

1.2.4 yarn workspace

  • yarn workspace 容許咱們使用 monorepo 的形式來管理項目
  • 在安裝 node_modules 的時候它不會安裝到每一個子項目的 node_modules 裏面,而是直接安裝到根目錄下面,這樣每一個子項目均可以讀取到根目錄的 node_modules
  • 整個項目只有根目錄下面會有一份 yarn.lock 文件。子項目也會被 linknode_modules 裏面,這樣就容許咱們就能夠直接用 import 導入對應的項目
  • yarn.lock 文件是自動生成的,也徹底 Yarn 來處理 yarn.lock 鎖定你安裝的每一個依賴項的版本,這能夠確保你不會意外得到不良依賴
1.2.4.1 package.json

package.jsoncss

{
  "name": "root",
  "private": true,
+ "workspaces": [
+ "packages/*"
+ ],
  "devDependencies": {
    "lerna": "^4.0.0"
  }
}
複製代碼
1.2.4.2 lerna.json

lerna.jsonhtml

{
  "packages": [
    "packages/*"
  ],
  "version": "1.0.0",
+ "useWorkspaces": true,
+ "npmClient": "yarn"
}
複製代碼
1.2.4.3 添加依賴

設置加速鏡像vue

yarn config set registry http://registry.npm.taobao.org
npm config set registry https://registry.npm.taobao.org
複製代碼
做用 命令
查看工做空間信息 yarn workspaces info
給根空間添加依賴 yarn add chalk cross-spawn fs-extra --ignore-workspace-root-check
給某個項目添加依賴 yarn workspace create-react-app3 add commander
刪除全部的 node_modules lerna clean 等於 yarn workspaces run clean
安裝和link yarn install 等於 lerna bootstrap --npm-client yarn --use-workspaces
從新獲取全部的 node_modules yarn install --force
查看緩存目錄 yarn cache dir
清除本地緩存 yarn cache clean

1.2.5 建立子項目

lerna create james-cli
lerna create james-cli-shared-utils
複製代碼
1.2.5.1 james-cli
1.2.5.1.1 package.json

packages\james-cli\bin\package.jsonnode

{
  "name": "james-cli",
  "version": "0.0.0",
  "description": "james-cli",
  "keywords": [
    "james-cli"
  ],
  "author": "james <1204788939@qq.com>",
  "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"
  }
}

複製代碼
1.2.5.1.2 vue.js

packages\james-cli\bin\vue.jsreact

#!/usr/bin/env node
console.log('vue cli');
複製代碼
1.2.5.2 james-cli-shared-utils
1.2.5.2.1 package.json

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

{
  "name": "james-cli-shared-utils",
  "version": "0.0.0",
  "description": " james-cli-shared-utils",
  "keywords": [
    "james-cli-shared-utils"
  ],
  "author": "james <1204788939@qq.com>",
  "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"
  }
}

複製代碼
1.2.5.2.2 index.js

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

console.log('james-cli-shared-utils');
複製代碼

1.2.6 建立軟連接

yarn
cd packages/james-cli
npm link
npm root -g
james-cli
複製代碼

1.2.7 create 命令

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

1.2.8 調試命令

使用 vscode 建立一個 debugger 調試器github

.vscode/launch.jsonweb

{
    "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>/**"
            ]
        }
    ]
}
複製代碼

1.3 安裝依賴

到兩個 package 分別安裝下依賴,會自動安裝到根目錄下的 node_modulesvuex

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
複製代碼

1.4 lerna vs yarn

  • 二者不少功能是等價的
  • yarn用來處理依賴,lerna用於初始化和發佈

1.5 commander.js

  • commander 是一款強大的命令行框架,提供了用戶命令行輸入和參數解析功能

安裝

npm install commander -D
複製代碼
#!/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)
複製代碼
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 command

node 1.2.commander.js create hello  
複製代碼

1.6 Inquirer.js

  • Inquirer是一個交互式命令行工具
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
}
const prompts = [
    presetPrompt,
    featurePrompt
]

;(async function(){
 let result = await inquirer.prompt(prompts);
 console.log(result);
})();
複製代碼

1.7 execa

  • execa 是能夠調用 shell 和本地外部程序
  • 它會啓動子進程執行,是對child_process.exec的封裝
const execa = require('execa');

(async () => {
    const {stdout} = await execa('echo', ['hello']);
    console.log(stdout);
})();
複製代碼

1.8 chalk

  • chalk能夠修改控制檯字符串的樣式,包括字體樣式、顏色以及背景顏色等
const chalk = require('chalk');
console.log(chalk.blue('Hello world!'));
複製代碼

1.9 ejs

  • ejs是高效的嵌入式 JavaScript 模板引擎
  • slashWindows 系統的反斜槓路徑轉換爲斜槓路徑,如foo\\barfoo/bar
  • globby是用於模式匹配目錄文件的

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')
<%_ } _%>
複製代碼

1.9.2 components

doc\template\components

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

<script>
export default {
  name: 'HelloWorld'
}
</script>
複製代碼

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(['**/*'], { 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);
})();
複製代碼

1.10 isbinaryfile

  • isbinaryfile能夠檢測一個文件是不是二進制文件
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);
複製代碼

1.11 ora

  • ora主要用來實現node.js命令行環境的 loading 效果,和顯示各類狀態的圖標等
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);
複製代碼

2.核心概念

  • @vue/cli是一個基於 Vue.js 進行快速開發的完整系統

2.1 插件

  • 插件
  • Vue CLI 使用了一套基於插件的架構。若是你查閱一個新建立項目的 package.json,就會發現依賴都是以 @vue/cli-plugin- 開頭的。插件能夠修改 webpack 的內部配置,也能夠向 vue-cli-service 注入命令。在項目建立的過程當中,絕大部分列出的特性都是經過插件來實現的
  • 每一個 CLI 插件都會包含一個 (用來建立文件的) 生成器和一個 (用來調整 webpack 核心配置和注入命令的) 運行時插件
  • 官方插件格式@vue/cli-plugin-eslint,社區插件vue-cli-plugin-apollo,指定的 scope 使用第三方插件@foo/vue-cli-plugin-bar

2.2 預設

  • 一個 Vue CLI preset 是一個包含建立新項目所需預約義選項和插件的 JSON 對象,讓用戶無需在命令提示中選擇它們
  • vue create 過程當中保存的 preset 會被放在你的 home 目錄下的一個配置文件中 (~/.vuerc)。你能夠經過直接編輯這個文件來調整、添加、刪除保存好的 preset
  • Preset 的數據會被插件生成器用來生成相應的項目文件
exports.defaultPreset = {
  useConfigFiles: false,
  cssPreprocessor: undefined,
  plugins: {
    '@vue/cli-plugin-babel': {},
    '@vue/cli-plugin-eslint': {
      config: 'base',
      lintOn: ['save']
    }
  }
}
複製代碼

2.3 特性

  • 在手工模式下,咱們能夠自由選擇如下特性
    • vueVersion
    • babel
    • typescript
    • pwa
    • router
    • vuex
    • cssPreprocessors
    • linter
    • unit
    • e2e
  • 選擇不一樣的特性會添加不一樣的插件,不一樣的插件就會生成不一樣的文件和修改項目的配置

2.4 create

咱們來看下 create 整個的流程

2.png

3.參數解析

3.1 vue.js

packages/james-cli/bin/vue.js

#!/usr/bin/env node
const program = require('commander');
program
    .version(`@vue/james-cli ${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)
複製代碼

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) => {
    return create(...args).catch(err => console.log(err));
}
複製代碼

4.獲取預設

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));
}
複製代碼

4.2 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)
  }
}
複製代碼

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;

複製代碼

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,
};

複製代碼

4.5 vueVersion.js

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

module.exports = (cli) => {
  //cli.injectFeature 是注入 featurePrompt,即初始化項目時選擇 babel,typescript,pwa 等等
  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,
  });
  //cli.injectPrompt 是根據選擇的 featurePrompt 而後注入對應的 prompt,當選擇了 unit,接下來會有如下的 prompt,選擇 Mocha + Chai 仍是 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',
  });
  //cli.onPromptComplete 就是一個回調,會根據選擇來添加對應的插件, 當選擇了 mocha ,那麼就會添加 @vue/cli-plugin-unit-mocha 插件
  cli.onPromptComplete((answers, options) => {
    if (answers.vueVersion) {
      options.vueVersion = answers.vueVersion;
    }
  });
};

複製代碼

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;
複製代碼

5.寫入package.json

5.1 cli-shared-utils\index.js

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

exports.chalk = require('chalk')
複製代碼

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',
+ 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;
複製代碼

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);
    // 確保目錄的存在。若是目錄結構不存在,就建立一個
    fs.ensureDirSync(path.dirname(filePath));
    fs.writeFileSync(filePath, value);
  });
}
module.exports = writeFileTree;
複製代碼

6.安裝依賴

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)//運行函數
        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;
複製代碼

7.實現插件機制

packages/james-cli/lib/Creator.js

3.png

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;
        this.injectedPrompts = []
        this.promptCompleteCbs = []
        this.run = this.run.bind(this)//運行函數
        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');
+ console.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
+ }
	  // 遍歷插件的generator,插件經過GeneratorAPI向package.json中加入依賴或字段,並經過render準備添加文件
    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;
複製代碼

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')
複製代碼

7.3 module.js

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

const Module = require('module');
const path = require('path');
function loadModule(request, context) {
  // 加載 CommonJS 模塊
  return Module.createRequire(path.resolve(context, 'package.json'))(request);
}
module.exports = {
  loadModule,
};
複製代碼

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,
};
複製代碼

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;
複製代碼

7.6 normalizeFilePaths.js

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

const slash = require('slash');
// 將Windows反斜槓路徑轉換爲斜槓路徑,如foo\\bar➔ foo/bar
function normalizeFilePaths(files = {}) {
  Object.entries(files).forEach(([filePath, file]) => {
    const normalized = slash(filePath);
    // 說明反斜槓路徑轉換爲斜槓路徑了
    if (filePath !== normalized) {
      files[normalized] = file;
      delete files[filePath];
    }
  });
  return files;
}

module.exports = normalizeFilePaths;
複製代碼

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(['**/*'], { 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);
          const content = 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;
複製代碼

7.8 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();
    // 將一些配置信息從 package.json 中提取到單獨的文件中,好比 postcss.config.js babel.config.js
    this.extractConfigFiles();
    // 遍歷 fileMiddleware,向 files 裏寫入文件,並插入 import 和 rootOptions
    await this.resolveFiles();
    // console.log(this.files);
    this.sortPkg();
    this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n';
    //把內存中的文件寫入硬盤
    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;
複製代碼

8.完成create命令

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;
        this.injectedPrompts = []
        this.promptCompleteCbs = []
        this.run = this.run.bind(this)//運行函數
        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');
        console.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');
+ console.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();
    }
    //遍歷插件的generator,插件經過GeneratorAPI向package.json中加入依賴或字段,並經過render準備添加文件
    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;
複製代碼

8.2 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();
    // 將一些配置信息從 package.json 中提取到單獨的文件中,好比 postcss.config.js babel.config.js
    this.extractConfigFiles();
    // 遍歷 fileMiddleware,向 files 裏寫入文件,並插入 import 和 rootOptions
    await this.resolveFiles();
    // console.log(this.files);
    this.sortPkg();
    this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n';
    //把內存中的文件寫入硬盤
    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;
複製代碼
相關文章
相關標籤/搜索