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
Lerna
Is one manage multiplenpm
Module 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 workspace
Allow us to usemonorepo
To manage the project- In the installation
node_modules
It will not be installed in every subprojectnode_modules
Instead, 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 directory
yarn.lock
File. Subprojects will also belink
到node_modules
Inside, which allows us to use it directlyimport
Import the corresponding project yarn.lock
The files are generated automatically and completelyYarn
To deal withyarn.lock
Locking 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 create
The 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
yarn
To deal with dependencies,lerna
Used 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, right
child_process.exec
The 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
- slashwill
Windows
The backslash path of the system is converted to a slash path, such asfoo\\bar
➔foo/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 environment
loading
Effects, 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 project
package.json
, you will find that dependencies are based on@vue/cli-plugin-
At the beginning of. Plugins can be modifiedwebpack
Internal configuration is also available tovue-cli-service
Injection 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 tuning
webpack
Runtime 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
- a
Vue CLI preset
Is 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 - in
vue create
Stored in the processpreset
It’s gonna be on yourhome
Directory in a configuration file (~/.vuerc
). You can adjust, add, or delete saved files by editing them directlypreset
Preset
The 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