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