UNPKG

@fontoxml/fontoxml-development-tools

Version:

Development tools for Fonto.

478 lines (422 loc) 12.9 kB
import path from 'path'; import { pathToFileURL } from 'url'; import Command from './Command.js'; import createLazyLoadedFunction from '../createLazyLoadedFunction.js'; /** @typedef {import('../ModuleRegistrationApi').default} ModuleRegistrationApi */ /** @typedef {import('../response/FdtResponse').default} FdtResponse */ /** @typedef {import('./FdtRequest').default} FdtRequest */ /** * A FDT custom version of Command that has extra options for checking Fonto specific commands. * * @augments Command */ export default class FdtCommand extends Command { /** * @constructor * @param {string} commandName * @param {(request: FdtRequest, response: FdtResponse) => (void | Promise<void>)} [controller] */ constructor(commandName, controller) { super(commandName, controller); /** @type {ModuleRegistrationApi | null} */ this._moduleRegistration = null; /** @type {{ caption: string, content: string }[]} */ this.examples = []; /** @type {boolean} */ this.isHelpCommand = false; /** @type {string | null} */ this.longDescription = null; /** @type {boolean} */ this.hideIfMissingRequiredProductLicenses = false; /** @type {string[]} */ this.requiredProductLicenses = []; /** @type {boolean} */ this.requiresBackendAppRepository = false; /** @type {boolean} */ this.requiresEditorRepository = false; /** @type {boolean} */ this.requiresToValidateLicense = false; /** @type {{ forUpgrade: boolean, productId?: string, productLabel?: string } | false} */ this.requiresBackendAppVersionCheck = false; /** @type {{ forUpgrade: boolean } | false} */ this.requiresEditorVersionCheck = false; /** @type {boolean | (request: FdtRequest) => boolean} */ this.rawOutput = false; this.setNewChildClass(FdtCommand); this.addPreController(this._preController.bind(this)); } /** * Get the module registration to which this command, or it's ancestors, belongs. * * @return {ModuleRegistrationApi|null} The module registration. */ getModuleRegistration() { let command = this; while (command) { if (command._moduleRegistration) { return command._moduleRegistration; } command = command.parent; } return null; } /** * Set the main controller. * * @param {string|(request: FdtRequest, response: FdtResponse) => (void | Promise<void>)} [controller] * * @return {FdtCommand} This command. */ setController(controller) { if (typeof controller === 'string') { controller = createLazyLoadedFunction(controller); } return super.setController(controller); } /** * Register an example usage of this command. Each example is rendered as a definition item, * meaning the definition is indented below the caption. * * @param {string} caption * @param {string} content * * @return {FdtCommand} */ addExample(caption, content) { this.examples.push({ caption, content, }); return this; } /** * Register a hidden command as a child of this, and register this as parent of the child. * * @param {string|Command} commandName The identifying name of this option, unique for its ancestry. * @param {string|(request: FdtRequest, response: FdtResponse) => (void | Promise<void>)} [controller] * * @return {Command} The child command. */ addHiddenCommand(commandName, controller) { const addedCommand = super.addCommand(commandName, controller); addedCommand.hidden = true; return addedCommand; } /** * Describe a hidden option. * * @param {Option|string} long The identifying name of this option, unique for its ancestry. * @param {string} [short] A one-character alias of this option, unique for its ancestry. * @param {string} [description] A description for the option. * @param {boolean} [required] If true, an omittance would throw an error. * * @return {Command} */ addHiddenOption(long, short, description, required) { super.addOption(long, short, description, required); const addedOption = this.options[this.options.length - 1]; addedOption.hidden = true; return this; } /** * Register a long description for a command - one that is printed in the full width of the terminal. * * @param {string} description * @return {FdtCommand} */ setLongDescription(description) { this.longDescription = description; return this; } /** * Configure this command to not execute a controller, but instead output the help as if the * --help option was passed to the command. * * Accepts either a boolean or a function which recieves the request object and should return a * boolean indicating if the command should be a help command. This can be useful when an option * is passed to a command which by default would otherwise pass a help command. * * @param {boolean|(request: FdtRequest) => boolean} [value=true] * * @return {FdtCommand} */ setAsHelpCommand(value = true) { this.isHelpCommand = value; return this; } /** * @param {FdtRequest} request * * @returns {boolean} */ isAsHelpCommand(request) { if (typeof this.isHelpCommand === 'function') { return !!this.isHelpCommand(request); } return !!this.isHelpCommand; } /** * Gets the "long" name of a command, which includes the parameters and the long names of it's ancestors. * * @return {string} */ getLongName() { return (this.parent ? [this.parent.getLongName()] : []) .concat([this.name]) .concat(this.parameters.map((parameter) => `<${parameter.name}>`)) .join(' '); } /** * Pre-controller which performs optional checks before executing the actual controller. * * @param {FdtRequest} req * @param {FdtResponse} res * * @return {Promise} */ _preController(req, res) { let preControllerPromise = Promise.resolve(); if (this.requiresBackendAppRepository) { preControllerPromise = preControllerPromise.then(() => { if (typeof this.requiresBackendAppRepository === 'string') { req.fdt.backendAppRepository.throwIfNotInsideBackendAppRepository( this.requiresBackendAppRepository ); } else { req.fdt.backendAppRepository.throwIfNotInsideBackendAppRepository(); } }); } if (this.requiresEditorRepository) { preControllerPromise = preControllerPromise.then(() => { req.fdt.editorRepository.throwIfNotInsideEditorRepository(); }); } if (this.requiresBackendAppVersionCheck) { preControllerPromise = preControllerPromise.then(() => { try { req.fdt.backendAppRepository.throwIfNotInsideBackendAppRepository( this.requiresBackendAppVersionCheck.productId ); req.fdt.ensureCompatibilityWithFdt( req.fdt.backendAppRepository.sdkVersion, this.requiresBackendAppVersionCheck.forUpgrade ? 'instance-for-upgrade' : 'instance', this.requiresBackendAppVersionCheck.productId || req.fdt.backendAppRepository.productId, this.requiresBackendAppVersionCheck.productLabel ); } catch (innerError) { const error = innerError instanceof res.InputError ? innerError : new res.ErrorWithSolution( 'Could not check version compatibility.', 'Make sure you are using a compatible FDT version.', innerError ); throw error; } }); } if (this.requiresEditorVersionCheck) { preControllerPromise = preControllerPromise.then(() => { try { req.fdt.editorRepository.throwIfNotInsideEditorRepository(); const productId = 'editor'; // Handle linked platform by skipping the version compatibility check. if ( !this.requiresEditorVersionCheck.forUpgrade && req.fdt.editorRepository.hasPlatformLinked && !req.command.isRawOutput(req) ) { // Determine the product label. const productLabel = req.fdt.license.getProductLabel(productId) || `Fonto ${productId}`; res.notice( `You are using a linked platform, please make sure this is compatible with FDT version "${req.fdt.version.format()}" and ${productLabel} version "${req.fdt.editorRepository.sdkVersion.format()}".` ); res.break(); return; } req.fdt.ensureCompatibilityWithFdt( req.fdt.editorRepository.sdkVersion, this.requiresEditorVersionCheck.forUpgrade ? 'instance-for-upgrade' : 'instance', productId ); } catch (innerError) { const error = innerError instanceof res.InputError ? innerError : new res.ErrorWithSolution( 'Could not check version compatibility.', 'Make sure you are using a compatible FDT version.', innerError ); throw error; } }); } if (this.requiresToValidateLicense) { preControllerPromise = preControllerPromise.then(() => { const destroySpinner = req.command.isRawOutput(req) ? () => undefined : res.spinner('Validating license...'); return req.fdt.license .validateAndUpdateLicenseData() .then((result) => { destroySpinner(); return result; }) .catch((error) => { destroySpinner(); throw error; }); }); } if (this.requiredProductLicenses.length > 0) { preControllerPromise = preControllerPromise.then(() => { const destroySpinner = req.command.isRawOutput(req) ? () => undefined : res.spinner('Checking required product licenses...'); try { req.fdt.license.ensureProductLicenses( this.requiredProductLicenses ); destroySpinner(); } catch (error) { destroySpinner(); throw error; } return true; }); } return preControllerPromise; } /** * Add a list of required product licenses for performing this command. If the user does not have * access to a product, the command will fail. * * @param {Array<string>} productIds * * @return {FdtCommand} */ addRequiredProductLicenses(productIds) { this.requiredProductLicenses = this.requiredProductLicenses.concat(productIds); return this; } /** * Hide the command if a required product license is not available. * * @param {boolean} [hide=true] * * @return {FdtCommand} */ setHideIfMissingRequiredProductLicenses(hide = true) { this.hideIfMissingRequiredProductLicenses = !!hide; return this; } /** * If set, check the license for validity online before executing the command, fail * otherwise. * * @param {boolean} [validateLicense=true] * * @return {FdtCommand} */ setRequiresLicenseValidation(validateLicense = true) { this.requiresToValidateLicense = !!validateLicense; return this; } /** * If set, check if running from a backedn app repository before executing the command, fail * otherwise. * * @param {boolean|string} [value=true] * * @return {FdtCommand} */ setRequiresBackendAppRepository(value = true) { this.requiresBackendAppRepository = typeof value === 'string' ? value : !!value; return this; } /** * If set, check if running from an editor repository before executing the command, fail * otherwise. * * @param {boolean} [value=true] * * @return {FdtCommand} */ setRequiresEditorRepository(value = true) { this.requiresEditorRepository = !!value; return this; } /** * Check if the current Backend App project version is compatible with the FDT version. * * @param {string} [productId] * @param {boolean} [forUpgrade=false] * @param {string} [productLabel] * * @return {FdtCommand} */ setRequiresBackendAppVersionToMatchFdt( productId, forUpgrade = false, productLabel ) { this.requiresBackendAppVersionCheck = { productId, forUpgrade: !!forUpgrade, productLabel, }; return this; } /** * Check if the current Fonto Editor project version is compatible with the FDT version. * * @param {boolean} [forUpgrade=false] * * @return {FdtCommand} */ setRequiresEditorVersionToMatchFdt(forUpgrade = false) { this.requiresEditorVersionCheck = { forUpgrade: !!forUpgrade }; return this; } /** * If set, the output will be optimized for raw/machine readable output. * * Accepts either a boolean, or a function, which receives the request object, and should return * a boolean indicating if the command should use raw output. This can be useful when raw output * is dependend on, for example, an option. * * Setting to, or returning, true has the following impact: * * Use response.raw() to skip any output formatting. * * @param {boolean|(request: FdtRequest) => boolean} [value=true] * * @return {FdtCommand} */ setRawOutput(value = true) { this.rawOutput = value; return this; } /** * @param {FdtRequest} request * * @returns {boolean} */ isRawOutput(request) { if (typeof this.rawOutput === 'function') { return !!this.rawOutput(request); } return !!this.rawOutput; } }