Skip to content

Commit 79b9f74

Browse files
authored
cache builtn extensions locations from gallery (microsoft#183467)
- revert hosted extensions - cache builtn extensions locations from gallery
1 parent 710d6ea commit 79b9f74

File tree

3 files changed

+125
-74
lines changed

3 files changed

+125
-74
lines changed

src/vs/platform/extensionResourceLoader/common/extensionResourceLoader.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ export interface IExtensionResourceLoaderService {
4040
*/
4141
readonly supportsExtensionGalleryResources: boolean;
4242

43+
/**
44+
* Return true if the given URI is a extension gallery resource.
45+
*/
46+
isExtensionGalleryResource(uri: URI): boolean;
47+
4348
/**
4449
* Computes the URL of a extension gallery resource. Returns `undefined` if gallery does not provide extension resources.
4550
*/
@@ -104,8 +109,8 @@ export abstract class AbstractExtensionResourceLoaderService implements IExtensi
104109

105110
public abstract readExtensionResource(uri: URI): Promise<string>;
106111

107-
protected isExtensionGalleryResource(uri: URI) {
108-
return this._extensionGalleryAuthority && this._extensionGalleryAuthority === this._getExtensionGalleryAuthority(uri);
112+
isExtensionGalleryResource(uri: URI): boolean {
113+
return !!this._extensionGalleryAuthority && this._extensionGalleryAuthority === this._getExtensionGalleryAuthority(uri);
109114
}
110115

111116
protected async getExtensionGalleryRequestHeaders(): Promise<IHeaders> {

src/vs/workbench/browser/web.api.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ import type { IProgress, IProgressCompositeOptions, IProgressDialogOptions, IPro
1818
import type { ITextEditorOptions } from 'vs/platform/editor/common/editor';
1919
import type { EditorGroupLayout } from 'vs/workbench/services/editor/common/editorGroupsService';
2020
import type { IEmbedderTerminalOptions } from 'vs/workbench/services/terminal/common/embedderTerminalService';
21-
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
22-
import { ITranslations } from 'vs/platform/extensionManagement/common/extensionNls';
2321

2422
/**
2523
* The `IWorkbench` interface is the API facade for web embedders
@@ -222,7 +220,7 @@ export interface IWorkbenchConstructionOptions {
222220
* - an extension in the Marketplace
223221
* - location of the extension where it is hosted.
224222
*/
225-
readonly additionalBuiltinExtensions?: readonly (MarketplaceExtension | UriComponents | HostedExtension)[];
223+
readonly additionalBuiltinExtensions?: readonly (MarketplaceExtension | UriComponents)[];
226224

227225
/**
228226
* List of extensions to be enabled if they are installed.
@@ -375,15 +373,6 @@ export interface IResourceUriProvider {
375373
export type ExtensionId = string;
376374

377375
export type MarketplaceExtension = ExtensionId | { readonly id: ExtensionId; preRelease?: boolean; migrateStorageFrom?: ExtensionId };
378-
export interface HostedExtension {
379-
readonly location: UriComponents;
380-
readonly preRelease?: boolean;
381-
readonly packageJSON?: IExtensionManifest;
382-
readonly defaultPackageTranslations?: ITranslations | null;
383-
readonly packageNLSUris?: Map<string, UriComponents>;
384-
readonly readmeUri?: UriComponents;
385-
readonly changelogUri?: UriComponents;
386-
}
387376

388377
export interface ICommonTelemetryPropertiesResolver {
389378
(): { [key: string]: any };

src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts

Lines changed: 117 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,6 @@ import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/use
4545
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
4646

4747
type GalleryExtensionInfo = { readonly id: string; preRelease?: boolean; migrateStorageFrom?: string };
48-
interface HostedExtensionInfo {
49-
readonly location: UriComponents;
50-
readonly preRelease?: boolean;
51-
readonly packageJSON?: IExtensionManifest;
52-
readonly defaultPackageTranslations?: ITranslations | null;
53-
readonly packageNLSUris?: Map<string, UriComponents>;
54-
readonly readmeUri?: UriComponents;
55-
readonly changelogUri?: UriComponents;
56-
}
5748
type ExtensionInfo = { readonly id: string; preRelease: boolean };
5849

5950
function isGalleryExtensionInfo(obj: unknown): obj is GalleryExtensionInfo {
@@ -63,16 +54,6 @@ function isGalleryExtensionInfo(obj: unknown): obj is GalleryExtensionInfo {
6354
&& (galleryExtensionInfo.migrateStorageFrom === undefined || typeof galleryExtensionInfo.migrateStorageFrom === 'string');
6455
}
6556

66-
function isHostedExtensionInfo(obj: unknown): obj is HostedExtensionInfo {
67-
const hostedExtensionInfo = obj as HostedExtensionInfo | undefined;
68-
return isUriComponents(hostedExtensionInfo?.location)
69-
&& (hostedExtensionInfo?.preRelease === undefined || typeof hostedExtensionInfo.preRelease === 'boolean')
70-
&& (hostedExtensionInfo?.packageJSON === undefined || typeof hostedExtensionInfo.packageJSON === 'object')
71-
&& (hostedExtensionInfo?.defaultPackageTranslations === undefined || hostedExtensionInfo?.defaultPackageTranslations === null || typeof hostedExtensionInfo.defaultPackageTranslations === 'object')
72-
&& (hostedExtensionInfo?.changelogUri === undefined || isUriComponents(hostedExtensionInfo?.changelogUri))
73-
&& (hostedExtensionInfo?.readmeUri === undefined || isUriComponents(hostedExtensionInfo?.readmeUri));
74-
}
75-
7657
function isUriComponents(thing: unknown): thing is UriComponents {
7758
if (!thing) {
7859
return false;
@@ -144,12 +125,13 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
144125
}
145126
}
146127

147-
private _customBuiltinExtensionsInfoPromise: Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: HostedExtensionInfo[] }> | undefined;
148-
private readCustomBuiltinExtensionsInfoFromEnv(): Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: HostedExtensionInfo[] }> {
128+
private _customBuiltinExtensionsInfoPromise: Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[]; extensionGalleryResources: URI[] }> | undefined;
129+
private readCustomBuiltinExtensionsInfoFromEnv(): Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[]; extensionGalleryResources: URI[] }> {
149130
if (!this._customBuiltinExtensionsInfoPromise) {
150131
this._customBuiltinExtensionsInfoPromise = (async () => {
151132
let extensions: ExtensionInfo[] = [];
152-
const extensionLocations: HostedExtensionInfo[] = [];
133+
const extensionLocations: URI[] = [];
134+
const extensionGalleryResources: URI[] = [];
153135
const extensionsToMigrate: [string, string][] = [];
154136
const customBuiltinExtensionsInfo = this.environmentService.options && Array.isArray(this.environmentService.options.additionalBuiltinExtensions)
155137
? this.environmentService.options.additionalBuiltinExtensions.map(additionalBuiltinExtension => isString(additionalBuiltinExtension) ? { id: additionalBuiltinExtension } : additionalBuiltinExtension)
@@ -160,11 +142,12 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
160142
if (e.migrateStorageFrom) {
161143
extensionsToMigrate.push([e.migrateStorageFrom, e.id]);
162144
}
163-
} else {
164-
if (isHostedExtensionInfo(e)) {
165-
extensionLocations.push(e);
145+
} else if (isUriComponents(e)) {
146+
const extensionLocation = URI.revive(e);
147+
if (this.extensionResourceLoaderService.isExtensionGalleryResource(extensionLocation)) {
148+
extensionGalleryResources.push(extensionLocation);
166149
} else {
167-
extensionLocations.push({ location: e });
150+
extensionLocations.push(extensionLocation);
168151
}
169152
}
170153
}
@@ -177,7 +160,10 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
177160
if (extensionLocations.length) {
178161
this.logService.info('Found additional builtin location extensions in env', extensionLocations.map(e => e.toString()));
179162
}
180-
return { extensions, extensionsToMigrate, extensionLocations };
163+
if (extensionGalleryResources.length) {
164+
this.logService.info('Found additional builtin extension gallery resources in env', extensionGalleryResources.map(e => e.toString()));
165+
}
166+
return { extensions, extensionsToMigrate, extensionLocations, extensionGalleryResources };
181167
})();
182168
}
183169
return this._customBuiltinExtensionsInfoPromise;
@@ -243,15 +229,9 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
243229
return [];
244230
}
245231
const result: IScannedExtension[] = [];
246-
await Promise.allSettled(extensionLocations.map(async ({ location, preRelease, packageNLSUris, packageJSON, defaultPackageTranslations, readmeUri, changelogUri }) => {
232+
await Promise.allSettled(extensionLocations.map(async extensionLocation => {
247233
try {
248-
const webExtension = await this.toWebExtension(URI.revive(location), undefined,
249-
packageJSON,
250-
packageNLSUris ? [...packageNLSUris.entries()].reduce((result, [key, value]) => { result.set(key, URI.revive(value)); return result; }, new Map<string, URI>()) : undefined,
251-
defaultPackageTranslations,
252-
URI.revive(readmeUri),
253-
URI.revive(changelogUri),
254-
{ isPreReleaseVersion: preRelease });
234+
const webExtension = await this.toWebExtension(extensionLocation);
255235
const extension = await this.toScannedExtension(webExtension, true);
256236
if (extension.isValid || !scanOptions?.skipInvalidExtensions) {
257237
result.push(extension);
@@ -271,9 +251,13 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
271251
return [];
272252
}
273253
const result: IScannedExtension[] = [];
274-
const { extensions } = await this.readCustomBuiltinExtensionsInfoFromEnv();
254+
const { extensions, extensionGalleryResources } = await this.readCustomBuiltinExtensionsInfoFromEnv();
275255
try {
276-
const useCache = this.storageService.get('additionalBuiltinExtensions', StorageScope.APPLICATION, '[]') === JSON.stringify(extensions);
256+
const cacheValue = JSON.stringify({
257+
extensions: extensions.sort((a, b) => a.id.localeCompare(b.id)),
258+
extensionGalleryResources: extensionGalleryResources.map(e => e.toString()).sort()
259+
});
260+
const useCache = this.storageService.get('additionalBuiltinExtensions', StorageScope.APPLICATION, '{}') === cacheValue;
277261
const webExtensions = await (useCache ? this.getCustomBuiltinExtensionsFromCache() : this.updateCustomBuiltinExtensionsCache());
278262
if (webExtensions.length) {
279263
await Promise.all(webExtensions.map(async webExtension => {
@@ -289,7 +273,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
289273
}
290274
}));
291275
}
292-
this.storageService.store('additionalBuiltinExtensions', JSON.stringify(extensions), StorageScope.APPLICATION, StorageTarget.MACHINE);
276+
this.storageService.store('additionalBuiltinExtensions', cacheValue, StorageScope.APPLICATION, StorageTarget.MACHINE);
293277
} catch (error) {
294278
this.logService.info('Ignoring following additional builtin extensions as there is an error while fetching them from gallery', extensions.map(({ id }) => id), getErrorMessage(error));
295279
}
@@ -366,31 +350,95 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
366350
if (!this._updateCustomBuiltinExtensionsCachePromise) {
367351
this._updateCustomBuiltinExtensionsCachePromise = (async () => {
368352
this.logService.info('Updating additional builtin extensions cache');
369-
const webExtensions: IWebExtension[] = [];
370-
const { extensions } = await this.readCustomBuiltinExtensionsInfoFromEnv();
371-
if (extensions.length) {
372-
const galleryExtensionsMap = await this.getExtensionsWithDependenciesAndPackedExtensions(extensions);
373-
const missingExtensions = extensions.filter(({ id }) => !galleryExtensionsMap.has(id.toLowerCase()));
374-
if (missingExtensions.length) {
375-
this.logService.info('Skipping the additional builtin extensions because their compatible versions are not found.', missingExtensions);
376-
}
377-
await Promise.all([...galleryExtensionsMap.values()].map(async gallery => {
378-
try {
379-
const webExtension = await this.toWebExtensionFromGallery(gallery, { isPreReleaseVersion: gallery.properties.isPreReleaseVersion, preRelease: gallery.properties.isPreReleaseVersion, isBuiltin: true });
380-
webExtensions.push(webExtension);
381-
} catch (error) {
382-
this.logService.info(`Ignoring additional builtin extension ${gallery.identifier.id} because there is an error while converting it into web extension`, getErrorMessage(error));
383-
}
384-
}));
353+
const { extensions, extensionGalleryResources } = await this.readCustomBuiltinExtensionsInfoFromEnv();
354+
const [galleryWebExtensions, extensionGalleryResourceWebExtensions] = await Promise.all([
355+
this.resolveBuiltinGalleryExtensions(extensions),
356+
this.resolveBuiltinExtensionGalleryResources(extensionGalleryResources)
357+
]);
358+
const webExtensionsMap = new Map<string, IWebExtension>();
359+
for (const webExtension of [...galleryWebExtensions, ...extensionGalleryResourceWebExtensions]) {
360+
webExtensionsMap.set(webExtension.identifier.id.toLowerCase(), webExtension);
385361
}
362+
await this.resolveDependenciesAndPackedExtensions(extensionGalleryResourceWebExtensions, webExtensionsMap);
363+
const webExtensions = [...webExtensionsMap.values()];
386364
await this.writeCustomBuiltinExtensionsCache(() => webExtensions);
387365
return webExtensions;
388366
})();
389367
}
390368
return this._updateCustomBuiltinExtensionsCachePromise;
391369
}
392370

393-
private async getExtensionsWithDependenciesAndPackedExtensions(toGet: IExtensionInfo[], result: Map<string, IGalleryExtension> = new Map<string, IGalleryExtension>()): Promise<Map<string, IGalleryExtension>> {
371+
private async resolveBuiltinExtensionGalleryResources(extensionGalleryResources: URI[]): Promise<IWebExtension[]> {
372+
if (extensionGalleryResources.length === 0) {
373+
return [];
374+
}
375+
const result = new Map<string, IWebExtension>();
376+
const extensionInfos: IExtensionInfo[] = [];
377+
await Promise.all(extensionGalleryResources.map(async extensionGalleryResource => {
378+
const webExtension = await this.toWebExtensionFromExtensionGalleryResource(extensionGalleryResource);
379+
result.set(webExtension.identifier.id.toLowerCase(), webExtension);
380+
extensionInfos.push({ id: webExtension.identifier.id, version: webExtension.version });
381+
}));
382+
const galleryExtensions = await this.galleryService.getExtensions(extensionInfos, CancellationToken.None);
383+
for (const galleryExtension of galleryExtensions) {
384+
const webExtension = result.get(galleryExtension.identifier.id.toLowerCase());
385+
if (webExtension) {
386+
result.set(galleryExtension.identifier.id.toLowerCase(), {
387+
...webExtension,
388+
readmeUri: galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined,
389+
changelogUri: galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined,
390+
metadata: { isPreReleaseVersion: galleryExtension.properties.isPreReleaseVersion, preRelease: galleryExtension.properties.isPreReleaseVersion, isBuiltin: true }
391+
});
392+
}
393+
}
394+
return [...result.values()];
395+
}
396+
397+
private async resolveBuiltinGalleryExtensions(extensions: IExtensionInfo[]): Promise<IWebExtension[]> {
398+
if (extensions.length === 0) {
399+
return [];
400+
}
401+
const webExtensions: IWebExtension[] = [];
402+
const galleryExtensionsMap = await this.getExtensionsWithDependenciesAndPackedExtensions(extensions);
403+
const missingExtensions = extensions.filter(({ id }) => !galleryExtensionsMap.has(id.toLowerCase()));
404+
if (missingExtensions.length) {
405+
this.logService.info('Skipping the additional builtin extensions because their compatible versions are not found.', missingExtensions);
406+
}
407+
await Promise.all([...galleryExtensionsMap.values()].map(async gallery => {
408+
try {
409+
const webExtension = await this.toWebExtensionFromGallery(gallery, { isPreReleaseVersion: gallery.properties.isPreReleaseVersion, preRelease: gallery.properties.isPreReleaseVersion, isBuiltin: true });
410+
webExtensions.push(webExtension);
411+
} catch (error) {
412+
this.logService.info(`Ignoring additional builtin extension ${gallery.identifier.id} because there is an error while converting it into web extension`, getErrorMessage(error));
413+
}
414+
}));
415+
return webExtensions;
416+
}
417+
418+
private async resolveDependenciesAndPackedExtensions(webExtensions: IWebExtension[], result: Map<string, IWebExtension>): Promise<void> {
419+
const extensionInfos: IExtensionInfo[] = [];
420+
for (const webExtension of webExtensions) {
421+
for (const e of [...(webExtension.manifest?.extensionDependencies ?? []), ...(webExtension.manifest?.extensionPack ?? [])]) {
422+
if (!result.has(e.toLowerCase())) {
423+
extensionInfos.push({ id: e, version: webExtension.version });
424+
}
425+
}
426+
}
427+
if (extensionInfos.length === 0) {
428+
return;
429+
}
430+
const galleryExtensions = await this.getExtensionsWithDependenciesAndPackedExtensions(extensionInfos, new Set<string>([...result.keys()]));
431+
await Promise.all([...galleryExtensions.values()].map(async gallery => {
432+
try {
433+
const webExtension = await this.toWebExtensionFromGallery(gallery, { isPreReleaseVersion: gallery.properties.isPreReleaseVersion, preRelease: gallery.properties.isPreReleaseVersion, isBuiltin: true });
434+
result.set(webExtension.identifier.id.toLowerCase(), webExtension);
435+
} catch (error) {
436+
this.logService.info(`Ignoring additional builtin extension ${gallery.identifier.id} because there is an error while converting it into web extension`, getErrorMessage(error));
437+
}
438+
}));
439+
}
440+
441+
private async getExtensionsWithDependenciesAndPackedExtensions(toGet: IExtensionInfo[], seen: Set<string> = new Set<string>(), result: Map<string, IGalleryExtension> = new Map<string, IGalleryExtension>()): Promise<Map<string, IGalleryExtension>> {
394442
if (toGet.length === 0) {
395443
return result;
396444
}
@@ -399,13 +447,13 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
399447
for (const extension of extensions) {
400448
result.set(extension.identifier.id.toLowerCase(), extension);
401449
for (const id of [...(isNonEmptyArray(extension.properties.dependencies) ? extension.properties.dependencies : []), ...(isNonEmptyArray(extension.properties.extensionPack) ? extension.properties.extensionPack : [])]) {
402-
if (!result.has(id.toLowerCase()) && !packsAndDependencies.has(id.toLowerCase())) {
450+
if (!result.has(id.toLowerCase()) && !packsAndDependencies.has(id.toLowerCase()) && !seen.has(id.toLowerCase())) {
403451
const extensionInfo = toGet.find(e => areSameExtensions(e, extension.identifier));
404452
packsAndDependencies.set(id.toLowerCase(), { id, preRelease: extensionInfo?.preRelease });
405453
}
406454
}
407455
}
408-
return this.getExtensionsWithDependenciesAndPackedExtensions([...packsAndDependencies.values()].filter(({ id }) => !result.has(id.toLowerCase())), result);
456+
return this.getExtensionsWithDependenciesAndPackedExtensions([...packsAndDependencies.values()].filter(({ id }) => !result.has(id.toLowerCase())), seen, result);
409457
}
410458

411459
async scanSystemExtensions(): Promise<IExtension[]> {
@@ -606,19 +654,28 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten
606654
if (!extensionLocation) {
607655
throw new Error('No extension gallery service configured.');
608656
}
657+
658+
return this.toWebExtensionFromExtensionGalleryResource(extensionLocation,
659+
galleryExtension.identifier,
660+
galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined,
661+
galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined,
662+
metadata);
663+
}
664+
665+
private async toWebExtensionFromExtensionGalleryResource(extensionLocation: URI, identifier?: IExtensionIdentifier, readmeUri?: URI, changelogUri?: URI, metadata?: Metadata): Promise<IWebExtension> {
609666
const extensionResources = await this.listExtensionResources(extensionLocation);
610667
const packageNLSResources = this.getPackageNLSResourceMapFromResources(extensionResources);
611668

612669
// The fallback, in English, will fill in any gaps missing in the localized file.
613670
const fallbackPackageNLSResource = extensionResources.find(e => basename(e) === 'package.nls.json');
614671
return this.toWebExtension(
615672
extensionLocation,
616-
galleryExtension.identifier,
673+
identifier,
617674
undefined,
618675
packageNLSResources,
619676
fallbackPackageNLSResource ? URI.parse(fallbackPackageNLSResource) : null,
620-
galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined,
621-
galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined,
677+
readmeUri,
678+
changelogUri,
622679
metadata);
623680
}
624681

0 commit comments

Comments
 (0)