From 7178cdceb6484350122bb953996bc5398c3b90dc Mon Sep 17 00:00:00 2001 From: Lazy <2818242447@qq.com> Date: Thu, 21 May 2026 13:24:38 +0800 Subject: [PATCH] Include installed extensions missing from VS Code API --- src/service/installedExtensions.service.ts | 102 ++++++++++++++++++ src/service/plugin.service.ts | 33 +++--- .../pluginService/pluginService.test.ts | 101 +++++++++++++++++ 3 files changed, 220 insertions(+), 16 deletions(-) create mode 100644 src/service/installedExtensions.service.ts create mode 100644 test/service/pluginService/pluginService.test.ts diff --git a/src/service/installedExtensions.service.ts b/src/service/installedExtensions.service.ts new file mode 100644 index 00000000..c414d88c --- /dev/null +++ b/src/service/installedExtensions.service.ts @@ -0,0 +1,102 @@ +"use strict"; + +import * as fs from "fs-extra"; +import * as path from "path"; + +export interface IInstalledExtensionMetadata { + galleryApiUrl: string; + id: string; + downloadUrl: string; + publisherId: string; + publisherDisplayName: string; + date: string; +} + +export interface IInstalledExtensionInformation { + metadata: IInstalledExtensionMetadata; + name: string; + version: string; + publisher: string; +} + +export class InstalledExtensionsService { + public static CreateExtensionFromPackageJSON( + packageJSON: any + ): IInstalledExtensionInformation { + const meta = packageJSON.__metadata || { + id: packageJSON.uuid, + publisherId: packageJSON.publisher, + publisherDisplayName: packageJSON.publisher + }; + + return { + metadata: { + galleryApiUrl: meta.galleryApiUrl, + id: meta.id, + downloadUrl: meta.downloadUrl, + publisherId: meta.publisherId, + publisherDisplayName: meta.publisherDisplayName, + date: meta.date + }, + name: packageJSON.name, + publisher: packageJSON.publisher, + version: packageJSON.version + }; + } + + public static MergeFromExtensionFolder( + extensions: IInstalledExtensionInformation[], + extensionFolder: string + ): IInstalledExtensionInformation[] { + if (!extensionFolder || !fs.existsSync(extensionFolder)) { + return extensions; + } + + const existingExtensions = extensions.map(ext => + InstalledExtensionsService.GetExtensionFullName(ext) + ); + + fs.readdirSync(extensionFolder).forEach(folderName => { + const packageFile = path.join( + extensionFolder, + folderName, + "package.json" + ); + if (!fs.existsSync(packageFile)) { + return; + } + + try { + const packageJSON = fs.readJSONSync(packageFile); + if ( + packageJSON.isBuiltin || + !packageJSON.name || + !packageJSON.publisher + ) { + return; + } + + const extension = InstalledExtensionsService.CreateExtensionFromPackageJSON( + packageJSON + ); + const extensionName = InstalledExtensionsService.GetExtensionFullName( + extension + ); + if (!existingExtensions.includes(extensionName)) { + extensions.push(extension); + existingExtensions.push(extensionName); + } + } catch (err) { + console.warn(`Sync : Unable to read extension ${folderName}: ${err}`); + } + }); + + return extensions; + } + + public static GetExtensionFullName( + extension: IInstalledExtensionInformation + ): string { + return `${extension.publisher}.${extension.name}`.toLowerCase(); + } +} diff --git a/src/service/plugin.service.ts b/src/service/plugin.service.ts index 12abe312..1755cf6c 100644 --- a/src/service/plugin.service.ts +++ b/src/service/plugin.service.ts @@ -1,5 +1,7 @@ "use strict"; import * as vscode from "vscode"; +import { state } from "../state"; +import { InstalledExtensionsService } from "./installedExtensions.service"; export class ExtensionInformation { public static fromJSON(text: string) { @@ -123,29 +125,28 @@ export class PluginService { } public static CreateExtensionList() { - return vscode.extensions.all + const extensions = vscode.extensions.all .filter(ext => !ext.packageJSON.isBuiltin) .map(ext => { - const meta = ext.packageJSON.__metadata || { - id: ext.packageJSON.uuid, - publisherId: ext.id, - publisherDisplayName: ext.packageJSON.publisher - }; - const data = new ExtensionMetadata( - meta.galleryApiUrl, - meta.id, - meta.downloadUrl, - meta.publisherId, - meta.publisherDisplayName, - meta.date + const extension = InstalledExtensionsService.CreateExtensionFromPackageJSON( + ext.packageJSON + ); + const data = Object.assign( + new ExtensionMetadata("", "", "", "", "", ""), + extension.metadata ); const info = new ExtensionInformation(); info.metadata = data; - info.name = ext.packageJSON.name; - info.publisher = ext.packageJSON.publisher; - info.version = ext.packageJSON.version; + info.name = extension.name; + info.publisher = extension.publisher; + info.version = extension.version; return info; }); + + return InstalledExtensionsService.MergeFromExtensionFolder( + extensions, + state.environment.EXTENSION_FOLDER + ); } public static async DeleteExtension( diff --git a/test/service/pluginService/pluginService.test.ts b/test/service/pluginService/pluginService.test.ts new file mode 100644 index 00000000..fae1a33e --- /dev/null +++ b/test/service/pluginService/pluginService.test.ts @@ -0,0 +1,101 @@ +import { expect } from "chai"; +import * as fs from "fs-extra"; +import * as os from "os"; +import * as path from "path"; + +import { + IInstalledExtensionInformation, + InstalledExtensionsService +} from "../../../src/service/installedExtensions.service"; + +describe("PluginService", () => { + describe("MergeInstalledExtensionsFromDisk", () => { + let extensionFolder: string; + + beforeEach(() => { + extensionFolder = fs.mkdtempSync( + path.join(os.tmpdir(), "settings-sync-extensions-") + ); + }); + + afterEach(() => { + fs.removeSync(extensionFolder); + }); + + it("should include installed extensions that are not returned by the VS Code API", () => { + const enabledExtension: IInstalledExtensionInformation = { + metadata: { + date: undefined, + downloadUrl: undefined, + galleryApiUrl: undefined, + id: "enabled-id", + publisherDisplayName: "Sample", + publisherId: "sample" + }, + name: "enabled", + publisher: "sample", + version: "1.0.0" + }; + + fs.mkdirpSync(path.join(extensionFolder, "sample.disabled-2.0.0")); + fs.writeJSONSync( + path.join(extensionFolder, "sample.disabled-2.0.0", "package.json"), + { + __metadata: { + id: "disabled-id", + publisherDisplayName: "Sample", + publisherId: "sample" + }, + name: "disabled", + publisher: "sample", + version: "2.0.0" + } + ); + + const extensions = InstalledExtensionsService.MergeFromExtensionFolder( + [enabledExtension], + extensionFolder + ); + + expect(extensions.map(ext => `${ext.publisher}.${ext.name}`)).to.deep.eq([ + "sample.enabled", + "sample.disabled" + ]); + expect(extensions[1].version).to.eq("2.0.0"); + }); + + it("should not duplicate extensions that are already returned by the VS Code API", () => { + const extension: IInstalledExtensionInformation = { + metadata: { + date: undefined, + downloadUrl: undefined, + galleryApiUrl: undefined, + id: "enabled-id", + publisherDisplayName: "Sample", + publisherId: "sample" + }, + name: "enabled", + publisher: "sample", + version: "1.0.0" + }; + + fs.mkdirpSync(path.join(extensionFolder, "sample.enabled-1.0.0")); + fs.writeJSONSync( + path.join(extensionFolder, "sample.enabled-1.0.0", "package.json"), + { + name: "enabled", + publisher: "sample", + version: "1.0.0" + } + ); + + const extensions = InstalledExtensionsService.MergeFromExtensionFolder( + [extension], + extensionFolder + ); + + expect(extensions).to.have.length(1); + expect(extensions[0].name).to.eq("enabled"); + }); + }); +});