zoro-cli
Version:
https://github.com/vuejs/vue-cli
245 lines (224 loc) • 6.57 kB
JavaScript
const { isString, isObject, isFunction } = require('zoro-cli-util/is')
const {
renderArray,
renderDir,
renderObject,
} = require('zoro-cli-util/renderFile')
const mergeScripts = require('zoro-cli-util/mergeScripts')
const path = require('path')
const { union, mergeWith } = require('lodash')
const Types = require('./types')
function extractCallDir() {
// extract api.render() callsite file location using error stack
const obj = {}
Error.captureStackTrace(obj)
const callSite = obj.stack.split('\n')[3]
const fileName = callSite.match(/\s\((.*):\d+:\d+\)$/)[1]
return path.dirname(fileName)
}
class GeneratorAPI {
constructor({
id,
generator,
options = {},
rootOptions = {},
pkg = {},
} = {}) {
this.id = id
this.generator = generator
this.options = options
this.rootOptions = rootOptions
this.pkg = pkg
}
/**
* Resolves the data when rendering templates.
*
* @private
*/
_resolveData(additionalData) {
return {
options: this.options,
rootOptions: this.rootOptions,
Types,
pkg: this.pkg,
...additionalData,
}
}
/**
* Inject a file processing middleware.
*
* @private
* @param {FileMiddleware} middleware - A middleware function that receives the
* virtual files tree object, and an ejs render function. Can be async.
*/
_injectFileAddMiddleware(middleware) {
this.generator.fileMiddlewares.push({ type: 'add', middleware })
}
_injectFileRemoveMiddleware(middleware) {
this.generator.fileMiddlewares.push({ type: 'remove', middleware })
}
/**
* Resolve path for a project.
*
* @param {string} _path - Relative path from project root
* @return {string} The resolved absolute path.
*/
resolve(_path) {
return path.resolve(this.generator.context, _path)
}
/**
* Extend the package.json of the project.
* Nested fields are deep-merged unless `{ merge: false }` is passed.
* Also resolves dependency conflicts between plugins.
* Tool configuration fields may be extracted into standalone files before
* files are written to disk.
*
* @param {object | () => object} fields - Fields to merge.
*/
extendPackage(fields, options = {}) {
const { overrideObj, overrideArr } = options
const pkg = this.generator.pkg
const toMerge = isFunction(fields) ? fields(pkg) : fields
Object.keys(toMerge).forEach(key => {
const value = toMerge[key]
const existing = pkg[key]
if (!existing) {
pkg[key] = value
} else if (Array.isArray(existing) && Array.isArray(value)) {
if (overrideArr) {
pkg[key] = value
} else {
pkg[key] = union(existing, value)
}
} else if (isObject(value)) {
if (overrideObj) {
pkg[key] = value
} else if (key === 'scripts') {
pkg[key] = mergeScripts(existing, value)
} else {
pkg[key] = mergeWith({}, existing, value, (destValue, srcValue) => {
if (Array.isArray(destValue) || Array.isArray(srcValue)) {
if (overrideArr) {
return srcValue
}
return union(destValue, srcValue)
}
if (destValue !== undefined && srcValue === undefined) {
// 如果是 ''/null/undefined, 不覆盖
return destValue
}
return undefined
})
}
} else {
pkg[key] = value
}
})
}
removeDependencies(names = []) {
names.forEach(name => {
const { pkg } = this.generator
if (pkg.dependencies) {
delete pkg.dependencies[name]
}
if (pkg.devDependencies) {
delete pkg.devDependencies[name]
}
})
}
/**
* Render template files into the virtual files tree object.
*
* @param {string | object | FileMiddleware} source -
* Can be one of:
* - relative path to caller directory;
* - Object of { dir: '', arr: [] };
* - dir is relative path to caller directory
* - arr is an array of paths relative to dir
* - Object hash of { sourceTemplate: targetFile } mappings;
* - a custom file middleware function.
* @param {object} [additionalData] - additional data available to templates.
* @param {object} [ejsOptions] - options for ejs.
*/
render(source, additionalData = {}, ejsOptions = {}) {
const baseDir = extractCallDir()
const data = this._resolveData(additionalData)
if (isString(source)) {
this._injectFileAddMiddleware(() =>
renderDir({
dir: path.join(baseDir, source),
data,
ejsOptions,
}),
)
} else if (isObject(source)) {
if (Array.isArray(source.arr)) {
const { arr, dir = '' } = source
this._injectFileAddMiddleware(() =>
renderArray({
dir: path.join(baseDir, dir),
arr,
data,
ejsOptions,
}),
)
} else {
this._injectFileAddMiddleware(() =>
renderObject({
dir: baseDir,
obj: source,
data,
ejsOptions,
}),
)
}
} else if (isFunction(source === 'function')) {
this._injectFileAddMiddleware(source)
}
}
remove(source) {
this._injectFileRemoveMiddleware(source)
}
setScriptsKeyOrder(keyOrder) {
this.generator.scriptsKeyOrder = keyOrder
}
setPkgKeyOrder(keyOrder) {
this.generator.pkgKeyOrder = keyOrder
}
_spliceKeyOrder(property, keyOrderMap) {
const arr = this.generator[property] || []
Object.keys(keyOrderMap).forEach(key => {
const index = arr.indexOf(key)
if (index !== -1) {
arr.splice(index + 1, 0, ...keyOrderMap[key])
} else {
arr.push(...keyOrderMap[key])
}
})
this.generator[property] = arr
}
spliceScriptsKeyOrder(keyOrderMap) {
this._spliceKeyOrder('scriptsKeyOrder', keyOrderMap)
}
splicePkgKeyOrder(keyOrderMap) {
this._spliceKeyOrder('pkgKeyOrder', keyOrderMap)
}
/**
* Push a file middleware that will be applied after all normal file
* middelwares have been applied.
*
* @param {FileMiddleware} cb
*/
postProcessFiles(cb) {
this.generator.postProcessFilesCbs.push(cb)
}
/**
* Push a callback to be called when the files have been written to disk.
*
* @param {function} cb
*/
onCreateComplete(cb) {
this.generator.completeCbs.push(cb)
}
}
module.exports = GeneratorAPI