mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 20:13:32 +01:00
231 lines
10 KiB
TypeScript
231 lines
10 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
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, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
|
|
|
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 {
|
|
|
|
declare readonly _serviceBrand: undefined;
|
|
|
|
private readonly systemExtensionsPromise: Promise<IScannedExtension[]>;
|
|
private readonly staticExtensionsPromise: Promise<IScannedExtension[]>;
|
|
private readonly extensionsResource: URI | undefined;
|
|
private readonly userExtensionsResourceLimiter: Queue<IUserExtension[]>;
|
|
|
|
constructor(
|
|
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
|
|
@IBuiltinExtensionsScannerService private readonly builtinExtensionsScannerService: IBuiltinExtensionsScannerService,
|
|
@IFileService private readonly fileService: IFileService,
|
|
@IRequestService private readonly requestService: IRequestService,
|
|
@ILogService private readonly logService: ILogService,
|
|
@IConfigurationService private readonly configurationService: IConfigurationService,
|
|
) {
|
|
this.extensionsResource = isWeb ? joinPath(environmentService.userRoamingDataHome, 'extensions.json') : undefined;
|
|
this.userExtensionsResourceLimiter = new Queue<IUserExtension[]>();
|
|
this.systemExtensionsPromise = isWeb ? this.builtinExtensionsScannerService.scanBuiltinExtensions() : Promise.resolve([]);
|
|
this.staticExtensionsPromise = this.readStaticExtensions();
|
|
}
|
|
private async readStaticExtensions(): Promise<IScannedExtension[]> {
|
|
const staticExtensions = this.environmentService.options && Array.isArray(this.environmentService.options.staticExtensions) ? this.environmentService.options.staticExtensions : [];
|
|
const defaultStaticExtensions = staticExtensions.map<IScannedExtension>(data => ({
|
|
identifier: { id: getGalleryExtensionId(data.packageJSON.publisher, data.packageJSON.name) },
|
|
location: data.extensionLocation,
|
|
type: ExtensionType.User,
|
|
packageJSON: data.packageJSON,
|
|
}));
|
|
const userStaticExtensions = isWeb ? await this.readStaticExtensionsFromConfiguration() : [];
|
|
return [...defaultStaticExtensions, ...userStaticExtensions];
|
|
}
|
|
private async readStaticExtensionsFromConfiguration(): Promise<IScannedExtension[]> {
|
|
const result: IScannedExtension[] = [];
|
|
for (const webExtension of this.configurationService.getValue<{ location: string }[]>('_extensions.defaultUserWebExtensions') || []) {
|
|
const extensionLocation = URI.parse(webExtension.location);
|
|
const manifestLocation = joinPath(extensionLocation, 'package.json');
|
|
const context = await this.requestService.request({ type: 'GET', url: manifestLocation.toString(true) }, CancellationToken.None);
|
|
if (!isSuccess(context)) {
|
|
console.error('Unable to load ' + manifestLocation);
|
|
}
|
|
const content = await asText(context);
|
|
if (content) {
|
|
const packageJSON = JSON.parse(content);
|
|
result.push({
|
|
identifier: { id: getGalleryExtensionId(packageJSON.publisher, packageJSON.name) },
|
|
packageJSON,
|
|
location: extensionLocation,
|
|
type: ExtensionType.User,
|
|
});
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async scanExtensions(type?: ExtensionType): Promise<IScannedExtension[]> {
|
|
const extensions = [];
|
|
if (type === undefined || type === ExtensionType.System) {
|
|
const systemExtensions = await this.systemExtensionsPromise;
|
|
extensions.push(...systemExtensions);
|
|
}
|
|
if (type === undefined || type === ExtensionType.User) {
|
|
const staticExtensions = await this.staticExtensionsPromise;
|
|
extensions.push(...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 async readUserExtensions(): Promise<IUserExtension[]> {
|
|
if (!this.extensionsResource) {
|
|
return [];
|
|
}
|
|
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[]> {
|
|
if (!this.extensionsResource) {
|
|
throw new Error('unsupported');
|
|
}
|
|
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);
|