mirror of
https://github.com/Microsoft/vscode
synced 2024-10-01 08:50:48 +00:00
* fix #214294 * fix version message * finetune message
This commit is contained in:
parent
0cce95eac9
commit
7717059b2e
|
@ -23,6 +23,7 @@ import {
|
|||
} from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions';
|
||||
import { areApiProposalsCompatible } from 'vs/platform/extensions/common/extensionValidator';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
@ -548,6 +549,10 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
|
|||
|
||||
compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease, productVersion);
|
||||
if (!compatibleExtension) {
|
||||
const incompatibleApiProposalsMessages: string[] = [];
|
||||
if (!areApiProposalsCompatible(extension.properties.enabledApiProposals ?? [], incompatibleApiProposalsMessages)) {
|
||||
throw new ExtensionManagementError(nls.localize('incompatibleAPI', "Can't install '{0}' extension. {1}", extension.displayName ?? extension.identifier.id, incompatibleApiProposalsMessages[0]), ExtensionManagementErrorCode.IncompatibleApi);
|
||||
}
|
||||
/** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */
|
||||
if (!installPreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) {
|
||||
throw new ExtensionManagementError(nls.localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.displayName ?? extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound);
|
||||
|
|
|
@ -18,7 +18,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
|
|||
import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
|
||||
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
|
||||
import { areApiProposalsCompatible, isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
|
@ -209,6 +209,7 @@ const PropertyType = {
|
|||
ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack',
|
||||
Engine: 'Microsoft.VisualStudio.Code.Engine',
|
||||
PreRelease: 'Microsoft.VisualStudio.Code.PreRelease',
|
||||
EnabledApiProposals: 'Microsoft.VisualStudio.Code.EnabledApiProposals',
|
||||
LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages',
|
||||
WebExtension: 'Microsoft.VisualStudio.Code.WebExtension',
|
||||
SponsorLink: 'Microsoft.VisualStudio.Code.SponsorLink',
|
||||
|
@ -430,6 +431,12 @@ function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean {
|
|||
return values.length > 0 && values[0].value === 'true';
|
||||
}
|
||||
|
||||
function getEnabledApiProposals(version: IRawGalleryExtensionVersion): string[] {
|
||||
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.EnabledApiProposals) : [];
|
||||
const value = (values.length > 0 && values[0].value) || '';
|
||||
return value ? value.split(',') : [];
|
||||
}
|
||||
|
||||
function getLocalizedLanguages(version: IRawGalleryExtensionVersion): string[] {
|
||||
const values = version.properties ? version.properties.filter(p => p.key === PropertyType.LocalizedLanguages) : [];
|
||||
const value = (values.length > 0 && values[0].value) || '';
|
||||
|
@ -548,6 +555,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
|
|||
dependencies: getExtensions(version, PropertyType.Dependency),
|
||||
extensionPack: getExtensions(version, PropertyType.ExtensionPack),
|
||||
engine: getEngine(version),
|
||||
enabledApiProposals: getEnabledApiProposals(version),
|
||||
localizedLanguages: getLocalizedLanguages(version),
|
||||
targetPlatform: getTargetPlatformForExtensionVersion(version),
|
||||
isPreReleaseVersion: isPreReleaseVersion(version)
|
||||
|
@ -704,7 +712,16 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
|||
}
|
||||
engine = manifest.engines.vscode;
|
||||
}
|
||||
return isEngineValid(engine, productVersion.version, productVersion.date);
|
||||
|
||||
if (!isEngineValid(engine, productVersion.version, productVersion.date)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!areApiProposalsCompatible(extension.properties.enabledApiProposals ?? [])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async isValidVersion(extension: string, rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise<boolean> {
|
||||
|
@ -915,7 +932,18 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
|||
continue;
|
||||
}
|
||||
// Allow any version if includePreRelease flag is set otherwise only release versions are allowed
|
||||
if (await this.isValidVersion(getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform, criteria.productVersion)) {
|
||||
if (await this.isValidVersion(
|
||||
getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName),
|
||||
rawGalleryExtensionVersion,
|
||||
includePreRelease ? 'any' : 'release',
|
||||
criteria.compatible,
|
||||
allTargetPlatforms,
|
||||
criteria.targetPlatform,
|
||||
criteria.productVersion)
|
||||
) {
|
||||
if (criteria.compatible && !areApiProposalsCompatible(getEnabledApiProposals(rawGalleryExtensionVersion))) {
|
||||
return null;
|
||||
}
|
||||
return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, queryContext);
|
||||
}
|
||||
if (version && rawGalleryExtensionVersion.version === version) {
|
||||
|
@ -1161,7 +1189,15 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
|
|||
const validVersions: IRawGalleryExtensionVersion[] = [];
|
||||
await Promise.all(galleryExtensions[0].versions.map(async (version) => {
|
||||
try {
|
||||
if (await this.isValidVersion(extension.identifier.id, version, includePreRelease ? 'any' : 'release', true, allTargetPlatforms, targetPlatform)) {
|
||||
if (
|
||||
(await this.isValidVersion(
|
||||
extension.identifier.id,
|
||||
version, includePreRelease ? 'any' : 'release',
|
||||
true,
|
||||
allTargetPlatforms,
|
||||
targetPlatform))
|
||||
&& areApiProposalsCompatible(getEnabledApiProposals(version))
|
||||
) {
|
||||
validVersions.push(version);
|
||||
}
|
||||
} catch (error) { /* Ignore error and skip version */ }
|
||||
|
|
|
@ -159,6 +159,7 @@ export interface IGalleryExtensionProperties {
|
|||
dependencies?: string[];
|
||||
extensionPack?: string[];
|
||||
engine?: string;
|
||||
enabledApiProposals?: string[];
|
||||
localizedLanguages?: string[];
|
||||
targetPlatform: TargetPlatform;
|
||||
isPreReleaseVersion: boolean;
|
||||
|
@ -437,6 +438,7 @@ export const enum ExtensionManagementErrorCode {
|
|||
Deprecated = 'Deprecated',
|
||||
Malicious = 'Malicious',
|
||||
Incompatible = 'Incompatible',
|
||||
IncompatibleApi = 'IncompatibleApi',
|
||||
IncompatibleTargetPlatform = 'IncompatibleTargetPlatform',
|
||||
ReleaseVersionNotFound = 'ReleaseVersionNotFound',
|
||||
Invalid = 'Invalid',
|
||||
|
|
|
@ -8,7 +8,8 @@ import Severity from 'vs/base/common/severity';
|
|||
import { URI } from 'vs/base/common/uri';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as semver from 'vs/base/common/semver/semver';
|
||||
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||
import { IExtensionManifest, parseApiProposals } from 'vs/platform/extensions/common/extensions';
|
||||
import { allApiProposals } from 'vs/platform/extensions/common/extensionsApiProposals';
|
||||
|
||||
export interface IParsedVersion {
|
||||
hasCaret: boolean;
|
||||
|
@ -314,12 +315,22 @@ export function validateExtensionManifest(productVersion: string, productDate: P
|
|||
}
|
||||
|
||||
const notices: string[] = [];
|
||||
const isValid = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices);
|
||||
if (!isValid) {
|
||||
const validExtensionVersion = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices);
|
||||
if (!validExtensionVersion) {
|
||||
for (const notice of notices) {
|
||||
validations.push([Severity.Error, notice]);
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionManifest.enabledApiProposals?.length) {
|
||||
const incompatibleNotices: string[] = [];
|
||||
if (!areApiProposalsCompatible([...extensionManifest.enabledApiProposals], incompatibleNotices)) {
|
||||
for (const notice of incompatibleNotices) {
|
||||
validations.push([Severity.Error, notice]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validations;
|
||||
}
|
||||
|
||||
|
@ -338,6 +349,38 @@ export function isEngineValid(engine: string, version: string, date: ProductDate
|
|||
return engine === '*' || isVersionValid(version, date, engine);
|
||||
}
|
||||
|
||||
export function areApiProposalsCompatible(apiProposals: string[]): boolean;
|
||||
export function areApiProposalsCompatible(apiProposals: string[], notices: string[]): boolean;
|
||||
export function areApiProposalsCompatible(apiProposals: string[], productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>): boolean;
|
||||
export function areApiProposalsCompatible(apiProposals: string[], arg1?: any): boolean {
|
||||
if (apiProposals.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const notices: string[] | undefined = Array.isArray(arg1) ? arg1 : undefined;
|
||||
const productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }> = (notices ? undefined : arg1) ?? allApiProposals;
|
||||
const incompatibleNotices: string[] = [];
|
||||
const parsedProposals = parseApiProposals(apiProposals);
|
||||
for (const { proposalName, version } of parsedProposals) {
|
||||
const existingProposal = productApiProposals[proposalName];
|
||||
if (!existingProposal) {
|
||||
continue;
|
||||
}
|
||||
if (!version) {
|
||||
continue;
|
||||
}
|
||||
if (existingProposal.version !== version) {
|
||||
if (existingProposal.version) {
|
||||
incompatibleNotices.push(nls.localize('apiProposalMismatch', "Extension is not compatible with API proposal {0}. Extension requires version {1} but product has version {2}.", proposalName, version, existingProposal.version));
|
||||
} else {
|
||||
incompatibleNotices.push(nls.localize('apiProposalMismatchNoVersion', "Extension is not compatible with API proposal {0}. Extension requires version {1} but product has no version defined.", proposalName, version));
|
||||
}
|
||||
}
|
||||
}
|
||||
notices?.push(...incompatibleNotices);
|
||||
return incompatibleNotices.length === 0;
|
||||
|
||||
}
|
||||
|
||||
function isVersionValid(currentVersion: string, date: ProductDate, requestedVersion: string, notices: string[] = []): boolean {
|
||||
|
||||
const desiredVersion = normalizeVersion(parseVersion(requestedVersion));
|
||||
|
|
|
@ -494,6 +494,13 @@ export function isResolverExtension(manifest: IExtensionManifest, remoteAuthorit
|
|||
return false;
|
||||
}
|
||||
|
||||
export function parseApiProposals(enabledApiProposals: string[]): { proposalName: string; version?: number }[] {
|
||||
return enabledApiProposals.map(proposal => {
|
||||
const [proposalName, version] = proposal.split('@');
|
||||
return { proposalName, version: version ? parseInt(version) : undefined };
|
||||
});
|
||||
}
|
||||
|
||||
export function parseEnabledApiProposalNames(enabledApiProposals: string[]): string[] {
|
||||
return enabledApiProposals.map(proposal => proposal.split('@')[0]);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import assert from 'assert';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
|
||||
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||
import { INormalizedVersion, IParsedVersion, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator';
|
||||
import { areApiProposalsCompatible, INormalizedVersion, IParsedVersion, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator';
|
||||
|
||||
suite('Extension Version Validator', () => {
|
||||
|
||||
|
@ -423,4 +423,21 @@ suite('Extension Version Validator', () => {
|
|||
};
|
||||
assert.strictEqual(isValidExtensionVersion('1.44.0', undefined, manifest, false, []), false);
|
||||
});
|
||||
|
||||
test('areApiProposalsCompatible', () => {
|
||||
assert.strictEqual(areApiProposalsCompatible([]), true);
|
||||
assert.strictEqual(areApiProposalsCompatible([], ['hello']), true);
|
||||
assert.strictEqual(areApiProposalsCompatible([], {}), true);
|
||||
assert.strictEqual(areApiProposalsCompatible(['proposal1'], {}), true);
|
||||
assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal1': { proposal: '' } }), true);
|
||||
assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal1': { proposal: '', version: 1 } }), true);
|
||||
assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '', version: 1 } }), true);
|
||||
assert.strictEqual(areApiProposalsCompatible(['proposal1'], { 'proposal2': { proposal: '' } }), true);
|
||||
assert.strictEqual(areApiProposalsCompatible(['proposal1', 'proposal2'], {}), true);
|
||||
assert.strictEqual(areApiProposalsCompatible(['proposal1', 'proposal2'], { 'proposal1': { proposal: '' } }), true);
|
||||
|
||||
assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '', version: 2 } }), false);
|
||||
assert.strictEqual(areApiProposalsCompatible(['proposal1@1'], { 'proposal1': { proposal: '' } }), false);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -533,7 +533,7 @@ export class LocalExtensionsProvider {
|
|||
addToSkipped.push(e);
|
||||
this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension`, gallery.displayName || gallery.identifier.id);
|
||||
}
|
||||
if (error instanceof ExtensionManagementError && [ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleTargetPlatform].includes(error.code)) {
|
||||
if (error instanceof ExtensionManagementError && [ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleApi, ExtensionManagementErrorCode.IncompatibleTargetPlatform].includes(error.code)) {
|
||||
this.logService.info(`${syncResourceLogLabel}: Skipped synchronizing extension because the compatible extension is not found.`, gallery.displayName || gallery.identifier.id);
|
||||
} else if (error) {
|
||||
this.logService.error(error);
|
||||
|
|
|
@ -134,7 +134,7 @@ export class PromptExtensionInstallFailureAction extends Action {
|
|||
return;
|
||||
}
|
||||
|
||||
if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleTargetPlatform, ExtensionManagementErrorCode.Malicious, ExtensionManagementErrorCode.Deprecated].includes(<ExtensionManagementErrorCode>this.error.name)) {
|
||||
if ([ExtensionManagementErrorCode.Incompatible, ExtensionManagementErrorCode.IncompatibleApi, ExtensionManagementErrorCode.IncompatibleTargetPlatform, ExtensionManagementErrorCode.Malicious, ExtensionManagementErrorCode.Deprecated].includes(<ExtensionManagementErrorCode>this.error.name)) {
|
||||
await this.dialogService.info(getErrorMessage(this.error));
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/use
|
|||
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
|
||||
import { IRemoteUserDataProfilesService } from 'vs/workbench/services/userDataProfile/common/remoteUserDataProfiles';
|
||||
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
|
||||
import { areApiProposalsCompatible } from 'vs/platform/extensions/common/extensionValidator';
|
||||
|
||||
export class NativeRemoteExtensionManagementService extends RemoteExtensionManagementService {
|
||||
|
||||
|
@ -134,6 +135,10 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag
|
|||
}
|
||||
|
||||
if (!compatibleExtension) {
|
||||
const incompatibleApiProposalsMessages: string[] = [];
|
||||
if (!areApiProposalsCompatible(extension.properties.enabledApiProposals ?? [], incompatibleApiProposalsMessages)) {
|
||||
throw new ExtensionManagementError(localize('incompatibleAPI', "Can't install '{0}' extension. {1}", extension.displayName ?? extension.identifier.id, incompatibleApiProposalsMessages[0]), ExtensionManagementErrorCode.IncompatibleApi);
|
||||
}
|
||||
/** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */
|
||||
if (!includePreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) {
|
||||
throw new ExtensionManagementError(localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound);
|
||||
|
|
|
@ -4,11 +4,17 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ExtensionIdentifier, IExtensionDescription, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||
import { allApiProposals, ApiProposalName } from 'vs/platform/extensions/common/extensionsApiProposals';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { Extensions, IExtensionFeatureMarkdownRenderer, IExtensionFeaturesRegistry, IRenderedData } from 'vs/workbench/services/extensionManagement/common/extensionFeatures';
|
||||
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
|
||||
|
||||
export class ExtensionsProposedApi {
|
||||
|
||||
|
@ -107,3 +113,35 @@ export class ExtensionsProposedApi {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ApiProposalsMarkdowneRenderer extends Disposable implements IExtensionFeatureMarkdownRenderer {
|
||||
|
||||
readonly type = 'markdown';
|
||||
|
||||
shouldRender(manifest: IExtensionManifest): boolean {
|
||||
return !!manifest.enabledApiProposals?.length;
|
||||
}
|
||||
|
||||
render(manifest: IExtensionManifest): IRenderedData<IMarkdownString> {
|
||||
const enabledApiProposals = manifest.enabledApiProposals || [];
|
||||
const data = new MarkdownString();
|
||||
if (enabledApiProposals.length) {
|
||||
for (const proposal of enabledApiProposals) {
|
||||
data.appendMarkdown(`- \`${proposal}\`\n`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
data,
|
||||
dispose: () => { }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
|
||||
id: 'enabledApiProposals',
|
||||
label: localize('enabledProposedAPIs', "API Proposals"),
|
||||
access: {
|
||||
canToggle: false
|
||||
},
|
||||
renderer: new SyncDescriptor(ApiProposalsMarkdowneRenderer),
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue