hins
Version:
[](https://codecov.io/gh/l-zoy/hins) [](https://github.com/l-zoy/hins/blob/master/LICENSE)  • 10.2 kB
text/typescript
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()
}
}