adapt-authoring-core/lib/App.js

import AbstractModule from './AbstractModule.js'
import Config from './Config.js'
import DependencyLoader from './DependencyLoader.js'
import Errors from './Errors.js'
import Lang from './Lang.js'
import Logger from './Logger.js'
import fs from 'fs'
import path from 'path'
import { runMigrations } from 'adapt-authoring-migrations'
import { metadataFileName, packageFileName, getArgs } from './Utils.js'

let instance
/**
 * Core functionality
 * @namespace core
 */
/**
 * The main application class
 * @memberof core
 * @extends {AbstractModule}
 */
class App extends AbstractModule {
  /**
   * The singleton instance. Self-initialises it if there isn't one.
   * @type {App}
   */
  static get instance () {
    if (!instance) instance = new App()
    return instance
  }

  /** @override */
  constructor () {
    process.env.NODE_ENV ??= 'production'
    const rootDir = process.env.ROOT_DIR ?? process.cwd()
    const adaptJson = JSON.parse(fs.readFileSync(path.join(rootDir, metadataFileName)))
    const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, packageFileName)))
    super(null, { ...packageJson, ...adaptJson, name: 'adapt-authoring-core', rootDir })
  }

  /** @override */
  async init () {
    try {
      /**
       * Instance of App instance (required by all AbstractModules)
       * @type {App}
       */
      this.app = this
      /**
       * Reference to the passed arguments (parsed for easy reference)
       * @type {Object}
       */
      this.args = getArgs()
      /**
       * Reference to the Config instance
       * @type {Config}
       */
      this.config = undefined
      /**
       * Reference to the error registry
       * @type {Errors}
       */
      this.errors = undefined
      /**
       * Reference to the Lang instance
       * @type {Lang}
       */
      this.lang = undefined
      /**
       * Reference to the Logger instance
       * @type {Logger}
       */
      this.logger = new Logger()
      /**
       * Reference to the DependencyLoader instance
       * @type {DependencyLoader}
       */
      this.dependencyloader = new DependencyLoader(this)
      /**
       * Git metadata for the application (branch and commit hash)
       * @type {Object}
       */
      this.git = this.getGitInfo()

      await this.dependencyloader.loadConfigs()

      const options = {
        dependencies: this.dependencies,
        configFilePath: path.join(this.rootDir, 'conf', `${process.env.NODE_ENV}.config.js`),
        rootDir: this.rootDir,
        log: (...args) => this.logger.log(...args)
      }

      await runMigrations({ ...options, dryRun: this.args['dry-run'] === true })

      this.config = await new Config({ ...options, appName: this.name }).load()
      this.logger = new Logger({ levels: this.getConfig('logLevels'), showTimestamp: this.getConfig('showLogTimestamp') })
      this.errors = new Errors(options)
      this.lang = new Lang({ ...options, defaultLang: this.getConfig('defaultLang') })

      await this.dependencyloader.loadModules()

      this.log('verbose', 'GIT', 'INFO', this.git)
      this.log('verbose', 'DIR', 'rootDir', this.rootDir)
      this.log('verbose', 'DIR', 'dataDir', this.getConfig('dataDir'))
      this.log('verbose', 'DIR', 'tempDir', this.getConfig('tempDir'))
    } catch (cause) {
      await this.setReady(new Error('Failed to start App', { cause }))
      process.exit(1)
    }
    const failedMods = this.dependencyloader.failedModules
    if (failedMods.length) this.log('warn', `${failedMods.length} module${failedMods.length === 1 ? '' : 's'} failed to load: ${failedMods}. See above for details`)
  }

  /**
   * The Adapt module dependencies and their configs
   * @type {Object}
   */
  get dependencies () {
    return this.dependencyloader.configs
  }

  /**
   * Attempts to load and parse the local git data for the root repository
   * @returns {Object}
   */
  getGitInfo () {
    try {
      const gitRoot = path.join(this.rootDir, '.git')
      const gitHead = fs.readFileSync(path.join(gitRoot, 'HEAD'), 'utf8').trim()
      return {
        branch: gitHead.split('/').pop(),
        commit: fs.readFileSync(path.join(gitRoot, gitHead.split(': ')[1]), 'utf8').trim()
      }
    } catch (e) {
      return {}
    }
  }

  /**
   * Enables waiting for other modules to load
   * @param {...String} modNames Names of modules to wait for
   * @return {Promise} Resolves when specified module has been loaded
   */
  async waitForModule (...modNames) {
    const results = await Promise.all(modNames.map(m => this.dependencyloader.waitForModule(m)))
    return results.length > 1 ? results : results[0]
  }
}

export default App