lianghaijie-cli
Version:
A simple CLI for scaffolding futu5_ipo projects.
179 lines (159 loc) • 5.74 kB
JavaScript
const chalk = require('chalk') // 高亮终端输出信息
const Metalsmith = require('metalsmith') // 静态网站生成器
const Handlebars = require('handlebars') // 模板引擎
const async = require('async') // 异步处理工具
const render = require('consolidate').handlebars.render // 渲染模板函数
const path = require('path')
const multimatch = require('multimatch') // 文件匹配,支持匹配多个条件
const getOptions = require('./options') // 自定义工具 - 获取模板配置文件
const ask = require('./ask') // 自定义工具 - 与用户交互
const filter = require('./filter') // 自定义工具 - 文件过滤
const logger = require('./logger') // 自定义工具 - 日志打印
// 注册 handlerbars helper 函数
Handlebars.registerHelper('if_eq', function (a, b, opts) {
return a === b
? opts.fn(this)
: opts.inverse(this)
})
Handlebars.registerHelper('unless_eq', function (a, b, opts) {
return a === b
? opts.inverse(this)
: opts.fn(this)
})
// 主逻辑
// 获取模板配置 -->
// 初始化 Metalsmith -->
// 添加一些变量至 Metalsmith -->
// handlebars模板注册 helper -->
// 配置对象中是否有 before 函数,有则执行 -->
// 询问问题 -->
// 过滤文件 -->
// 渲染模板文件 -->
// 配置对象中是否有 after 函数,有则执行 -->
// 最后构建项目内容 -->
// 构建完成,成功若配置对象中有 complete 函数则执行,否则打印配置对象中的 completeMessage 息,如果有错误,执行回调函数 done(err)
/**
* Generate a template given a `src` and `dest`.
*
* @param {String} name
* @param {String} src
* @param {String} dest
* @param {Function} done
*/
module.exports = function generate (name, src, dest, done) {
const opts = getOptions(name, src) // 获取模板配置对象
const metalsmith = Metalsmith(path.join(src, 'template')) // 初始化 metalsmith 对象
const data = Object.assign(metalsmith.metadata(), {
destDirName: name,
inPlace: dest === process.cwd(),
noEscape: true
})
// 注册模板配置对象中的 helpers
opts.helpers && Object.keys(opts.helpers).map(key => {
Handlebars.registerHelper(key, opts.helpers[key])
})
const helpers = { chalk, logger }
// 执行模板配置对象的 before 函数
if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
opts.metalsmith.before(metalsmith, opts, helpers)
}
metalsmith.use(askQuestions(opts.prompts)) // 根据模板配置对象询问问题
.use(filterFiles(opts.filters)) // 根据问题答案集合集合模版配置对象中的 filters 对象过滤文件
.use(renderTemplateFiles(opts.skipInterpolation)) //渲染模板文件
// 执行模板配置对象的 after 函数
if (typeof opts.metalsmith === 'function') {
opts.metalsmith(metalsmith, opts, helpers)
} else if (opts.metalsmith && typeof opts.metalsmith.after === 'function') {
opts.metalsmith.after(metalsmith, opts, helpers)
}
metalsmith.clean(false)
.source('.') // start from template root instead of `./src` which is Metalsmith's default for `source`
.destination(dest)
.build((err, files) => {
done(err)
// 执行模板配置对象的 complete 函数
if (typeof opts.complete === 'function') {
const helpers = { chalk, logger, files }
opts.complete(data, helpers)
} else {
logMessage(opts.completeMessage, data)
}
})
return data
}
/**
* 询问问题 - 中间件
*
* @param {Object} prompts
* @return {Function}
*/
function askQuestions (prompts) {
return (files, metalsmith, done) => {
ask(prompts, metalsmith.metadata(), done)
}
}
/**
* 过滤文件 - 中间件
*
* @param {Object} filters
* @return {Function}
*/
function filterFiles (filters) {
return (files, metalsmith, done) => {
filter(files, filters, metalsmith.metadata(), done)
}
}
/**
* Template in place plugin. 渲染模板
*
* @param {Object} files
* @param {Metalsmith} metalsmith
* @param {Function} done
*/
function renderTemplateFiles (skipInterpolation) {
skipInterpolation = typeof skipInterpolation === 'string'
? [skipInterpolation]
: skipInterpolation
return (files, metalsmith, done) => {
const keys = Object.keys(files)
const metalsmithMetadata = metalsmith.metadata()
async.each(keys, (file, next) => {
// 跳过符合模板配置对象中 skipInterpolation 数组中文件
// 如:跳过某个文件夹下所有 .vue 文件 以 避免 '{{}}' 与 handlebars 冲突
if (skipInterpolation && multimatch([file], skipInterpolation, { dot: true }).length) {
return next()
}
const str = files[file].contents.toString()
// do not attempt to render files that do not have mustaches
// 跳过没有使用 handlbers 语法的文件,即文件不需要引擎渲染
if (!/{{([^{}]+)}}/g.test(str)) {
return next()
}
render(str, metalsmithMetadata, (err, res) => {
if (err) {
err.message = `[${file}] ${err.message}`
return next(err)
}
// 使用了 handlerbars 语法的文件内容替换成渲染值
files[file].contents = new Buffer(res)
next()
})
}, done)
}
}
/**
* Display template complete message. 构建成功打印信息
*
* @param {String} message
* @param {Object} data
*/
function logMessage (message, data) {
if (!message) return
render(message, data, (err, res) => {
if (err) {
console.error('\n Error when rendering template complete message: ' + err.message.trim())
} else {
console.log('\n' + res.split(/\r?\n/g).map(line => ' ' + line).join('\n'))
}
})
}