support managing web extensions
- Allow installing extensions with web resource asset type - Allow uninstalling
This commit is contained in:
parent
95f3c81dd3
commit
41f32e0bd6
|
@ -520,7 +520,8 @@
|
|||
"**/vs/workbench/api/**/common/**",
|
||||
"vscode-textmate",
|
||||
"vscode-oniguruma",
|
||||
"iconv-lite-umd"
|
||||
"iconv-lite-umd",
|
||||
"semver-umd"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -295,6 +295,8 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
|
|||
installCount: getStatistic(galleryExtension.statistics, 'install'),
|
||||
rating: getStatistic(galleryExtension.statistics, 'averagerating'),
|
||||
ratingCount: getStatistic(galleryExtension.statistics, 'ratingcount'),
|
||||
assetUri: URI.parse(version.assetUri),
|
||||
assetTypes: version.files.map(({ assetType }) => assetType),
|
||||
assets,
|
||||
properties: {
|
||||
dependencies: getExtensions(version, PropertyType.Dependency),
|
||||
|
|
|
@ -78,6 +78,8 @@ export interface IGalleryExtension {
|
|||
installCount: number;
|
||||
rating: number;
|
||||
ratingCount: number;
|
||||
assetUri: URI;
|
||||
assetTypes: string[];
|
||||
assets: IGalleryExtensionAssets;
|
||||
properties: IGalleryExtensionProperties;
|
||||
telemetryData: any;
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Event } from 'vs/base/common/event';
|
|||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtension, IScannedExtension, ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IWorkspace, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
|
||||
|
@ -142,4 +142,6 @@ export const IWebExtensionsScannerService = createDecorator<IWebExtensionsScanne
|
|||
export interface IWebExtensionsScannerService {
|
||||
readonly _serviceBrand: undefined;
|
||||
scanExtensions(type?: ExtensionType): Promise<IScannedExtension[]>;
|
||||
addExtension(galleryExtension: IGalleryExtension): Promise<IScannedExtension>;
|
||||
removeExtension(identifier: IExtensionIdentifier, version?: string): Promise<void>;
|
||||
}
|
||||
|
|
|
@ -4,27 +4,39 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ExtensionType, IExtensionIdentifier, IExtensionManifest, IScannedExtension } from 'vs/platform/extensions/common/extensions';
|
||||
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IGalleryExtension, IReportedExtension, IGalleryMetadata } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IGalleryExtension, IReportedExtension, IGalleryMetadata, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IRequestService, isSuccess, asText } from 'vs/platform/request/common/request';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export class WebExtensionManagementService implements IExtensionManagementService {
|
||||
export class WebExtensionManagementService extends Disposable implements IExtensionManagementService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
onInstallExtension: Event<InstallExtensionEvent> = Event.None;
|
||||
onDidInstallExtension: Event<DidInstallExtensionEvent> = Event.None;
|
||||
onUninstallExtension: Event<IExtensionIdentifier> = Event.None;
|
||||
onDidUninstallExtension: Event<DidUninstallExtensionEvent> = Event.None;
|
||||
private readonly _onInstallExtension = this._register(new Emitter<InstallExtensionEvent>());
|
||||
readonly onInstallExtension: Event<InstallExtensionEvent> = this._onInstallExtension.event;
|
||||
|
||||
private readonly _onDidInstallExtension = this._register(new Emitter<DidInstallExtensionEvent>());
|
||||
readonly onDidInstallExtension: Event<DidInstallExtensionEvent> = this._onDidInstallExtension.event;
|
||||
|
||||
private readonly _onUninstallExtension = this._register(new Emitter<IExtensionIdentifier>());
|
||||
readonly onUninstallExtension: Event<IExtensionIdentifier> = this._onUninstallExtension.event;
|
||||
|
||||
private _onDidUninstallExtension = this._register(new Emitter<DidUninstallExtensionEvent>());
|
||||
onDidUninstallExtension: Event<DidUninstallExtensionEvent> = this._onDidUninstallExtension.event;
|
||||
|
||||
constructor(
|
||||
@IWebExtensionsScannerService private readonly webExtensionsScannerService: IWebExtensionsScannerService,
|
||||
@IRequestService private readonly requestService: IRequestService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getInstalled(type?: ExtensionType): Promise<ILocalExtension[]> {
|
||||
|
@ -32,6 +44,45 @@ export class WebExtensionManagementService implements IExtensionManagementServic
|
|||
return Promise.all(extensions.map(e => this.toLocalExtension(e)));
|
||||
}
|
||||
|
||||
async installFromGallery(gallery: IGalleryExtension): Promise<ILocalExtension> {
|
||||
this.logService.info('Installing extension:', gallery.identifier.id);
|
||||
this._onInstallExtension.fire({ identifier: gallery.identifier, gallery });
|
||||
try {
|
||||
const existingExtension = await this.getUserExtension(gallery.identifier);
|
||||
if (existingExtension && existingExtension.manifest.version !== gallery.version) {
|
||||
await this.webExtensionsScannerService.removeExtension(existingExtension.identifier, existingExtension.manifest.version);
|
||||
}
|
||||
const scannedExtension = await this.webExtensionsScannerService.addExtension(gallery);
|
||||
const local = await this.toLocalExtension(scannedExtension);
|
||||
this._onDidInstallExtension.fire({ local, identifier: gallery.identifier, operation: InstallOperation.Install, gallery });
|
||||
return local;
|
||||
} catch (error) {
|
||||
this._onDidInstallExtension.fire({ error, identifier: gallery.identifier, operation: InstallOperation.Install, gallery });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(extension: ILocalExtension): Promise<void> {
|
||||
this._onUninstallExtension.fire(extension.identifier);
|
||||
try {
|
||||
await this.webExtensionsScannerService.removeExtension(extension.identifier);
|
||||
this._onDidUninstallExtension.fire({ identifier: extension.identifier });
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
this._onDidUninstallExtension.fire({ error, identifier: extension.identifier });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension> {
|
||||
return local;
|
||||
}
|
||||
|
||||
private async getUserExtension(identifier: IExtensionIdentifier): Promise<ILocalExtension | undefined> {
|
||||
const userExtensions = await this.getInstalled(ExtensionType.User);
|
||||
return userExtensions.find(e => areSameExtensions(e.identifier, identifier));
|
||||
}
|
||||
|
||||
private async toLocalExtension(scannedExtension: IScannedExtension): Promise<ILocalExtension> {
|
||||
let manifest = scannedExtension.packageJSON;
|
||||
if (scannedExtension.packageNLSUrl) {
|
||||
|
@ -60,10 +111,7 @@ export class WebExtensionManagementService implements IExtensionManagementServic
|
|||
unzip(zipLocation: URI): Promise<IExtensionIdentifier> { throw new Error('unsupported'); }
|
||||
getManifest(vsix: URI): Promise<IExtensionManifest> { throw new Error('unsupported'); }
|
||||
install(vsix: URI, isMachineScoped?: boolean): Promise<ILocalExtension> { throw new Error('unsupported'); }
|
||||
installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise<ILocalExtension> { throw new Error('unsupported'); }
|
||||
uninstall(extension: ILocalExtension, force?: boolean): Promise<void> { throw new Error('unsupported'); }
|
||||
reinstallFromGallery(extension: ILocalExtension): Promise<void> { throw new Error('unsupported'); }
|
||||
getExtensionsReport(): Promise<IReportedExtension[]> { throw new Error('unsupported'); }
|
||||
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension> { throw new Error('unsupported'); }
|
||||
|
||||
}
|
||||
|
|
|
@ -3,11 +3,44 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IBuiltinExtensionsScannerService, IScannedExtension, ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import * as semver from 'semver-umd';
|
||||
import { IBuiltinExtensionsScannerService, IScannedExtension, ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { Queue } from 'vs/base/common/async';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { asText, isSuccess, IRequestService } from 'vs/platform/request/common/request';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { groupByExtension, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
|
||||
interface IUserExtension {
|
||||
identifier: IExtensionIdentifier;
|
||||
version: string;
|
||||
uri: URI;
|
||||
readmeUri?: URI;
|
||||
changelogUri?: URI;
|
||||
packageNLSUri?: URI;
|
||||
}
|
||||
|
||||
interface IStoredUserExtension {
|
||||
identifier: IExtensionIdentifier;
|
||||
version: string;
|
||||
uri: UriComponents;
|
||||
readmeUri?: UriComponents;
|
||||
changelogUri?: UriComponents;
|
||||
packageNLSUri?: UriComponents;
|
||||
}
|
||||
|
||||
const AssetTypeWebResource = 'Microsoft.VisualStudio.Code.WebResources';
|
||||
|
||||
function getExtensionLocation(assetUri: URI): URI { return joinPath(assetUri, AssetTypeWebResource, 'extension'); }
|
||||
|
||||
export class WebExtensionsScannerService implements IWebExtensionsScannerService {
|
||||
|
||||
|
@ -15,11 +48,18 @@ export class WebExtensionsScannerService implements IWebExtensionsScannerService
|
|||
|
||||
private readonly systemExtensionsPromise: Promise<IScannedExtension[]>;
|
||||
private readonly staticExtensions: IScannedExtension[];
|
||||
private readonly extensionsResource: URI;
|
||||
private readonly userExtensionsResourceLimiter: Queue<IUserExtension[]>;
|
||||
|
||||
constructor(
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@IBuiltinExtensionsScannerService private readonly builtinExtensionsScannerService: IBuiltinExtensionsScannerService,
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IRequestService private readonly requestService: IRequestService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
this.extensionsResource = joinPath(environmentService.userRoamingDataHome, 'extensions.json');
|
||||
this.userExtensionsResourceLimiter = new Queue<IUserExtension[]>();
|
||||
this.systemExtensionsPromise = isWeb ? this.builtinExtensionsScannerService.scanBuiltinExtensions() : Promise.resolve([]);
|
||||
const staticExtensions = environmentService.options && Array.isArray(environmentService.options.staticExtensions) ? environmentService.options.staticExtensions : [];
|
||||
this.staticExtensions = staticExtensions.map(data => <IScannedExtension>{
|
||||
|
@ -37,10 +77,117 @@ export class WebExtensionsScannerService implements IWebExtensionsScannerService
|
|||
}
|
||||
if (type === undefined || type === ExtensionType.User) {
|
||||
extensions.push(...this.staticExtensions);
|
||||
const userExtensions = await this.scanUserExtensions();
|
||||
extensions.push(...userExtensions);
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
async addExtension(galleryExtension: IGalleryExtension): Promise<IScannedExtension> {
|
||||
if (!galleryExtension.assetTypes.some(type => type.startsWith(AssetTypeWebResource))) {
|
||||
throw new Error(`Missing ${AssetTypeWebResource} asset type`);
|
||||
}
|
||||
|
||||
const packageNLSUri = joinPath(getExtensionLocation(galleryExtension.assetUri), 'package.nls.json');
|
||||
const context = await this.requestService.request({ type: 'GET', url: packageNLSUri.toString() }, CancellationToken.None);
|
||||
const packageNLSExists = isSuccess(context);
|
||||
|
||||
const userExtensions = await this.readUserExtensions();
|
||||
const userExtension: IUserExtension = {
|
||||
identifier: galleryExtension.identifier,
|
||||
version: galleryExtension.version,
|
||||
uri: galleryExtension.assetUri,
|
||||
readmeUri: galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined,
|
||||
changelogUri: galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined,
|
||||
packageNLSUri: packageNLSExists ? packageNLSUri : undefined
|
||||
};
|
||||
userExtensions.push(userExtension);
|
||||
await this.writeUserExtensions(userExtensions);
|
||||
|
||||
const scannedExtension = await this.toScannedExtension(userExtension);
|
||||
if (scannedExtension) {
|
||||
return scannedExtension;
|
||||
}
|
||||
throw new Error('Error while scanning extension');
|
||||
}
|
||||
|
||||
async removeExtension(identifier: IExtensionIdentifier, version?: string): Promise<void> {
|
||||
let userExtensions = await this.readUserExtensions();
|
||||
userExtensions = userExtensions.filter(extension => !(areSameExtensions(extension.identifier, identifier) && version ? extension.version === version : true));
|
||||
await this.writeUserExtensions(userExtensions);
|
||||
}
|
||||
|
||||
private async scanUserExtensions(): Promise<IScannedExtension[]> {
|
||||
let userExtensions = await this.readUserExtensions();
|
||||
const byExtension: IUserExtension[][] = groupByExtension(userExtensions, e => e.identifier);
|
||||
userExtensions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0]);
|
||||
const scannedExtensions: IScannedExtension[] = [];
|
||||
await Promise.all(userExtensions.map(async userExtension => {
|
||||
try {
|
||||
const scannedExtension = await this.toScannedExtension(userExtension);
|
||||
if (scannedExtension) {
|
||||
scannedExtensions.push(scannedExtension);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error, 'Error while scanning user extension', userExtension.identifier.id);
|
||||
}
|
||||
}));
|
||||
return scannedExtensions;
|
||||
}
|
||||
|
||||
private async toScannedExtension(userExtension: IUserExtension): Promise<IScannedExtension | null> {
|
||||
const context = await this.requestService.request({ type: 'GET', url: joinPath(userExtension.uri, 'Microsoft.VisualStudio.Code.Manifest').toString() }, CancellationToken.None);
|
||||
if (isSuccess(context)) {
|
||||
const content = await asText(context);
|
||||
if (content) {
|
||||
const packageJSON = JSON.parse(content);
|
||||
return {
|
||||
identifier: userExtension.identifier,
|
||||
location: getExtensionLocation(userExtension.uri),
|
||||
packageJSON,
|
||||
type: ExtensionType.User,
|
||||
readmeUrl: userExtension.readmeUri,
|
||||
changelogUrl: userExtension.changelogUri,
|
||||
packageNLSUrl: userExtension.packageNLSUri,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private readUserExtensions(): Promise<IUserExtension[]> {
|
||||
return this.userExtensionsResourceLimiter.queue(async () => {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.extensionsResource);
|
||||
const storedUserExtensions: IStoredUserExtension[] = JSON.parse(content.value.toString());
|
||||
return storedUserExtensions.map(e => ({
|
||||
identifier: e.identifier,
|
||||
version: e.version,
|
||||
uri: URI.revive(e.uri),
|
||||
readmeUri: URI.revive(e.readmeUri),
|
||||
changelogUri: URI.revive(e.changelogUri),
|
||||
packageNLSUri: URI.revive(e.packageNLSUri),
|
||||
}));
|
||||
} catch (error) { /* Ignore */ }
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
private writeUserExtensions(userExtensions: IUserExtension[]): Promise<IUserExtension[]> {
|
||||
return this.userExtensionsResourceLimiter.queue(async () => {
|
||||
const storedUserExtensions: IStoredUserExtension[] = userExtensions.map(e => ({
|
||||
identifier: e.identifier,
|
||||
version: e.version,
|
||||
uri: e.uri.toJSON(),
|
||||
readmeUri: e.readmeUri?.toJSON(),
|
||||
changelogUri: e.changelogUri?.toJSON(),
|
||||
packageNLSUri: e.packageNLSUri?.toJSON(),
|
||||
}));
|
||||
await this.fileService.writeFile(this.extensionsResource, VSBuffer.fromString(JSON.stringify(storedUserExtensions)));
|
||||
return userExtensions;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IWebExtensionsScannerService, WebExtensionsScannerService);
|
||||
|
|
Loading…
Reference in a new issue