From 9ce9551a8b9f8024b4537334f647cc5d41b6217e Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 17 Jun 2021 15:26:44 +0200 Subject: [PATCH] Improvements & clean up - Deprecate static extensions - Introduce additional builtin extensions - Install web extension from location --- resources/web/code-web.js | 18 +- .../browser/extensions.contribution.ts | 33 ++- .../environment/browser/environmentService.ts | 2 +- .../common/extensionManagement.ts | 8 +- .../common/extensionManagementService.ts | 7 + .../common/webExtensionManagementService.ts | 29 ++- .../common/webExtensionsScannerService.ts | 215 +++++++++++------- .../extensions/browser/extensionService.ts | 2 +- src/vs/workbench/workbench.web.api.ts | 11 +- 9 files changed, 217 insertions(+), 108 deletions(-) diff --git a/resources/web/code-web.js b/resources/web/code-web.js index 9cc661f72f6..ae0ba183ee5 100644 --- a/resources/web/code-web.js +++ b/resources/web/code-web.js @@ -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']) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 956164ef0a8..9aa258e75ed 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -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, { submenu: extensionsFilterSubMenu, diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 66dfdfcc779..2de1c28652c 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -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) { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 9de45f134d0..be964909da9 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -27,6 +27,7 @@ export interface IExtensionManagementServerService { export const IWorkbenchExtensionManagementService = refineServiceDecorator(IExtensionManagementService); export interface IWorkbenchExtensionManagementService extends IExtensionManagementService { readonly _serviceBrand: undefined; + installWebExtension(location: URI): Promise; installExtensions(extensions: IGalleryExtension[], installOptions?: InstallOptions): Promise; updateFromGallery(gallery: IGalleryExtension, extension: ILocalExtension): Promise; getExtensionManagementServerToInstall(manifest: IExtensionManifest): IExtensionManagementServer | null @@ -112,9 +113,12 @@ export interface IWebExtensionsScannerService { scanSystemExtensions(): Promise; scanUserExtensions(): Promise; scanExtensionsUnderDevelopment(): Promise; - scanSingleExtension(extensionLocation: URI, extensionType: ExtensionType): Promise; + scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType): Promise; canAddExtension(galleryExtension: IGalleryExtension): boolean; - addExtension(galleryExtension: IGalleryExtension): Promise; + addExtension(location: URI): Promise; + addExtensionFromGallery(galleryExtension: IGalleryExtension): Promise; removeExtension(identifier: IExtensionIdentifier, version?: string): Promise; + + scanExtensionManifest(extensionLocation: URI): Promise; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index e4921e4e0a3..785cc92b062 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -195,6 +195,13 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return Promise.reject('No Servers to Install'); } + async installWebExtension(location: URI): Promise { + 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 { const manifest = await this.getManifest(vsix); if (manifest) { diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index a5376860264..d4a1bb240b4 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -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 { + 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 { 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 { throw new Error('unsupported'); } unzip(zipLocation: URI): Promise { throw new Error('unsupported'); } getManifest(vsix: URI): Promise { throw new Error('unsupported'); } - install(vsix: URI): Promise { throw new Error('unsupported'); } reinstallFromGallery(extension: ILocalExtension): Promise { throw new Error('unsupported'); } getExtensionsReport(): Promise { throw new Error('unsupported'); } updateExtensionScope(): Promise { throw new Error('unsupported'); } diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts index 5f2d8540ecc..95a17655cb2 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts @@ -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 = Promise.resolve([]); private readonly staticExtensionsPromise: Promise = Promise.resolve([]); - private readonly userConfiguredExtensionsPromise: Promise = Promise.resolve([]); + private readonly customBuiltinExtensionsPromise: Promise = 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>(); @@ -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 { + private async readSystemExtensions(): Promise { 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 { - 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 { + 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 { - 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 { 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 { + async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType): Promise { 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 { + 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 { + async addExtensionFromGallery(galleryExtension: IGalleryExtension): Promise { 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 { + const webExtension = await this.toWebExtensionFromLocation(location); + return this.addWebExtension(webExtension); } async removeExtension(identifier: IExtensionIdentifier, version?: string): Promise { @@ -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 { 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 { + private async toWebExtensionFromGallery(galleryExtension: IGalleryExtension): Promise { 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 { + 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 { const context = await this.requestService.request({ type: 'GET', url: joinPath(webExtension.location, 'package.json').toString() }, CancellationToken.None); if (!isSuccess(context)) { diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 31371cdbc66..24a713937a3 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -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); } diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 1ab4c0b194a..90f2a192609 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -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.