UNPKG

book-cliiii

Version:

Command line interface for front end project

312 lines (279 loc) 9.72 kB
/* 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 */ _resolveData(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;