Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { IBuiltinExtensionsScannerService, ExtensionType, IExtensionIdentifier, IExtension, IExtensionManifest, TargetPlatform, IRelaxedExtensionManifest, parseEnabledApiProposalNames } from '../../../../platform/extensions/common/extensions.js';
7
import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js';
8
import { IScannedExtension, IWebExtensionsScannerService, ScanOptions } from '../common/extensionManagement.js';
9
import { isWeb, Language } from '../../../../base/common/platform.js';
10
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
11
import { joinPath } from '../../../../base/common/resources.js';
12
import { URI, UriComponents } from '../../../../base/common/uri.js';
13
import { FileOperationError, FileOperationResult, IFileService } from '../../../../platform/files/common/files.js';
14
import { Queue } from '../../../../base/common/async.js';
15
import { VSBuffer } from '../../../../base/common/buffer.js';
16
import { ILogService } from '../../../../platform/log/common/log.js';
17
import { CancellationToken } from '../../../../base/common/cancellation.js';
18
import { IExtensionGalleryService, IExtensionInfo, IGalleryExtension, IGalleryMetadata, Metadata } from '../../../../platform/extensionManagement/common/extensionManagement.js';
19
import { areSameExtensions, getGalleryExtensionId, getExtensionId, isMalicious } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
20
import { Disposable } from '../../../../base/common/lifecycle.js';
21
import { ITranslations, localizeManifest } from '../../../../platform/extensionManagement/common/extensionNls.js';
22
import { localize, localize2 } from '../../../../nls.js';
23
import * as semver from '../../../../base/common/semver/semver.js';
24
import { isString, isUndefined } from '../../../../base/common/types.js';
25
import { getErrorMessage } from '../../../../base/common/errors.js';
26
import { ResourceMap } from '../../../../base/common/map.js';
27
import { IExtensionManifestPropertiesService } from '../../extensions/common/extensionManifestPropertiesService.js';
28
import { IExtensionResourceLoaderService, migratePlatformSpecificExtensionGalleryResourceURL } from '../../../../platform/extensionResourceLoader/common/extensionResourceLoader.js';
29
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
30
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
31
import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js';
32
import { IEditorService } from '../../editor/common/editorService.js';
33
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
34
import { basename } from '../../../../base/common/path.js';
35
import { IExtensionStorageService } from '../../../../platform/extensionManagement/common/extensionStorage.js';
36
import { isNonEmptyArray } from '../../../../base/common/arrays.js';
37
import { ILifecycleService, LifecyclePhase } from '../../lifecycle/common/lifecycle.js';
38
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
39
import { IProductService } from '../../../../platform/product/common/productService.js';
40
import { validateExtensionManifest } from '../../../../platform/extensions/common/extensionValidator.js';
41
import Severity from '../../../../base/common/severity.js';
42
import { IStringDictionary } from '../../../../base/common/collections.js';
43
import { IUserDataProfileService } from '../../userDataProfile/common/userDataProfile.js';
44
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
45
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
46
47
type GalleryExtensionInfo = { readonly id: string; preRelease?: boolean; migrateStorageFrom?: string };
48
type ExtensionInfo = { readonly id: string; preRelease: boolean };
49
50
function isGalleryExtensionInfo(obj: unknown): obj is GalleryExtensionInfo {
51
const galleryExtensionInfo = obj as GalleryExtensionInfo | undefined;
52
return typeof galleryExtensionInfo?.id === 'string'
53
&& (galleryExtensionInfo.preRelease === undefined || typeof galleryExtensionInfo.preRelease === 'boolean')
54
&& (galleryExtensionInfo.migrateStorageFrom === undefined || typeof galleryExtensionInfo.migrateStorageFrom === 'string');
55
}
56
57
function isUriComponents(thing: unknown): thing is UriComponents {
58
if (!thing) {
59
return false;
60
}
61
return isString((<any>thing).path) &&
62
isString((<any>thing).scheme);
63
}
64
65
interface IStoredWebExtension {
66
readonly identifier: IExtensionIdentifier;
67
readonly version: string;
68
readonly location: UriComponents;
69
readonly manifest?: IExtensionManifest;
70
readonly readmeUri?: UriComponents;
71
readonly changelogUri?: UriComponents;
72
// deprecated in favor of packageNLSUris & fallbackPackageNLSUri
73
readonly packageNLSUri?: UriComponents;
74
readonly packageNLSUris?: IStringDictionary<UriComponents>;
75
readonly fallbackPackageNLSUri?: UriComponents;
76
readonly defaultManifestTranslations?: ITranslations | null;
77
readonly metadata?: Metadata;
78
}
79
80
interface IWebExtension {
81
identifier: IExtensionIdentifier;
82
version: string;
83
location: URI;
84
manifest?: IExtensionManifest;
85
readmeUri?: URI;
86
changelogUri?: URI;
87
// deprecated in favor of packageNLSUris & fallbackPackageNLSUri
88
packageNLSUri?: URI;
89
packageNLSUris?: Map<string, URI>;
90
fallbackPackageNLSUri?: URI;
91
defaultManifestTranslations?: ITranslations | null;
92
metadata?: Metadata;
93
}
94
95
export class WebExtensionsScannerService extends Disposable implements IWebExtensionsScannerService {
96
97
declare readonly _serviceBrand: undefined;
98
99
private readonly systemExtensionsCacheResource: URI | undefined = undefined;
100
private readonly customBuiltinExtensionsCacheResource: URI | undefined = undefined;
101
private readonly resourcesAccessQueueMap = new ResourceMap<Queue<IWebExtension[]>>();
102
private readonly extensionsEnabledWithApiProposalVersion: string[];
103
104
constructor(
105
@IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService,
106
@IBuiltinExtensionsScannerService private readonly builtinExtensionsScannerService: IBuiltinExtensionsScannerService,
107
@IFileService private readonly fileService: IFileService,
108
@ILogService private readonly logService: ILogService,
109
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
110
@IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService,
111
@IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService,
112
@IExtensionStorageService private readonly extensionStorageService: IExtensionStorageService,
113
@IStorageService private readonly storageService: IStorageService,
114
@IProductService private readonly productService: IProductService,
115
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
116
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
117
@ILifecycleService lifecycleService: ILifecycleService,
118
) {
119
super();
120
if (isWeb) {
121
this.systemExtensionsCacheResource = joinPath(environmentService.userRoamingDataHome, 'systemExtensionsCache.json');
122
this.customBuiltinExtensionsCacheResource = joinPath(environmentService.userRoamingDataHome, 'customBuiltinExtensionsCache.json');
123
124
// Eventually update caches
125
lifecycleService.when(LifecyclePhase.Eventually).then(() => this.updateCaches());
126
}
127
this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? [];
128
}
129
130
private _customBuiltinExtensionsInfoPromise: Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[]; extensionGalleryResources: URI[] }> | undefined;
131
private readCustomBuiltinExtensionsInfoFromEnv(): Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[]; extensionGalleryResources: URI[] }> {
132
if (!this._customBuiltinExtensionsInfoPromise) {
133
this._customBuiltinExtensionsInfoPromise = (async () => {
134
let extensions: ExtensionInfo[] = [];
135
const extensionLocations: URI[] = [];
136
const extensionGalleryResources: URI[] = [];
137
const extensionsToMigrate: [string, string][] = [];
138
const customBuiltinExtensionsInfo = this.environmentService.options && Array.isArray(this.environmentService.options.additionalBuiltinExtensions)
139
? this.environmentService.options.additionalBuiltinExtensions.map(additionalBuiltinExtension => isString(additionalBuiltinExtension) ? { id: additionalBuiltinExtension } : additionalBuiltinExtension)
140
: [];
141
for (const e of customBuiltinExtensionsInfo) {
142
if (isGalleryExtensionInfo(e)) {
143
extensions.push({ id: e.id, preRelease: !!e.preRelease });
144
if (e.migrateStorageFrom) {
145
extensionsToMigrate.push([e.migrateStorageFrom, e.id]);
146
}
147
} else if (isUriComponents(e)) {
148
const extensionLocation = URI.revive(e);
149
if (await this.extensionResourceLoaderService.isExtensionGalleryResource(extensionLocation)) {
150
extensionGalleryResources.push(extensionLocation);
151
} else {
152
extensionLocations.push(extensionLocation);
153
}
154
}
155
}
156
if (extensions.length) {
157
extensions = await this.checkAdditionalBuiltinExtensions(extensions);
158
}
159
if (extensions.length) {
160
this.logService.info('Found additional builtin gallery extensions in env', extensions);
161
}
162
if (extensionLocations.length) {
163
this.logService.info('Found additional builtin location extensions in env', extensionLocations.map(e => e.toString()));
164
}
165
if (extensionGalleryResources.length) {
166
this.logService.info('Found additional builtin extension gallery resources in env', extensionGalleryResources.map(e => e.toString()));
167
}
168
return { extensions, extensionsToMigrate, extensionLocations, extensionGalleryResources };
169
})();
170
}
171
return this._customBuiltinExtensionsInfoPromise;
172
}
173
174
private async checkAdditionalBuiltinExtensions(extensions: ExtensionInfo[]): Promise<ExtensionInfo[]> {
175
const extensionsControlManifest = await this.galleryService.getExtensionsControlManifest();
176
const result: ExtensionInfo[] = [];
177
for (const extension of extensions) {
178
if (isMalicious({ id: extension.id }, extensionsControlManifest.malicious)) {
179
this.logService.info(`Checking additional builtin extensions: Ignoring '${extension.id}' because it is reported to be malicious.`);
180
continue;
181
}
182
const deprecationInfo = extensionsControlManifest.deprecated[extension.id.toLowerCase()];
183
if (deprecationInfo?.extension?.autoMigrate) {
184
const preReleaseExtensionId = deprecationInfo.extension.id;
185
this.logService.info(`Checking additional builtin extensions: '${extension.id}' is deprecated, instead using '${preReleaseExtensionId}'`);
186
result.push({ id: preReleaseExtensionId, preRelease: !!extension.preRelease });
187
} else {
188
result.push(extension);
189
}
190
}
191
return result;
192
}
193
194
/**
195
* All system extensions bundled with the product
196
*/
197
private async readSystemExtensions(): Promise<IExtension[]> {
198
const systemExtensions = await this.builtinExtensionsScannerService.scanBuiltinExtensions();
199
const cachedSystemExtensions = await Promise.all((await this.readSystemExtensionsCache()).map(e => this.toScannedExtension(e, true, ExtensionType.System)));
200
201
const result = new Map<string, IExtension>();
202
for (const extension of [...systemExtensions, ...cachedSystemExtensions]) {
203
const existing = result.get(extension.identifier.id.toLowerCase());
204
if (existing) {
205
// Incase there are duplicates always take the latest version
206
if (semver.gt(existing.manifest.version, extension.manifest.version)) {
207
continue;
208
}
209
}
210
result.set(extension.identifier.id.toLowerCase(), extension);
211
}
212
return [...result.values()];
213
}
214
215
/**
216
* All extensions defined via `additionalBuiltinExtensions` API
217
*/
218
private async readCustomBuiltinExtensions(scanOptions?: ScanOptions): Promise<IScannedExtension[]> {
219
const [customBuiltinExtensionsFromLocations, customBuiltinExtensionsFromGallery] = await Promise.all([
220
this.getCustomBuiltinExtensionsFromLocations(scanOptions),
221
this.getCustomBuiltinExtensionsFromGallery(scanOptions),
222
]);
223
const customBuiltinExtensions: IScannedExtension[] = [...customBuiltinExtensionsFromLocations, ...customBuiltinExtensionsFromGallery];
224
await this.migrateExtensionsStorage(customBuiltinExtensions);
225
return customBuiltinExtensions;
226
}
227
228
private async getCustomBuiltinExtensionsFromLocations(scanOptions?: ScanOptions): Promise<IScannedExtension[]> {
229
const { extensionLocations } = await this.readCustomBuiltinExtensionsInfoFromEnv();
230
if (!extensionLocations.length) {
231
return [];
232
}
233
const result: IScannedExtension[] = [];
234
await Promise.allSettled(extensionLocations.map(async extensionLocation => {
235
try {
236
const webExtension = await this.toWebExtension(extensionLocation);
237
const extension = await this.toScannedExtension(webExtension, true);
238
if (extension.isValid || !scanOptions?.skipInvalidExtensions) {
239
result.push(extension);
240
} else {
241
this.logService.info(`Skipping invalid additional builtin extension ${webExtension.identifier.id}`);
242
}
243
} catch (error) {
244
this.logService.info(`Error while fetching the additional builtin extension ${extensionLocation.toString()}.`, getErrorMessage(error));
245
}
246
}));
247
return result;
248
}
249
250
private async getCustomBuiltinExtensionsFromGallery(scanOptions?: ScanOptions): Promise<IScannedExtension[]> {
251
if (!this.galleryService.isEnabled()) {
252
this.logService.info('Ignoring fetching additional builtin extensions from gallery as it is disabled.');
253
return [];
254
}
255
const result: IScannedExtension[] = [];
256
const { extensions, extensionGalleryResources } = await this.readCustomBuiltinExtensionsInfoFromEnv();
257
try {
258
const cacheValue = JSON.stringify({
259
extensions: extensions.sort((a, b) => a.id.localeCompare(b.id)),
260
extensionGalleryResources: extensionGalleryResources.map(e => e.toString()).sort()
261
});
262
const useCache = this.storageService.get('additionalBuiltinExtensions', StorageScope.APPLICATION, '{}') === cacheValue;
263
const webExtensions = await (useCache ? this.getCustomBuiltinExtensionsFromCache() : this.updateCustomBuiltinExtensionsCache());
264
if (webExtensions.length) {
265
await Promise.all(webExtensions.map(async webExtension => {
266
try {
267
const extension = await this.toScannedExtension(webExtension, true);
268
if (extension.isValid || !scanOptions?.skipInvalidExtensions) {
269
result.push(extension);
270
} else {
271
this.logService.info(`Skipping invalid additional builtin gallery extension ${webExtension.identifier.id}`);
272
}
273
} catch (error) {
274
this.logService.info(`Ignoring additional builtin extension ${webExtension.identifier.id} because there is an error while converting it into scanned extension`, getErrorMessage(error));
275
}
276
}));
277
}
278
this.storageService.store('additionalBuiltinExtensions', cacheValue, StorageScope.APPLICATION, StorageTarget.MACHINE);
279
} catch (error) {
280
this.logService.info('Ignoring following additional builtin extensions as there is an error while fetching them from gallery', extensions.map(({ id }) => id), getErrorMessage(error));
281
}
282
return result;
283
}
284
285
private async getCustomBuiltinExtensionsFromCache(): Promise<IWebExtension[]> {
286
const cachedCustomBuiltinExtensions = await this.readCustomBuiltinExtensionsCache();
287
const webExtensionsMap = new Map<string, IWebExtension>();
288
for (const webExtension of cachedCustomBuiltinExtensions) {
289
const existing = webExtensionsMap.get(webExtension.identifier.id.toLowerCase());
290
if (existing) {
291
// Incase there are duplicates always take the latest version
292
if (semver.gt(existing.version, webExtension.version)) {
293
continue;
294
}
295
}
296
/* Update preRelease flag in the cache - https://github.com/microsoft/vscode/issues/142831 */
297
if (webExtension.metadata?.isPreReleaseVersion && !webExtension.metadata?.preRelease) {
298
webExtension.metadata.preRelease = true;
299
}
300
webExtensionsMap.set(webExtension.identifier.id.toLowerCase(), webExtension);
301
}
302
return [...webExtensionsMap.values()];
303
}
304
305
private _migrateExtensionsStoragePromise: Promise<void> | undefined;
306
private async migrateExtensionsStorage(customBuiltinExtensions: IExtension[]): Promise<void> {
307
if (!this._migrateExtensionsStoragePromise) {
308
this._migrateExtensionsStoragePromise = (async () => {
309
const { extensionsToMigrate } = await this.readCustomBuiltinExtensionsInfoFromEnv();
310
if (!extensionsToMigrate.length) {
311
return;
312
}
313
const fromExtensions = await this.galleryService.getExtensions(extensionsToMigrate.map(([id]) => ({ id })), CancellationToken.None);
314
try {
315
await Promise.allSettled(extensionsToMigrate.map(async ([from, to]) => {
316
const toExtension = customBuiltinExtensions.find(extension => areSameExtensions(extension.identifier, { id: to }));
317
if (toExtension) {
318
const fromExtension = fromExtensions.find(extension => areSameExtensions(extension.identifier, { id: from }));
319
const fromExtensionManifest = fromExtension ? await this.galleryService.getManifest(fromExtension, CancellationToken.None) : null;
320
const fromExtensionId = fromExtensionManifest ? getExtensionId(fromExtensionManifest.publisher, fromExtensionManifest.name) : from;
321
const toExtensionId = getExtensionId(toExtension.manifest.publisher, toExtension.manifest.name);
322
this.extensionStorageService.addToMigrationList(fromExtensionId, toExtensionId);
323
} else {
324
this.logService.info(`Skipped migrating extension storage from '${from}' to '${to}', because the '${to}' extension is not found.`);
325
}
326
}));
327
} catch (error) {
328
this.logService.error(error);
329
}
330
})();
331
}
332
return this._migrateExtensionsStoragePromise;
333
}
334
335
private async updateCaches(): Promise<void> {
336
await this.updateSystemExtensionsCache();
337
await this.updateCustomBuiltinExtensionsCache();
338
}
339
340
private async updateSystemExtensionsCache(): Promise<void> {
341
const systemExtensions = await this.builtinExtensionsScannerService.scanBuiltinExtensions();
342
const cachedSystemExtensions = (await this.readSystemExtensionsCache())
343
.filter(cached => {
344
const systemExtension = systemExtensions.find(e => areSameExtensions(e.identifier, cached.identifier));
345
return systemExtension && semver.gt(cached.version, systemExtension.manifest.version);
346
});
347
await this.writeSystemExtensionsCache(() => cachedSystemExtensions);
348
}
349
350
private _updateCustomBuiltinExtensionsCachePromise: Promise<IWebExtension[]> | undefined;
351
private async updateCustomBuiltinExtensionsCache(): Promise<IWebExtension[]> {
352
if (!this._updateCustomBuiltinExtensionsCachePromise) {
353
this._updateCustomBuiltinExtensionsCachePromise = (async () => {
354
this.logService.info('Updating additional builtin extensions cache');
355
const { extensions, extensionGalleryResources } = await this.readCustomBuiltinExtensionsInfoFromEnv();
356
const [galleryWebExtensions, extensionGalleryResourceWebExtensions] = await Promise.all([
357
this.resolveBuiltinGalleryExtensions(extensions),
358
this.resolveBuiltinExtensionGalleryResources(extensionGalleryResources)
359
]);
360
const webExtensionsMap = new Map<string, IWebExtension>();
361
for (const webExtension of [...galleryWebExtensions, ...extensionGalleryResourceWebExtensions]) {
362
webExtensionsMap.set(webExtension.identifier.id.toLowerCase(), webExtension);
363
}
364
await this.resolveDependenciesAndPackedExtensions(extensionGalleryResourceWebExtensions, webExtensionsMap);
365
const webExtensions = [...webExtensionsMap.values()];
366
await this.writeCustomBuiltinExtensionsCache(() => webExtensions);
367
return webExtensions;
368
})();
369
}
370
return this._updateCustomBuiltinExtensionsCachePromise;
371
}
372
373
private async resolveBuiltinExtensionGalleryResources(extensionGalleryResources: URI[]): Promise<IWebExtension[]> {
374
if (extensionGalleryResources.length === 0) {
375
return [];
376
}
377
const result = new Map<string, IWebExtension>();
378
const extensionInfos: IExtensionInfo[] = [];
379
await Promise.all(extensionGalleryResources.map(async extensionGalleryResource => {
380
try {
381
const webExtension = await this.toWebExtensionFromExtensionGalleryResource(extensionGalleryResource);
382
result.set(webExtension.identifier.id.toLowerCase(), webExtension);
383
extensionInfos.push({ id: webExtension.identifier.id, version: webExtension.version });
384
} catch (error) {
385
this.logService.info(`Ignoring additional builtin extension from gallery resource ${extensionGalleryResource.toString()} because there is an error while converting it into web extension`, getErrorMessage(error));
386
}
387
}));
388
const galleryExtensions = await this.galleryService.getExtensions(extensionInfos, CancellationToken.None);
389
for (const galleryExtension of galleryExtensions) {
390
const webExtension = result.get(galleryExtension.identifier.id.toLowerCase());
391
if (webExtension) {
392
result.set(galleryExtension.identifier.id.toLowerCase(), {
393
...webExtension,
394
identifier: { id: webExtension.identifier.id, uuid: galleryExtension.identifier.uuid },
395
readmeUri: galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined,
396
changelogUri: galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined,
397
metadata: { isPreReleaseVersion: galleryExtension.properties.isPreReleaseVersion, preRelease: galleryExtension.properties.isPreReleaseVersion, isBuiltin: true, pinned: true }
398
});
399
}
400
}
401
return [...result.values()];
402
}
403
404
private async resolveBuiltinGalleryExtensions(extensions: IExtensionInfo[]): Promise<IWebExtension[]> {
405
if (extensions.length === 0) {
406
return [];
407
}
408
const webExtensions: IWebExtension[] = [];
409
const galleryExtensionsMap = await this.getExtensionsWithDependenciesAndPackedExtensions(extensions);
410
const missingExtensions = extensions.filter(({ id }) => !galleryExtensionsMap.has(id.toLowerCase()));
411
if (missingExtensions.length) {
412
this.logService.info('Skipping the additional builtin extensions because their compatible versions are not found.', missingExtensions);
413
}
414
await Promise.all([...galleryExtensionsMap.values()].map(async gallery => {
415
try {
416
const webExtension = await this.toWebExtensionFromGallery(gallery, { isPreReleaseVersion: gallery.properties.isPreReleaseVersion, preRelease: gallery.properties.isPreReleaseVersion, isBuiltin: true });
417
webExtensions.push(webExtension);
418
} catch (error) {
419
this.logService.info(`Ignoring additional builtin extension ${gallery.identifier.id} because there is an error while converting it into web extension`, getErrorMessage(error));
420
}
421
}));
422
return webExtensions;
423
}
424
425
private async resolveDependenciesAndPackedExtensions(webExtensions: IWebExtension[], result: Map<string, IWebExtension>): Promise<void> {
426
const extensionInfos: IExtensionInfo[] = [];
427
for (const webExtension of webExtensions) {
428
for (const e of [...(webExtension.manifest?.extensionDependencies ?? []), ...(webExtension.manifest?.extensionPack ?? [])]) {
429
if (!result.has(e.toLowerCase())) {
430
extensionInfos.push({ id: e, version: webExtension.version });
431
}
432
}
433
}
434
if (extensionInfos.length === 0) {
435
return;
436
}
437
const galleryExtensions = await this.getExtensionsWithDependenciesAndPackedExtensions(extensionInfos, new Set<string>([...result.keys()]));
438
await Promise.all([...galleryExtensions.values()].map(async gallery => {
439
try {
440
const webExtension = await this.toWebExtensionFromGallery(gallery, { isPreReleaseVersion: gallery.properties.isPreReleaseVersion, preRelease: gallery.properties.isPreReleaseVersion, isBuiltin: true });
441
result.set(webExtension.identifier.id.toLowerCase(), webExtension);
442
} catch (error) {
443
this.logService.info(`Ignoring additional builtin extension ${gallery.identifier.id} because there is an error while converting it into web extension`, getErrorMessage(error));
444
}
445
}));
446
}
447
448
private async getExtensionsWithDependenciesAndPackedExtensions(toGet: IExtensionInfo[], seen: Set<string> = new Set<string>(), result: Map<string, IGalleryExtension> = new Map<string, IGalleryExtension>()): Promise<Map<string, IGalleryExtension>> {
449
if (toGet.length === 0) {
450
return result;
451
}
452
const extensions = await this.galleryService.getExtensions(toGet, { compatible: true, targetPlatform: TargetPlatform.WEB }, CancellationToken.None);
453
const packsAndDependencies = new Map<string, IExtensionInfo>();
454
for (const extension of extensions) {
455
result.set(extension.identifier.id.toLowerCase(), extension);
456
for (const id of [...(isNonEmptyArray(extension.properties.dependencies) ? extension.properties.dependencies : []), ...(isNonEmptyArray(extension.properties.extensionPack) ? extension.properties.extensionPack : [])]) {
457
if (!result.has(id.toLowerCase()) && !packsAndDependencies.has(id.toLowerCase()) && !seen.has(id.toLowerCase())) {
458
const extensionInfo = toGet.find(e => areSameExtensions(e, extension.identifier));
459
packsAndDependencies.set(id.toLowerCase(), { id, preRelease: extensionInfo?.preRelease });
460
}
461
}
462
}
463
return this.getExtensionsWithDependenciesAndPackedExtensions([...packsAndDependencies.values()].filter(({ id }) => !result.has(id.toLowerCase())), seen, result);
464
}
465
466
async scanSystemExtensions(): Promise<IExtension[]> {
467
return this.readSystemExtensions();
468
}
469
470
async scanUserExtensions(profileLocation: URI, scanOptions?: ScanOptions): Promise<IScannedExtension[]> {
471
const extensions = new Map<string, IScannedExtension>();
472
473
// Custom builtin extensions defined through `additionalBuiltinExtensions` API
474
const customBuiltinExtensions = await this.readCustomBuiltinExtensions(scanOptions);
475
for (const extension of customBuiltinExtensions) {
476
extensions.set(extension.identifier.id.toLowerCase(), extension);
477
}
478
479
// User Installed extensions
480
const installedExtensions = await this.scanInstalledExtensions(profileLocation, scanOptions);
481
for (const extension of installedExtensions) {
482
extensions.set(extension.identifier.id.toLowerCase(), extension);
483
}
484
485
return [...extensions.values()];
486
}
487
488
async scanExtensionsUnderDevelopment(): Promise<IExtension[]> {
489
const devExtensions = this.environmentService.options?.developmentOptions?.extensions;
490
const result: IExtension[] = [];
491
if (Array.isArray(devExtensions)) {
492
await Promise.allSettled(devExtensions.map(async devExtension => {
493
try {
494
const location = URI.revive(devExtension);
495
if (URI.isUri(location)) {
496
const webExtension = await this.toWebExtension(location);
497
result.push(await this.toScannedExtension(webExtension, false));
498
} else {
499
this.logService.info(`Skipping the extension under development ${devExtension} as it is not URI type.`);
500
}
501
} catch (error) {
502
this.logService.info(`Error while fetching the extension under development ${devExtension.toString()}.`, getErrorMessage(error));
503
}
504
}));
505
}
506
return result;
507
}
508
509
async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, profileLocation: URI): Promise<IScannedExtension | null> {
510
if (extensionType === ExtensionType.System) {
511
const systemExtensions = await this.scanSystemExtensions();
512
return systemExtensions.find(e => e.location.toString() === extensionLocation.toString()) || null;
513
}
514
const userExtensions = await this.scanUserExtensions(profileLocation);
515
return userExtensions.find(e => e.location.toString() === extensionLocation.toString()) || null;
516
}
517
518
async scanExtensionManifest(extensionLocation: URI): Promise<IExtensionManifest | null> {
519
try {
520
return await this.getExtensionManifest(extensionLocation);
521
} catch (error) {
522
this.logService.warn(`Error while fetching manifest from ${extensionLocation.toString()}`, getErrorMessage(error));
523
return null;
524
}
525
}
526
527
async addExtensionFromGallery(galleryExtension: IGalleryExtension, metadata: Metadata, profileLocation: URI): Promise<IScannedExtension> {
528
const webExtension = await this.toWebExtensionFromGallery(galleryExtension, metadata);
529
return this.addWebExtension(webExtension, profileLocation);
530
}
531
532
async addExtension(location: URI, metadata: Metadata, profileLocation: URI): Promise<IScannedExtension> {
533
const webExtension = await this.toWebExtension(location, undefined, undefined, undefined, undefined, undefined, undefined, metadata);
534
const extension = await this.toScannedExtension(webExtension, false);
535
await this.addToInstalledExtensions([webExtension], profileLocation);
536
return extension;
537
}
538
539
async removeExtension(extension: IScannedExtension, profileLocation: URI): Promise<void> {
540
await this.writeInstalledExtensions(profileLocation, installedExtensions => installedExtensions.filter(installedExtension => !areSameExtensions(installedExtension.identifier, extension.identifier)));
541
}
542
543
async updateMetadata(extension: IScannedExtension, metadata: Partial<Metadata>, profileLocation: URI): Promise<IScannedExtension> {
544
let updatedExtension: IWebExtension | undefined = undefined;
545
await this.writeInstalledExtensions(profileLocation, installedExtensions => {
546
const result: IWebExtension[] = [];
547
for (const installedExtension of installedExtensions) {
548
if (areSameExtensions(extension.identifier, installedExtension.identifier)) {
549
installedExtension.metadata = { ...installedExtension.metadata, ...metadata };
550
updatedExtension = installedExtension;
551
result.push(installedExtension);
552
} else {
553
result.push(installedExtension);
554
}
555
}
556
return result;
557
});
558
if (!updatedExtension) {
559
throw new Error('Extension not found');
560
}
561
return this.toScannedExtension(updatedExtension, extension.isBuiltin);
562
}
563
564
async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI, filter: (extension: IScannedExtension) => boolean): Promise<void> {
565
const extensionsToCopy: IWebExtension[] = [];
566
const fromWebExtensions = await this.readInstalledExtensions(fromProfileLocation);
567
await Promise.all(fromWebExtensions.map(async webExtension => {
568
const scannedExtension = await this.toScannedExtension(webExtension, false);
569
if (filter(scannedExtension)) {
570
extensionsToCopy.push(webExtension);
571
}
572
}));
573
if (extensionsToCopy.length) {
574
await this.addToInstalledExtensions(extensionsToCopy, toProfileLocation);
575
}
576
}
577
578
private async addWebExtension(webExtension: IWebExtension, profileLocation: URI): Promise<IScannedExtension> {
579
const isSystem = !!(await this.scanSystemExtensions()).find(e => areSameExtensions(e.identifier, webExtension.identifier));
580
const isBuiltin = !!webExtension.metadata?.isBuiltin;
581
const extension = await this.toScannedExtension(webExtension, isBuiltin);
582
583
if (isSystem) {
584
await this.writeSystemExtensionsCache(systemExtensions => {
585
// Remove the existing extension to avoid duplicates
586
systemExtensions = systemExtensions.filter(extension => !areSameExtensions(extension.identifier, webExtension.identifier));
587
systemExtensions.push(webExtension);
588
return systemExtensions;
589
});
590
return extension;
591
}
592
593
// Update custom builtin extensions to custom builtin extensions cache
594
if (isBuiltin) {
595
await this.writeCustomBuiltinExtensionsCache(customBuiltinExtensions => {
596
// Remove the existing extension to avoid duplicates
597
customBuiltinExtensions = customBuiltinExtensions.filter(extension => !areSameExtensions(extension.identifier, webExtension.identifier));
598
customBuiltinExtensions.push(webExtension);
599
return customBuiltinExtensions;
600
});
601
602
const installedExtensions = await this.readInstalledExtensions(profileLocation);
603
// Also add to installed extensions if it is installed to update its version
604
if (installedExtensions.some(e => areSameExtensions(e.identifier, webExtension.identifier))) {
605
await this.addToInstalledExtensions([webExtension], profileLocation);
606
}
607
return extension;
608
}
609
610
// Add to installed extensions
611
await this.addToInstalledExtensions([webExtension], profileLocation);
612
return extension;
613
}
614
615
private async addToInstalledExtensions(webExtensions: IWebExtension[], profileLocation: URI): Promise<void> {
616
await this.writeInstalledExtensions(profileLocation, installedExtensions => {
617
// Remove the existing extension to avoid duplicates
618
installedExtensions = installedExtensions.filter(installedExtension => webExtensions.some(extension => !areSameExtensions(installedExtension.identifier, extension.identifier)));
619
installedExtensions.push(...webExtensions);
620
return installedExtensions;
621
});
622
}
623
624
private async scanInstalledExtensions(profileLocation: URI, scanOptions?: ScanOptions): Promise<IScannedExtension[]> {
625
let installedExtensions = await this.readInstalledExtensions(profileLocation);
626
627
// If current profile is not a default profile, then add the application extensions to the list
628
if (!this.uriIdentityService.extUri.isEqual(profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource)) {
629
// Remove application extensions from the non default profile
630
installedExtensions = installedExtensions.filter(i => !i.metadata?.isApplicationScoped);
631
// Add application extensions from the default profile to the list
632
const defaultProfileExtensions = await this.readInstalledExtensions(this.userDataProfilesService.defaultProfile.extensionsResource);
633
installedExtensions.push(...defaultProfileExtensions.filter(i => i.metadata?.isApplicationScoped));
634
}
635
636
installedExtensions.sort((a, b) => a.identifier.id < b.identifier.id ? -1 : a.identifier.id > b.identifier.id ? 1 : semver.rcompare(a.version, b.version));
637
const result = new Map<string, IScannedExtension>();
638
for (const webExtension of installedExtensions) {
639
const existing = result.get(webExtension.identifier.id.toLowerCase());
640
if (existing && semver.gt(existing.manifest.version, webExtension.version)) {
641
continue;
642
}
643
const extension = await this.toScannedExtension(webExtension, false);
644
if (extension.isValid || !scanOptions?.skipInvalidExtensions) {
645
result.set(extension.identifier.id.toLowerCase(), extension);
646
} else {
647
this.logService.info(`Skipping invalid installed extension ${webExtension.identifier.id}`);
648
}
649
}
650
return [...result.values()];
651
}
652
653
private async toWebExtensionFromGallery(galleryExtension: IGalleryExtension, metadata?: Metadata): Promise<IWebExtension> {
654
const extensionLocation = await this.extensionResourceLoaderService.getExtensionGalleryResourceURL({
655
publisher: galleryExtension.publisher,
656
name: galleryExtension.name,
657
version: galleryExtension.version,
658
targetPlatform: galleryExtension.properties.targetPlatform === TargetPlatform.WEB ? TargetPlatform.WEB : undefined
659
}, 'extension');
660
661
if (!extensionLocation) {
662
throw new Error('No extension gallery service configured.');
663
}
664
665
return this.toWebExtensionFromExtensionGalleryResource(extensionLocation,
666
galleryExtension.identifier,
667
galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined,
668
galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined,
669
metadata);
670
}
671
672
private async toWebExtensionFromExtensionGalleryResource(extensionLocation: URI, identifier?: IExtensionIdentifier, readmeUri?: URI, changelogUri?: URI, metadata?: Metadata): Promise<IWebExtension> {
673
const extensionResources = await this.listExtensionResources(extensionLocation);
674
const packageNLSResources = this.getPackageNLSResourceMapFromResources(extensionResources);
675
676
// The fallback, in English, will fill in any gaps missing in the localized file.
677
const fallbackPackageNLSResource = extensionResources.find(e => basename(e) === 'package.nls.json');
678
return this.toWebExtension(
679
extensionLocation,
680
identifier,
681
undefined,
682
packageNLSResources,
683
fallbackPackageNLSResource ? URI.parse(fallbackPackageNLSResource) : null,
684
readmeUri,
685
changelogUri,
686
metadata);
687
}
688
689
private getPackageNLSResourceMapFromResources(extensionResources: string[]): Map<string, URI> {
690
const packageNLSResources = new Map<string, URI>();
691
extensionResources.forEach(e => {
692
// Grab all package.nls.{language}.json files
693
const regexResult = /package\.nls\.([\w-]+)\.json/.exec(basename(e));
694
if (regexResult?.[1]) {
695
packageNLSResources.set(regexResult[1], URI.parse(e));
696
}
697
});
698
return packageNLSResources;
699
}
700
701
private async toWebExtension(extensionLocation: URI, identifier?: IExtensionIdentifier, manifest?: IExtensionManifest, packageNLSUris?: Map<string, URI>, fallbackPackageNLSUri?: URI | ITranslations | null, readmeUri?: URI, changelogUri?: URI, metadata?: Metadata): Promise<IWebExtension> {
702
if (!manifest) {
703
try {
704
manifest = await this.getExtensionManifest(extensionLocation);
705
} catch (error) {
706
throw new Error(`Error while fetching manifest from the location '${extensionLocation.toString()}'. ${getErrorMessage(error)}`);
707
}
708
}
709
710
if (!this.extensionManifestPropertiesService.canExecuteOnWeb(manifest)) {
711
throw new Error(localize('not a web extension', "Cannot add '{0}' because this extension is not a web extension.", manifest.displayName || manifest.name));
712
}
713
714
if (fallbackPackageNLSUri === undefined) {
715
try {
716
fallbackPackageNLSUri = joinPath(extensionLocation, 'package.nls.json');
717
await this.extensionResourceLoaderService.readExtensionResource(fallbackPackageNLSUri);
718
} catch (error) {
719
fallbackPackageNLSUri = undefined;
720
}
721
}
722
const defaultManifestTranslations: ITranslations | null | undefined = fallbackPackageNLSUri ? URI.isUri(fallbackPackageNLSUri) ? await this.getTranslations(fallbackPackageNLSUri) : fallbackPackageNLSUri : null;
723
724
return {
725
identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name), uuid: identifier?.uuid },
726
version: manifest.version,
727
location: extensionLocation,
728
manifest,
729
readmeUri,
730
changelogUri,
731
packageNLSUris,
732
fallbackPackageNLSUri: URI.isUri(fallbackPackageNLSUri) ? fallbackPackageNLSUri : undefined,
733
defaultManifestTranslations,
734
metadata,
735
};
736
}
737
738
private async toScannedExtension(webExtension: IWebExtension, isBuiltin: boolean, type: ExtensionType = ExtensionType.User): Promise<IScannedExtension> {
739
const validations: [Severity, string][] = [];
740
let manifest: IRelaxedExtensionManifest | undefined = webExtension.manifest;
741
742
if (!manifest) {
743
try {
744
manifest = await this.getExtensionManifest(webExtension.location);
745
} catch (error) {
746
validations.push([Severity.Error, `Error while fetching manifest from the location '${webExtension.location}'. ${getErrorMessage(error)}`]);
747
}
748
}
749
750
if (!manifest) {
751
const [publisher, name] = webExtension.identifier.id.split('.');
752
manifest = {
753
name,
754
publisher,
755
version: webExtension.version,
756
engines: { vscode: '*' },
757
};
758
}
759
760
const packageNLSUri = webExtension.packageNLSUris?.get(Language.value().toLowerCase());
761
const fallbackPackageNLS = webExtension.defaultManifestTranslations ?? webExtension.fallbackPackageNLSUri;
762
763
if (packageNLSUri) {
764
manifest = await this.translateManifest(manifest, packageNLSUri, fallbackPackageNLS);
765
} else if (fallbackPackageNLS) {
766
manifest = await this.translateManifest(manifest, fallbackPackageNLS);
767
}
768
769
const uuid = (<IGalleryMetadata | undefined>webExtension.metadata)?.id;
770
771
const validateApiVersion = this.extensionsEnabledWithApiProposalVersion.includes(webExtension.identifier.id.toLowerCase());
772
validations.push(...validateExtensionManifest(this.productService.version, this.productService.date, webExtension.location, manifest, false, validateApiVersion));
773
let isValid = true;
774
for (const [severity, message] of validations) {
775
if (severity === Severity.Error) {
776
isValid = false;
777
this.logService.error(message);
778
}
779
}
780
781
if (manifest.enabledApiProposals && validateApiVersion) {
782
manifest.enabledApiProposals = parseEnabledApiProposalNames([...manifest.enabledApiProposals]);
783
}
784
785
return {
786
identifier: { id: webExtension.identifier.id, uuid: webExtension.identifier.uuid || uuid },
787
location: webExtension.location,
788
manifest,
789
type,
790
isBuiltin,
791
readmeUrl: webExtension.readmeUri,
792
changelogUrl: webExtension.changelogUri,
793
metadata: webExtension.metadata,
794
targetPlatform: TargetPlatform.WEB,
795
validations,
796
isValid,
797
preRelease: !!webExtension.metadata?.preRelease,
798
};
799
}
800
801
private async listExtensionResources(extensionLocation: URI): Promise<string[]> {
802
try {
803
const result = await this.extensionResourceLoaderService.readExtensionResource(extensionLocation);
804
return JSON.parse(result);
805
} catch (error) {
806
this.logService.warn('Error while fetching extension resources list', getErrorMessage(error));
807
}
808
return [];
809
}
810
811
private async translateManifest(manifest: IExtensionManifest, nlsURL: ITranslations | URI, fallbackNLS?: ITranslations | URI): Promise<IRelaxedExtensionManifest> {
812
try {
813
const translations = URI.isUri(nlsURL) ? await this.getTranslations(nlsURL) : nlsURL;
814
const fallbackTranslations = URI.isUri(fallbackNLS) ? await this.getTranslations(fallbackNLS) : fallbackNLS;
815
if (translations) {
816
manifest = localizeManifest(this.logService, manifest, translations, fallbackTranslations);
817
}
818
} catch (error) { /* ignore */ }
819
return manifest;
820
}
821
822
private async getExtensionManifest(location: URI): Promise<IExtensionManifest> {
823
const url = joinPath(location, 'package.json');
824
const content = await this.extensionResourceLoaderService.readExtensionResource(url);
825
return JSON.parse(content);
826
}
827
828
private async getTranslations(nlsUrl: URI): Promise<ITranslations | undefined> {
829
try {
830
const content = await this.extensionResourceLoaderService.readExtensionResource(nlsUrl);
831
return JSON.parse(content);
832
} catch (error) {
833
this.logService.error(`Error while fetching translations of an extension`, nlsUrl.toString(), getErrorMessage(error));
834
}
835
return undefined;
836
}
837
838
private async readInstalledExtensions(profileLocation: URI): Promise<IWebExtension[]> {
839
return this.withWebExtensions(profileLocation);
840
}
841
842
private writeInstalledExtensions(profileLocation: URI, updateFn: (extensions: IWebExtension[]) => IWebExtension[]): Promise<IWebExtension[]> {
843
return this.withWebExtensions(profileLocation, updateFn);
844
}
845
846
private readCustomBuiltinExtensionsCache(): Promise<IWebExtension[]> {
847
return this.withWebExtensions(this.customBuiltinExtensionsCacheResource);
848
}
849
850
private writeCustomBuiltinExtensionsCache(updateFn: (extensions: IWebExtension[]) => IWebExtension[]): Promise<IWebExtension[]> {
851
return this.withWebExtensions(this.customBuiltinExtensionsCacheResource, updateFn);
852
}
853
854
private readSystemExtensionsCache(): Promise<IWebExtension[]> {
855
return this.withWebExtensions(this.systemExtensionsCacheResource);
856
}
857
858
private writeSystemExtensionsCache(updateFn: (extensions: IWebExtension[]) => IWebExtension[]): Promise<IWebExtension[]> {
859
return this.withWebExtensions(this.systemExtensionsCacheResource, updateFn);
860
}
861
862
private async withWebExtensions(file: URI | undefined, updateFn?: (extensions: IWebExtension[]) => IWebExtension[]): Promise<IWebExtension[]> {
863
if (!file) {
864
return [];
865
}
866
return this.getResourceAccessQueue(file).queue(async () => {
867
let webExtensions: IWebExtension[] = [];
868
869
// Read
870
try {
871
const content = await this.fileService.readFile(file);
872
const storedWebExtensions: IStoredWebExtension[] = JSON.parse(content.value.toString());
873
for (const e of storedWebExtensions) {
874
if (!e.location || !e.identifier || !e.version) {
875
this.logService.info('Ignoring invalid extension while scanning', storedWebExtensions);
876
continue;
877
}
878
let packageNLSUris: Map<string, URI> | undefined;
879
if (e.packageNLSUris) {
880
packageNLSUris = new Map<string, URI>();
881
Object.entries(e.packageNLSUris).forEach(([key, value]) => packageNLSUris!.set(key, URI.revive(value)));
882
}
883
884
webExtensions.push({
885
identifier: e.identifier,
886
version: e.version,
887
location: URI.revive(e.location),
888
manifest: e.manifest,
889
readmeUri: URI.revive(e.readmeUri),
890
changelogUri: URI.revive(e.changelogUri),
891
packageNLSUris,
892
fallbackPackageNLSUri: URI.revive(e.fallbackPackageNLSUri),
893
defaultManifestTranslations: e.defaultManifestTranslations,
894
packageNLSUri: URI.revive(e.packageNLSUri),
895
metadata: e.metadata,
896
});
897
}
898
899
try {
900
webExtensions = await this.migrateWebExtensions(webExtensions, file);
901
} catch (error) {
902
this.logService.error(`Error while migrating scanned extensions in ${file.toString()}`, getErrorMessage(error));
903
}
904
905
} catch (error) {
906
/* Ignore */
907
if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) {
908
this.logService.error(error);
909
}
910
}
911
912
// Update
913
if (updateFn) {
914
await this.storeWebExtensions(webExtensions = updateFn(webExtensions), file);
915
}
916
917
return webExtensions;
918
});
919
}
920
921
private async migrateWebExtensions(webExtensions: IWebExtension[], file: URI): Promise<IWebExtension[]> {
922
let update = false;
923
webExtensions = await Promise.all(webExtensions.map(async webExtension => {
924
if (!webExtension.manifest) {
925
try {
926
webExtension.manifest = await this.getExtensionManifest(webExtension.location);
927
update = true;
928
} catch (error) {
929
this.logService.error(`Error while updating manifest of an extension in ${file.toString()}`, webExtension.identifier.id, getErrorMessage(error));
930
}
931
}
932
if (isUndefined(webExtension.defaultManifestTranslations)) {
933
if (webExtension.fallbackPackageNLSUri) {
934
try {
935
const content = await this.extensionResourceLoaderService.readExtensionResource(webExtension.fallbackPackageNLSUri);
936
webExtension.defaultManifestTranslations = JSON.parse(content);
937
update = true;
938
} catch (error) {
939
this.logService.error(`Error while fetching default manifest translations of an extension`, webExtension.identifier.id, getErrorMessage(error));
940
}
941
} else {
942
update = true;
943
webExtension.defaultManifestTranslations = null;
944
}
945
}
946
const migratedLocation = migratePlatformSpecificExtensionGalleryResourceURL(webExtension.location, TargetPlatform.WEB);
947
if (migratedLocation) {
948
update = true;
949
webExtension.location = migratedLocation;
950
}
951
if (isUndefined(webExtension.metadata?.hasPreReleaseVersion) && webExtension.metadata?.preRelease) {
952
update = true;
953
webExtension.metadata.hasPreReleaseVersion = true;
954
}
955
return webExtension;
956
}));
957
if (update) {
958
await this.storeWebExtensions(webExtensions, file);
959
}
960
return webExtensions;
961
}
962
963
private async storeWebExtensions(webExtensions: IWebExtension[], file: URI): Promise<void> {
964
function toStringDictionary(dictionary: Map<string, URI> | undefined): IStringDictionary<UriComponents> | undefined {
965
if (!dictionary) {
966
return undefined;
967
}
968
const result: IStringDictionary<UriComponents> = Object.create(null);
969
dictionary.forEach((value, key) => result[key] = value.toJSON());
970
return result;
971
}
972
const storedWebExtensions: IStoredWebExtension[] = webExtensions.map(e => ({
973
identifier: e.identifier,
974
version: e.version,
975
manifest: e.manifest,
976
location: e.location.toJSON(),
977
readmeUri: e.readmeUri?.toJSON(),
978
changelogUri: e.changelogUri?.toJSON(),
979
packageNLSUris: toStringDictionary(e.packageNLSUris),
980
defaultManifestTranslations: e.defaultManifestTranslations,
981
fallbackPackageNLSUri: e.fallbackPackageNLSUri?.toJSON(),
982
metadata: e.metadata
983
}));
984
await this.fileService.writeFile(file, VSBuffer.fromString(JSON.stringify(storedWebExtensions)));
985
}
986
987
private getResourceAccessQueue(file: URI): Queue<IWebExtension[]> {
988
let resourceQueue = this.resourcesAccessQueueMap.get(file);
989
if (!resourceQueue) {
990
this.resourcesAccessQueueMap.set(file, resourceQueue = new Queue<IWebExtension[]>());
991
}
992
return resourceQueue;
993
}
994
995
}
996
997
if (isWeb) {
998
registerAction2(class extends Action2 {
999
constructor() {
1000
super({
1001
id: 'workbench.extensions.action.openInstalledWebExtensionsResource',
1002
title: localize2('openInstalledWebExtensionsResource', 'Open Installed Web Extensions Resource'),
1003
category: Categories.Developer,
1004
f1: true,
1005
precondition: IsWebContext
1006
});
1007
}
1008
run(serviceAccessor: ServicesAccessor): void {
1009
const editorService = serviceAccessor.get(IEditorService);
1010
const userDataProfileService = serviceAccessor.get(IUserDataProfileService);
1011
editorService.openEditor({ resource: userDataProfileService.currentProfile.extensionsResource });
1012
}
1013
});
1014
}
1015
1016
registerSingleton(IWebExtensionsScannerService, WebExtensionsScannerService, InstantiationType.Delayed);
1017
1018