UNPKG

@fontoxml/fontoxml-development-tools

Version:

Development tools for Fonto.

286 lines (252 loc) 8.14 kB
import { readFileSync } from 'fs'; import path from 'path'; import { pathToFileURL } from 'url'; import DeepOption from './request/DeepOption.js'; import DeepParameter from './request/DeepParameter.js'; import InputError from './request/InputError.js'; import IsolatedOption from './request/IsolatedOption.js'; import MultiOption from './request/MultiOption.js'; import Option from './request/Option.js'; import Parameter from './request/Parameter.js'; import createLazyLoadedFunction from './createLazyLoadedFunction.js'; const APP = Symbol('app'); const CONTEXTINFORMERS = Symbol('context informers'); const EXPORTS = Symbol('exports'); const LOCATION = Symbol('location'); const PACKAGEJSON = Symbol('package.json'); /** @typedef {import('./App').default} App */ /** @typedef {import('./ConfigManager').default} ConfigManager */ /** @typedef {import('./request/FdtCommand').default} FdtCommand */ /** @typedef {(request: FdtRequest, response: FdtResponse) => (void | Promise<void>)} ContextInformer */ /** * The unified API for registering module functionality with the FDT instance. The callback exposed * by a module will get an instance of ModuleRegistrationApi to register itself with. */ export default class ModuleRegistrationApi { /** * Should not be called by a module. * * @param {App} app The app. * @param {string} moduleLocation The location of the module that is being registered. * * @constructor */ constructor(app, moduleLocation) { /** @type {App} */ this[APP] = app; /** @type {string} */ this[LOCATION] = moduleLocation; /** @type {{ name: string, version?: string, description?: string }} */ this[PACKAGEJSON] = JSON.parse( readFileSync(path.join(this[LOCATION], 'package.json'), 'utf8') ); /** @type {{ [exportName: string]: { exportPath: string; exportIdentifier: string; value?: unknown } }} */ this[EXPORTS] = []; /** @type {ContextInformer[]} */ this[CONTEXTINFORMERS] = []; /** @type {boolean} */ this.hidden = false; this.DeepOption = DeepOption; this.DeepParameter = DeepParameter; this.InputError = InputError; this.IsolatedOption = IsolatedOption; this.MultiOption = MultiOption; this.Option = Option; this.Parameter = Parameter; } /** * Should not be called by a module. Loads and evaluates the Javascript code belonging to a module. * * @param {...any} extraArguments */ async load(...extraArguments) { const module = await import( pathToFileURL(path.join(this[LOCATION], 'index.js')) ); const defaultExport = module.default; if (typeof defaultExport !== 'function') { throw new Error(`${this[PACKAGEJSON].name} is not a function.`); } await defaultExport(this, ...extraArguments); } /** * Passes on the information returned by App#getInfo. * * @see App#getInfo * * @return {{name: string, version?: Version}} The app information. */ getAppInfo() { return this[APP].getInfo(); } /** * Returns metadata about the module that is being registered. * * @return {{name: string, version?: string, builtIn: boolean, description?: string, path: string}} The module information. */ getInfo() { return { name: this[PACKAGEJSON].name, version: this[PACKAGEJSON].version, builtIn: this[APP].builtInModules.some((mod) => mod === this), description: this[PACKAGEJSON].description, path: this[LOCATION], }; } /** * Returns the path to this module. * * @return {string} */ getPath() { return this[LOCATION]; } /** * Retrieve the path to a registered module by its name. * * NOTE: Only paths to modules loaded earlier than the current module can be looked up. * * @param {string} moduleName The name of the module (see its package.json). * * @return {(string|undefined)} */ getPathToModule(moduleName) { return this[APP].getPathToModule(moduleName); } /** * Adds a command to the root of the FDT instance. * * @see FdtCommand#addCommand * * @param {string} commandName * @param {string|(request: FdtRequest, response: FdtResponse) => (void | Promise<void>)} [controller] * * @return {FdtCommand} The command object that was created. */ registerCommand(commandName, controller) { const command = this[APP].cli.addCommand(commandName, controller); command._moduleRegistration = this; return command; } /** * Adds a hidden command to the root of the FDT instance. * * @see FdtCommand#addHiddenCommand * * @param {string} commandName * @param {string|(request: FdtRequest, response: FdtResponse) => (void | Promise<void>)} [controller] * * @return {FdtCommand} The command object that was created. */ registerHiddenCommand(commandName, controller) { const command = this[APP].cli.addHiddenCommand(commandName, controller); command._moduleRegistration = this; return command; } /** * NOTE: Only use this from the module's index.js, not from within commands, or else your config might be overridden. * * @see ConfigManager#registerConfiguration * * @template {*} [TValue=*] * * @param {string} configName * @param {TValue} defaultValue * @param {*|((config: TValue) => * | null)} [serialize] * * @return {TValue} The configuration value. */ registerConfiguration(configName, defaultValue, serialize) { return this[APP].config.registerConfig( configName, defaultValue, serialize ); } /** * Get the registered configuration. * * @template {*} [TValue=*] * * @param {string} configName * * @return {TValue} The configuration value. */ getConfiguration(configName) { return this[APP].config[configName]; } /** * Register a subcontroller to render additional info provided by a module in the "who" command * provided by FDT. * * @param {string | ContextInformer} contextInformer * * @return {ContextInformer} The context informer. */ registerContextInformer(contextInformer) { if (typeof contextInformer === 'string') { contextInformer = createLazyLoadedFunction(contextInformer); } this[CONTEXTINFORMERS].push(contextInformer); return contextInformer; } /** * Should not be called by a module. Return the list of context informers registered by this module. * * @return {ContextInformer[]} The context informers. */ getContextInformers() { return this[CONTEXTINFORMERS].slice(0); } /** * Registers an export which can be used by other modules. * * @param {string} exportName The name of the export by which it can be retrieved. * @param {string} exportPath The absolute file path to where the export is located. * @param {string} [exportIdentifier='default'] The identifier by which it is exported in the file specified by `exportPath`. * * @return {string} The export path. */ registerExport(exportName, exportPath, exportIdentifier = 'default') { if (!path.isAbsolute(exportPath)) { throw new Error( `Export "${exportName}" should be registered using an absolute path instead of a relative one.`, ); } this[EXPORTS][exportName] = { exportPath, exportIdentifier }; return exportName; } /** * Get an export from this module. * * @param {string} exportName The name of the export to retrieve. * * @return {Promise<unknown | null>} The export value if found, otherwise null. */ async getExport(exportName) { const exportDefinition = this[EXPORTS][exportName]; if (!exportDefinition) { return null; } if (!exportDefinition.value) { const module = await import(pathToFileURL(exportDefinition.exportPath)); const value = module[exportDefinition.exportIdentifier]; if (!value) { return null; } exportDefinition.value = value; } return exportDefinition.value; } /** * Get an export from the specified module. * * @param {string} moduleName The name of the module (see its package.json). * @param {string} exportName The name of the export to retrieve. * * @return {Promise<unknown | null>} The export value if found, otherwise null. */ async getExportFromModule(moduleName, exportName) { return this[APP].getExportFromModule(moduleName, exportName); } }