@jdbk/book-cli
Version:
Command line interface for front end project
314 lines (281 loc) • 9.82 kB
JavaScript
/* eslint-disable global-require */
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-underscore-dangle */
/** @module env/repository */
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');
const { isBinaryFileSync } = require('isbinaryfile');
const resolve = require('resolve');
const execa = require('execa');
const normalizeFilePaths = require('./normalizeFilePaths');
const writeFileTree = require('./writeFileTree');
// const logger = require("./logger");
const REPOSITORY_FOLDER = './abc';
const isString = (val) => typeof val === 'string';
const isFunction = (val) => typeof val === 'function';
const isObject = (val) => val && typeof val === 'object';
const mergeArrayWithDedupe = (a, b) => Array.from(new Set([...a, ...b]));
function pruneObject(obj) {
if (typeof obj === 'object') {
for (const k in obj) {
// eslint-disable-next-line no-prototype-builtins
if (!obj.hasOwnProperty(k)) {
// eslint-disable-next-line no-continue
continue;
}
if (obj[k] == null) {
delete obj[k];
} else {
obj[k] = pruneObject(obj[k]);
}
}
}
return obj;
}
function extractCallDir() {
// 摘录api.render() 使用错误堆栈的调用站点文件位置
const obj = {};
Error.captureStackTrace(obj);
const callSite = obj.stack.split('\n')[3];
// 在命名函数内调用时堆栈的regexp
const namedStackRegExp = /\s\((.*):\d+:\d+\)$/;
// 在匿名内部调用时堆栈的regexp
const anonymousStackRegExp = /at (.*):\d+:\d+$/;
let matchResult = callSite.match(namedStackRegExp);
if (!matchResult) {
matchResult = callSite.match(anonymousStackRegExp);
}
const fileName = matchResult[1];
return path.dirname(fileName);
}
const replaceBlockRE = /<%# REPLACE %>([^]*?)<%# END_REPLACE %>/g;
/**
* 读取文件内容
* @param {String} name - 文件名称
* @param {Object} data - 文件内容
* @param {Object} ejsOptions - ejs相关配置
*/
function renderFile(name, data, ejsOptions) {
if (isBinaryFileSync(name)) {
return fs.readFileSync(name); // return buffer
}
const template = fs.readFileSync(name, 'utf-8');
// 通过yaml前端的自定义模板继承。
// ---
// extend: 'source-file'
// replace: !!js/regexp /some-regex/
// OR
// replace:
// - !!js/regexp /foo/
// - !!js/regexp /bar/
// ---
const yaml = require('yaml-front-matter');
const parsed = yaml.loadFront(template);
const content = parsed.__content;
let finalTemplate = `${content.trim()}\n`;
if (parsed.when) {
finalTemplate = `<%_ if (${parsed.when}) { _%>${finalTemplate}<%_ } _%>`;
// 使用ejs.渲染测试条件表达式
// 如果计算结果为错误值,请尽早返回以避免扩展表达式的额外开销
const result = ejs.render(finalTemplate, data, ejsOptions);
if (!result) {
return '';
}
}
if (parsed.extend) {
const extendPath = path.isAbsolute(parsed.extend)
? parsed.extend
: resolve.sync(parsed.extend, { basedir: path.dirname(name) });
finalTemplate = fs.readFileSync(extendPath, 'utf-8');
if (parsed.replace) {
if (Array.isArray(parsed.replace)) {
const replaceMatch = content.match(replaceBlockRE);
if (replaceMatch) {
const replaces = replaceMatch.map((m) => m.replace(replaceBlockRE, '$1').trim());
parsed.replace.forEach((r, i) => {
finalTemplate = finalTemplate.replace(r, replaces[i]);
});
}
} else {
finalTemplate = finalTemplate.replace(parsed.replace, content.trim());
}
}
}
return ejs.render(finalTemplate, data, ejsOptions);
}
/**
* @private
*/
class Generator {
constructor(repositoryPath = REPOSITORY_FOLDER) {
this.files = {};
this.fileMiddlewares = [];
this._repositoryPath = path.resolve(repositoryPath);
}
async generate() {
await this.resolveFiles();
await writeFileTree(this._repositoryPath, this.files);
}
async resolveFiles() {
const { files } = this;
for (const middleware of this.fileMiddlewares) {
// eslint-disable-next-line no-await-in-loop
await middleware(files, ejs.render);
}
// 规范化windows上的文件路径
// 所有路径都转换为使用/而不是\
normalizeFilePaths(files);
}
/**
* @private
* @property
* 仓库绝对路径( npm--prefix)
*/
get repositoryPath() {
return this._repositoryPath;
}
set repositoryPath(repositoryPath) {
this._repositoryPath = path.resolve(repositoryPath);
delete this._nodeModulesPath;
}
/**
* 将模板文件渲染到虚拟文件树对象中。
*
* @param {string | object | FileMiddleware} source -
* 可以是:
* - 目录的相对路径;
* - { sourceTemplate: targetFile } 形式hash映射;
* - 自定义文件中间件功能。
* @param {object} [additionalData] - 模板可用的其他数据。
* @param {object} [ejsOptions] - ejs相关选项
*/
render(source, additionalData = {}, ejsOptions = {}) {
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) => {
console.log(filename);
// 当发布到npm时,dotfiles被忽略,因此在模板中
// 我们需要使用下划线代替(例如 “_gitignore”)
if (filename.charAt(0) === '_' && filename.charAt(1) !== '_') {
return `.${filename.slice(1)}`;
}
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, ejsOptions);
// 只有set file不全是空白,或者是缓冲区(二进制文件)
// if (Buffer.isBuffer(content) || /[^\s]/.test(content)) {
files[targetPath] = content;
// }
}
});
} else if (isObject(source)) {
this._injectFileMiddleware((files) => {
const data = this._resolveData(additionalData);
// eslint-disable-next-line guard-for-in
for (const targetPath in source) {
const sourcePath = path.resolve(baseDir, source[targetPath]);
const content = renderFile(sourcePath, data, ejsOptions);
if (Buffer.isBuffer(content) || content.trim()) {
files[targetPath] = content;
}
}
});
} else if (isFunction(source)) {
this._injectFileMiddleware(source);
}
}
/**
* 注入一个文件处理中间件
*
* @private
* @param {FileMiddleware} middleware -
* 接收虚拟文件树对象的中间件函数和ejs呈现函数。可以是异步的。
*/
_injectFileMiddleware(middleware) {
this.fileMiddlewares.push(middleware);
}
/**
* 在呈现模板时解析数据。
*
* @private
*/
// eslint-disable-next-line class-methods-use-this
_resolveData(additionalData) {
return additionalData;
// return {
// options: this.options,
// rootOptions: this.rootOptions,
// plugins: this.pluginsData,
// ...additionalData,
// };
}
/**
* 扩展项目的 package.json.
* 也解决了插件之间的依赖冲突。
* 在将文件写入磁盘之前,可以将工具配置字段提取到独立文件中。
*
* @param {object | () => object} fields - 要合并的字段。
* @param {object} [options] - 扩展/合并字段的选项。
* @param {boolean} [options.prune=false] - 合并后从对象中删除空字段或未定义字段。
* @param {boolean} [options.merge=true] - 深度合并嵌套字段,
* 请注意,不管此选项如何,依赖项字段始终是深度合并的。
* @param {boolean} [options.warnIncompatibleVersions=true] - 输出警告
* 如果两个依赖版本范围不相交。
*/
// extendPackage(fields, options = {}) {
// const extendOptions = {
// prune: false,
// merge: true,
// warnIncompatibleVersions: true,
// };
// Object.assign(extendOptions, options);
// const { pkg } = this.generator;
// const toMerge = isFunction(fields) ? fields(pkg) : fields;
// // eslint-disable-next-line guard-for-in
// for (const key in toMerge) {
// const value = toMerge[key];
// const existing = pkg[key];
// if (
// isObject(value)
// && (key === 'dependencies' || key === 'devDependencies')
// ) {
// // use special version resolution merge
// pkg[key] = mergeDeps(
// this.id,
// existing || {},
// value,
// this.generator.depSources,
// extendOptions,
// );
// } else if (!extendOptions.merge || !(key in pkg)) {
// pkg[key] = value;
// } else if (Array.isArray(value) && Array.isArray(existing)) {
// pkg[key] = mergeArrayWithDedupe(existing, value);
// } else if (isObject(value) && isObject(existing)) {
// pkg[key] = deepmerge(existing, value, {
// arrayMerge: mergeArrayWithDedupe,
// });
// } else {
// pkg[key] = value;
// }
// }
// if (extendOptions.prune) {
// pruneObject(pkg);
// }
// }
}
module.exports = Generator;