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