UNPKG

takin

Version:

Front end engineering base toolchain and scaffold

411 lines 15.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Runner = exports.RunnerExtendable = exports.isHookRegistered = exports.registerHooks = exports.registerHook = void 0; const zod_1 = require("zod"); const cli_1 = require("../cli"); const config_1 = require("../config"); const errors_1 = require("../errors"); const generator_1 = require("../generator"); const logger_1 = require("../logger"); require("./customZod"); const hooks_1 = require("./hooks"); Object.defineProperty(exports, "isHookRegistered", { enumerable: true, get: function () { return hooks_1.isHookRegistered; } }); Object.defineProperty(exports, "registerHook", { enumerable: true, get: function () { return hooks_1.registerHook; } }); Object.defineProperty(exports, "registerHooks", { enumerable: true, get: function () { return hooks_1.registerHooks; } }); const methods_1 = require("./methods"); /** * 为插件 apply 方法提供唯一 runner */ function createRunnerByPlugin(runner, pluginName) { function changeable(field) { return { configurable: false, get() { return runner[field]; }, set(val) { runner[field] = val; } }; } const methods = new methods_1.MethodsContainer(runner.methods, pluginName); return Object.create(runner, { commandName: changeable('commandName'), commandArgs: changeable('commandArgs'), commandOptions: changeable('commandOptions'), userConfig: changeable('userConfig'), commandInvokedBy: changeable('commandInvokedBy'), // 确保插件中调用的 methods 已自动携带插件名称信息 methods: { configurable: false, get() { return methods; } }, currentPluginName: { configurable: false, get() { return pluginName; } } }); } // runner id, 内部使用 let RUNNER_ID = 0; /** * 用于扩展 Runner 属性或方法 * * @example * const takinInstance = takin('name') * * // 步骤一: 这里扩展方法或属性的类型 * declare module 'takin' { * interface RunnerExtendable { * customProp: any * customMethod(..args: any[]): void * } * } * * // 步骤二: 这里通过 extendRunner 提供扩展方法和属性的实现 * takinInstance.hooks.extendRunner.tap('ExtendRunnerPluginName', (Runner) => { * return class extends Runner { * override customProp: any * override customMethod(..args: any[]): void * } * }) * * // 完成步骤一和步骤二之后 * // 插件 apply 方法拿到的 runner 就会包含 customProp 属性和 customMethod 方法 */ class RunnerExtendable { } exports.RunnerExtendable = RunnerExtendable; /** * 命令运行器 */ class Runner extends RunnerExtendable { /** * @constructor * @param config - Config 实例 * @param userConfig - 本地执行的用户配置 * @param context - 传入的 Runner 上下文 */ constructor(config, userConfig, context) { super(); /** * Runner 命令被触发的方式, 用于辅助判断执行结果的退出方式和抛错方式 * - cli 代表命令是从命令行中解析出来的 * - api 代表命令是直接通过 API 传入的 */ this.commandInvokedBy = 'cli'; // 记录已使用的插件 this.plugins = new Map(); this.config = config; const cliName = config.name; this.runnerId = ++RUNNER_ID; this.logger = (0, logger_1.createLogger)('info', { prefix: `[${cliName}]`, debugPrefix: this.getRunnerName() }); this.context = new Map(); if (context) { for (const [k, v] of context instanceof Map ? context : Object.entries(context)) { this.context.set(k, v); } } this.methods = new methods_1.MethodsContainer(); this.cli = new cli_1.Cli(cliName, this); this.generator = generator_1.Generator; this.userConfig = userConfig; // 初始化 hooks this.hooks = (0, hooks_1.createHooks)(); // 自动记录 pluginName this.hooks.cli.intercept({ tap: (tapInfo) => { this.cli.pluginName = tapInfo.name; } }); } /** * 执行 Runner */ static async run({ config, userConfig, command, plugins, context }) { const runner = new this(config, userConfig, context); await runner.run(command, plugins); return runner; } /** * 获取当前工作目录 * @returns 当前工作目录 */ getCwd() { return this.config.cwd; } /** * 获取 runner 运行后的结果 */ getResult() { return this.result; } /** * 获取已载入的插件 */ getPlugins() { return this.plugins; } /** * 获取当前插件的名称 */ getCurrentPluginName() { return this.currentPluginName; } /** * 获取命令配置 */ getCommandOptions() { return { name: this.commandName, args: this.commandArgs || [], options: this.commandOptions || {} }; } /** * 获取 runner 名称 */ getRunnerName() { return `${this.config.name}:runner:#${this.runnerId}`; } /** * 基于传入的命令或命令行参数运行 runner * @param command - 指定需要运行的命令及参数 * @param plugins - 当前 runner 需要运行的插件列表 * @returns 命令执行的结果 */ async run(command, plugins) { let error; let result; try { this.logger.time('total run'); result = await this._run(command, plugins); } catch (err) { // 记录 Error error = err; } this.logger.timeEnd('total run'); // 异常退出 if (error != null) { const errorMsg = logger_1.COLORS.error((error === null || error === void 0 ? void 0 : error.message) || `unknown ${(error === null || error === void 0 ? void 0 : error.name) || 'error'}`); // 仅输出一次错误日志, 避免因为在 runner 内部通过 takin.run 执行方法抛错 // 导致的多次日志输出 if (!(error === null || error === void 0 ? void 0 : error.isErrorLogged)) { this.logger.error(errorMsg, { error }); } // 标记为已打印日志 error.isErrorLogged = true; // 触发 failed hook await this.hooks.failed.promise(error); // 如果从是 cli 执行, 则直接异常退出 if (this.commandInvokedBy === 'cli') { process.exit(1); } // 其他情况如通过 api 调用, 则直接将错误抛出 else { throw error; } } else { // 返回执行结果 return result; } } /** * 运行 runner */ async _run(command, plugins) { // 1. 执行指定的或所有的插件 this.logger.time('loadPlugins'); this.applySpecificOrAllPlugins(plugins); this.logger.timeEnd('loadPlugins'); // 2. 执行初始化逻辑 this.logger.time('Hooks.initialize'); this.hooks.initialize.call(this); this.logger.timeEnd('Hooks.initialize'); // 3. 运行命令行注册, 并获取匹配的命令 this.logger.time('Hooks.cli'); await this.prepareCliAndParseMatchedCommand(command); this.logger.timeEnd('Hooks.cli'); // 4. 获取匹配的命令和参数 this.logger.time('Hooks.matchedCommand'); const matchedCommand = (await this.cli.prepareMatchedCommandAndArgs()) || {}; await this.hooks.matchedCommand.promise(matchedCommand); this.logger.timeEnd('Hooks.matchedCommand'); // 5. 加载 config 阶段, 会基于获取到的配置路径, 并完成加载操作 this.logger.time('Hooks.loadConfig'); await this.hooks.loadConfig.promise(matchedCommand); this.logger.timeEnd('Hooks.loadConfig'); // 6. 基于命令和命令选项修改配置, 一般用于设定或修改默认值 this.logger.time('Hooks.modifyUserConfig'); this.userConfig = await this.hooks.modifyUserConfig.promise(this.userConfig, matchedCommand); this.logger.timeEnd('Hooks.modifyUserConfig'); // 7. 获取用户自定义的 schema this.logger.time('Hooks.registerUserConfig'); const schema = await this.hooks.registerUserConfig.promise(zod_1.z.object({}), zod_1.z); this.logger.timeEnd('Hooks.registerUserConfig'); // 8. 判断是否需要继续执行后续逻辑 this.logger.time('Hooks.shouldRun'); const shouldRun = this.hooks.shouldRun.call(this); this.logger.time('Hooks.shouldRun'); if (shouldRun === false) { this.logger.debug('stop by shouldRun === false'); return; } // 9. 检查是否需要校验用户配置 this.logger.time('Hooks.shouldValidateUserConfig'); const shouldValidateUserConfig = this.hooks.shouldValidateUserConfig.call(this); this.logger.time('Hooks.shouldValidateUserConfig'); // 10. 校验用户配置 if (shouldValidateUserConfig !== false) { this.logger.time('validateUserConfig'); const result = await schema .passthrough() .safeParseAsync(this.userConfig, {}); this.logger.timeEnd('validateUserConfig'); if (result.success) { this.userConfig = result.data; } else { const messages = result.error.issues.map((d) => `${d.path.join('.')}: ${d.message}`); messages.unshift(''); throw new errors_1.RunnerError(`校验配置出错, 请检查以下配置:${messages.join('\n ')}`); } } else { this.logger.debug('skip validateUserConfig'); } // 11. 执行命令开始前的 hook this.logger.time('Hooks.userConfigValidated'); await this.hooks.userConfigValidated.promise(this.userConfig); this.logger.timeEnd('Hooks.userConfigValidated'); // 12. 执行命令开始前的 hook this.logger.time('Hooks.beforeRun'); await this.hooks.beforeRun.promise(this); this.logger.timeEnd('Hooks.beforeRun'); // 13. 执行命令方法 await this.invokeCommandAction(matchedCommand); // 14. 执行运行后 hook this.logger.time('Hooks.done'); await this.hooks.done.promise(this); this.logger.time('Hooks.done'); // 返回 命令执行的结果, 如果有的话 return this.result; } /** * 添加可执行的命令 * @param options - 选项 */ addCommandAction({ name, pluginName, callback }) { const fullCommand = `command:${name}`; this.logger.debug(`Register: ${fullCommand}`); // 这里使用 async/await 包裹一层, 确保返回值是 promise this.hooks.run.for(fullCommand).tapPromise(pluginName, async (command) => { const result = await callback(command); this.result = result; return result; }); } /** * 运行命令对应的 action */ async invokeCommandAction(command = {}) { const label = `Execute: command:${command.name || 'unknown'}`; this.logger.time(label); await this.hooks.run.for(`command:${command.name}`).promise(command); this.logger.timeEnd(label); } /** * 解析命令行或传入的命令 * @param command - 命令配置 * @returns 解析后的命令 */ async prepareCliAndParseMatchedCommand(command) { var _a; // 准备 cli 命令 await this.hooks.cli.promise(this.cli); // 解析命令和参数, 并判断命令触发方式 if ((command === null || command === void 0 ? void 0 : command.name) != null || ((_a = command === null || command === void 0 ? void 0 : command.args) === null || _a === void 0 ? void 0 : _a.length) || (command === null || command === void 0 ? void 0 : command.options) != null) { this.cli.parseByCommand(command); this.commandInvokedBy = 'api'; } // 从命令行获取匹配的命令 else { this.cli.parse(process.argv, { run: false }); this.commandInvokedBy = 'cli'; } const { matchedCommandName, options, args } = this.cli; this.commandName = matchedCommandName; this.commandArgs = args; this.commandOptions = options; return { name: matchedCommandName, options }; } /** * 执行所有插件 */ applySpecificOrAllPlugins(plugins) { let allPlugins; // 优先使用传入的插件 // 通常用于使用 runner 执行特定的逻辑 if (plugins === null || plugins === void 0 ? void 0 : plugins.length) { allPlugins = this.config.resolveUserPlugins(plugins, false, config_1.PluginTypes.runner); // 应用插件 allPlugins.forEach(({ plugin, version }, pluginName) => { plugin.apply(createRunnerByPlugin(this, pluginName)); this.logger.debug(`插件: ${pluginName}@${version} 已应用`); }); } // 载入所有符合要求的插件 // 包含: 内置已使用插件和用户插件 else { const cnf = this.userConfig; let userPlugins = []; allPlugins = this.config.resolveUserPlugins(cnf === null || cnf === void 0 ? void 0 : cnf.plugins, false, config_1.PluginTypes.config); // 用于记录名称和版本信息 const reverseMap = new Map(); allPlugins.forEach(({ plugin, version }, name) => { userPlugins.push(plugin); reverseMap.set(plugin, { name, version }); }); const internalPlugins = []; this.config.usedPlugins.forEach((pluginInfo, pluginName) => { if (this.config.warnDuplicatePlugin(allPlugins, pluginName, pluginInfo.version)) return; internalPlugins.push(pluginInfo.plugin); // 记录内部插件 allPlugins.set(pluginName, pluginInfo); reverseMap.set(pluginInfo.plugin, { name: pluginName, version: pluginInfo.version }); }); const [prePlugins, normalPlugins, postPlugins] = this.config.sortUserPlugins(userPlugins); [prePlugins, internalPlugins, normalPlugins, postPlugins].forEach((pluginSet) => { pluginSet.forEach((p) => { const info = reverseMap.get(p); if (!info) return; p.apply(createRunnerByPlugin(this, info.name)); this.logger.debug(`插件: ${info.name}@${info.version} 已应用`); }); }); reverseMap.clear(); userPlugins = []; } this.plugins = allPlugins; } } exports.Runner = Runner; //# sourceMappingURL=index.js.map