UNPKG

hins

Version:

[![codecov](https://codecov.io/gh/l-zoy/hins/branch/main/graph/badge.svg)](https://codecov.io/gh/l-zoy/hins) [![GitHub license](https://img.shields.io/github/license/l-zoy/hins)](https://github.com/l-zoy/hins/blob/master/LICENSE) ![node-current](https://i

387 lines (328 loc) 10.2 kB
import cloneDeep from 'lodash.clonedeep' import uniq from 'lodash.uniq' import assert from 'assert' import slash from 'slash' import path from 'path' import resolvePlugins, { pathToRegister } from './resolvePlugins' import { ICoreStage, ICoreApplyHookTypes, Cycle } from './enum' import ReadConfig from './ReadConfig' import AsyncHook from './AsyncHook' import env from './env' import Api from './Api' import type { IConfigPlugins, ICoreApplyHook, IApplyPlugin, ITypeHooks, ICoreStart, ICommands, INonEmpty, IWorkDir, IMethods, IPlugin, IConfig, ICore, IHook } from './types' export default class Core { /** * @desc directory path */ cwd: IWorkDir /** * @desc extra command */ args?: ICoreStart /** * @desc registered Plugins */ plugins: Record<string, IPlugin> = {} /** * @desc list of plugins when registering, */ extraPlugins: IApplyPlugin[] = [] /** * @desc initial Plugins */ initPlugins: IApplyPlugin[] = [] /** * @desc registered commands */ commands: Record<string, ICommands> = {} /** * @desc Apply Plugin enumeration value, provide a plug-in use */ ApplyHookType = ICoreApplyHookTypes /** * @desc plugin Methods */ pluginMethods: Record<string, IMethods> = {} /** * @desc { Record<string, IHook[]> } */ hooksByPluginId: Record<string, IHook[]> = {} /** * @desc lifecycle stage */ stage: ICoreStage = ICoreStage.uninitialized /** * @desc enum lifecycle */ coreStage = ICoreStage /** * @desc internal Plugins */ internalPlugins: IConfigPlugins /** * @desc Initialize the configuration file, the config at this time is still to be verified */ initConfig: IConfig = {} /** * @desc the final processed config */ config: IConfig = {} /** * @desc Config Instance */ configInstance: ReadConfig /** * @desc runtime babel */ babelRegister: INonEmpty<ICore>['babelRegister'] /** * @desc monitor config */ watchConfig: INonEmpty<ICore>['watchConfig'] /** * @desc applyHooks shortcut */ applyAddHooks: ITypeHooks /** * @desc applyHooks shortcut */ applyModifyHooks: ITypeHooks /** * @desc applyHooks shortcut */ applyEventHooks: ITypeHooks /** * @desc api Instance */ ApiInstance: Api /** * @name Core * @param { Function } options.babelRegister - provide config runtime. `type:Function` * @param { Array } options.possibleConfigName - config name path `type:string[]` * @param { Array } options.plugins Array - default plugin `type:string[]` * @param { object } options.isWatch - watch config `type:object` * @param { string } options.cwd - work path `type:string` */ constructor(options: ICore) { // text prompt when watch config this.watchConfig = options.watchConfig ?? { changeLog: (event, paths) => { console.log(` ${event} `, paths) }, reloadLog: () => { console.log(`Try to restart...`) } } this.cwd = options.cwd ?? process.cwd() this.internalPlugins = options.plugins ?? [] this.babelRegister = options.babelRegister ?? (() => {}) // apply hooks alias for easy use this.applyAddHooks = (options) => this.applyHooks({ ...options, type: ICoreApplyHookTypes.add }) this.applyModifyHooks = (options) => this.applyHooks({ ...options, type: ICoreApplyHookTypes.modify }) this.applyEventHooks = (options) => this.applyHooks({ ...options, type: ICoreApplyHookTypes.event }) this.configInstance = new ReadConfig({ possibleConfigName: options.possibleConfigName ?? [], core: this }) this.initConfig = this.configInstance.getUserConfig() this.ApiInstance = new Api({ core: this }) this.registerLifeCycle() } registerLifeCycle() { // Initialize the registration lifecycle hook this.ApiInstance.path = 'internal' Cycle.forEach((name) => { this.ApiInstance.registerMethod({ name }) }) } setStage(stage: ICoreStage) { this.stage = stage } init() { this.initPlugins = resolvePlugins({ plugins: this.internalPlugins, cwd: this.cwd }) // duplicate processing, no need to deal with it later this.babelRegister(uniq(this.initPlugins.map((plugin) => slash(plugin.path)))) env(path.join(this.cwd, '.env')) } async applyHooks(options: ICoreApplyHook) { const { add, modify, event } = this.ApplyHookType const { key, type, args } = options let { initialValue } = options if (type === add && initialValue && !Array.isArray(initialValue)) { throw new Error('when ApplyHooksType is `add`, initialValue must be an array') } if (type === add && initialValue === undefined) { initialValue = [] } const hooks = this.hooksByPluginId[key] ?? [] const asyncHook = new AsyncHook() // Add hook method into the actuator // Prepare for later const apply = (func: (hook: IHook) => (memo: any) => Promise<any>) => { asyncHook.tap( hooks.map((hook) => ({ before: hook.before, name: hook.pluginId, stage: hook.stage, fn: func(hook) })) ) } // `add` requires return values, these return values will eventually be combined into an array // `modify`, need to modify the first parameter and return // `event`, no return value switch (type) { case add: apply((hook) => async (memo) => { const items = await hook.fn(args) return memo.concat(items) }) break case modify: apply((hook) => async (memo) => hook.fn(memo, args)) break case event: apply((hook) => async () => { await hook.fn(args) }) break default: throw new Error( `applyPlugin failed, type is not defined or is not matched, got ${type}.` ) } return asyncHook.tapCall(initialValue) } async readyPlugins() { this.setStage(ICoreStage.init) this.extraPlugins = cloneDeep(this.initPlugins) this.setStage(ICoreStage.initPlugins) while (this.extraPlugins.length) { const { path, apply } = this.extraPlugins.shift()! this.ApiInstance.path = path // guarantee that you can use it when you register // may change later // this is not very good const api = new Proxy(this.ApiInstance, { get: (target, prop: string) => { if (prop === 'config' && this.stage < ICoreStage.pluginReady) { console.warn(`Cannot get config before plugin registration`) } // circular reference here if (prop === 'ApiInstance') { return undefined } // the plugin Method has the highest weight, // followed by Service finally plugin API // Because pluginMethods needs to be available in the register phase // the latest updates must be obtained through the agent dynamics // to achieve the effect of registration and use return ( this.pluginMethods[prop] ?? (this[prop] ? typeof this[prop] === 'function' ? this[prop].bind(this) : this[prop] : target[prop]) ) } }) // Plugin is cached here for checking this.plugins[path] = { path, apply } // Plugin or Plugins // Execute plugin method and pass in api.any // There are two situations here // 1. Import the plug-in collection, then return a string[] // 2. Execute plug-in method and pass in api // there is an extra, no `require` is used, but `import` is used // and ʻimport` is a Promise, so `await` is needed here // An error will be reported here because `ESlint` prohibits all circular use of `await` // It is safe to use `await` in a loop without callback // eslint-disable-next-line no-await-in-loop const rest = await apply()(api) // If it is an Array // It represents a collection of plugins added to the top of extraPlugins // Path verification pathToRegister has been done // `reverse` to ensure the order of plugins if (rest && Array.isArray(rest.plugins) && rest.plugins.length) { this.babelRegister(rest.plugins) rest.plugins.reverse().forEach((path) => { this.extraPlugins.unshift(pathToRegister(path)) }) } } this.setStage(ICoreStage.pluginReady) await this.applyEventHooks({ key: 'onPluginReady' }) } async readyConfig() { // merge defaults // verify config value this.setStage(ICoreStage.getConfig) this.config = await this.applyModifyHooks({ key: 'modifyConfig', initialValue: this.configInstance.getPluginConfig(this.initConfig) }) } /** * @name start * @param { string } options.args - other argument. * @param { string } options.command - command */ async start(options: ICoreStart) { const { args, command, reloadCommand } = options this.args = options // sometimes it needs to be distributed // for example: // Register an empty command, // execute the operation in the command plugin, // and then decide which command to execute if (!reloadCommand) { this.init() await this.readyPlugins() await this.readyConfig() this.setStage(ICoreStage.start) // potential problems, // do you need to repeat the implementation here to be verified await this.applyEventHooks({ key: 'onStart', args: { args } }) } const event = this.commands[command] assert(event, `start command failed, command "${command}" does not exists.`) return event.fn({ args }) } /** * @name reset * @desc In order not to fock here, you need to initialize the properties */ reset() { const property = ['hooksByPluginId', 'pluginMethods', 'plugins', 'commands'] property.forEach((key) => { this[key] = {} }) this.registerLifeCycle() } }