@fontoxml/fontoxml-development-tools
Version:
Development tools for Fonto.
286 lines (252 loc) • 8.14 kB
JavaScript
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);
}
}