准备工作
- 包可以使用命令行,是在bin目录下定义自己的脚本文件, 类型这样
- 在package.json中注册bin脚本
{
"bin": {
"demo": "bin/demo",
"demo-init": "bin/demo-init"
}
}
- 安装依赖
{
"dependencies": {
"async": "^3.2.0",
"chalk": "^4.0.0", // logger用
"commander": "^5.0.0", // 开发命令行的主要包
"consolidate": "^0.15.1",
"handlebars": "^4.7.6", // 模板
"inquirer": "^7.1.0",
"metalsmith": "^2.3.0",
"read-metadata": "^1.0.0", // 读取meta 文件
}
}
开始开发
- 定义主入口文件
const program = require('commander')
program
.version('1.0.0')
.usage('<command> [options]')
.command('init', '主命令的的-h展示内容') // 使用这种方式, 在使用init命令时,会调用同级目录的demo-init 文件夹
- init命令的实现, init命令要实现的是根据用户输入,拼装参数,copy模板操作
首先定义meta文件,这里以vue-cli的文件为例子
const path = require('path')
const fs = require('fs')
const {
sortDependencies,
installDependencies,
runLintFix,
printMessage,
} = require('./utils')
const pkg = require('./package.json')
const templateVersion = pkg.version
const { addTestAnswers } = require('./scenarios')
module.exports = {
metalsmith: {
// When running tests for the template, this adds answers for the selected scenario
before: addTestAnswers
},
helpers: {
if_or(v1, v2, options) {
if (v1 || v2) {
return options.fn(this)
}
return options.inverse(this)
},
template_version() {
return templateVersion
},
},
prompts: { // 定义问题,根据问题保存用户选择
name: {
when: 'isNotTest',
type: 'string',
required: true,
message: 'Project name',
},
description: {
when: 'isNotTest',
type: 'string',
required: false,
message: 'Project description',
default: 'A Vue.js project',
},
author: {
when: 'isNotTest',
type: 'string',
message: 'Author',
},
build: {
when: 'isNotTest',
type: 'list',
message: 'Vue build',
choices: [
{
name: 'Runtime + Compiler: recommended for most users',
value: 'standalone',
short: 'standalone',
},
{
name:
'Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific HTML) are ONLY allowed in .vue files - render functions are required elsewhere',
value: 'runtime',
short: 'runtime',
},
],
},
router: {
when: 'isNotTest',
type: 'confirm',
message: 'Install vue-router?',
},
lint: {
when: 'isNotTest',
type: 'confirm',
message: 'Use ESLint to lint your code?',
},
lintConfig: {
when: 'isNotTest && lint',
type: 'list',
message: 'Pick an ESLint preset',
choices: [
{
name: 'Standard (https://github.com/standard/standard)',
value: 'standard',
short: 'Standard',
},
{
name: 'Airbnb (https://github.com/airbnb/javascript)',
value: 'airbnb',
short: 'Airbnb',
},
{
name: 'none (configure it yourself)',
value: 'none',
short: 'none',
},
],
},
unit: {
when: 'isNotTest',
type: 'confirm',
message: 'Set up unit tests',
},
runner: {
when: 'isNotTest && unit',
type: 'list',
message: 'Pick a test runner',
choices: [
{
name: 'Jest',
value: 'jest',
short: 'jest',
},
{
name: 'Karma and Mocha',
value: 'karma',
short: 'karma',
},
{
name: 'none (configure it yourself)',
value: 'noTest',
short: 'noTest',
},
],
},
e2e: {
when: 'isNotTest',
type: 'confirm',
message: 'Setup e2e tests with Nightwatch?',
},
autoInstall: {
when: 'isNotTest',
type: 'list',
message:
'Should we run `npm install` for you after the project has been created? (recommended)',
choices: [
{
name: 'Yes, use NPM',
value: 'npm',
short: 'npm',
},
{
name: 'Yes, use Yarn',
value: 'yarn',
short: 'yarn',
},
{
name: 'No, I will handle that myself',
value: false,
short: 'no',
},
],
},
},
filters: { // 定义filter,根据用户输入来判断是否要copy文件
'.eslintrc.js': 'lint',
'.eslintignore': 'lint',
'config/test.env.js': 'unit || e2e',
'build/webpack.test.conf.js': "unit && runner === 'karma'",
'test/unit/**/*': 'unit',
'test/unit/index.js': "unit && runner === 'karma'",
'test/unit/jest.conf.js': "unit && runner === 'jest'",
'test/unit/karma.conf.js': "unit && runner === 'karma'",
'test/unit/specs/index.js': "unit && runner === 'karma'",
'test/unit/setup.js': "unit && runner === 'jest'",
'test/e2e/**/*': 'e2e',
'src/router/**/*': 'router',
},
complete: function(data, { chalk }) { // 构建完成后,用户选择了install,则自动install项目所需依赖
const green = chalk.green
sortDependencies(data, green)
const cwd = path.join(process.cwd(), data.inPlace ? '' : data.destDirName)
if (data.autoInstall) {
installDependencies(cwd, data.autoInstall, green)
.then(() => {
return runLintFix(cwd, data, green)
})
.then(() => {
printMessage(data, green)
})
.catch(e => {
console.log(chalk.red('Error:'), e)
})
} else {
printMessage(data, chalk)
}
},
}
module.exports = function generate(name, src, dest, done) {
const opts = getOptions(name, src);
const metalsmith = Metalsmith(src); // 定义工作目录
const data = Object.assign(metalsmith.metadata(), {
destDirName: name,
inPlace: dest === process.cwd(),
noEscape: true
});
metalsmith.use(askQuestions(opts.prompts)) // 设置询问问题
.use(filterFiles(opts.filters)) // 根据问题答案,来操作filters
.use(renderTemplateFiles(opts.skipInterpolation)) // cp模板
metalsmith
.clean(false)
.source('.')
.destination(dest)
.build((err, files) => {
done(err);
if (typeof opts.complete === 'function') {
const helpers = { chalk, logger, files }
opts.complete(data, helpers)
}
})
}