class_base.js

import Plugin from './plugin.js'

/**
 * To be recognized as a valid Bajo plugin, your package must be an `ES6 module` and have a `package.json` file
 * with the additional `bajo` property and at least the `type` property set to `plugin`.
 *
 * Other than `type`, you might need to specify these additional properties:
 * - `type`: `string`, must be set to `plugin`
 * - `alias`: `string`, the alias name of your plugin. Must be unique among all plugins. If it is missing, it will be generated by Bajo as camel-cased version of your plugin namespace.
 * - `dependencies`: `array`, if your plugin requires one or more other plugins to work, you need to put their **package names** in this array
 * - `appletSupport`: `boolean`, if your plugin doesn't support applet mode, set this to `false` to skip `init()` and `start()` of your plugin
 *
 * Example:
 * ```json
 * {
 *   "name": "my-plugin",
 *   "version": "1.0.0",
 *   "description": "My Bajo plugin",
 *   "type": "module",
 *   "main": "index.js",
 *   "bajo": {
 *     "type": "plugin",
 *     "alias": "myplugin",
 *     "dependencies": ["bajo-config", "bajo-cli"]
 *   }
 * }
 * ...
 * ```
 *
 * > **Note**: The dot symbol in `package.json` has been replaced with `·` symbol because of JSDoc theme limitation
 *
 * > **Warning**: Do not confuse with your app's {@link package·json|package.json} specification, since this one is for plugin while the other is for app.
 * @memberof Base
 * @typedef package·json
 */

/**
 * Base class for all plugins.
 *
 * This is the class you must extend when creating a new plugin. It provides basic methods and properties to manage your plugin.
 *
 * You need to wrap your plugin in a factory function with a single parameter `pkgName`, which will be called by the Bajo framework
 * during boot process. The factory function must return your plugin class.
 *
 * Don't forget to write `package.json` for your plugin that follows {@link Base.package·json|this specification}. Please do not
 * confuse this with your app's {@link package·json|package.json} specification, since this one is for your plugin only.
 *
 * Example:
 * ```js
 * // index.js
 * async function factory (pkgName) {
 *   const { Base } = this.app.baseClass // get Base from app's baseClass repository
 *   const me = this // 'this' is Bajo instance. See Bajo boot process for more details
 *   class BajoCache extends Base {
 *     constructor () {
 *       super(pkgName, me.app)
 *       this.config = {} // your plugin's configuration object. If omitted, it will be set to an empty object
 *     }
 *
 *     init = async () => {
 *       // your plugin's initialization code here
 *     }
 *
 *     start = async () => {
 *       // your plugin's start code here
 *     }
 *   }
 *   return BajoCache
 * }
 * ```
 *
 * @class
 */

class Base extends Plugin {
  /**
   * Constructor.
   *
   * @param {string} pkgName - Package name (the one in package.json)
   * @param {Object} app - App instance reference. Useful to call app method inside a plugin
   */
  constructor (pkgName, app) {
    super(pkgName, app)

    /**
     * Array of plugin dependencies. If your plugin depends on other plugins, you can specify their package names here e.g. `['bajo-config', 'bajo-cli']`, NOT the plugin name/namespace
     * @type {string[]}
     */
    this.dependencies = []

    this.state = {}

    /**
     * Package information from `package.json`. It will be automatically loaded by the framework during plugin initialization
     * @type {object}
     */
    this.pkg = {}
  }

  /**
   * Load plugin configuration. This method will be called by the framework during boot process.
   *
   * @method
   * @async
   * @returns {Promise<void>}
   */
  loadConfig = async () => {
    const { defaultsDeep } = this.app.lib.aneka
    const { get, keys, pick, isEmpty, upperFirst, camelCase } = this.app.lib._
    const { log, getModuleDir, readAllConfigs } = this.app.bajo
    const { parseObject } = this.app.lib

    const defKeys = keys(this.config).concat(this.app.getAllNs())
    defKeys.push('title')
    log.trace('- %s', this.ns)
    const dir = this.ns === this.app.mainNs ? (`${this.app.bajo.dir.base}/${this.app.mainNs}`) : getModuleDir(this.pkgName)
    let cfg = {}

    this.dir = {
      pkg: dir,
      data: `${this.app.bajo.dir.data}/plugins/${this.ns}`
    }
    // merge with config from datadir
    try {
      let altCfg = get(this, `app.options.config.${this.ns}`, {})
      if (isEmpty(altCfg)) altCfg = await readAllConfigs(`${this.app.bajo.dir.data}/config/${camelCase(this.ns)}`)
      cfg = defaultsDeep({}, altCfg, cfg)
    } catch (err) {}
    const cfgEnv = get(this, `app.env.${this.ns}`, {})
    const cfgArgv = get(this, `app.argv.${this.ns}`, {})
    const envValue = this[`config${upperFirst(this.app.bajo.config.env)}`]
    cfg = pick(defaultsDeep({}, envValue ?? {}, cfgEnv ?? {}, cfgArgv ?? {}, cfg ?? {}, envValue ?? {}, this.config ?? {}), defKeys)
    this.config = parseObject(cfg, { parseValue: true })
  }

  /**
   * Plugin initialization. This method will be called by the framework during boot process, after configuration is loaded.
   * @method
   * @async
   * @returns {Promise<void>}
   */
  init = async () => {
  }

  /**
   * Plugin start. This method will be called by the framework during boot process, after initialization. You still can
   * modifiy your plugin's configuration here before it is deep frozen.
   * @method
   * @async
   * @returns {Promise<void>}
   */
  start = async () => {
  }

  /**
   * Reserved for future use. This method will be called before plugin is stopped.
   * @method
   * @async
   * @returns {Promise<void>}
   */
  stop = async () => {
  }

  /**
   * Upon app termination, this method will be called first. Mostly useful for system cleanup,
   * delete temporary files, freeing resources etc.
   * @method
   * @async
   * @returns {Promise<void>}
   */
  exit = async () => {
    await this.dispose()
  }

  /**
   * Dispose internal references.
   * @async
   * @method
   * @returns {Promise<void>}
   */
  dispose = async () => {
    await super.dispose()
    this.state = null
  }
}

export default Base