class_helper_plugin.js

import createMethod from '../../lib/create-method.js'
import semver from 'semver'
import lodash from 'lodash'

const {
  merge,
  forOwn,
  groupBy,
  find,
  reduce,
  map,
  trim,
  keys,
  intersection,
  each,
  camelCase,
  get
} = lodash

/**
 * @module
 */

/**
 * Scan plugins ```method``` directories, and turn + attach its found files as methods dynamically.
 *
 * @async
 */
export async function attachMethods () {
  const { eachPlugins } = this.bajo
  const me = this // the app
  me.bajo.log.debug('attachMethods')
  await eachPlugins(async function () {
    const { name: ns, pkgName } = this
    const dir = ns === me.bajo.mainNs ? (`${me.bajo.dir.base}/${me.bajo.mainNs}`) : me.bajo.getModuleDir(pkgName)
    const num = await createMethod.call(me[ns], `${dir}/method`, pkgName)
    me.bajo.log.trace('- %s (%d)', ns, num)
  })
}

/**
 * Build configurations
 *
 * @async
 */
export async function buildConfigs () {
  this.bajo.log.debug('readConfigs')
  for (const pkg of this.bajo.pluginPkgs) {
    const plugin = this[camelCase(pkg)]
    await plugin.loadConfig()
    plugin.initPrint()
    plugin.initLog()
  }
}

/**
 * Ensure for names and aliases to be unique and no clashes with other plugins
 *
 * @async
 */
export async function checkNameAliases () {
  const { eachPlugins } = this.bajo
  this.bajo.log.debug('checkAliasNameClash')
  const refs = []
  await eachPlugins(async function () {
    const { name: ns, pkgName } = this
    const { alias } = this.constructor
    let item = find(refs, { ns })
    if (item) throw this.error('pluginNameClash%s%s%s%s', ns, pkgName, item.ns, item.pkgName, { code: 'BAJO_NAME_CLASH' })
    item = find(refs, { alias })
    if (item) throw this.error('pluginNameClash%s%s%s%s', alias, pkgName, item.alias, item.pkgName, { code: 'BAJO_ALIAS_CLASH' })
    refs.push({ ns, alias, pkgName })
  })
}

/**
 * Ensure dependencies are met
 *
 * @async
 */
export async function checkDependencies () {
  async function runner () {
    const { name: ns, pkgName } = this
    const { join } = this.app.bajo
    this.app.bajo.log.trace('- %s', ns)
    const { dependencies } = this.app.pluginFactory[this.name]
    const odep = reduce(dependencies, (o, k) => {
      const item = map(k.split('@'), m => trim(m))
      if (k[0] === '@') o['@' + item[1]] = item[2]
      else o[item[0]] = item[1]
      return o
    }, {})
    const deps = keys(odep)
    if (deps.length > 0) {
      if (intersection(this.app.bajo.pluginPkgs, deps).length !== deps.length) {
        throw this.error('dependencyUnfulfilled%s%s', pkgName, join(deps), { code: 'BAJO_DEPENDENCY' })
      }
      each(deps, d => {
        if (!odep[d]) return
        const ver = get(this.app[camelCase(d)], 'config.pkg.version')
        if (!ver) return
        if (!semver.satisfies(ver, odep[d])) {
          throw this.error('semverCheckFailed%s%s', pkgName, `${d}@${odep[d]}`, { code: 'BAJO_DEPENDENCY_SEMVER' })
        }
      })
    }
  }

  const { eachPlugins } = this.bajo
  this.bajo.log.debug('checkDeps')
  await eachPlugins(async function () {
    await runner.call(this)
  })
}

/**
 * Collect and build hooks and push them to the bajo's hook system
 *
 * @async
 */
export async function collectHooks () {
  const { eachPlugins, runHook, isLogInRange, importModule, breakNsPathFromFile } = this.bajo
  const me = this
  me.bajo.hooks = this.bajo.hooks ?? []
  me.bajo.log.debug('collectHooks')
  // collects
  await eachPlugins(async function ({ dir, file }) {
    const { name: ns } = this
    const { fullNs, path } = breakNsPathFromFile({ file, dir, baseNs: ns, suffix: '/hook/' })
    const mod = await importModule(file, { asHandler: true })
    if (!mod) return undefined
    merge(mod, { ns: fullNs, path, src: ns })
    me.bajo.hooks.push(mod)
  }, { glob: 'hook/**/*.js', prefix: me.bajo.name })
  // for log trace purpose only
  if (!isLogInRange('trace')) return
  const items = groupBy(me.bajo.hooks, 'ns')
  forOwn(items, (v, k) => {
    const hooks = groupBy(v, 'path')
    forOwn(hooks, (v1, k1) => {
      me.bajo.log.trace('- %s:%s (%d)', k, k1, v1.length)
    })
  })
  // run handler
  await runHook('bajo:afterCollectHooks')
}

/**
 * Finally, run all plugins
 *
 * @async
 */
export async function run () {
  const me = this
  const { runHook, eachPlugins, join } = me.bajo
  const { freeze } = me.bajo
  const methods = ['init']
  if (!me.bajo.applet) methods.push('start')
  for (const method of methods) {
    await runHook(`bajo:${camelCase(`before ${method} all plugins`)}`)
    await eachPlugins(async function () {
      const { name: ns } = this
      if (method === 'start') freeze(me[ns].config)
      await runHook(`${ns}:${camelCase(`before ${method}`)}`)
      await me[ns][method]()
      await runHook(`${ns}:${camelCase(`after ${method}`)}`)
    })
    await runHook(`bajo:${camelCase(`after ${method} all plugins`)}`)
  }
  me.bajo.log.debug('loadedPlugins%s', join(map(me.bajo.pluginPkgs, b => camelCase(b))))
}