Improvements & clean up

- Deprecate static extensions
- Introduce additional builtin extensions
- Install web extension from location
This commit is contained in:
Sandeep Somavarapu 2021-06-17 15:26:44 +02:00
parent bf06adcc48
commit 9ce9551a8b
No known key found for this signature in database
GPG key ID: 1FED25EC4646638B
9 changed files with 217 additions and 108 deletions

View file

@ -183,10 +183,7 @@ async function getCommandlineProvidedExtensionInfos() {
const packageJSON = await getExtensionPackageJSON(extensionPath);
if (packageJSON) {
const extensionId = `${packageJSON.publisher}.${packageJSON.name}`;
extensions.push({
packageJSON,
extensionLocation: { scheme: SCHEME, authority: AUTHORITY, path: `/extension/${extensionId}` }
});
extensions.push({ scheme: SCHEME, authority: AUTHORITY, path: `/extension/${extensionId}` });
locations[extensionId] = extensionPath;
}
}));
@ -208,13 +205,6 @@ async function getExtensionPackageJSON(extensionPath) {
if (packageJSON.main && !packageJSON.browser) {
return; // unsupported
}
const packageNLSPath = path.join(extensionPath, 'package.nls.json');
const packageNLSExists = await exists(packageNLSPath);
if (packageNLSExists) {
packageJSON = extensions.translatePackageJSON(packageJSON, packageNLSPath); // temporary, until fixed in core
}
return packageJSON;
} catch (e) {
console.log(e);
@ -407,7 +397,7 @@ async function handleRoot(req, res) {
}
const { extensions: builtInExtensions } = await builtInExtensionsPromise;
const { extensions: staticExtensions, locations: staticLocations } = await commandlineProvidedExtensionsPromise;
const { extensions: additionalBuiltinExtensions, locations: staticLocations } = await commandlineProvidedExtensionsPromise;
const dedupedBuiltInExtensions = [];
for (const builtInExtension of builtInExtensions) {
@ -422,7 +412,7 @@ async function handleRoot(req, res) {
if (args.verbose) {
fancyLog(`${ansiColors.magenta('BuiltIn extensions')}: ${dedupedBuiltInExtensions.map(e => path.basename(e.extensionPath)).join(', ')}`);
fancyLog(`${ansiColors.magenta('Additional extensions')}: ${staticExtensions.map(e => path.basename(e.extensionLocation.path)).join(', ') || 'None'}`);
fancyLog(`${ansiColors.magenta('Additional extensions')}: ${additionalBuiltinExtensions.map(e => path.basename(e.extensionLocation.path)).join(', ') || 'None'}`);
}
const secondaryHost = (
@ -432,7 +422,7 @@ async function handleRoot(req, res) {
);
const webConfigJSON = {
folderUri: folderUri,
staticExtensions,
additionalBuiltinExtensions,
webWorkerExtensionHostIframeSrc: `${SCHEME}://${secondaryHost}/static/out/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html`
};
if (args['wrap-iframe']) {

View file

@ -9,7 +9,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { MenuRegistry, MenuId, registerAction2, Action2, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ExtensionsLabel, ExtensionsLocalizedLabel, ExtensionsChannelId, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output';
@ -74,6 +74,7 @@ import { EditorExtensions } from 'vs/workbench/common/editor';
import { WORKSPACE_TRUST_EXTENSION_SUPPORT } from 'vs/workbench/services/workspaces/common/workspaceTrust';
import { ExtensionsCompletionItemsProvider } from 'vs/workbench/contrib/extensions/browser/extensionsCompletionItemsProvider';
import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
// Singletons
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService);
@ -772,6 +773,36 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
}
});
this.registerExtensionAction({
id: 'workbench.extensions.action.installWebExtensionFromLocation',
title: { value: localize('installWebExtensionFromLocation', "Install Web Extension..."), original: 'Install Web Extension...' },
category: CATEGORIES.Developer,
menu: [{
id: MenuId.CommandPalette,
when: ContextKeyOrExpr.create([CONTEXT_HAS_WEB_SERVER])
}],
run: async (accessor: ServicesAccessor) => {
const quickInputService = accessor.get(IQuickInputService);
const extensionManagementService = accessor.get(IWorkbenchExtensionManagementService);
const disposables = new DisposableStore();
const quickPick = disposables.add(quickInputService.createQuickPick());
quickPick.title = localize('installFromLocation', "Install Web Extension from Location");
quickPick.customButton = true;
quickPick.customLabel = localize('install button', "Install");
quickPick.placeholder = localize('installFromLocationPlaceHolder', "Location of the web extension");
quickPick.ignoreFocusOut = true;
disposables.add(quickPick.onDidAccept(() => {
quickPick.hide();
if (quickPick.value) {
extensionManagementService.installWebExtension(URI.parse(quickPick.value));
}
}));
disposables.add(quickPick.onDidHide(() => disposables.dispose()));
quickPick.show();
}
});
const extensionsFilterSubMenu = new MenuId('extensionsFilterSubMenu');
MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, <ISubmenuItem>{
submenu: extensionsFilterSubMenu,

View file

@ -279,7 +279,7 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
const developmentOptions = this.options.developmentOptions;
if (developmentOptions) {
if (developmentOptions.extensions?.length) {
extensionHostDebugEnvironment.extensionDevelopmentLocationURI = developmentOptions.extensions.map(e => URI.revive(e.extensionLocation));
extensionHostDebugEnvironment.extensionDevelopmentLocationURI = developmentOptions.extensions.map(e => URI.revive(e));
extensionHostDebugEnvironment.isExtensionDevelopment = true;
}
if (developmentOptions) {

View file

@ -27,6 +27,7 @@ export interface IExtensionManagementServerService {
export const IWorkbenchExtensionManagementService = refineServiceDecorator<IExtensionManagementService, IWorkbenchExtensionManagementService>(IExtensionManagementService);
export interface IWorkbenchExtensionManagementService extends IExtensionManagementService {
readonly _serviceBrand: undefined;
installWebExtension(location: URI): Promise<ILocalExtension>;
installExtensions(extensions: IGalleryExtension[], installOptions?: InstallOptions): Promise<ILocalExtension[]>;
updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension): Promise<ILocalExtension>;
getExtensionManagementServerToInstall(manifest: IExtensionManifest): IExtensionManagementServer | null
@ -112,9 +113,12 @@ export interface IWebExtensionsScannerService {
scanSystemExtensions(): Promise<IExtension[]>;
scanUserExtensions(): Promise<IExtension[]>;
scanExtensionsUnderDevelopment(): Promise<IExtension[]>;
scanSingleExtension(extensionLocation: URI, extensionType: ExtensionType): Promise<IExtension | null>;
scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType): Promise<IExtension | null>;
canAddExtension(galleryExtension: IGalleryExtension): boolean;
addExtension(galleryExtension: IGalleryExtension): Promise<IExtension>;
addExtension(location: URI): Promise<IExtension>;
addExtensionFromGallery(galleryExtension: IGalleryExtension): Promise<IExtension>;
removeExtension(identifier: IExtensionIdentifier, version?: string): Promise<void>;
scanExtensionManifest(extensionLocation: URI): Promise<IExtensionManifest | null>;
}

View file

@ -195,6 +195,13 @@ export class ExtensionManagementService extends Disposable implements IWorkbench
return Promise.reject('No Servers to Install');
}
async installWebExtension(location: URI): Promise<ILocalExtension> {
if (!this.extensionManagementServerService.webExtensionManagementServer) {
throw new Error('Web extension management server is not found');
}
return this.extensionManagementServerService.webExtensionManagementServer.extensionManagementService.install(location);
}
protected async installVSIX(vsix: URI, server: IExtensionManagementServer, options: InstallVSIXOptions | undefined): Promise<ILocalExtension> {
const manifest = await this.getManifest(vsix);
if (manifest) {

View file

@ -7,7 +7,7 @@ import { ExtensionType, IExtension, IExtensionIdentifier, IExtensionManifest } f
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 { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { areSameExtensions, getGalleryExtensionId } 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';
@ -53,6 +53,30 @@ export class WebExtensionManagementService extends Disposable implements IExtens
return this.webExtensionsScannerService.canAddExtension(gallery);
}
async install(location: URI): Promise<ILocalExtension> {
const manifest = await this.webExtensionsScannerService.scanExtensionManifest(location);
if (!manifest) {
throw new Error(`Cannot find packageJSON from the location ${location.toString()}`);
}
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
this.logService.info('Installing extension:', identifier.id);
this._onInstallExtension.fire({ identifier: identifier });
try {
const existingExtension = await this.getUserExtension(identifier);
const extension = await this.webExtensionsScannerService.addExtension(location);
const local = this.toLocalExtension(extension);
if (existingExtension && existingExtension.manifest.version !== manifest.version) {
await this.webExtensionsScannerService.removeExtension(existingExtension.identifier, existingExtension.manifest.version);
}
this._onDidInstallExtension.fire({ local, identifier: extension.identifier, operation: InstallOperation.Install });
return local;
} catch (error) {
this._onDidInstallExtension.fire({ error, identifier, operation: InstallOperation.Install });
throw error;
}
}
async installFromGallery(gallery: IGalleryExtension): Promise<ILocalExtension> {
if (!(await this.canInstall(gallery))) {
throw new Error(localize('cannot be installed', "Cannot install '{0}' because this extension is not a web extension.", gallery.displayName || gallery.name));
@ -61,7 +85,7 @@ export class WebExtensionManagementService extends Disposable implements IExtens
this._onInstallExtension.fire({ identifier: gallery.identifier, gallery });
try {
const existingExtension = await this.getUserExtension(gallery.identifier);
const scannedExtension = await this.webExtensionsScannerService.addExtension(gallery);
const scannedExtension = await this.webExtensionsScannerService.addExtensionFromGallery(gallery);
const local = await this.toLocalExtension(scannedExtension);
if (existingExtension && existingExtension.manifest.version !== gallery.version) {
await this.webExtensionsScannerService.removeExtension(existingExtension.identifier, existingExtension.manifest.version);
@ -107,7 +131,6 @@ export class WebExtensionManagementService extends Disposable implements IExtens
zip(extension: ILocalExtension): Promise<URI> { throw new Error('unsupported'); }
unzip(zipLocation: URI): Promise<IExtensionIdentifier> { throw new Error('unsupported'); }
getManifest(vsix: URI): Promise<IExtensionManifest> { throw new Error('unsupported'); }
install(vsix: URI): Promise<ILocalExtension> { throw new Error('unsupported'); }
reinstallFromGallery(extension: ILocalExtension): Promise<void> { throw new Error('unsupported'); }
getExtensionsReport(): Promise<IReportedExtension[]> { throw new Error('unsupported'); }
updateExtensionScope(): Promise<ILocalExtension> { throw new Error('unsupported'); }

View file

@ -11,20 +11,19 @@ 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 { Promises, 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 { IExtensionGalleryService, 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';
import type { IStaticExtension } from 'vs/workbench/workbench.web.api';
import { Disposable } from 'vs/base/common/lifecycle';
import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls';
import { localize } from 'vs/nls';
import * as semver from 'vs/base/common/semver/semver';
import { isArray, isFunction, isString, isUndefined } from 'vs/base/common/types';
import { isFunction, isString, isUndefined } from 'vs/base/common/types';
import { getErrorMessage } from 'vs/base/common/errors';
import { ResourceMap } from 'vs/base/common/map';
@ -52,9 +51,9 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
private readonly builtinExtensionsPromise: Promise<IExtension[]> = Promise.resolve([]);
private readonly staticExtensionsPromise: Promise<IExtension[]> = Promise.resolve([]);
private readonly userConfiguredExtensionsPromise: Promise<IExtension[]> = Promise.resolve([]);
private readonly customBuiltinExtensionsPromise: Promise<IExtension[]> = Promise.resolve([]);
private readonly staticExtensionsResource: URI | undefined = undefined;
private readonly customBuiltinExtensionsCacheResource: URI | undefined = undefined;
private readonly installedExtensionsResource: URI | undefined = undefined;
private readonly resourcesAccessQueueMap = new ResourceMap<Queue<IWebExtension[]>>();
@ -64,23 +63,22 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
@IFileService private readonly fileService: IFileService,
@IRequestService private readonly requestService: IRequestService,
@ILogService private readonly logService: ILogService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
) {
super();
if (isWeb) {
this.installedExtensionsResource = joinPath(environmentService.userRoamingDataHome, 'extensions.json');
this.staticExtensionsResource = joinPath(environmentService.userRoamingDataHome, 'staticExtensions.json');
this.builtinExtensionsPromise = this.readBuiltinExtensions();
this.customBuiltinExtensionsCacheResource = joinPath(environmentService.userRoamingDataHome, 'customBuiltinExtensionsCache.json');
this.builtinExtensionsPromise = this.readSystemExtensions();
this.staticExtensionsPromise = this.readStaticExtensions();
this.userConfiguredExtensionsPromise = this.readUserConfiguredExtensions();
this.customBuiltinExtensionsPromise = this.readCustomBuiltinExtensions();
}
}
/**
* All builtin extensions bundled with the product
* All system extensions bundled with the product
*/
private async readBuiltinExtensions(): Promise<IExtension[]> {
private async readSystemExtensions(): Promise<IExtension[]> {
let builtinExtensions = await this.builtinExtensionsScannerService.scanBuiltinExtensions();
if (isFunction(this.environmentService.options?.builtinExtensionsFilter)) {
builtinExtensions = builtinExtensions.filter(e => this.environmentService.options!.builtinExtensionsFilter!(e.identifier.id));
@ -92,25 +90,55 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
* All extensions defined via `staticExtensions` API
*/
private async readStaticExtensions(): Promise<IExtension[]> {
const staticExtensions: (string | IStaticExtension)[] = this.environmentService.options && Array.isArray(this.environmentService.options.staticExtensions) ? this.environmentService.options.staticExtensions : [];
const staticExtensionIds = [], result: IExtension[] = [];
const staticExtensions = this.environmentService.options && Array.isArray(this.environmentService.options.staticExtensions) ? this.environmentService.options.staticExtensions : [];
const result: IExtension[] = [];
for (const e of staticExtensions) {
const extension = this.parseStaticExtension(e, isUndefined(e.isBuiltin) ? true : e.isBuiltin);
if (extension) {
result.push(extension);
}
}
return result;
}
/**
* All extensions defined via `additionalBuiltinExtensions` API
*/
private async readCustomBuiltinExtensions(): Promise<IExtension[]> {
const customBuiltinExtensions = this.environmentService.options && Array.isArray(this.environmentService.options.additionalBuiltinExtensions) ? this.environmentService.options.additionalBuiltinExtensions : [];
const extensionIds: string[] = [], extensionLocations: URI[] = [], result: IExtension[] = [];
for (const e of customBuiltinExtensions) {
if (isString(e)) {
staticExtensionIds.push(e);
extensionIds.push(e);
} else {
const extension = this.parseStaticExtension(e, isUndefined(e.isBuiltin) ? true : e.isBuiltin);
if (extension) {
result.push(extension);
extensionLocations.push(URI.revive(e));
}
}
await Promises.allSettled([
(async () => {
if (extensionLocations.length) {
await Promises.allSettled(extensionLocations.map(async location => {
try {
const webExtension = await this.toWebExtensionFromLocation(location);
result.push(await this.toExtension(webExtension, true));
} catch (error) {
this.logService.info(`Error while fetching the the static extension ${location.toString()}.`, getErrorMessage(error));
}
}));
}
}
}
if (staticExtensionIds.length) {
try {
result.push(...await this.getStaticExtensionsFromGallery(staticExtensionIds));
} catch (error) {
this.logService.info('Ignoring following static extensions as there is an error while fetching them from gallery', staticExtensionIds, getErrorMessage(error));
}
}
})(),
(async () => {
if (extensionIds.length) {
try {
result.push(...await this.getStaticExtensionsFromGallery(extensionIds));
} catch (error) {
this.logService.info('Ignoring following static extensions as there is an error while fetching them from gallery', extensionIds, getErrorMessage(error));
}
}
})(),
]);
return result;
}
@ -120,7 +148,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
return [];
}
const cachedStaticWebExtensions = await this.readWebExtensions(this.staticExtensionsResource);
const cachedStaticWebExtensions = await this.readWebExtensions(this.customBuiltinExtensionsCacheResource);
const webExtensions: IWebExtension[] = [];
extensionIds = extensionIds.map(id => id.toLowerCase());
@ -142,7 +170,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
await Promise.all(galleryExtensions.map(async gallery => {
try {
if (this.canAddExtension(gallery)) {
webExtensions.push(await this.toWebExtension(gallery));
webExtensions.push(await this.toWebExtensionFromGallery(gallery));
} else {
this.logService.info(`Ignoring static gallery extension ${gallery.identifier.id} because it is not a web extension`);
}
@ -165,7 +193,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
}
try {
await this.writeWebExtensions(this.staticExtensionsResource, webExtensions);
await this.writeWebExtensions(this.customBuiltinExtensionsCacheResource, webExtensions);
} catch (error) {
this.logService.info(`Ignoring the error while adding static gallery extensions`, getErrorMessage(error));
}
@ -190,44 +218,6 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
return null;
}
private async readUserConfiguredExtensions(): Promise<IExtension[]> {
const result: IStaticExtension[] = [];
const userConfiguredExtensions = this.configurationService.getValue<{ location: string }[]>('_extensions.defaultUserWebExtensions');
if (isArray(userConfiguredExtensions)) {
for (const userConfiguredExtension of userConfiguredExtensions) {
try {
const extensionLocation = URI.parse(userConfiguredExtension.location);
const manifestLocation = joinPath(extensionLocation, 'package.json');
const context = await this.requestService.request({ type: 'GET', url: manifestLocation.toString(true) }, CancellationToken.None);
if (!isSuccess(context)) {
this.logService.warn('Skipped user static extension as there is an error while fetching manifest', manifestLocation);
continue;
}
const content = await asText(context);
if (!content) {
this.logService.warn('Skipped user static extension as there is manifest is not found', manifestLocation);
continue;
}
const packageJSON = JSON.parse(content);
result.push({
packageJSON,
extensionLocation,
});
} catch (error) {
this.logService.warn('Skipped user static extension as there is an error while fetching manifest', userConfiguredExtension);
}
}
}
const extensions: IExtension[] = [];
for (const e of result) {
const extension = this.parseStaticExtension(e, false);
if (extension) {
extensions.push(extension);
}
}
return extensions;
}
async scanSystemExtensions(): Promise<IExtension[]> {
return this.builtinExtensionsPromise;
}
@ -239,9 +229,9 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
const staticExtensions = await this.staticExtensionsPromise;
extensions.push(...staticExtensions);
// User configured extensions
const userConfiguredExtensions = await this.userConfiguredExtensionsPromise;
extensions.push(...userConfiguredExtensions);
// Custom builtin extensions defined through `additionalBuiltinExtensions` API
const customBuiltinExtensions = await this.customBuiltinExtensionsPromise;
extensions.push(...customBuiltinExtensions);
// User Installed extensions
const installedExtensions = await this.scanInstalledExtensions();
@ -254,17 +244,24 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
const devExtensions = this.environmentService.options?.developmentOptions?.extensions;
const result: IExtension[] = [];
if (Array.isArray(devExtensions)) {
for (const e of devExtensions) {
const extension = this.parseStaticExtension(e, false);
if (extension) {
result.push(extension);
await Promises.allSettled(devExtensions.map(async devExtension => {
try {
const location = URI.revive(devExtension);
if (URI.isUri(location)) {
const webExtension = await this.toWebExtensionFromLocation(location);
result.push(await this.toExtension(webExtension, false));
} else {
this.logService.info(`Skipping the extension under development ${devExtension} as it is not URI type.`);
}
} catch (error) {
this.logService.info(`Error while fetching the extension under development ${devExtension.toString()}.`, getErrorMessage(error));
}
}
}));
}
return result;
}
async scanSingleExtension(extensionLocation: URI, extensionType: ExtensionType): Promise<IExtension | null> {
async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType): Promise<IExtension | null> {
if (extensionType === ExtensionType.System) {
const systemExtensions = await this.scanSystemExtensions();
return systemExtensions.find(e => e.location.toString() === extensionLocation.toString()) || null;
@ -273,6 +270,18 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
return userExtensions.find(e => e.location.toString() === extensionLocation.toString()) || null;
}
async scanExtensionManifest(extensionLocation: URI): Promise<IExtensionManifest | null> {
const packageJSONUri = joinPath(extensionLocation, 'package.json');
const context = await this.requestService.request({ type: 'GET', url: packageJSONUri.toString() }, CancellationToken.None);
if (isSuccess(context)) {
const content = await asText(context);
if (content) {
return JSON.parse(content);
}
}
return null;
}
canAddExtension(galleryExtension: IGalleryExtension): boolean {
if (this.environmentService.options?.assumeGalleryExtensionsAreAddressable) {
return true;
@ -281,17 +290,18 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
return !!galleryExtension.properties.webExtension && !!galleryExtension.webResource;
}
async addExtension(galleryExtension: IGalleryExtension): Promise<IExtension> {
async addExtensionFromGallery(galleryExtension: IGalleryExtension): Promise<IExtension> {
if (!this.canAddExtension(galleryExtension)) {
throw new Error(localize('cannot be installed', "Cannot install '{0}' because this extension is not a web extension.", galleryExtension.displayName || galleryExtension.name));
}
const webExtension = await this.toWebExtension(galleryExtension);
const extension = await this.toExtension(webExtension, false);
const installedExtensions = await this.readInstalledExtensions();
installedExtensions.push(webExtension);
await this.writeInstalledExtensions(installedExtensions);
return extension;
const webExtension = await this.toWebExtensionFromGallery(galleryExtension);
return this.addWebExtension(webExtension);
}
async addExtension(location: URI): Promise<IExtension> {
const webExtension = await this.toWebExtensionFromLocation(location);
return this.addWebExtension(webExtension);
}
async removeExtension(identifier: IExtensionIdentifier, version?: string): Promise<void> {
@ -300,6 +310,14 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
await this.writeInstalledExtensions(installedExtensions);
}
private async addWebExtension(webExtension: IWebExtension) {
const extension = await this.toExtension(webExtension, false);
const installedExtensions = await this.readInstalledExtensions();
installedExtensions.push(webExtension);
await this.writeInstalledExtensions(installedExtensions);
return extension;
}
private async scanInstalledExtensions(): Promise<IExtension[]> {
let installedExtensions = await this.readInstalledExtensions();
const byExtension: IWebExtension[][] = groupByExtension(installedExtensions, e => e.identifier);
@ -315,7 +333,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
return extensions;
}
private async toWebExtension(galleryExtension: IGalleryExtension): Promise<IWebExtension> {
private async toWebExtensionFromGallery(galleryExtension: IGalleryExtension): Promise<IWebExtension> {
const extensionLocation = joinPath(galleryExtension.assetUri, 'Microsoft.VisualStudio.Code.WebResources', 'extension');
const packageNLSUri = joinPath(extensionLocation, 'package.nls.json');
const context = await this.requestService.request({ type: 'GET', url: packageNLSUri.toString() }, CancellationToken.None);
@ -330,6 +348,35 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
};
}
private async toWebExtensionFromLocation(extensionLocation: URI): Promise<IWebExtension> {
const packageJSONUri = joinPath(extensionLocation, 'package.json');
const packageNLSUri: URI = joinPath(extensionLocation, 'package.nls.json');
const [result1, result2] = await Promises.allSettled([
this.requestService.request({ type: 'GET', url: packageJSONUri.toString() }, CancellationToken.None),
this.requestService.request({ type: 'GET', url: packageNLSUri.toString() }, CancellationToken.None)
]);
if (result1.status === 'rejected' || !isSuccess(result1.value)) {
throw new Error(`Cannot find the package.json from the location '${extensionLocation.toString()}'`);
}
const content = await asText(result1.value);
if (!content) {
throw new Error(`Error while fetching package.json for extension '${extensionLocation.toString()}'. Server returned no content`);
}
const manifest = JSON.parse(content);
const packageNLSExists = result2.status === 'fulfilled' && isSuccess(result2.value);
return {
identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name) },
version: manifest.version,
location: extensionLocation,
packageNLSUri: packageNLSExists ? packageNLSUri : undefined
};
}
private async toExtension(webExtension: IWebExtension, isBuiltin: boolean): Promise<IExtension> {
const context = await this.requestService.request({ type: 'GET', url: joinPath(webExtension.location, 'package.json').toString() }, CancellationToken.None);
if (!isSuccess(context)) {

View file

@ -93,7 +93,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
return this._remoteAgentService.scanSingleExtension(extension.location, extension.type === ExtensionType.System);
}
const scannedExtension = await this._webExtensionsScannerService.scanSingleExtension(extension.location, extension.type);
const scannedExtension = await this._webExtensionsScannerService.scanExistingExtension(extension.location, extension.type);
if (scannedExtension) {
return toExtensionDescription(scannedExtension);
}

View file

@ -318,8 +318,15 @@ interface IWorkbenchConstructionOptions {
/**
* Add static extensions that cannot be uninstalled but only be disabled.
* @deprecated
*/
readonly staticExtensions?: readonly (string | IStaticExtension)[];
readonly staticExtensions?: readonly IStaticExtension[];
/**
* Additional builtin extensions that cannot be uninstalled but only be disabled.
* It can be an Id of an extension published in the Marketplace or location of the extension where it is hosted.
*/
readonly additionalBuiltinExtensions?: readonly (string | UriComponents)[];
/**
* Filter for built-in extensions.
@ -438,7 +445,7 @@ interface IDevelopmentOptions {
/**
* Add extensions under development.
*/
readonly extensions?: readonly IStaticExtension[];
readonly extensions?: readonly UriComponents[];
/**
* Whether to enable the smoke test driver.