@fontoxml/fontoxml-development-tools
Version:
Development tools for Fonto.
437 lines (408 loc) • 12.5 kB
JavaScript
import BackendAppRepository from './BackendAppRepository.js';
import EditorRepository from './EditorRepository.js';
import { getUpgradeSupportedVersions } from './versionMapping.js';
/** @typedef {import('./App').default} App */
/** @typedef {import('./Version').default} Version */
/** @typedef {import('./request/FdtRequest').default} FdtRequest */
/** @typedef {import('./response/FdtResponse').default} FdtResponse */
/**
* All intents check if the specified version is the same minor range as the base version (i.e. the
* fdt version). The only exception is `instance-for-update`, which also allows the specified
* version to be one minor version lower.
*
* The intents are also used to output specific warning/error messages.
*
* @typedef {'instance' | 'instance-for-upgrade' | 'upgrade' | 'initialize' | 'download' | 'schema-compile'} CompatibilityIntent
*/
/**
* @param {CompatibilityIntent} intent
* @param {string} productLabel
* @param {string|null} versionString
*
* @returns {string}
*/
function getDescriptionFromIntent(intent, productLabel, versionString = null) {
switch (intent) {
case 'initialize':
case 'download':
return ` to ${intent} ${productLabel}${
versionString ? ` ${versionString}` : ''
}`;
case 'upgrade':
return ` to upgrade ${productLabel}${
versionString ? ` to ${versionString}` : ''
}`;
case 'schema-compile':
return ` to compile a schema for ${productLabel}${
versionString ? ` ${versionString}` : ''
}`;
default:
return '';
}
}
/**
* @template {CompatibilityIntent} Intent
*
* @param {Intent} intent
* @param {Version} fdtVersion
* @param {Version} version
* @param {string} productLabel
*
* @returns {Intent extends 'instance-for-upgrade' ? null : string}
*/
export function createCompatibilityMessageForTrue(
intent,
fdtVersion,
version,
productLabel
) {
return `The FDT version ${fdtVersion.format()} does not match version ${version.format()} of ${productLabel}. To guarantee compatibility, please use exactly matching versions${getDescriptionFromIntent(
intent,
productLabel
)}.`;
}
/**
* @template {CompatibilityIntent} Intent
*
* @param {Intent} intent
* @param {Version} fdtVersion
* @param {Version} version
* @param {string} productLabel
* @param {Intent extends 'instance-for-upgrade' ? Version[] : undefined} upgradeSupportedVersions
*
* @returns {string}
*/
export function createCompatibilityMessageForFalse(
intent,
fdtVersion,
version,
productLabel,
upgradeSupportedVersions
) {
const versionsMessage = `The FDT version ${fdtVersion.format()} is a different minor version than ${productLabel} version ${version.format()}.`;
const supportedTargetVersions = fdtVersion.isUnversioned
? fdtVersion.format()
: `${fdtVersion.format('minor')}.x`;
switch (intent) {
case 'instance':
return `${versionsMessage} This version of FDT only supports versions ${supportedTargetVersions}.`;
case 'instance-for-upgrade': {
const supportedVersions = upgradeSupportedVersions
.map(
(version) =>
`"${
version.isUnversioned
? version.format()
: `${version.format('minor')}.x`
}"`
)
.join(', ');
return `${versionsMessage} This version of FDT only supports upgrading from the following ${productLabel} versions: ${supportedVersions}.`;
}
case 'upgrade':
case 'initialize':
case 'schema-compile':
case 'download':
return `The FDT version ${fdtVersion.format()} should not be used${getDescriptionFromIntent(
intent,
productLabel,
version.format()
)}. This version of FDT only supports versions ${supportedTargetVersions}.`;
default:
throw new Error(
`Cannot create compatibility message for false with intent ${intent}.`
);
}
}
/**
* @param {CompatibilityIntent} _intent
* @param {Version} fdtVersion
* @param {Version} version
* @param {string} productLabel
*
* @returns {string}
*/
export function createCompatibilityMessageForNightlyA(
_intent,
fdtVersion,
version,
productLabel
) {
return `The FDT version ${fdtVersion.format()} is a nightly. This may not be compatible with ${productLabel} version ${version.format()}.`;
}
/**
* @param {CompatibilityIntent} intent
* @param {Version} fdtVersion
* @param {Version} version
* @param {string} productLabel
*
* @returns {string}
*/
export function createCompatibilityMessageForNightlyB(
intent,
fdtVersion,
version,
productLabel
) {
switch (intent) {
case 'instance':
case 'instance-for-upgrade':
return `The current ${productLabel} version ${version.format()} is a nightly. This may not be compatible with FDT version ${fdtVersion.format()}.`;
case 'upgrade':
case 'initialize':
case 'schema-compile':
case 'download':
return `You selected version ${version.format()}${getDescriptionFromIntent(
intent,
productLabel
)}. This is a nightly, which may not be compatible with FDT version ${fdtVersion.format()}.`;
default:
throw new Error(
`Cannot create compatibility message for nightly_b with intent ${intent}.`
);
}
}
/**
* @param {CompatibilityIntent} _intent
* @param {Version} fdtVersion
* @param {Version} version
* @param {string} productLabel
*
* @returns {string}
*/
function createCompatibilityMessageForUnparsedLiteralA(
_intent,
fdtVersion,
version,
productLabel
) {
return `Unable to parse the FDT version ${fdtVersion.format()}. Please try reinstalling FDT, or otherwise make sure it is compatible with ${productLabel} version ${version.format()}.`;
}
/**
* @param {CompatibilityIntent} _intent
* @param {Version} fdtVersion
* @param {Version} version
* @param {string} productLabel
*
* @returns {string}
*/
function createCompatibilityMessageForUnparsedLiteralB(
_intent,
fdtVersion,
version,
productLabel
) {
return `Unable to parse version ${version.format()} of ${productLabel}. Please make sure it is compatible with FDT version ${fdtVersion.format()}.`;
}
/**
* @param {FdtRequest} request
* @param {FdtResponse} response
* @param {Version} version
* @param {CompatibilityIntent} intent
* @param {string} [productId]
* @param {string} [productLabel]
*
* @throws An error when it is not compatible.
* @return {void | never}
*/
function ensureCompatibilityWithFdt(
request,
response,
version,
intent,
productId,
productLabel
) {
/** @type {Version} */
const fdtVersion = request.fdt.version;
/** @type {boolean} */
const fdtIsLocalVersion = !!request.fdt.isLocalVersion;
// For upgrade:
// * fdtVersion === versionToUpgradeTo (Should be checked in the upgrade commands)
// * currentVersion === versionToUpgradeTo - 1minor
// * currentVersion === fdtVersion - 1minor
// Determine the product label.
if (!productLabel) {
productLabel = productId
? request.fdt.license.getProductLabel(productId) ||
`Fonto ${productId}`
: 'Fonto project';
}
const upgradeSupportedVersions =
intent === 'instance-for-upgrade'
? getUpgradeSupportedVersions(fdtVersion)
: null;
const compatibilityResult = fdtVersion.checkCompatibility(
version,
intent === 'instance-for-upgrade' ? 'upgrade_minor' : 'minor',
upgradeSupportedVersions
);
switch (compatibilityResult) {
case true:
if (
!request.command.isRawOutput(request) &&
fdtVersion.compare(version) !== 0 &&
intent !== 'instance-for-upgrade'
) {
// Output a warning to encourage the use of the exact same full version (including
// patch, pre-release, etc.) if it is not.
// Except for when the intent is 'instance-for-upgrade' because the current version
// will most likely not match the FDT version, as you are upgrading to the FDT
// version (of compatible version).
response.notice(
createCompatibilityMessageForTrue(
intent,
fdtVersion,
version,
productLabel
)
);
response.break();
}
return;
case false: {
const error = new response.ErrorWithSolution(
createCompatibilityMessageForFalse(
intent,
fdtVersion,
version,
productLabel,
upgradeSupportedVersions || []
)
);
// eslint-disable-next-line no-unneeded-ternary
const defaultEnforceCompatibility = fdtIsLocalVersion
? false
: // TODO: Change the `defaultEnforceCompatibility` to `true` when we want to start enforcing version compatibility.
false;
const enforceCompatibility =
typeof process.env.FDT_ENFORCE_COMPATIBILITY !== 'string'
? defaultEnforceCompatibility
: !['0', 'false', 'no', 'n', 'off'].includes(
process.env.FDT_ENFORCE_COMPATIBILITY
);
if (!enforceCompatibility) {
if (!request.command.isRawOutput(request)) {
response.error(error.message);
if (error.solution) {
response.break();
response.notice(error.solution);
}
response.break();
}
return;
}
throw error;
}
case 'nightly_a':
if (!request.command.isRawOutput(request)) {
response.notice(
createCompatibilityMessageForNightlyA(
intent,
fdtVersion,
version,
productLabel
)
);
response.break();
}
return;
case 'nightly_b':
if (!request.command.isRawOutput(request)) {
response.notice(
createCompatibilityMessageForNightlyB(
intent,
fdtVersion,
version,
productLabel
)
);
response.break();
}
return;
case 'unparsed_literal_a':
// Note that this can only happen when one of the versions is constructed with
// `allowUnparsedLiteral` set to true.
if (!request.command.isRawOutput(request)) {
response.notice(
createCompatibilityMessageForUnparsedLiteralA(
intent,
fdtVersion,
version,
productLabel
)
);
response.break();
}
return;
case 'unparsed_literal_b':
// Note that this can only happen when one of the versions is constructed with
// `allowUnparsedLiteral` set to true.
if (!request.command.isRawOutput(request)) {
response.notice(
createCompatibilityMessageForUnparsedLiteralB(
intent,
fdtVersion,
version,
productLabel
)
);
response.break();
}
return;
default:
throw new Error(
`Version compatibility check did not return positive for ${intent} ("${compatibilityResult}").`
);
}
}
/**
* @param {FdtRequest} request
* @param {FdtResponse} response
* @param {App} app
*
* @return {Promise<void>}
*/
export default async function enrichRequestObject(request, response, app) {
const backendAppRepository = new BackendAppRepository();
await backendAppRepository.init(app, request);
const editorRepository = new EditorRepository();
await editorRepository.init(app, request);
request.fdt = {
version: app.version,
isLocalVersion: !!app.isLocalVersion,
backendAppRepository,
editorRepository,
getExportFromModule: app.getExportFromModule.bind(app),
getPathToModule: app.getPathToModule.bind(app),
license: app.license,
/**
* @param {Version} version The version to check compatibility with FDT for.
* @param {CompatibilityIntent} intent Indicates the intent for the compatibility
* check which affects the rules and messages.
* @param {string} [productId] Used to determine the product label if
* `productLabel` is not specified.
* @param {string} [productLabel] Will try to get the label based on the
* `procuctId` if not specified, or default to
* 'Fonto project' if no `productId` is specified.
*
* @throws An error when it is not compatible.
* @return {void | never}
*/
ensureCompatibilityWithFdt: (
version,
intent,
productId,
productLabel
) => {
return ensureCompatibilityWithFdt(
request,
response,
version,
intent,
productId,
productLabel
);
},
};
}