takin
Version:
Front end engineering base toolchain and scaffold
321 lines • 13 kB
JavaScript
"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