npm i lerna -g
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 | 发布 |
{
"name": "root",
"private": true,
"devDependencies": {
"lerna": "^4.0.0"
}
}
{
"packages": [
"packages/*"
],
"version": "0.0.0"
}
node_modules
.DS_Store
design
*.log
packages/test
dist
temp
.vuerc
.version
.versions
.changelog
package.json
{
"name": "root",
"private": true,
+ "workspaces": [
+ "packages/*"
+ ],
"devDependencies": {
"lerna": "^4.0.0"
}
}
lerna.json
{
"packages": [
"packages/*"
],
"version": "1.0.0",
+ "useWorkspaces": true,
+ "npmClient": "yarn"
}
设置加速镜像
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 |
lerna create zhang-cli
lerna create zhang-cli-shared-utils
packages\zhang-cli\bin\vue.js
{
"name": "zhang-cli",
"version": "0.0.0",
"description": "zhang-cli",
"author": "zhangrenyang <zhang_renyang@126.com>",
"homepage": "",
"license": "MIT",
"main": "bin/vue.js"
}
packages\zhang-cli\bin\vue.js
#!/usr/bin/env node
console.log('vue cli');
packages\zhang-cli-shared-utils\package.json
{
"name": "zhang-cli-shared-utils",
"version": "0.0.0",
"description": "zhang-cli-shared-utils",
"author": "zhangrenyang <zhang_renyang@126.com>",
"homepage": "",
"license": "MIT",
"main": "index.js"
}
packages\zhang-cli-shared-utils\index.js
yarn
cd packages/zhang-cli
npm link
npm root -g
zhang-cli
{
"name": "root",
"private": true,
"workspaces": [
"packages/*"
],
"devDependencies": {
"lerna": "^4.0.0"
},
"scripts": {
+ "create": "node ./packages/zhang-cli/bin/vue.js create hello1"
}
}
.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>/**"
]
}
]
}
npm config set registry=https://registry.npm.taobao.org
yarn config set registry https://registry.npm.taobao.org
cd packages/zhang-cli-shared-utils
yarn workspace zhang-cli-shared-utils add chalk execa
cd packages/zhang-cli
yarn workspace zhang-cli add zhang-cli-shared-utils commander inquirer execa chalk ejs globby lodash.clonedeep fs-extra ora isbinaryfile
yarn
用来处理依赖,lerna
用于初始化和发布#!/usr/bin/env node
const program = require('commander');
program
.version(`zhang-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)
node 1.2.commander.js
Usage: 1.2.commander <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
const inquirer = require('inquirer')
const isManualMode = answers => answers.preset === '__manual__';
let defaultPreset = {
useConfigFiles: false,
cssPreprocessor: undefined,
plugins: {
'@vue/cli-plugin-babel': {},
'@vue/cli-plugin-eslint': {
config: 'base',
lintOn: ['save']
}
}
}
let 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);
})();
child_process.exec
的封装const execa = require('execa');
(async () => {
const {stdout} = await execa('echo', ['hello']);
console.log(stdout);
})();
const chalk = require('chalk');
console.log(chalk.blue('Hello world!'));
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')
<%_ } _%>
doc\template\components
<template>
<h1>HelloWorld</h1>
</template>
<script>
export default {
name: 'HelloWorld'
}
</script>
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);
})();
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);
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);
vue-cli-service
注入命令。在项目创建的过程中,绝大部分列出的特性都是通过插件来实现的@vue/cli-plugin-eslint
,社区插件vue-cli-plugin-apollo
,指定的 scope 使用第三方插件@foo/vue-cli-plugin-bar
exports.defaultPreset = {
useConfigFiles: false,
cssPreprocessor: undefined,
plugins: {
'@vue/cli-plugin-babel': {},
'@vue/cli-plugin-eslint': {
config: 'base',
lintOn: ['save']
}
}
}
packages\zhang-cli\bin\vue.js
#!/usr/bin/env node
const program = require('commander');
program
.version(`@vue/zhang-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)
packages\zhang-\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);
})
}
packages\zhang-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);
})
}
packages\zhang-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)
}
}
packages\zhang-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;
packages\zhang-cli\lib\util\createTools.js
exports.getPromptModules = () => {
return [
'vueVersion'
].map(file => require(`../promptModules/${file}`))
}
packages\zhang-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
}
})
}
packages\zhang-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;
packages\zhang-cli-shared-utils\index.js
exports.chalk = require('chalk')
packages\zhang-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('zhang-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;
packages\zhang-cli\lib\util\writeFileTree.js
const fs = require('fs-extra')
const path = require('path')
module.exports = async function writeFileTree(dir, files) {
Object.keys(files).forEach((name) => {
const filePath = path.join(dir, name)
fs.ensureDirSync(path.dirname(filePath))
fs.writeFileSync(filePath, files[name])
})
}
packages\zhang-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('zhang-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;
packages\zhang-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('zhang-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
+ }
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;
packages\zhang-cli-shared-utils\index.js
+['pluginResolution','module'].forEach(m => {
+ Object.assign(exports, require(`./lib/${m}`))
+})
exports.chalk = require('chalk')
exports.execa = require('execa')
packages\zhang-cli-shared-utils\lib\module.js
const Module = require('module')
const path = require('path')
exports.loadModule = function (request, context) {
return Module.createRequire(path.resolve(context, 'package.json'))(request)
}
packages\zhang-cli-shared-utils\lib\pluginResolution.js
const pluginRE = /^@vue\/cli-plugin-/
exports.toShortPluginId = id => id.replace(pluginRE, '');//@vue/cli-plugin-babel => babel
exports.isPlugin = id => pluginRE.test(id)
exports.matchesPluginId = (input, full) => {
return full === input;
}
packages\zhang-cli\lib\util\mergeDeps.js
function mergeDeps(sourceDeps, depsToInject) {
const result = Object.assign({}, sourceDeps)
for (const depName in depsToInject) {
result[depName] = depsToInject[depName];
}
return result
}
module.exports = mergeDeps;
packages\zhang-cli\lib\util\normalizeFilePaths.js
const slash = require('slash')
module.exports = function normalizeFilePaths (files) {
Object.keys(files).forEach(file => {
const normalized = slash(file)
if (file !== normalized) {
files[normalized] = files[file]
delete files[file]
}
})
return files
}
packages\zhang-cli\lib\GeneratorAPI.js
const { toShortPluginId } = require('zhang-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) && (key === 'dependencies' || key === 'devDependencies')) {
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
packages\zhang-cli\lib\Generator.js
const { isPlugin,matchesPluginId } = require('zhang-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.files = {}
this.fileMiddlewares = []
this.pkg = pkg;
this.allPluginIds = Object.keys(this.pkg.dependencies || {})
.concat(Object.keys(this.pkg.devDependencies || {}))
.filter(isPlugin)
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');
}
async resolveFiles() {
const files = this.files
for (const middleware of this.fileMiddlewares) {
await middleware(files, ejs.render)
}
normalizeFilePaths(files)
}
extractConfigFiles() {
console.log('extractConfigFiles');
}
async initPlugins() {
const { rootOptions } = this
for (const plugin of this.plugins) {
const { id, apply, options } = plugin
const api = new GeneratorAPI(id, this, options, rootOptions)
await apply(api, options, rootOptions)
}
}
hasPlugin(_id) {
return [
...this.plugins.map(p => p.id),
...this.allPluginIds
].some(id => {
return matchesPluginId(_id, id);
})
}
}
module.exports = Generator;
packages\zhang-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('zhang-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(`🎉 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;
packages\zhang-cli\lib\Generator.js
const { isPlugin,matchesPluginId } = require('zhang-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.files = {}
this.fileMiddlewares = []
this.pkg = pkg;
this.allPluginIds = Object.keys(this.pkg.dependencies || {})
.concat(Object.keys(this.pkg.devDependencies || {}))
.filter(isPlugin)
const cliService = plugins.find(p => p.id === '@vue/cli-service')
this.rootOptions = cliService.options;
}
async generate() {
await this.initPlugins()
this.extractConfigFiles()
await this.resolveFiles()
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');
}
async resolveFiles() {
const files = this.files
for (const middleware of this.fileMiddlewares) {
await middleware(files, ejs.render)
}
normalizeFilePaths(files)
}
extractConfigFiles() {
console.log('extractConfigFiles');
}
async initPlugins() {
const { rootOptions } = this
for (const plugin of this.plugins) {
const { id, apply, options } = plugin
const api = new GeneratorAPI(id, this, options, rootOptions)
await apply(api, options, rootOptions)
}
}
hasPlugin(_id) {
return [
...this.plugins.map(p => p.id),
...this.allPluginIds
].some(id => {
return matchesPluginId(_id, id);
})
}
+ printExitLogs(){
+ console.log('printExitLogs');
+ }
}
module.exports = Generator;