UNPKG

takin

Version:

Front end engineering base toolchain and scaffold

321 lines 13 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Generator = void 0; const crypto_1 = __importDefault(require("crypto")); const fast_glob_1 = __importDefault(require("fast-glob")); const fs_extra_1 = __importDefault(require("fs-extra")); const lodash_1 = require("lodash"); const os_1 = __importDefault(require("os")); const path_1 = __importStar(require("path")); const prompts_1 = __importDefault(require("prompts")); const constants_1 = require("./constants"); const utils = __importStar(require("./deps")); const downloader_1 = require("./downloader"); const logger_1 = require("./logger"); const utils_1 = require("./utils"); const TEMPLATE_EXTNAME = '.tpl'; const TEMPLATE_FILE_REGEXP = new RegExp(`\\${TEMPLATE_EXTNAME}$`); const DEFAULT_CUSTOM_GENERATOR_NAME = 'custom-generator'; class Generator { constructor(options) { /** * 工具方法, 包含: * - chalk * - debug * - execa * - esbuild * - fastGlob * - fsExtra * - got * - json5 * - jsoncParser * - lodash * - prompts * - tapable * - tarFs * - zod */ this.utils = utils; /** * 日志工具 */ this.logger = logger_1.logger; const { from, to, defaults = {}, questions = [], baseDir = constants_1.DEFAULT_ROOT, globOptions, customGeneratorName = DEFAULT_CUSTOM_GENERATOR_NAME } = options; this.baseDir = baseDir; this.defaults = defaults; this.answers = {}; this.from = from; this.to = (0, path_1.resolve)(baseDir, to); this.questions = questions || []; this.globOptions = globOptions || { dot: true, ignore: ['**/node_modules/**'] }; this.customGeneratorName = customGeneratorName; if (options.downloaded) this.downloaded = options.downloaded; if (options.customized) this.customized = options.customized; if (options.prompted) this.prompted = options.prompted; if (options.written) this.written = options.written; } static async run(options) { await new this(options).run(); } /** * 下载完成后执行 * @param generator - 当前 generator 实例 */ // eslint-disable-next-line @typescript-eslint/no-unused-vars downloaded(generator) { // do nothing } /** * 获取并初始化自定义 Generator 之后执行 * @param generator - 当前 generator 实例 */ // eslint-disable-next-line @typescript-eslint/no-unused-vars customized(generator) { // do nothing } /** * 终端交互问答提示后执行 * @param generator - 当前 generator 实例 */ // eslint-disable-next-line @typescript-eslint/no-unused-vars prompted(generator) { // do nothing } /** * 写入到目标文件夹之后执行 * @param generator - 当前 generator 实例 */ // eslint-disable-next-line @typescript-eslint/no-unused-vars written(generator) { // do nothing } /** * 运行生成器, 步骤为 * 1. 下载模版, 完成后执行 downloaded 回调 * 2. 获取自定义生成器, 完成后执行 customized 回调 * 3. 获取问题并在终端交互, 完成后执行 prompted 回调 * 4. 写入模版到目标文件夹, 完成后执行 written 回调 */ async run() { var _a, _b, _c, _d; await this.downloading(); await ((_a = this === null || this === void 0 ? void 0 : this.downloaded) === null || _a === void 0 ? void 0 : _a.call(this, this)); const generator = (await this.customizing()) || this; await ((_b = generator === null || generator === void 0 ? void 0 : generator.customized) === null || _b === void 0 ? void 0 : _b.call(generator, generator)); await generator.prompting(); await ((_c = generator === null || generator === void 0 ? void 0 : generator.prompted) === null || _c === void 0 ? void 0 : _c.call(generator, generator)); await generator.writing(); await ((_d = generator === null || generator === void 0 ? void 0 : generator.written) === null || _d === void 0 ? void 0 : _d.call(generator, generator)); } /** * 下载模版内容 * @returns 自定义 Generator 或 空 */ async downloading() { const from = (0, path_1.resolve)(this.baseDir, this.from); // 这里和 downloader 协议做一下区分 // 优先判断 xxx/xxx 是否为本地路径 if (await fs_extra_1.default.pathExists(from)) { this.from = from; } else { const { type, options } = (0, downloader_1.autoDetectDownloaderTypeAndOptions)(this.from); const tempDir = (0, path_1.join)(os_1.default.tmpdir(), 'generator-' + crypto_1.default.createHash('md5').update(this.from).digest('hex')); await fs_extra_1.default.ensureDir(tempDir); await (0, downloader_1.download)(type, options, tempDir); this.from = tempDir; } } /** * 尝试获取自定义生成器 * @returns 自定义生成器 或 空 */ async customizing() { // 载入自定义 Generator try { const generatorPath = (0, utils_1.lookupFile)(this.from, [this.customGeneratorName], [ constants_1.SupportConfigExtensions.js, constants_1.SupportConfigExtensions.mjs, constants_1.SupportConfigExtensions.ts ], { pathOnly: true }); if (!generatorPath) return; const packageJsonFile = (0, path_1.join)(this.from, 'package.json'); let isMjs = false; const ext = (0, path_1.extname)(generatorPath); // 通过 package.json 判断是否是 mjs try { if (await fs_extra_1.default.pathExists(packageJsonFile)) { const pkg = (await fs_extra_1.default.readJSON(packageJsonFile)); // 检查 package.json 中 "type" 字段 是否为 "module" 并设置 `isMjs` 为 true if ((pkg === null || pkg === void 0 ? void 0 : pkg.type) === 'module') isMjs = true; } } catch (e) { } // 载入并替换自定义生成器 if (await fs_extra_1.default.pathExists(generatorPath)) { const tempFilePath = path_1.default.join(this.from, [ this.customGeneratorName, crypto_1.default.createHash('md5').update(this.from).digest('hex'), 'temp' ].join('-') + '.js'); const factory = (await (0, utils_1.importJsOrMjsOrTsFromFile)({ cwd: this.baseDir, filePath: generatorPath, isMjs: isMjs || ext === constants_1.SupportConfigExtensions.mjs, isTs: ext === constants_1.SupportConfigExtensions.ts, tempFilePath, autoDeleteTempFile: true })); const G = factory(Generator); // 这里不传入 GeneratorCallbacks 允许自定义生成通过定制 callbacks // 及 覆盖函数实现来自定义逻辑和流程 const g = new G({ from: this.from, to: this.to, defaults: this.defaults, questions: this.questions, baseDir: this.baseDir, globOptions: this.globOptions, customGeneratorName: this.customGeneratorName }); logger_1.logger.debug(`载入自定义 Generator 成功: ${generatorPath}`); return g; } } catch (err) { const error = err; logger_1.logger.error(`自定义生成器载入失败: ${error.message}`, { error }); } } /** * 生成终端的 prompts 问题 */ async prompting() { var _a; if ((_a = this.questions) === null || _a === void 0 ? void 0 : _a.length) { this.answers = await (0, prompts_1.default)(this.questions, { onCancel() { // 用户取消时自动退出 process.exit(0); } }); } } /** * 写入逻辑 */ async writing() { const context = { ...this.defaults, ...this.answers }; if ((await fs_extra_1.default.stat(this.from)).isDirectory()) { await this.copyDirectory({ context, path: this.from, to: this.to }); } else { if (this.from.endsWith(TEMPLATE_EXTNAME)) { await this.copyTemplate({ path: this.from, to: this.to, context }); } else { const absTarget = this.to; logger_1.logger.success(`拷贝: ${path_1.default.relative(this.baseDir, absTarget)}`); await fs_extra_1.default.mkdirp((0, path_1.dirname)(absTarget)); await fs_extra_1.default.copyFile(this.from, absTarget); } } } /** * 拷贝模版文件 * * 基于 lodash.template 方法来解析模版,并增加了对 * 1. <%_ _%> 的支持,可用于移除前后的空格或 Tab 符号 * 2. 增加了对 -%> 的支持,可用于移除控制语句引入的换行符 * @param opts - 拷贝选项 */ async copyTemplate(opts) { let tpl = await fs_extra_1.default.readFile(opts.path, 'utf-8'); // 移除控制语句前 <%_ _%> 后的空格或 Tab 符号 tpl = tpl.replace(/[ \t]*<%_/gm, '<%').replace(/_%>[ \t]*/gm, '%>'); // 移除控制语句结尾引入的换行符 tpl = tpl.replace(/-%>(?:\r\n|\r|\n)?/gm, '%>'); const content = (0, lodash_1.template)(tpl)(opts.context); await fs_extra_1.default.mkdirp((0, path_1.dirname)(opts.to)); logger_1.logger.success(`写入: ${(0, path_1.relative)(this.baseDir, opts.to)}`); await fs_extra_1.default.writeFile(opts.to, content, 'utf-8'); } /** * 拷贝目录 * @param opts - 拷贝选项 */ async copyDirectory(opts) { for await (const f of fast_glob_1.default.stream('**/*', { ...this.globOptions, cwd: opts.path })) { const file = f.toString(); const absFile = (0, path_1.join)(opts.path, file); if ((await fs_extra_1.default.stat(absFile)).isDirectory()) return; if (file.endsWith(TEMPLATE_EXTNAME)) { await this.copyTemplate({ path: absFile, to: (0, path_1.join)(opts.to, file.replace(TEMPLATE_FILE_REGEXP, '')), context: opts.context }); } else { const absTarget = (0, path_1.join)(opts.to, file); logger_1.logger.success(`拷贝: ${path_1.default.relative(this.baseDir, absTarget)}`); await fs_extra_1.default.mkdirp((0, path_1.dirname)(absTarget)); await fs_extra_1.default.copyFile(absFile, absTarget, fs_extra_1.default.constants.COPYFILE_FICLONE); } } } } exports.Generator = Generator; //# sourceMappingURL=generator.js.map