UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

448 lines (447 loc) 13.5 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { isEqualOrParent, joinPath } from '@sussudio/base/common/resources.mjs'; import Severity from '@sussudio/base/common/severity.mjs'; import * as nls from 'vscode-nls.mjs'; import * as semver from '@sussudio/base/common/semver/semver.mjs'; const VERSION_REGEXP = /^(\^|>=)?((\d+)|x)\.((\d+)|x)\.((\d+)|x)(\-.*)?$/; const NOT_BEFORE_REGEXP = /^-(\d{4})(\d{2})(\d{2})$/; export function isValidVersionStr(version) { version = version.trim(); return version === '*' || VERSION_REGEXP.test(version); } export function parseVersion(version) { if (!isValidVersionStr(version)) { return null; } version = version.trim(); if (version === '*') { return { hasCaret: false, hasGreaterEquals: false, majorBase: 0, majorMustEqual: false, minorBase: 0, minorMustEqual: false, patchBase: 0, patchMustEqual: false, preRelease: null, }; } const m = version.match(VERSION_REGEXP); if (!m) { return null; } return { hasCaret: m[1] === '^', hasGreaterEquals: m[1] === '>=', majorBase: m[2] === 'x' ? 0 : parseInt(m[2], 10), majorMustEqual: m[2] === 'x' ? false : true, minorBase: m[4] === 'x' ? 0 : parseInt(m[4], 10), minorMustEqual: m[4] === 'x' ? false : true, patchBase: m[6] === 'x' ? 0 : parseInt(m[6], 10), patchMustEqual: m[6] === 'x' ? false : true, preRelease: m[8] || null, }; } export function normalizeVersion(version) { if (!version) { return null; } const majorBase = version.majorBase; const majorMustEqual = version.majorMustEqual; const minorBase = version.minorBase; let minorMustEqual = version.minorMustEqual; const patchBase = version.patchBase; let patchMustEqual = version.patchMustEqual; if (version.hasCaret) { if (majorBase === 0) { patchMustEqual = false; } else { minorMustEqual = false; patchMustEqual = false; } } let notBefore = 0; if (version.preRelease) { const match = NOT_BEFORE_REGEXP.exec(version.preRelease); if (match) { const [, year, month, day] = match; notBefore = Date.UTC(Number(year), Number(month) - 1, Number(day)); } } return { majorBase: majorBase, majorMustEqual: majorMustEqual, minorBase: minorBase, minorMustEqual: minorMustEqual, patchBase: patchBase, patchMustEqual: patchMustEqual, isMinimum: version.hasGreaterEquals, notBefore, }; } export function isValidVersion(_inputVersion, _inputDate, _desiredVersion) { let version; if (typeof _inputVersion === 'string') { version = normalizeVersion(parseVersion(_inputVersion)); } else { version = _inputVersion; } let productTs; if (_inputDate instanceof Date) { productTs = _inputDate.getTime(); } else if (typeof _inputDate === 'string') { productTs = new Date(_inputDate).getTime(); } let desiredVersion; if (typeof _desiredVersion === 'string') { desiredVersion = normalizeVersion(parseVersion(_desiredVersion)); } else { desiredVersion = _desiredVersion; } if (!version || !desiredVersion) { return false; } const majorBase = version.majorBase; const minorBase = version.minorBase; const patchBase = version.patchBase; let desiredMajorBase = desiredVersion.majorBase; let desiredMinorBase = desiredVersion.minorBase; let desiredPatchBase = desiredVersion.patchBase; const desiredNotBefore = desiredVersion.notBefore; let majorMustEqual = desiredVersion.majorMustEqual; let minorMustEqual = desiredVersion.minorMustEqual; let patchMustEqual = desiredVersion.patchMustEqual; if (desiredVersion.isMinimum) { if (majorBase > desiredMajorBase) { return true; } if (majorBase < desiredMajorBase) { return false; } if (minorBase > desiredMinorBase) { return true; } if (minorBase < desiredMinorBase) { return false; } if (productTs && productTs < desiredNotBefore) { return false; } return patchBase >= desiredPatchBase; } // Anything < 1.0.0 is compatible with >= 1.0.0, except exact matches if (majorBase === 1 && desiredMajorBase === 0 && (!majorMustEqual || !minorMustEqual || !patchMustEqual)) { desiredMajorBase = 1; desiredMinorBase = 0; desiredPatchBase = 0; majorMustEqual = true; minorMustEqual = false; patchMustEqual = false; } if (majorBase < desiredMajorBase) { // smaller major version return false; } if (majorBase > desiredMajorBase) { // higher major version return !majorMustEqual; } // at this point, majorBase are equal if (minorBase < desiredMinorBase) { // smaller minor version return false; } if (minorBase > desiredMinorBase) { // higher minor version return !minorMustEqual; } // at this point, minorBase are equal if (patchBase < desiredPatchBase) { // smaller patch version return false; } if (patchBase > desiredPatchBase) { // higher patch version return !patchMustEqual; } // at this point, patchBase are equal if (productTs && productTs < desiredNotBefore) { return false; } return true; } export function validateExtensionManifest( productVersion, productDate, extensionLocation, extensionManifest, extensionIsBuiltin, ) { const validations = []; if (typeof extensionManifest.publisher !== 'undefined' && typeof extensionManifest.publisher !== 'string') { validations.push([ Severity.Error, nls.localize('extensionDescription.publisher', 'property publisher must be of type `string`.'), ]); return validations; } if (typeof extensionManifest.name !== 'string') { validations.push([ Severity.Error, nls.localize('extensionDescription.name', 'property `{0}` is mandatory and must be of type `string`', 'name'), ]); return validations; } if (typeof extensionManifest.version !== 'string') { validations.push([ Severity.Error, nls.localize( 'extensionDescription.version', 'property `{0}` is mandatory and must be of type `string`', 'version', ), ]); return validations; } if (!extensionManifest.engines) { validations.push([ Severity.Error, nls.localize( 'extensionDescription.engines', 'property `{0}` is mandatory and must be of type `object`', 'engines', ), ]); return validations; } if (typeof extensionManifest.engines.vscode !== 'string') { validations.push([ Severity.Error, nls.localize( 'extensionDescription.engines.vscode', 'property `{0}` is mandatory and must be of type `string`', 'engines.vscode', ), ]); return validations; } if (typeof extensionManifest.extensionDependencies !== 'undefined') { if (!isStringArray(extensionManifest.extensionDependencies)) { validations.push([ Severity.Error, nls.localize( 'extensionDescription.extensionDependencies', 'property `{0}` can be omitted or must be of type `string[]`', 'extensionDependencies', ), ]); return validations; } } if (typeof extensionManifest.activationEvents !== 'undefined') { if (!isStringArray(extensionManifest.activationEvents)) { validations.push([ Severity.Error, nls.localize( 'extensionDescription.activationEvents1', 'property `{0}` can be omitted or must be of type `string[]`', 'activationEvents', ), ]); return validations; } if (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined') { validations.push([ Severity.Error, nls.localize( 'extensionDescription.activationEvents2', 'properties `{0}` and `{1}` must both be specified or must both be omitted', 'activationEvents', 'main', ), ]); return validations; } } if (typeof extensionManifest.extensionKind !== 'undefined') { if (typeof extensionManifest.main === 'undefined') { validations.push([ Severity.Warning, nls.localize( 'extensionDescription.extensionKind', 'property `{0}` can be defined only if property `main` is also defined.', 'extensionKind', ), ]); // not a failure case } } if (typeof extensionManifest.main !== 'undefined') { if (typeof extensionManifest.main !== 'string') { validations.push([ Severity.Error, nls.localize('extensionDescription.main1', 'property `{0}` can be omitted or must be of type `string`', 'main'), ]); return validations; } else { const mainLocation = joinPath(extensionLocation, extensionManifest.main); if (!isEqualOrParent(mainLocation, extensionLocation)) { validations.push([ Severity.Warning, nls.localize( 'extensionDescription.main2', "Expected `main` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", mainLocation.path, extensionLocation.path, ), ]); // not a failure case } } if (typeof extensionManifest.activationEvents === 'undefined') { validations.push([ Severity.Error, nls.localize( 'extensionDescription.main3', 'properties `{0}` and `{1}` must both be specified or must both be omitted', 'activationEvents', 'main', ), ]); return validations; } } if (typeof extensionManifest.browser !== 'undefined') { if (typeof extensionManifest.browser !== 'string') { validations.push([ Severity.Error, nls.localize( 'extensionDescription.browser1', 'property `{0}` can be omitted or must be of type `string`', 'browser', ), ]); return validations; } else { const browserLocation = joinPath(extensionLocation, extensionManifest.browser); if (!isEqualOrParent(browserLocation, extensionLocation)) { validations.push([ Severity.Warning, nls.localize( 'extensionDescription.browser2', "Expected `browser` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", browserLocation.path, extensionLocation.path, ), ]); // not a failure case } } if (typeof extensionManifest.activationEvents === 'undefined') { validations.push([ Severity.Error, nls.localize( 'extensionDescription.browser3', 'properties `{0}` and `{1}` must both be specified or must both be omitted', 'activationEvents', 'browser', ), ]); return validations; } } if (!semver.valid(extensionManifest.version)) { validations.push([Severity.Error, nls.localize('notSemver', 'Extension version is not semver compatible.')]); return validations; } const notices = []; const isValid = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices); if (!isValid) { for (const notice of notices) { validations.push([Severity.Error, notice]); } } return validations; } export function isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices) { if ( extensionIsBuiltin || (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined') ) { // No version check for builtin or declarative extensions return true; } return isVersionValid(productVersion, productDate, extensionManifest.engines.vscode, notices); } export function isEngineValid(engine, version, date) { // TODO@joao: discuss with alex '*' doesn't seem to be a valid engine version return engine === '*' || isVersionValid(version, date, engine); } function isVersionValid(currentVersion, date, requestedVersion, notices = []) { const desiredVersion = normalizeVersion(parseVersion(requestedVersion)); if (!desiredVersion) { notices.push( nls.localize( 'versionSyntax', 'Could not parse `engines.vscode` value {0}. Please use, for example: ^1.22.0, ^1.22.x, etc.', requestedVersion, ), ); return false; } // enforce that a breaking API version is specified. // for 0.X.Y, that means up to 0.X must be specified // otherwise for Z.X.Y, that means Z must be specified if (desiredVersion.majorBase === 0) { // force that major and minor must be specific if (!desiredVersion.majorMustEqual || !desiredVersion.minorMustEqual) { notices.push( nls.localize( 'versionSpecificity1', 'Version specified in `engines.vscode` ({0}) is not specific enough. For vscode versions before 1.0.0, please define at a minimum the major and minor desired version. E.g. ^0.10.0, 0.10.x, 0.11.0, etc.', requestedVersion, ), ); return false; } } else { // force that major must be specific if (!desiredVersion.majorMustEqual) { notices.push( nls.localize( 'versionSpecificity2', 'Version specified in `engines.vscode` ({0}) is not specific enough. For vscode versions after 1.0.0, please define at a minimum the major desired version. E.g. ^1.10.0, 1.10.x, 1.x.x, 2.x.x, etc.', requestedVersion, ), ); return false; } } if (!isValidVersion(currentVersion, date, desiredVersion)) { notices.push( nls.localize( 'versionMismatch', 'Extension is not compatible with Code {0}. Extension requires: {1}.', currentVersion, requestedVersion, ), ); return false; } return true; } function isStringArray(arr) { if (!Array.isArray(arr)) { return false; } for (let i = 0, len = arr.length; i < len; i++) { if (typeof arr[i] !== 'string') { return false; } } return true; }