From 37c36363d835306f22979d8154702510b6ce693e Mon Sep 17 00:00:00 2001 From: Louis <133622055+louis-jan@users.noreply.github.com> Date: Wed, 1 Nov 2023 09:48:28 +0700 Subject: [PATCH] refactor: plugin manager and execution as ts (#504) * refactor: plugin manager and execution as ts * chore: refactoring --- .../execution/ExtensionPoint.test.js | 116 ------- .../plugin-manager/execution/Plugin.test.js | 22 -- .../execution/activation-manager.test.js | 307 ------------------ .../execution/extension-manager.test.js | 116 ------- .../plugin-manager/execution/index.test.js | 28 -- .../core/plugin-manager/facade/index.test.js | 196 ----------- .../plugin-manager/pluginMgr/Plugin.test.js | 212 ------------ .../core/plugin-manager/pluginMgr/globals.js | 57 ---- .../plugin-manager/pluginMgr/index.test.js | 150 --------- .../core/plugin-manager/pluginMgr/router.js | 91 ------ .../plugin-manager/pluginMgr/store.test.js | 108 ------ .../facade/index.js => plugin/facade.ts} | 14 +- electron/core/plugin/globals.ts | 36 ++ .../pluginMgr/index.js => plugin/index.ts} | 108 +++--- .../pluginMgr/Plugin.js => plugin/plugin.ts} | 138 ++++---- electron/core/plugin/router.ts | 97 ++++++ .../pluginMgr/store.js => plugin/store.ts} | 66 ++-- electron/main.ts | 2 +- electron/package.json | 2 + electron/preload.ts | 3 +- web/app/_components/Preferences.tsx | 15 +- web/containers/Providers/index.tsx | 13 +- web/helpers/EventHandler.tsx | 49 +-- web/hooks/useGetDownloadedModels.ts | 2 +- web/hooks/useGetModelById.ts | 2 +- web/hooks/useGetSystemResources.ts | 2 +- .../Activation.js => web/plugin/Activation.ts | 4 +- .../plugin/ExtensionPoint.ts | 49 +-- .../Plugin.js => web/plugin/Plugin.ts | 18 +- .../plugin/activation-manager.ts | 34 +- .../plugin/extension-manager.ts | 36 +- .../facade.js => web/plugin/facade.ts | 89 ++--- .../plugin/import-manager.ts | 12 +- .../execution/index.js => web/plugin/index.ts | 12 +- .../Settings/CorePlugins/PluginsCatalog.tsx | 15 +- .../CorePlugins/PreferencePlugins/index.tsx | 7 +- web/screens/Settings/index.tsx | 7 +- web/services/pluginService.ts | 3 +- web/tsconfig.json | 4 +- web/types/index.d.ts | 1 + 40 files changed, 518 insertions(+), 1725 deletions(-) delete mode 100644 electron/core/plugin-manager/execution/ExtensionPoint.test.js delete mode 100644 electron/core/plugin-manager/execution/Plugin.test.js delete mode 100644 electron/core/plugin-manager/execution/activation-manager.test.js delete mode 100644 electron/core/plugin-manager/execution/extension-manager.test.js delete mode 100644 electron/core/plugin-manager/execution/index.test.js delete mode 100644 electron/core/plugin-manager/facade/index.test.js delete mode 100644 electron/core/plugin-manager/pluginMgr/Plugin.test.js delete mode 100644 electron/core/plugin-manager/pluginMgr/globals.js delete mode 100644 electron/core/plugin-manager/pluginMgr/index.test.js delete mode 100644 electron/core/plugin-manager/pluginMgr/router.js delete mode 100644 electron/core/plugin-manager/pluginMgr/store.test.js rename electron/core/{plugin-manager/facade/index.js => plugin/facade.ts} (74%) create mode 100644 electron/core/plugin/globals.ts rename electron/core/{plugin-manager/pluginMgr/index.js => plugin/index.ts} (54%) rename electron/core/{plugin-manager/pluginMgr/Plugin.js => plugin/plugin.ts} (59%) create mode 100644 electron/core/plugin/router.ts rename electron/core/{plugin-manager/pluginMgr/store.js => plugin/store.ts} (65%) rename electron/core/plugin-manager/execution/Activation.js => web/plugin/Activation.ts (89%) rename electron/core/plugin-manager/execution/ExtensionPoint.js => web/plugin/ExtensionPoint.ts (80%) rename electron/core/plugin-manager/execution/Plugin.js => web/plugin/Plugin.ts (72%) rename electron/core/plugin-manager/execution/activation-manager.js => web/plugin/activation-manager.ts (74%) rename electron/core/plugin-manager/execution/extension-manager.js => web/plugin/extension-manager.ts (80%) rename electron/core/plugin-manager/execution/facade.js => web/plugin/facade.ts (74%) rename electron/core/plugin-manager/execution/import-manager.js => web/plugin/import-manager.ts (85%) rename electron/core/plugin-manager/execution/index.js => web/plugin/index.ts (70%) diff --git a/electron/core/plugin-manager/execution/ExtensionPoint.test.js b/electron/core/plugin-manager/execution/ExtensionPoint.test.js deleted file mode 100644 index 54d7b37ec..000000000 --- a/electron/core/plugin-manager/execution/ExtensionPoint.test.js +++ /dev/null @@ -1,116 +0,0 @@ -import Ep from './ExtensionPoint' - -/** @type {Ep} */ -let ep -const changeListener = jest.fn() - -const objectRsp = { foo: 'bar' } -const funcRsp = arr => { - arr || (arr = []) - arr.push({ foo: 'baz' }) - return arr -} - -beforeEach(() => { - ep = new Ep('test-ep') - ep.register('test-ext-obj', objectRsp) - ep.register('test-ext-func', funcRsp, 10) - ep.onRegister('test', changeListener) -}) - - -it('should create a new extension point by providing a name', () => { - expect(ep.name).toEqual('test-ep') -}) - -it('should register extension with extension point', () => { - expect(ep._extensions).toContainEqual({ - name: 'test-ext-func', - response: funcRsp, - priority: 10 - }) -}) - -it('should register extension with a default priority of 0 if not provided', () => { - expect(ep._extensions).toContainEqual({ - name: 'test-ext-obj', - response: objectRsp, - priority: 0 - }) -}) - -it('should execute the change listeners on registering a new extension', () => { - changeListener.mockClear() - ep.register('test-change-listener', true) - expect(changeListener.mock.calls.length).toBeTruthy() -}) - -it('should unregister an extension with the provided name if it exists', () => { - ep.unregister('test-ext-obj') - - expect(ep._extensions).not.toContainEqual( - expect.objectContaining({ - name: 'test-ext-obj' - }) - ) -}) - -it('should not unregister any extensions if the provided name does not exist', () => { - ep.unregister('test-ext-invalid') - - expect(ep._extensions.length).toBe(2) -}) - -it('should execute the change listeners on unregistering an extension', () => { - changeListener.mockClear() - ep.unregister('test-ext-obj') - expect(changeListener.mock.calls.length).toBeTruthy() -}) - -it('should empty the registry of all extensions on clearing', () => { - ep.clear() - - expect(ep._extensions).toEqual([]) -}) - -it('should execute the change listeners on clearing extensions', () => { - changeListener.mockClear() - ep.clear() - expect(changeListener.mock.calls.length).toBeTruthy() -}) - -it('should return the relevant extension using the get method', () => { - const ext = ep.get('test-ext-obj') - - expect(ext).toEqual({ foo: 'bar' }) -}) - -it('should return the false using the get method if the extension does not exist', () => { - const ext = ep.get('test-ext-invalid') - - expect(ext).toBeUndefined() -}) - -it('should provide an array with all responses, including promises where necessary, using the execute method', async () => { - ep.register('test-ext-async', () => new Promise(resolve => setTimeout(resolve, 0, { foo: 'delayed' }))) - const arr = ep.execute([]) - - const res = await Promise.all(arr) - - expect(res).toContainEqual({ foo: 'bar' }) - expect(res).toContainEqual([{ foo: 'baz' }]) - expect(res).toContainEqual({ foo: 'delayed' }) - expect(res.length).toBe(3) -}) - -it('should provide an array including all responses in priority order, using the executeSerial method provided with an array', async () => { - const res = await ep.executeSerial([]) - - expect(res).toEqual([{ "foo": "bar" }, { "foo": "baz" }]) -}) - -it('should provide an array including the last response using the executeSerial method provided with something other than an array', async () => { - const res = await ep.executeSerial() - - expect(res).toEqual([{ "foo": "baz" }]) -}) diff --git a/electron/core/plugin-manager/execution/Plugin.test.js b/electron/core/plugin-manager/execution/Plugin.test.js deleted file mode 100644 index 7982decdc..000000000 --- a/electron/core/plugin-manager/execution/Plugin.test.js +++ /dev/null @@ -1,22 +0,0 @@ -import { setImporter } from "./import-manager" -import Plugin from './Plugin' - -describe('triggerExport', () => { - it('should call the provided export on the plugin\'s main file', async () => { - // Set up mock importer with mock main plugin file - const mockExport = jest.fn() - const mockImporter = jest.fn(() => ({ - lifeCycleFn: mockExport - })) - setImporter(mockImporter) - - // Call triggerExport on new plugin - const plgUrl = 'main' - const plugin = new Plugin('test', plgUrl, ['ap1'], true) - await plugin.triggerExport('lifeCycleFn') - - // Check results - expect(mockImporter.mock.lastCall).toEqual([plgUrl]) - expect(mockExport.mock.calls.length).toBeTruthy() - }) -}) \ No newline at end of file diff --git a/electron/core/plugin-manager/execution/activation-manager.test.js b/electron/core/plugin-manager/execution/activation-manager.test.js deleted file mode 100644 index dd7ff9251..000000000 --- a/electron/core/plugin-manager/execution/activation-manager.test.js +++ /dev/null @@ -1,307 +0,0 @@ -import { setup } from './index' -import { register, trigger, remove, clear, get } from "./activation-manager"; -import { add } from './extension-manager' - -let mockPlugins = {} -setup({ - importer(plugin) { return mockPlugins[plugin] } -}) - -afterEach(() => { - clear() - mockPlugins = {} -}) - -describe('register', () => { - it('should add a new activation point to the register when a new, valid plugin is registered', - () => { - register({ - name: 'test', - url: 'testPkg', - activationPoints: ['ap1', 'ap2'], - active: true - }) - - expect(get()).toEqual([ - { - plugin: 'test', - url: 'testPkg', - activationPoint: 'ap1', - activated: false - }, - { - plugin: 'test', - url: 'testPkg', - activationPoint: 'ap2', - activated: false - } - ]) - } - ) - - it('should not add an activation point to the register when an existing, valid plugin is registered', - () => { - register({ - name: 'test', - url: 'testPkg', - activationPoints: ['ap1', 'ap2'], - active: true - }) - - register({ - name: 'test', - url: 'testPkg', - activationPoints: ['ap2', 'ap3'], - active: true - }) - - expect(get()).toEqual([ - { - plugin: 'test', - url: 'testPkg', - activationPoint: 'ap1', - activated: false - }, - { - plugin: 'test', - url: 'testPkg', - activationPoint: 'ap2', - activated: false - }, - { - plugin: 'test', - url: 'testPkg', - activationPoint: 'ap3', - activated: false - }, - ]) - } - ) - - it('should throw an error when an invalid plugin is registered', - () => { - const noActivationPoints = () => register({ - name: 'test', - url: 'testPkg', - active: true - }) - - expect(noActivationPoints).toThrow(/does not have any activation points set up in its manifest/) - } - ) -}) - -describe('trigger', () => { - it('should trigger all and only the activations with for the given execution point on triggering an execution, using the defined importer', - async () => { - const triggered = [] - - mockPlugins.plugin1 = { - ap1() { triggered.push('plugin1-ap1') } - } - mockPlugins.plugin2 = { - ap2() { triggered.push('plugin2-ap2') } - } - mockPlugins.plugin3 = { - ap1() { triggered.push('plugin3-ap1') }, - ap2() { triggered.push('plugin3-ap2') } - } - - register({ - name: 'plugin1', - url: 'plugin1', - activationPoints: ['ap1'], - active: true - }) - register({ - name: 'plugin2', - url: 'plugin2', - activationPoints: ['ap2'], - active: true - }) - register({ - name: 'plugin3', - url: 'plugin3', - activationPoints: ['ap1', 'ap2'], - active: true - }) - - await trigger('ap1') - - expect(triggered).toEqual(['plugin1-ap1', 'plugin3-ap1']) - } - ) - - it('should return an error if an activation point is triggered on a plugin that does not include it', - async () => { - mockPlugins.plugin1 = { - wrongAp() { } - } - - register({ - name: 'plugin1', - url: 'plugin1', - activationPoints: ['ap1'] - }) - - await expect(() => trigger('ap1')).rejects.toThrow(/was triggered but does not exist on plugin/) - } - ) - - it('should provide the registered extension points to the triggered activation point if presetEPs is set to true in the setup', - async () => { - setup({ - importer(plugin) { return mockPlugins[plugin] }, - presetEPs: true, - }) - - let ap1Res - - mockPlugins.plugin1 = { - ap1: eps => ap1Res = eps - } - register({ - name: 'plugin1', - url: 'plugin1', - activationPoints: ['ap1'] - }) - - add('ep1') - add('ep2') - - await trigger('ap1') - - expect(ap1Res.ep1.constructor.name).toEqual('ExtensionPoint') - expect(ap1Res.ep2.constructor.name).toEqual('ExtensionPoint') - } - ) - - it('should allow registration, execution and serial execution of execution points when an activation point is triggered if presetEPs is set to false in the setup', - async () => { - setup({ - importer(plugin) { return mockPlugins[plugin] }, - }) - - let ap1Res - - mockPlugins.plugin1 = { - ap1: eps => ap1Res = eps - } - register({ - name: 'plugin1', - url: 'plugin1', - activationPoints: ['ap1'] - }) - - await trigger('ap1') - - expect(typeof ap1Res.register).toBe('function') - expect(typeof ap1Res.execute).toBe('function') - expect(typeof ap1Res.executeSerial).toBe('function') - } - ) - - it('should not provide any reference to extension points during activation point triggering if presetEPs is set to null in the setup', - async () => { - setup({ - importer(plugin) { return mockPlugins[plugin] }, - presetEPs: null, - }) - - let ap1Res = true - - mockPlugins.plugin1 = { - ap1: eps => ap1Res = eps - } - register({ - name: 'plugin1', - url: 'plugin1', - activationPoints: ['ap1'] - }) - - await trigger('ap1') - - expect(ap1Res).not.toBeDefined() - } - ) -}) - -describe('remove and clear', () => { - - beforeEach(() => { - register({ - name: 'plugin1', - url: 'plugin1', - activationPoints: ['ap1', 'ap2'], - active: true - }) - - register({ - name: 'plugin2', - url: 'plugin2', - activationPoints: ['ap2', 'ap3'], - active: true - }) - }) - it('should remove all and only the activations for the given plugin from the register when removing activations', - () => { - remove('plugin1') - - expect(get()).toEqual([ - { - plugin: 'plugin2', - url: 'plugin2', - activationPoint: 'ap2', - activated: false - }, - { - plugin: 'plugin2', - url: 'plugin2', - activationPoint: 'ap3', - activated: false - }, - ]) - } - ) - - it('should not remove any activations from the register if no plugin name is provided', - () => { - remove() - - expect(get()).toEqual([ - { - plugin: 'plugin1', - url: 'plugin1', - activationPoint: 'ap1', - activated: false - }, - { - plugin: 'plugin1', - url: 'plugin1', - activationPoint: 'ap2', - activated: false - }, - { - plugin: 'plugin2', - url: 'plugin2', - activationPoint: 'ap2', - activated: false - }, - { - plugin: 'plugin2', - url: 'plugin2', - activationPoint: 'ap3', - activated: false - }, - ]) - } - ) - - it('should remove all activations from the register when clearing the register', - () => { - clear() - - expect(get()).toEqual([]) - } - ) -}) diff --git a/electron/core/plugin-manager/execution/extension-manager.test.js b/electron/core/plugin-manager/execution/extension-manager.test.js deleted file mode 100644 index dd934f872..000000000 --- a/electron/core/plugin-manager/execution/extension-manager.test.js +++ /dev/null @@ -1,116 +0,0 @@ -import { add, remove, register, get, execute, executeSerial, unregisterAll } from './extension-manager' -import ExtensionPoint from './ExtensionPoint' - -beforeEach(() => { - add('ep1') - add('ep2') -}) - -afterEach(() => { - remove('ep1') - remove('ep2') - remove('ep3') -}) - -describe('get', () => { - it('should return the extension point with the given name if it exists', () => { - expect(get('ep1')).toBeInstanceOf(ExtensionPoint) - }) - - it('should return all extension points if no name is provided', () => { - expect(get()).toEqual(expect.objectContaining({ ep1: expect.any(ExtensionPoint) })) - expect(get()).toEqual(expect.objectContaining({ ep2: expect.any(ExtensionPoint) })) - }) -}) - -describe('Add and remove', () => { - it('should add a new extension point with the given name using the add function', () => { - add('ep1') - - expect(get('ep1')).toBeInstanceOf(ExtensionPoint) - }) - - it('should remove only the extension point with the given name using the remove function', () => { - remove('ep1') - - expect(get()).not.toEqual(expect.objectContaining({ ep1: expect.anything() })) - expect(get()).toEqual(expect.objectContaining({ ep2: expect.any(ExtensionPoint) })) - }) - - it('should not remove any extension points if no name is provided using the remove function', () => { - remove() - - expect(get()).toEqual(expect.objectContaining({ ep1: expect.any(ExtensionPoint) })) - expect(get()).toEqual(expect.objectContaining({ ep2: expect.any(ExtensionPoint) })) - }) -}) - -describe('register', () => { - it('should register an extension to an existing extension point if the point has already been created', () => { - register('ep1', 'extension1', { foo: 'bar' }) - - expect(get('ep1')._extensions).toContainEqual(expect.objectContaining({ name: 'extension1' })) - }) - - it('should create an extension point and register an extension to it if the point has not yet been created', () => { - register('ep3', 'extension1', { foo: 'bar' }) - - expect(get('ep3')._extensions).toContainEqual(expect.objectContaining({ name: 'extension1' })) - }) -}) - -describe('unregisterAll', () => { - it('should unregister all extension points matching the give name regex', () => { - // Register example extensions - register('ep1', 'remove1', { foo: 'bar' }) - register('ep2', 'remove2', { foo: 'bar' }) - register('ep1', 'keep', { foo: 'bar' }) - - // Remove matching extensions - unregisterAll(/remove/) - - // Extract all registered extensions - const eps = Object.values(get()).map(ep => ep._extensions) - const extensions = eps.flat() - - // Test extracted extensions - expect(extensions).toContainEqual(expect.objectContaining({ name: 'keep' })) - expect(extensions).not.toContainEqual(expect.objectContaining({ name: 'ep1' })) - expect(extensions).not.toContainEqual(expect.objectContaining({ name: 'ep2' })) - }) -}) - -describe('execute', () => { - it('should execute the extensions registered to the named extension point with the provided input', () => { - const result = [] - register('ep1', 'extension1', input => result.push(input + 'bar')) - register('ep1', 'extension2', input => result.push(input + 'baz')) - - execute('ep1', 'foo') - - expect(result).toEqual(['foobar', 'foobaz']) - }) - - it('should throw an error if the named extension point does not exist', () => { - register('ep1', 'extension1', { foo: 'bar' }) - - expect(() => execute('ep3')).toThrow(/not a valid extension point/) - }) -}) - -describe('executeSerial', () => { - it('should execute the extensions in serial registered to the named extension point with the provided input', async () => { - register('ep1', 'extension1', input => input + 'bar') - register('ep1', 'extension2', input => input + 'baz') - - const result = await executeSerial('ep1', 'foo') - - expect(result).toEqual('foobarbaz') - }) - - it('should throw an error if the named extension point does not exist', () => { - register('ep1', 'extension1', { foo: 'bar' }) - - expect(() => executeSerial('ep3')).toThrow(/not a valid extension point/) - }) -}) diff --git a/electron/core/plugin-manager/execution/index.test.js b/electron/core/plugin-manager/execution/index.test.js deleted file mode 100644 index 75f8e5933..000000000 --- a/electron/core/plugin-manager/execution/index.test.js +++ /dev/null @@ -1,28 +0,0 @@ -import { setup } from "." -import { importer, presetEPs } from "./import-manager" - -describe('setup', () => { - const mockImporter = jest.fn() - - it('should store the importer function', () => { - setup({ importer: mockImporter }) - - expect(importer).toBe(mockImporter) - }) - - it('should set presetEPS to false if not provided', () => { - expect(presetEPs).toBe(false) - }) - - it('should set presetEPS to the provided value if it is true', () => { - setup({ presetEPs: true }) - - expect(presetEPs).toBe(true) - }) - - it('should set presetEPS to the provided value if it is null', () => { - setup({ presetEPs: null }) - - expect(presetEPs).toBe(null) - }) -}) \ No newline at end of file diff --git a/electron/core/plugin-manager/facade/index.test.js b/electron/core/plugin-manager/facade/index.test.js deleted file mode 100644 index 0e4bae62d..000000000 --- a/electron/core/plugin-manager/facade/index.test.js +++ /dev/null @@ -1,196 +0,0 @@ -jest.mock('electron', () => { - const handlers = {} - - return { - ipcMain: { - handle(channel, callback) { - handlers[channel] = callback - } - }, - ipcRenderer: { - invoke(channel, ...args) { - return Promise.resolve(handlers[channel].call(undefined, 'event', ...args)) - } - }, - webContents: { - getAllWebContents: jest.fn(() => []) - }, - contextBridge: { - exposeInMainWorld(key, val) { - global.window = { [key]: val } - } - } - } -}) - -jest.mock('../pluginMgr/store', () => { - const setActive = jest.fn(() => true) - const uninstall = jest.fn() - const update = jest.fn(() => true) - const isUpdateAvailable = jest.fn(() => false) - - class Plugin { - constructor(name) { - this.name = name - this.activationPoints = ['test'] - } - setActive = setActive - uninstall = uninstall - update = update - isUpdateAvailable = isUpdateAvailable - } - - return { - getPlugin: jest.fn(name => new Plugin(name)), - getActivePlugins: jest.fn(() => [new Plugin('test')]), - installPlugins: jest.fn(async plugins => plugins.map(name => new Plugin(name))), - removePlugin: jest.fn() - } -}) - -const { rmSync } = require('fs') -const { webContents } = require('electron') -const useFacade = require('./index') -const { getActive, install, toggleActive, uninstall, update, updatesAvailable, registerActive } = require('../execution/facade') -const { setPluginsPath, setConfirmInstall } = require('../pluginMgr/globals') -const router = require('../pluginMgr/router') -const { getPlugin, getActivePlugins, removePlugin } = require('../pluginMgr/store') -const { get: getActivations } = require('../execution/activation-manager') - -const pluginsPath = './testPlugins' -const confirmInstall = jest.fn(() => true) - -beforeAll(async () => { - setPluginsPath(pluginsPath) - router() - useFacade() -}) - -afterAll(() => { - rmSync(pluginsPath, { recursive: true }) -}) - -describe('install', () => { - it('should return cancelled state if the confirmPlugin callback returns falsy', async () => { - setConfirmInstall(() => false) - const plugins = await install(['test-install']) - expect(plugins).toEqual(false) - }) - - it('should perform a security check of the install using confirmInstall if facade is used', async () => { - setConfirmInstall(confirmInstall) - await install(['test-install']) - expect(confirmInstall.mock.calls.length).toBeTruthy() - }) - - it('should register all installed plugins', async () => { - const pluginName = 'test-install' - await install([pluginName]) - expect(getActivations()).toContainEqual(expect.objectContaining({ - plugin: pluginName - })) - }) - - it('should return a list of plugins', async () => { - setConfirmInstall(confirmInstall) - const pluginName = 'test-install' - const plugins = await install([pluginName]) - expect(plugins).toEqual([expect.objectContaining({ name: pluginName })]) - }) -}) - -describe('uninstall', () => { - it('should uninstall all plugins with the provided name, remove it from the store and refresh all renderers', async () => { - // Reset mock functions - const mockUninstall = getPlugin().uninstall - mockUninstall.mockClear() - removePlugin.mockClear() - webContents.getAllWebContents.mockClear() - getPlugin.mockClear() - - // Uninstall plugins - const specs = ['test-uninstall-1', 'test-uninstall-2'] - await uninstall(specs) - - // Test result - expect(getPlugin.mock.calls).toEqual(specs.map(spec => [spec])) - expect(mockUninstall.mock.calls.length).toBeTruthy() - expect(removePlugin.mock.calls.length).toBeTruthy() - expect(webContents.getAllWebContents.mock.calls.length).toBeTruthy() - }) -}) - -describe('getActive', () => { - it('should return all active plugins', async () => { - getActivePlugins.mockClear() - await getActive() - expect(getActivePlugins.mock.calls.length).toBeTruthy() - }) -}) - -describe('registerActive', () => { - it('should register all active plugins', async () => { - await registerActive() - expect(getActivations()).toContainEqual(expect.objectContaining({ - plugin: 'test' - })) - }) -}) - -describe('update', () => { - const specs = ['test-uninstall-1', 'test-uninstall-2'] - const mockUpdate = getPlugin().update - - beforeAll(async () => { - // Reset mock functions - mockUpdate.mockClear() - webContents.getAllWebContents.mockClear() - getPlugin.mockClear() - - // Update plugins - await update(specs) - }) - - it('should call the update function on all provided plugins', async () => { - // Check result - expect(getPlugin.mock.calls).toEqual(specs.map(spec => [spec])) - expect(mockUpdate.mock.calls.length).toBe(2) - }) - - it('should reload the renderers if reload is true', () => { - expect(webContents.getAllWebContents.mock.calls.length).toBeTruthy() - }) - - it('should not reload the renderer if reload is false', async () => { - webContents.getAllWebContents.mockClear() - await update(['test-uninstall'], false) - expect(webContents.getAllWebContents.mock.calls.length).toBeFalsy() - }) -}) - -describe('toggleActive', () => { - it('call the setActive function on the plugin with the provided name, with the provided active state', async () => { - await toggleActive('test-toggleActive', true) - expect(getPlugin.mock.lastCall).toEqual(['test-toggleActive']) - const mockSetActive = getPlugin().setActive - expect(mockSetActive.mock.lastCall).toEqual([true]) - }) -}) - -describe('updatesAvailable', () => { - it('should return the new versions for the provided plugins if provided', async () => { - // Reset mock functions - const mockIsUpdAvailable = getPlugin().isUpdateAvailable - mockIsUpdAvailable.mockClear() - getPlugin.mockClear() - - // Get available updates - const testPlugin1 = 'test-plugin-1' - const testPlugin2 = 'test-update-2' - const updates = await updatesAvailable([testPlugin1, testPlugin2]) - expect(updates).toEqual({ - [testPlugin1]: false, - [testPlugin2]: false, - }) - }) -}) diff --git a/electron/core/plugin-manager/pluginMgr/Plugin.test.js b/electron/core/plugin-manager/pluginMgr/Plugin.test.js deleted file mode 100644 index c0326bd06..000000000 --- a/electron/core/plugin-manager/pluginMgr/Plugin.test.js +++ /dev/null @@ -1,212 +0,0 @@ -import { init } from "." -import { join } from 'path' -import Plugin from "./Plugin" -import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "fs" - -const pluginsDir = './testPlugins' -const testPluginDir = './testPluginSrc' -const testPluginName = 'test-plugin' -const manifest = join(testPluginDir, 'package.json') -const main = 'index' - -/** @type Plugin */ -let plugin - -beforeAll(() => { - init({ - confirmInstall: () => true, - pluginsPath: pluginsDir, - }) - - mkdirSync(testPluginDir) - - writeFileSync(manifest, JSON.stringify({ - name: testPluginName, - activationPoints: [], - main, - }), 'utf8') - - plugin = new Plugin(testPluginDir) -}) - -afterAll(() => { - rmSync(pluginsDir, { recursive: true }) - rmSync(testPluginDir, { recursive: true }) -}) - - -describe('subscribe', () => { - let res = false - it('should register the provided callback', () => { - plugin.subscribe('test', () => res = true) - plugin.setActive(true) - - expect(res).toBeTruthy() - }) -}) - -describe('unsubscribe', () => { - it(`should remove the provided callback from the register - after which it should not be executed anymore when the plugin is updated`, () => { - let res = false - plugin.subscribe('test', () => res = true) - plugin.unsubscribe('test') - plugin.setActive(true) - - expect(res).toBeFalsy() - }) -}) - -describe('install', () => { - beforeAll(async () => { - await plugin._install() - }) - - it('should store all the relevant manifest values on the plugin', async () => { - expect(plugin).toMatchObject({ - origin: testPluginDir, - installOptions: { - version: false, - fullMetadata: false, - }, - name: testPluginName, - url: `plugin://${testPluginName}/${main}`, - activationPoints: [] - }) - }) - - it('should create a folder for the plugin if it does not yet exist and copy the plugin files to it', () => { - expect(existsSync(join(pluginsDir, testPluginName))).toBeTruthy() - }) - - it('should replace the existing plugin files in the plugin folder if it already exist', async () => { - writeFileSync(manifest, JSON.stringify({ - name: testPluginName, - activationPoints: [], - main: 'updated', - }), 'utf8') - - await plugin._install() - - const savedPkg = JSON.parse(readFileSync(join(pluginsDir, testPluginName, 'package.json'))) - - expect(savedPkg.main).toBe('updated') - }) - - it('should throw an error and the plugin should be set to inactive if no manifest could be found', async () => { - rmSync(join(testPluginDir, 'package.json')) - - await expect(() => plugin._install()).rejects.toThrow(/does not contain a valid manifest/) - }) - - it('should throw an error and the plugin should be set to inactive if plugin does not contain any activation points', async () => { - writeFileSync(manifest, JSON.stringify({ - name: testPluginName, - main, - }), 'utf8') - - await expect(() => plugin._install()).rejects.toThrow('The plugin does not contain any activation points') - expect(plugin.active).toBe(false) - }) -}) - -describe('update', () => { - let updatedPlugin - let subscription = false - let beforeUpd - - beforeAll(async () => { - writeFileSync(manifest, JSON.stringify({ - name: testPluginName, - activationPoints: [], - version: '0.0.1', - main, - }), 'utf8') - - await plugin._install() - - plugin.subscribe('test', () => subscription = true) - beforeUpd = Object.assign({}, plugin) - - await plugin.update() - }) - - it('should not do anything if no version update is available', () => { - expect(beforeUpd).toMatchObject(plugin) - }) - - it('should update the plugin files to the latest version if there is a new version available for the plugin', async () => { - writeFileSync(manifest, JSON.stringify({ - name: testPluginName, - activationPoints: [], - version: '0.0.2', - main, - }), 'utf8') - - await plugin.update() - - expect(plugin).toMatchObject({ - origin: testPluginDir, - installOptions: { - version: false, - fullMetadata: false, - }, - name: testPluginName, - version: '0.0.2', - url: `plugin://${testPluginName}/${main}`, - activationPoints: [] - }) - }) - - it('should execute callbacks subscribed to this plugin, providing the plugin as a parameter', () => { - expect(subscription).toBeTruthy() - }) -}) - -describe('isUpdateAvailable', () => { - it('should return false if no new version is available', async () => { - await expect(plugin.isUpdateAvailable()).resolves.toBe(false) - }) - - it('should return the latest version number if a new version is available', async () => { - writeFileSync(manifest, JSON.stringify({ - name: testPluginName, - activationPoints: [], - version: '0.0.3', - main, - }), 'utf8') - - await expect(plugin.isUpdateAvailable()).resolves.toBe('0.0.3') - }) -}) - -describe('setActive', () => { - it('should set the plugin to be active', () => { - plugin.setActive(true) - expect(plugin.active).toBeTruthy() - }) - - it('should execute callbacks subscribed to this plugin, providing the plugin as a parameter', () => { - let res = false - plugin.subscribe('test', () => res = true) - plugin.setActive(true) - - expect(res).toBeTruthy() - }) -}) - -describe('uninstall', () => { - let subscription = false - beforeAll(async () => { - plugin.subscribe('test', () => subscription = true) - await plugin.uninstall() - }) - - it('should remove the installed plugin from the plugins folder', () => { - expect(existsSync(join(pluginsDir, testPluginName))).toBe(false) - }) - - it('should execute callbacks subscribed to this plugin, providing the plugin as a parameter', () => { - expect(subscription).toBeTruthy() - }) -}) diff --git a/electron/core/plugin-manager/pluginMgr/globals.js b/electron/core/plugin-manager/pluginMgr/globals.js deleted file mode 100644 index a0fc3718d..000000000 --- a/electron/core/plugin-manager/pluginMgr/globals.js +++ /dev/null @@ -1,57 +0,0 @@ -import { existsSync, mkdirSync, writeFileSync } from "fs" -import { join, resolve } from "path" - -export let pluginsPath = null - -/** - * @private - * Set path to plugins directory and create the directory if it does not exist. - * @param {string} plgPath path to plugins directory - */ -export function setPluginsPath(plgPath) { - // Create folder if it does not exist - let plgDir - try { - plgDir = resolve(plgPath) - if (plgDir.length < 2) throw new Error() - - if (!existsSync(plgDir)) mkdirSync(plgDir) - - const pluginsJson = join(plgDir, 'plugins.json') - if (!existsSync(pluginsJson)) writeFileSync(pluginsJson, '{}', 'utf8') - - pluginsPath = plgDir - - } catch (error) { - throw new Error('Invalid path provided to the plugins folder') - } - -} - -/** -* @private - * Get the path to the plugins.json file. - * @returns location of plugins.json - */ -export function getPluginsFile() { return join(pluginsPath, 'plugins.json') } - - -export let confirmInstall = function () { - return new Error( - 'The facade.confirmInstall callback needs to be set in when initializing Pluggable Electron in the main process.' - ) -} - -/** - * @private - * Set callback to use as confirmInstall. - * @param {confirmInstall} cb Callback - */ -export function setConfirmInstall(cb) { confirmInstall = cb } - -/** - * This function is executed when plugins are installed to verify that the user indeed wants to install the plugin. - * @callback confirmInstall - * @param {Array.} plg The specifiers used to locate the packages (from NPM or local file) - * @returns {Promise} Whether to proceed with the plugin installation - */ diff --git a/electron/core/plugin-manager/pluginMgr/index.test.js b/electron/core/plugin-manager/pluginMgr/index.test.js deleted file mode 100644 index 37055db62..000000000 --- a/electron/core/plugin-manager/pluginMgr/index.test.js +++ /dev/null @@ -1,150 +0,0 @@ -import { usePlugins, getStore, init } from './index' -import { installPlugins, getPlugin, getAllPlugins, getActivePlugins, addPlugin, removePlugin } from './store' -import Plugin from './Plugin' -import { existsSync, rmSync, mkdirSync, writeFileSync } from 'fs' -import { join } from 'path' -import { protocol } from 'electron' - -// Set up variables for test folders and test plugins. -const pluginDir = './testPlugins' -const registeredPluginName = 'registered-plugin' -const demoPlugin = { - origin: ".\\demo-plugin\\demo-plugin-1.5.0.tgz", - installOptions: { - version: false, - fullMetadata: false - }, - name: "demoPlugin", - version: "1.5.0", - activationPoints: [ - "init" - ], - main: "index.js", - _active: true, - url: "plugin://demo-plugin/index.js" -} - -describe('before setting a plugin path', () => { - describe('getStore', () => { - it('should throw an error if called without a plugin path set', () => { - expect(() => getStore()).toThrowError('The plugin path has not yet been set up. Please run usePlugins before accessing the store') - }) - }) - - describe('usePlugins', () => { - it('should throw an error if called without a plugin path whilst no plugin path is set', () => { - expect(() => usePlugins()).toThrowError('A path to the plugins folder is required to use Pluggable Electron') - }) - - it('should throw an error if called with an invalid plugin path', () => { - expect(() => usePlugins('http://notsupported')).toThrowError('Invalid path provided to the plugins folder') - }) - - it('should create the plugin path if it does not yet exist', () => { - // Execute usePlugins with a folder that does not exist - const newPluginDir = './test-new-plugins' - usePlugins(newPluginDir) - expect(existsSync(newPluginDir)).toBe(true) - - // Remove created folder to clean up - rmSync(newPluginDir, { recursive: true }) - }) - }) -}) - -describe('after setting a plugin path', () => { - let pm - - beforeAll(() => { - // Create folders to contain plugins - mkdirSync(pluginDir) - - // Create initial - writeFileSync(join(pluginDir, 'plugins.json'), JSON.stringify({ demoPlugin }), 'utf8') - - // Register a plugin before using plugins - const registeredPLugin = new Plugin(registeredPluginName) - registeredPLugin.name = registeredPluginName - addPlugin(registeredPLugin, false) - - // Load plugins - pm = usePlugins(pluginDir) - }) - - afterAll(() => { - rmSync(pluginDir, { recursive: true }) - }) - - describe('getStore', () => { - it('should return the plugin lifecycle functions if no plugin path is provided', () => { - expect(getStore()).toEqual({ - installPlugins, - getPlugin, - getAllPlugins, - getActivePlugins, - removePlugin, - }) - }) - }) - - describe('usePlugins', () => { - it('should return the plugin lifecycle functions if a plugin path is provided', () => { - expect(pm).toEqual({ - installPlugins, - getPlugin, - getAllPlugins, - getActivePlugins, - removePlugin, - }) - }) - - it('should load the plugins defined in plugins.json in the provided plugins folder if a plugin path is provided', () => { - expect(getPlugin('demoPlugin')).toEqual(demoPlugin) - }) - - it('should unregister any registered plugins before registering the new ones if a plugin path is provided', () => { - expect(() => getPlugin(registeredPluginName)).toThrowError(`Plugin ${registeredPluginName} does not exist`) - }) - }) -}) - -describe('init', () => { - // Enabling the facade and registering the confirm install function is tested with the router. - let pm - - beforeAll(() => { - // Create test plugins folder - mkdirSync(pluginDir) - - // Initialize Pluggable Electron without a plugin folder - pm = init({ confirmInstall: () => true }) - }) - - afterAll(() => { - // Remove test plugins folder - rmSync(pluginDir, { recursive: true }) - }) - - it('should make the plugin files available through the plugin protocol', async () => { - expect(protocol.isProtocolRegistered('plugin')).toBeTruthy() - }) - - it('should return an empty object if no plugin path is provided', () => { - expect(pm).toEqual({}) - }) - - it('should return the plugin lifecycle functions if a plugin path is provided', () => { - pm = init({ - confirmInstall: () => true, - pluginsPath: pluginDir, - }) - - expect(pm).toEqual({ - installPlugins, - getPlugin, - getAllPlugins, - getActivePlugins, - removePlugin, - }) - }) -}) \ No newline at end of file diff --git a/electron/core/plugin-manager/pluginMgr/router.js b/electron/core/plugin-manager/pluginMgr/router.js deleted file mode 100644 index 17e5ba2f5..000000000 --- a/electron/core/plugin-manager/pluginMgr/router.js +++ /dev/null @@ -1,91 +0,0 @@ -import { ipcMain, webContents } from "electron" - -import { getPlugin, getActivePlugins, installPlugins, removePlugin, getAllPlugins } from "./store" -import { pluginsPath, confirmInstall } from './globals' - -// Throw an error if pluginsPath has not yet been provided by usePlugins. -const checkPluginsPath = () => { - if (!pluginsPath) throw Error('Path to plugins folder has not yet been set up.') -} -let active = false -/** - * Provide the renderer process access to the plugins. - **/ -export default function () { - if (active) return - // Register IPC route to install a plugin - ipcMain.handle('pluggable:install', async (e, plugins) => { - checkPluginsPath() - - // Validate install request from backend for security. - const specs = plugins.map(plg => typeof plg === 'object' ? plg.specifier : plg) - const conf = await confirmInstall(specs) - if (!conf) return { cancelled: true } - - // Install and activate all provided plugins - const installed = await installPlugins(plugins) - return JSON.parse(JSON.stringify(installed)) - }) - - // Register IPC route to uninstall a plugin - ipcMain.handle('pluggable:uninstall', async (e, plugins, reload) => { - checkPluginsPath() - - // Uninstall all provided plugins - for (const plg of plugins) { - const plugin = getPlugin(plg) - await plugin.uninstall() - removePlugin(plugin.name) - } - - // Reload all renderer pages if needed - reload && webContents.getAllWebContents().forEach(wc => wc.reload()) - return true - }) - - // Register IPC route to update a plugin - ipcMain.handle('pluggable:update', (e, plugins, reload) => { - checkPluginsPath() - - // Update all provided plugins - let updated = [] - for (const plg of plugins) { - const plugin = getPlugin(plg) - const res = plugin.update() - if (res) updated.push(plugin) - } - - // Reload all renderer pages if needed - if (updated.length && reload) webContents.getAllWebContents().forEach(wc => wc.reload()) - - return JSON.parse(JSON.stringify(updated)) - }) - - // Register IPC route to check if updates are available for a plugin - ipcMain.handle('pluggable:updatesAvailable', (e, names) => { - checkPluginsPath() - - const plugins = names ? names.map(name => getPlugin(name)) : getAllPlugins() - - const updates = {} - for (const plugin of plugins) { - updates[plugin.name] = plugin.isUpdateAvailable() - } - return updates - }) - - // Register IPC route to get the list of active plugins - ipcMain.handle('pluggable:getActivePlugins', () => { - checkPluginsPath() - return JSON.parse(JSON.stringify(getActivePlugins())) - }) - - // Register IPC route to toggle the active state of a plugin - ipcMain.handle('pluggable:togglePluginActive', (e, plg, active) => { - checkPluginsPath() - const plugin = getPlugin(plg) - return JSON.parse(JSON.stringify(plugin.setActive(active))) - }) - - active = true -} diff --git a/electron/core/plugin-manager/pluginMgr/store.test.js b/electron/core/plugin-manager/pluginMgr/store.test.js deleted file mode 100644 index e99cb7c7d..000000000 --- a/electron/core/plugin-manager/pluginMgr/store.test.js +++ /dev/null @@ -1,108 +0,0 @@ -import { getActivePlugins, getAllPlugins, getPlugin, installPlugins } from './store' -import { init } from "." -import { join } from 'path' -import Plugin from "./Plugin" -import { mkdirSync, writeFileSync, rmSync } from "fs" - -// Temporary directory to install plugins to -const pluginsDir = './testPlugins' - -// Temporary directory containing the active plugin to install -const activePluginDir = './activePluginSrc' -const activePluginName = 'active-plugin' -const activeManifest = join(activePluginDir, 'package.json') - -// Temporary directory containing the inactive plugin to install -const inactivePluginDir = './inactivePluginSrc' -const inactivePluginName = 'inactive-plugin' -const inactiveManifest = join(inactivePluginDir, 'package.json') - -// Mock name for the entry file in the plugins -const main = 'index' - -/** @type Array. */ -let activePlugins -/** @type Array. */ -let inactivePlugins - -beforeAll(async () => { - // Initialize pluggable Electron - init({ - confirmInstall: () => true, - pluginsPath: pluginsDir, - }) - - // Create active plugin - mkdirSync(activePluginDir) - writeFileSync(activeManifest, JSON.stringify({ - name: activePluginName, - activationPoints: [], - main, - }), 'utf8') - - // Create active plugin - mkdirSync(inactivePluginDir) - writeFileSync(inactiveManifest, JSON.stringify({ - name: inactivePluginName, - activationPoints: [], - main, - }), 'utf8') - - // Install plugins - activePlugins = await installPlugins([activePluginDir], true) - activePlugins[0].setActive(true) - inactivePlugins = await installPlugins([{ - specifier: inactivePluginDir, - activate: false - }], true) -}) - -afterAll(() => { - // Remove all test files and folders - rmSync(pluginsDir, { recursive: true }) - rmSync(activePluginDir, { recursive: true }) - rmSync(inactivePluginDir, { recursive: true }) -}) - -describe('installPlugins', () => { - it('should create a new plugin found at the given location and return it if store is false', async () => { - const res = await installPlugins([activePluginDir], false) - - expect(res[0]).toBeInstanceOf(Plugin) - }) - - it('should create a new plugin found at the given location and register it if store is true', () => { - expect(activePlugins[0]).toBeInstanceOf(Plugin) - expect(getPlugin(activePluginName)).toBe(activePlugins[0]) - }) - - it('should activate the installed plugin by default', () => { - expect(getPlugin(activePluginName).active).toBe(true) - }) - - it('should set plugin to inactive if activate is set to false in the install options', async () => { - expect(inactivePlugins[0].active).toBe(false) - }) -}) - -describe('getPlugin', () => { - it('should return the plugin with the given name if it is registered', () => { - expect(getPlugin(activePluginName)).toBeInstanceOf(Plugin) - }) - - it('should return an error if the plugin with the given name is not registered', () => { - expect(() => getPlugin('wrongName')).toThrowError('Plugin wrongName does not exist') - }) -}) - -describe('getAllPlugins', () => { - it('should return a list of all registered plugins', () => { - expect(getAllPlugins()).toEqual([activePlugins[0], inactivePlugins[0]]) - }) -}) - -describe('getActivePlugins', () => { - it('should return a list of all and only the registered plugins that are active', () => { - expect(getActivePlugins()).toEqual(activePlugins) - }) -}) \ No newline at end of file diff --git a/electron/core/plugin-manager/facade/index.js b/electron/core/plugin/facade.ts similarity index 74% rename from electron/core/plugin-manager/facade/index.js rename to electron/core/plugin/facade.ts index 27a2468c7..bd1089109 100644 --- a/electron/core/plugin-manager/facade/index.js +++ b/electron/core/plugin/facade.ts @@ -1,23 +1,23 @@ const { ipcRenderer, contextBridge } = require("electron"); -function useFacade() { +export function useFacade() { const interfaces = { - install(plugins) { + install(plugins: any[]) { return ipcRenderer.invoke("pluggable:install", plugins); }, - uninstall(plugins, reload) { + uninstall(plugins: any[], reload: boolean) { return ipcRenderer.invoke("pluggable:uninstall", plugins, reload); }, getActive() { return ipcRenderer.invoke("pluggable:getActivePlugins"); }, - update(plugins, reload) { + update(plugins: any[], reload: boolean) { return ipcRenderer.invoke("pluggable:update", plugins, reload); }, - updatesAvailable(plugin) { + updatesAvailable(plugin: any) { return ipcRenderer.invoke("pluggable:updatesAvailable", plugin); }, - toggleActive(plugin, active) { + toggleActive(plugin: any, active: boolean) { return ipcRenderer.invoke("pluggable:togglePluginActive", plugin, active); }, }; @@ -28,5 +28,3 @@ function useFacade() { return interfaces; } - -module.exports = useFacade; diff --git a/electron/core/plugin/globals.ts b/electron/core/plugin/globals.ts new file mode 100644 index 000000000..69df7925c --- /dev/null +++ b/electron/core/plugin/globals.ts @@ -0,0 +1,36 @@ +import { existsSync, mkdirSync, writeFileSync } from "fs"; +import { join, resolve } from "path"; + +export let pluginsPath: string | undefined = undefined; + +/** + * @private + * Set path to plugins directory and create the directory if it does not exist. + * @param {string} plgPath path to plugins directory + */ +export function setPluginsPath(plgPath: string) { + // Create folder if it does not exist + let plgDir; + try { + plgDir = resolve(plgPath); + if (plgDir.length < 2) throw new Error(); + + if (!existsSync(plgDir)) mkdirSync(plgDir); + + const pluginsJson = join(plgDir, "plugins.json"); + if (!existsSync(pluginsJson)) writeFileSync(pluginsJson, "{}", "utf8"); + + pluginsPath = plgDir; + } catch (error) { + throw new Error("Invalid path provided to the plugins folder"); + } +} + +/** + * @private + * Get the path to the plugins.json file. + * @returns location of plugins.json + */ +export function getPluginsFile() { + return join(pluginsPath ?? "", "plugins.json"); +} \ No newline at end of file diff --git a/electron/core/plugin-manager/pluginMgr/index.js b/electron/core/plugin/index.ts similarity index 54% rename from electron/core/plugin-manager/pluginMgr/index.js rename to electron/core/plugin/index.ts index ae7bff759..e8c64747b 100644 --- a/electron/core/plugin-manager/pluginMgr/index.js +++ b/electron/core/plugin/index.ts @@ -1,40 +1,52 @@ -import { readFileSync } from "fs" -import { protocol } from 'electron' -import { normalize } from "path" +import { readFileSync } from "fs"; +import { protocol } from "electron"; +import { normalize } from "path"; -import Plugin from "./Plugin" -import { getAllPlugins, removePlugin, persistPlugins, installPlugins, getPlugin, getActivePlugins, addPlugin } from "./store" -import { pluginsPath as storedPluginsPath, setPluginsPath, getPluginsFile, setConfirmInstall } from './globals' -import router from "./router" +import Plugin from "./plugin"; +import { + getAllPlugins, + removePlugin, + persistPlugins, + installPlugins, + getPlugin, + getActivePlugins, + addPlugin, +} from "./store"; +import { + pluginsPath as storedPluginsPath, + setPluginsPath, + getPluginsFile, +} from "./globals"; +import router from "./router"; /** * Sets up the required communication between the main and renderer processes. * Additionally sets the plugins up using {@link usePlugins} if a pluginsPath is provided. * @param {Object} options configuration for setting up the renderer facade. - * @param {confirmInstall} [options.confirmInstall] Function to validate that a plugin should be installed. + * @param {confirmInstall} [options.confirmInstall] Function to validate that a plugin should be installed. * @param {Boolean} [options.useFacade=true] Whether to make a facade to the plugins available in the renderer. * @param {string} [options.pluginsPath] Optional path to the plugins folder. * @returns {pluginManager|Object} A set of functions used to manage the plugin lifecycle if usePlugins is provided. * @function */ -export function init(options) { - if (!Object.prototype.hasOwnProperty.call(options, 'useFacade') || options.useFacade) { - // Store the confirmInstall function - setConfirmInstall(options.confirmInstall) +export function init(options: any) { + if ( + !Object.prototype.hasOwnProperty.call(options, "useFacade") || + options.useFacade + ) { // Enable IPC to be used by the facade - router() + router(); } // Create plugins protocol to serve plugins to renderer - registerPluginProtocol() + registerPluginProtocol(); // perform full setup if pluginsPath is provided if (options.pluginsPath) { - return usePlugins(options.pluginsPath) + return usePlugins(options.pluginsPath); } - return {} - + return {}; } /** @@ -43,11 +55,11 @@ export function init(options) { * @returns {boolean} Whether the protocol registration was successful */ function registerPluginProtocol() { - return protocol.registerFileProtocol('plugin', (request, callback) => { - const entry = request.url.substr(8) - const url = normalize(storedPluginsPath + entry) - callback({ path: url }) - }) + return protocol.registerFileProtocol("plugin", (request, callback) => { + const entry = request.url.substr(8); + const url = normalize(storedPluginsPath + entry); + callback({ path: url }); + }); } /** @@ -56,34 +68,38 @@ function registerPluginProtocol() { * @param {string} pluginsPath Path to the plugins folder. Required if not yet set up. * @returns {pluginManager} A set of functions used to manage the plugin lifecycle. */ -export function usePlugins(pluginsPath) { - if (!pluginsPath) throw Error('A path to the plugins folder is required to use Pluggable Electron') +export function usePlugins(pluginsPath: string) { + if (!pluginsPath) + throw Error( + "A path to the plugins folder is required to use Pluggable Electron" + ); // Store the path to the plugins folder - setPluginsPath(pluginsPath) + setPluginsPath(pluginsPath); // Remove any registered plugins for (const plugin of getAllPlugins()) { - removePlugin(plugin.name, false) + if (plugin.name) removePlugin(plugin.name, false); } // Read plugin list from plugins folder - const plugins = JSON.parse(readFileSync(getPluginsFile())) + const plugins = JSON.parse(readFileSync(getPluginsFile(), "utf-8")); try { // Create and store a Plugin instance for each plugin in list for (const p in plugins) { - loadPlugin(plugins[p]) + loadPlugin(plugins[p]); } - persistPlugins() - + persistPlugins(); } catch (error) { // Throw meaningful error if plugin loading fails - throw new Error('Could not successfully rebuild list of installed plugins.\n' - + error - + '\nPlease check the plugins.json file in the plugins folder.') + throw new Error( + "Could not successfully rebuild list of installed plugins.\n" + + error + + "\nPlease check the plugins.json file in the plugins folder." + ); } // Return the plugin lifecycle functions - return getStore() + return getStore(); } /** @@ -92,16 +108,24 @@ export function usePlugins(pluginsPath) { * @private * @param {Object} plg Plugin info */ -function loadPlugin(plg) { +function loadPlugin(plg: any) { // Create new plugin, populate it with plg details and save it to the store - const plugin = new Plugin() + const plugin = new Plugin(); for (const key in plg) { - plugin[key] = plg[key] + if (Object.prototype.hasOwnProperty.call(plg, key)) { + // Use Object.defineProperty to set the properties as writable + Object.defineProperty(plugin, key, { + value: plg[key], + writable: true, + enumerable: true, + configurable: true, + }); + } } - addPlugin(plugin, false) - plugin.subscribe('pe-persist', persistPlugins) + addPlugin(plugin, false); + plugin.subscribe("pe-persist", persistPlugins); } /** @@ -110,7 +134,9 @@ function loadPlugin(plg) { */ export function getStore() { if (!storedPluginsPath) { - throw new Error('The plugin path has not yet been set up. Please run usePlugins before accessing the store') + throw new Error( + "The plugin path has not yet been set up. Please run usePlugins before accessing the store" + ); } return { @@ -119,5 +145,5 @@ export function getStore() { getAllPlugins, getActivePlugins, removePlugin, - } + }; } diff --git a/electron/core/plugin-manager/pluginMgr/Plugin.js b/electron/core/plugin/plugin.ts similarity index 59% rename from electron/core/plugin-manager/pluginMgr/Plugin.js rename to electron/core/plugin/plugin.ts index 093e4015f..f0fc073d7 100644 --- a/electron/core/plugin-manager/pluginMgr/Plugin.js +++ b/electron/core/plugin/plugin.ts @@ -1,11 +1,11 @@ -import { rmdir } from "fs/promises" -import { resolve, join } from "path" -import { manifest, extract } from "pacote" -import Arborist from '@npmcli/arborist' +import { rmdir } from "fs/promises"; +import { resolve, join } from "path"; +import { manifest, extract } from "pacote"; +import * as Arborist from "@npmcli/arborist"; -import { pluginsPath } from "./globals" +import { pluginsPath } from "./globals"; -/** +/** * An NPM package that can be used as a Pluggable Electron plugin. * Used to hold all the information and functions necessary to handle the plugin lifecycle. */ @@ -21,30 +21,39 @@ class Plugin { * @property {string} description The description of plugin as defined in the manifest. * @property {string} icon The icon of plugin as defined in the manifest. */ + origin?: string; + installOptions: any; + name?: string; + url?: string; + version?: string; + activationPoints?: Array; + main?: string; + description?: string; + icon?: string; /** @private */ - _active = false + _active = false; /** * @private * @property {Object.} #listeners A list of callbacks to be executed when the Plugin is updated. */ - #listeners = {} + listeners: Record void> = {}; /** * Set installOptions with defaults for options that have not been provided. * @param {string} [origin] Original specification provided to fetch the package. * @param {Object} [options] Options provided to pacote when fetching the manifest. */ - constructor(origin, options = {}) { + constructor(origin?: string, options = {}) { const defaultOpts = { version: false, fullMetadata: false, - Arborist - } + Arborist, + }; - this.origin = origin - this.installOptions = { ...defaultOpts, ...options } + this.origin = origin; + this.installOptions = { ...defaultOpts, ...options }; } /** @@ -52,7 +61,10 @@ class Plugin { * @type {string} */ get specifier() { - return this.origin + (this.installOptions.version ? '@' + this.installOptions.version : '') + return ( + this.origin + + (this.installOptions.version ? "@" + this.installOptions.version : "") + ); } /** @@ -60,31 +72,34 @@ class Plugin { * @type {boolean} */ get active() { - return this._active + return this._active; } /** * Set Package details based on it's manifest - * @returns {Promise.} Resolves to true when the action completed + * @returns {Promise.} Resolves to true when the action completed */ - async #getManifest() { + async getManifest() { // Get the package's manifest (package.json object) try { - const mnf = await manifest(this.specifier, this.installOptions) + const mnf = await manifest(this.specifier, this.installOptions); // set the Package properties based on the it's manifest - this.name = mnf.name - this.version = mnf.version - this.activationPoints = mnf.activationPoints || null - this.main = mnf.main - this.description = mnf.description - this.icon = mnf.icon - + this.name = mnf.name; + this.version = mnf.version; + this.activationPoints = mnf.activationPoints + ? (mnf.activationPoints as string[]) + : undefined; + this.main = mnf.main; + this.description = mnf.description; + this.icon = mnf.icon as any; } catch (error) { - throw new Error(`Package ${this.origin} does not contain a valid manifest: ${error}`) + throw new Error( + `Package ${this.origin} does not contain a valid manifest: ${error}` + ); } - return true + return true; } /** @@ -95,26 +110,29 @@ class Plugin { async _install() { try { // import the manifest details - await this.#getManifest() + await this.getManifest(); // Install the package in a child folder of the given folder - await extract(this.specifier, join(pluginsPath, this.name), this.installOptions) + await extract( + this.specifier, + join(pluginsPath ?? "", this.name ?? ""), + this.installOptions + ); if (!Array.isArray(this.activationPoints)) - throw new Error('The plugin does not contain any activation points') + throw new Error("The plugin does not contain any activation points"); // Set the url using the custom plugins protocol - this.url = `plugin://${this.name}/${this.main}` - - this.#emitUpdate() + this.url = `plugin://${this.name}/${this.main}`; + this.emitUpdate(); } catch (err) { // Ensure the plugin is not stored and the folder is removed if the installation fails - this.setActive(false) - throw err + this.setActive(false); + throw err; } - return [this] + return [this]; } /** @@ -122,24 +140,24 @@ class Plugin { * @param {string} name name of the callback to register * @param {callback} cb The function to execute on update */ - subscribe(name, cb) { - this.#listeners[name] = cb + subscribe(name: string, cb: () => void) { + this.listeners[name] = cb; } /** * Remove subscription * @param {string} name name of the callback to remove */ - unsubscribe(name) { - delete this.#listeners[name] + unsubscribe(name: string) { + delete this.listeners[name]; } /** * Execute listeners */ - #emitUpdate() { - for (const cb in this.#listeners) { - this.#listeners[cb].call(null, this) + emitUpdate() { + for (const cb in this.listeners) { + this.listeners[cb].call(null, this); } } @@ -149,13 +167,13 @@ class Plugin { * @returns {boolean} Whether an update was performed. */ async update(version = false) { - if (this.isUpdateAvailable()) { - this.installOptions.version = version - await this._install(false) - return true + if (await this.isUpdateAvailable()) { + this.installOptions.version = version; + await this._install(); + return true; } - return false + return false; } /** @@ -163,19 +181,21 @@ class Plugin { * @returns the latest available version if a new version is available or false if not. */ async isUpdateAvailable() { - const mnf = await manifest(this.origin) - return mnf.version !== this.version ? mnf.version : false + if (this.origin) { + const mnf = await manifest(this.origin); + return mnf.version !== this.version ? mnf.version : false; + } } /** * Remove plugin and refresh renderers. - * @returns {Promise} + * @returns {Promise} */ async uninstall() { - const plgPath = resolve(pluginsPath, this.name) - await rmdir(plgPath, { recursive: true }) + const plgPath = resolve(pluginsPath ?? "", this.name ?? ""); + await rmdir(plgPath, { recursive: true }); - this.#emitUpdate() + this.emitUpdate(); } /** @@ -183,11 +203,11 @@ class Plugin { * @param {boolean} active State to set _active to * @returns {Plugin} This plugin */ - setActive(active) { - this._active = active - this.#emitUpdate() - return this + setActive(active: boolean) { + this._active = active; + this.emitUpdate(); + return this; } } -export default Plugin +export default Plugin; diff --git a/electron/core/plugin/router.ts b/electron/core/plugin/router.ts new file mode 100644 index 000000000..09c79485b --- /dev/null +++ b/electron/core/plugin/router.ts @@ -0,0 +1,97 @@ +import { ipcMain, webContents } from "electron"; + +import { + getPlugin, + getActivePlugins, + installPlugins, + removePlugin, + getAllPlugins, +} from "./store"; +import { pluginsPath } from "./globals"; +import Plugin from "./plugin"; + +// Throw an error if pluginsPath has not yet been provided by usePlugins. +const checkPluginsPath = () => { + if (!pluginsPath) + throw Error("Path to plugins folder has not yet been set up."); +}; +let active = false; +/** + * Provide the renderer process access to the plugins. + **/ +export default function () { + if (active) return; + // Register IPC route to install a plugin + ipcMain.handle("pluggable:install", async (e, plugins) => { + checkPluginsPath(); + + // Install and activate all provided plugins + const installed = await installPlugins(plugins); + return JSON.parse(JSON.stringify(installed)); + }); + + // Register IPC route to uninstall a plugin + ipcMain.handle("pluggable:uninstall", async (e, plugins, reload) => { + checkPluginsPath(); + + // Uninstall all provided plugins + for (const plg of plugins) { + const plugin = getPlugin(plg); + await plugin.uninstall(); + if (plugin.name) removePlugin(plugin.name); + } + + // Reload all renderer pages if needed + reload && webContents.getAllWebContents().forEach((wc) => wc.reload()); + return true; + }); + + // Register IPC route to update a plugin + ipcMain.handle("pluggable:update", async (e, plugins, reload) => { + checkPluginsPath(); + + // Update all provided plugins + const updated: Plugin[] = []; + for (const plg of plugins) { + const plugin = getPlugin(plg); + const res = await plugin.update(); + if (res) updated.push(plugin); + } + + // Reload all renderer pages if needed + if (updated.length && reload) + webContents.getAllWebContents().forEach((wc) => wc.reload()); + + return JSON.parse(JSON.stringify(updated)); + }); + + // Register IPC route to check if updates are available for a plugin + ipcMain.handle("pluggable:updatesAvailable", (e, names) => { + checkPluginsPath(); + + const plugins = names + ? names.map((name: string) => getPlugin(name)) + : getAllPlugins(); + + const updates: Record = {}; + for (const plugin of plugins) { + updates[plugin.name] = plugin.isUpdateAvailable(); + } + return updates; + }); + + // Register IPC route to get the list of active plugins + ipcMain.handle("pluggable:getActivePlugins", () => { + checkPluginsPath(); + return JSON.parse(JSON.stringify(getActivePlugins())); + }); + + // Register IPC route to toggle the active state of a plugin + ipcMain.handle("pluggable:togglePluginActive", (e, plg, active) => { + checkPluginsPath(); + const plugin = getPlugin(plg); + return JSON.parse(JSON.stringify(plugin.setActive(active))); + }); + + active = true; +} diff --git a/electron/core/plugin-manager/pluginMgr/store.js b/electron/core/plugin/store.ts similarity index 65% rename from electron/core/plugin-manager/pluginMgr/store.js rename to electron/core/plugin/store.ts index 16a647930..cfd25e5ca 100644 --- a/electron/core/plugin-manager/pluginMgr/store.js +++ b/electron/core/plugin/store.ts @@ -8,9 +8,9 @@ * @prop {removePlugin} removePlugin */ -import { writeFileSync } from "fs" -import Plugin from "./Plugin" -import { getPluginsFile } from './globals' +import { writeFileSync } from "fs"; +import Plugin from "./plugin"; +import { getPluginsFile } from "./globals"; /** * @module store @@ -21,7 +21,7 @@ import { getPluginsFile } from './globals' * Register of installed plugins * @type {Object.} plugin - List of installed plugins */ -const plugins = {} +const plugins: Record = {}; /** * Get a plugin from the stored plugins. @@ -29,12 +29,12 @@ const plugins = {} * @returns {Plugin} Retrieved plugin * @alias pluginManager.getPlugin */ -export function getPlugin(name) { +export function getPlugin(name: string) { if (!Object.prototype.hasOwnProperty.call(plugins, name)) { - throw new Error(`Plugin ${name} does not exist`) + throw new Error(`Plugin ${name} does not exist`); } - return plugins[name] + return plugins[name]; } /** @@ -42,7 +42,9 @@ export function getPlugin(name) { * @returns {Array.} All plugin objects * @alias pluginManager.getAllPlugins */ -export function getAllPlugins() { return Object.values(plugins) } +export function getAllPlugins() { + return Object.values(plugins); +} /** * Get list of active plugin objects. @@ -50,7 +52,7 @@ export function getAllPlugins() { return Object.values(plugins) } * @alias pluginManager.getActivePlugins */ export function getActivePlugins() { - return Object.values(plugins).filter(plugin => plugin.active) + return Object.values(plugins).filter((plugin) => plugin.active); } /** @@ -60,10 +62,10 @@ export function getActivePlugins() { * @returns {boolean} Whether the delete was successful * @alias pluginManager.removePlugin */ -export function removePlugin(name, persist = true) { - const del = delete plugins[name] - if (persist) persistPlugins() - return del +export function removePlugin(name: string, persist = true) { + const del = delete plugins[name]; + if (persist) persistPlugins(); + return del; } /** @@ -72,11 +74,11 @@ export function removePlugin(name, persist = true) { * @param {boolean} persist Whether to save the changes to plugins to file * @returns {void} */ -export function addPlugin(plugin, persist = true) { - plugins[plugin.name] = plugin +export function addPlugin(plugin: Plugin, persist = true) { + if (plugin.name) plugins[plugin.name] = plugin; if (persist) { - persistPlugins() - plugin.subscribe('pe-persist', persistPlugins) + persistPlugins(); + plugin.subscribe("pe-persist", persistPlugins); } } @@ -85,11 +87,11 @@ export function addPlugin(plugin, persist = true) { * @returns {void} */ export function persistPlugins() { - const persistData = {} + const persistData: Record = {}; for (const name in plugins) { - persistData[name] = plugins[name] + persistData[name] = plugins[name]; } - writeFileSync(getPluginsFile(), JSON.stringify(persistData), 'utf8') + writeFileSync(getPluginsFile(), JSON.stringify(persistData), "utf8"); } /** @@ -99,26 +101,26 @@ export function persistPlugins() { * @returns {Promise.>} New plugin * @alias pluginManager.installPlugins */ -export async function installPlugins(plugins, store = true) { - const installed = [] +export async function installPlugins(plugins: any, store = true) { + const installed: Plugin[] = []; for (const plg of plugins) { // Set install options and activation based on input type - const isObject = typeof plg === 'object' - const spec = isObject ? [plg.specifier, plg] : [plg] - const activate = isObject ? plg.activate !== false : true + const isObject = typeof plg === "object"; + const spec = isObject ? [plg.specifier, plg] : [plg]; + const activate = isObject ? plg.activate !== false : true; // Install and possibly activate plugin - const plugin = new Plugin(...spec) - await plugin._install() - if (activate) plugin.setActive(true) + const plugin = new Plugin(...spec); + await plugin._install(); + if (activate) plugin.setActive(true); // Add plugin to store if needed - if (store) addPlugin(plugin) - installed.push(plugin) + if (store) addPlugin(plugin); + installed.push(plugin); } // Return list of all installed plugins - return installed + return installed; } /** @@ -126,4 +128,4 @@ export async function installPlugins(plugins, store = true) { * options used to install the plugin with some extra options. * @param {string} specifier the NPM specifier that identifies the package. * @param {boolean} [activate] Whether this plugin should be activated after installation. Defaults to true. - */ \ No newline at end of file + */ diff --git a/electron/main.ts b/electron/main.ts index 92a4ad5f6..4237bca6b 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -9,7 +9,7 @@ import { import { readdirSync, writeFileSync } from "fs"; import { resolve, join, extname } from "path"; import { rmdir, unlink, createWriteStream } from "fs"; -import { init } from "./core/plugin-manager/pluginMgr"; +import { init } from "./core/plugin/index"; import { setupMenu } from "./utils/menu"; import { dispose } from "./utils/disposable"; diff --git a/electron/package.json b/electron/package.json index 452c69ab8..b6834dfdd 100644 --- a/electron/package.json +++ b/electron/package.json @@ -74,6 +74,8 @@ "devDependencies": { "@electron/notarize": "^2.1.0", "@playwright/test": "^1.38.1", + "@types/npmcli__arborist": "^5.6.4", + "@types/pacote": "^11.1.7", "@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/parser": "^6.7.3", "electron": "26.2.1", diff --git a/electron/preload.ts b/electron/preload.ts index ceaede049..14815bb67 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,6 +1,5 @@ // Make Pluggable Electron's facade available to the renderer on window.plugins -//@ts-ignore -const useFacade = require("../core/plugin-manager/facade"); +import { useFacade } from "./core/plugin/facade"; useFacade(); //@ts-ignore const { contextBridge, ipcRenderer } = require("electron"); diff --git a/web/app/_components/Preferences.tsx b/web/app/_components/Preferences.tsx index 7870cbf2d..5cb10fc77 100644 --- a/web/app/_components/Preferences.tsx +++ b/web/app/_components/Preferences.tsx @@ -1,9 +1,6 @@ 'use client' import { useEffect, useRef, useState } from 'react' -import { - plugins, - extensionPoints, -} from '@/../../electron/core/plugin-manager/execution/index' +import { plugins, extensionPoints } from '@plugin' import { ChartPieIcon, CommandLineIcon, @@ -13,7 +10,7 @@ import { import { MagnifyingGlassIcon } from '@heroicons/react/20/solid' import classNames from 'classnames' import { DataService, PluginService, preferences } from '@janhq/core' -import { execute } from '../../../electron/core/plugin-manager/execution/extension-manager' +import { execute } from '@plugin/extension-manager' import LoadingIndicator from './LoadingIndicator' import { executeSerial } from '@services/pluginService' @@ -33,7 +30,7 @@ export const Preferences = () => { * Loads the plugin catalog module from a CDN and sets it as the plugin catalog state. */ useEffect(() => { - executeSerial(DataService.GetPluginManifest).then((data) => { + executeSerial(DataService.GetPluginManifest).then((data: any) => { setPluginCatalog(data) }) }, []) @@ -52,7 +49,7 @@ export const Preferences = () => { if (extensionPoints.get('experimentComponent')) { const components = await Promise.all( - extensionPoints.execute('experimentComponent') + extensionPoints.execute('experimentComponent', {}) ) if (components.length > 0) { setIsTestAvailable(true) @@ -67,7 +64,7 @@ export const Preferences = () => { if (extensionPoints.get('PluginPreferences')) { const data = await Promise.all( - extensionPoints.execute('PluginPreferences') + extensionPoints.execute('PluginPreferences', {}) ) setPreferenceItems(Array.isArray(data) ? data : []) Promise.all( @@ -149,7 +146,7 @@ export const Preferences = () => { } if (extensionPoints.get(PluginService.OnPreferencesUpdate)) timeout = setTimeout( - () => execute(PluginService.OnPreferencesUpdate), + () => execute(PluginService.OnPreferencesUpdate, {}), 100 ) } diff --git a/web/containers/Providers/index.tsx b/web/containers/Providers/index.tsx index e8c185d79..9362cfa2f 100644 --- a/web/containers/Providers/index.tsx +++ b/web/containers/Providers/index.tsx @@ -7,15 +7,14 @@ import JotaiWrapper from '@helpers/JotaiWrapper' import { ModalWrapper } from '@helpers/ModalWrapper' import { useEffect, useState } from 'react' import CompactLogo from '@containers/Logo/CompactLogo' -import { - setup, - plugins, - activationPoints, - extensionPoints, -} from '../../../electron/core/plugin-manager/execution/index' +import { setup, plugins, activationPoints, extensionPoints } from '@plugin' import EventListenerWrapper from '@helpers/EventListenerWrapper' import { setupCoreServices } from '@services/coreService' -import { executeSerial, isCorePluginInstalled, setupBasePlugins } from '@services/pluginService' +import { + executeSerial, + isCorePluginInstalled, + setupBasePlugins, +} from '@services/pluginService' const Providers = (props: PropsWithChildren) => { const [setupCore, setSetupCore] = useState(false) diff --git a/web/helpers/EventHandler.tsx b/web/helpers/EventHandler.tsx index 4991c43be..7c8fc1c3f 100644 --- a/web/helpers/EventHandler.tsx +++ b/web/helpers/EventHandler.tsx @@ -9,12 +9,15 @@ import { updateConversationAtom, updateConversationWaitingForResponseAtom, } from './atoms/Conversation.atom' -import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager' +import { executeSerial } from '@plugin/extension-manager' import { debounce } from 'lodash' -import { setDownloadStateAtom, setDownloadStateSuccessAtom } from "./atoms/DownloadState.atom"; -import { downloadedModelAtom } from "./atoms/DownloadedModel.atom"; -import { ModelManagementService } from "@janhq/core"; -import { getDownloadedModels } from "../hooks/useGetDownloadedModels"; +import { + setDownloadStateAtom, + setDownloadStateSuccessAtom, +} from './atoms/DownloadState.atom' +import { downloadedModelAtom } from './atoms/DownloadedModel.atom' +import { ModelManagementService } from '@janhq/core' +import { getDownloadedModels } from '../hooks/useGetDownloadedModels' let currentConversation: Conversation | undefined = undefined @@ -25,7 +28,6 @@ const debouncedUpdateConversation = debounce( 1000 ) - export default function EventHandler({ children }: { children: ReactNode }) { const addNewMessage = useSetAtom(addNewMessageAtom) const updateMessage = useSetAtom(updateMessageAtom) @@ -34,9 +36,9 @@ export default function EventHandler({ children }: { children: ReactNode }) { const { getConversationById } = useGetUserConversations() const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom) - const setDownloadState = useSetAtom(setDownloadStateAtom); - const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom); - const setDownloadedModels = useSetAtom(downloadedModelAtom); + const setDownloadState = useSetAtom(setDownloadStateAtom) + const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom) + const setDownloadedModels = useSetAtom(downloadedModelAtom) async function handleNewMessageResponse(message: NewMessageResponse) { if (message.conversationId) { @@ -97,18 +99,21 @@ export default function EventHandler({ children }: { children: ReactNode }) { } function handleDownloadUpdate(state: any) { - if (!state) return; - setDownloadState(state); + if (!state) return + setDownloadState(state) } function handleDownloadSuccess(state: any) { if (state && state.fileName && state.success === true) { - setDownloadStateSuccess(state.fileName); - executeSerial(ModelManagementService.UpdateFinishedDownloadAt, state.fileName).then(() => { + setDownloadStateSuccess(state.fileName) + executeSerial( + ModelManagementService.UpdateFinishedDownloadAt, + state.fileName + ).then(() => { getDownloadedModels().then((models) => { - setDownloadedModels(models); - }); - }); + setDownloadedModels(models) + }) + }) } } @@ -117,12 +122,12 @@ export default function EventHandler({ children }: { children: ReactNode }) { events.on(EventName.OnNewMessageResponse, handleNewMessageResponse) events.on(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate) events.on( - "OnMessageResponseFinished", + 'OnMessageResponseFinished', // EventName.OnMessageResponseFinished, handleMessageResponseFinished ) - events.on(EventName.OnDownloadUpdate, handleDownloadUpdate); - events.on(EventName.OnDownloadSuccess, handleDownloadSuccess); + events.on(EventName.OnDownloadUpdate, handleDownloadUpdate) + events.on(EventName.OnDownloadSuccess, handleDownloadSuccess) } }, []) @@ -131,12 +136,12 @@ export default function EventHandler({ children }: { children: ReactNode }) { events.off(EventName.OnNewMessageResponse, handleNewMessageResponse) events.off(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate) events.off( - "OnMessageResponseFinished", + 'OnMessageResponseFinished', // EventName.OnMessageResponseFinished, handleMessageResponseFinished ) - events.off(EventName.OnDownloadUpdate, handleDownloadUpdate); - events.off(EventName.OnDownloadSuccess, handleDownloadSuccess); + events.off(EventName.OnDownloadUpdate, handleDownloadUpdate) + events.off(EventName.OnDownloadSuccess, handleDownloadSuccess) } }, []) return <>{children} diff --git a/web/hooks/useGetDownloadedModels.ts b/web/hooks/useGetDownloadedModels.ts index 1474c55a0..49703bbab 100644 --- a/web/hooks/useGetDownloadedModels.ts +++ b/web/hooks/useGetDownloadedModels.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react' import { ModelManagementService } from '@janhq/core' import { useAtom } from 'jotai' import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom' -import { extensionPoints } from '../../electron/core/plugin-manager/execution' +import { extensionPoints } from '@plugin' import { executeSerial } from '@services/pluginService' export function useGetDownloadedModels() { diff --git a/web/hooks/useGetModelById.ts b/web/hooks/useGetModelById.ts index d97abdc18..65990b198 100644 --- a/web/hooks/useGetModelById.ts +++ b/web/hooks/useGetModelById.ts @@ -1,5 +1,5 @@ import { ModelManagementService } from '@janhq/core' -import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager' +import { executeSerial } from '@plugin/extension-manager' export default function useGetModelById() { const getModelById = async ( diff --git a/web/hooks/useGetSystemResources.ts b/web/hooks/useGetSystemResources.ts index a6f751f76..77a91bca1 100644 --- a/web/hooks/useGetSystemResources.ts +++ b/web/hooks/useGetSystemResources.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { extensionPoints } from '../../electron/core/plugin-manager/execution' +import { extensionPoints } from '@plugin' import { SystemMonitoringService } from '@janhq/core' import { useSetAtom } from 'jotai' import { totalRamAtom } from '@helpers/atoms/SystemBar.atom' diff --git a/electron/core/plugin-manager/execution/Activation.js b/web/plugin/Activation.ts similarity index 89% rename from electron/core/plugin-manager/execution/Activation.js rename to web/plugin/Activation.ts index 3381e428f..fe517d4f9 100644 --- a/electron/core/plugin-manager/execution/Activation.js +++ b/web/plugin/Activation.ts @@ -1,4 +1,4 @@ -import { callExport } from "./import-manager.js" +import { callExport } from "./import-manager" class Activation { /** @type {string} Name of the registered plugin. */ @@ -13,7 +13,7 @@ class Activation { /** @type {boolean} Whether the activation has been activated. */ activated - constructor(plugin, activationPoint, url) { + constructor(plugin: string, activationPoint: string, url: string) { this.plugin = plugin this.activationPoint = activationPoint this.url = url diff --git a/electron/core/plugin-manager/execution/ExtensionPoint.js b/web/plugin/ExtensionPoint.ts similarity index 80% rename from electron/core/plugin-manager/execution/ExtensionPoint.js rename to web/plugin/ExtensionPoint.ts index b3caba012..be9770232 100644 --- a/electron/core/plugin-manager/execution/ExtensionPoint.js +++ b/web/plugin/ExtensionPoint.ts @@ -18,29 +18,29 @@ class ExtensionPoint { * @type {Array.} The list of all extensions registered with this extension point. * @private */ - _extensions = [] + _extensions: any[] = [] /** * @type {Array.} A list of functions to be executed when the list of extensions changes. * @private */ - #changeListeners = [] + changeListeners: any[] = [] - constructor(name) { + constructor(name: string) { this.name = name } /** * Register new extension with this extension point. - * The registered response will be executed (if callback) or returned (if object) + * The registered response will be executed (if callback) or returned (if object) * when the extension point is executed (see below). * @param {string} name Unique name for the extension. * @param {Object|Callback} response Object to be returned or function to be called by the extension point. * @param {number} [priority] Order priority for execution used for executing in serial. * @returns {void} */ - register(name, response, priority = 0) { - const index = this._extensions.findIndex(p => p.priority > priority) + register(name: string, response: any, priority: number = 0) { + const index = this._extensions.findIndex((p) => p.priority > priority) const newExt = { name, response, priority } if (index > -1) { this._extensions.splice(index, 0, newExt) @@ -48,7 +48,7 @@ class ExtensionPoint { this._extensions.push(newExt) } - this.#emitChange() + this.emitChange() } /** @@ -56,11 +56,11 @@ class ExtensionPoint { * @param {RegExp } name Matcher for the name of the extension to remove. * @returns {void} */ - unregister(name) { - const index = this._extensions.findIndex(ext => ext.name.match(name)) + unregister(name: string) { + const index = this._extensions.findIndex((ext) => ext.name.match(name)) if (index > -1) this._extensions.splice(index, 1) - this.#emitChange() + this.emitChange() } /** @@ -69,7 +69,7 @@ class ExtensionPoint { */ clear() { this._extensions = [] - this.#emitChange() + this.emitChange() } /** @@ -77,8 +77,8 @@ class ExtensionPoint { * @param {string} name Name of the extension to return * @returns {Object|Callback|undefined} The response of the extension. If this is a function the function is returned, not its response. */ - get(name) { - const ep = this._extensions.find(ext => ext.name === name) + get(name: string) { + const ep = this._extensions.find((ext) => ext.name === name) return ep && ep.response } @@ -88,8 +88,8 @@ class ExtensionPoint { * @param {*} input Input to be provided as a parameter to each response if response is a callback. * @returns {Array} List of responses from the extensions. */ - execute(input) { - return this._extensions.map(p => { + execute(input: any) { + return this._extensions.map((p) => { if (typeof p.response === 'function') { return p.response(input) } else { @@ -105,7 +105,7 @@ class ExtensionPoint { * @param {*} input Input to be provided as a parameter to the 1st callback * @returns {Promise.<*>} Result of the last extension that was called */ - async executeSerial(input) { + async executeSerial(input: any) { return await this._extensions.reduce(async (throughput, p) => { let tp = await throughput if (typeof p.response === 'function') { @@ -122,24 +122,25 @@ class ExtensionPoint { * @param {string} name Name of the listener needed if it is to be removed. * @param {Function} callback The callback function to trigger on a change. */ - onRegister(name, callback) { - if (typeof callback === 'function') this.#changeListeners.push({ name, callback }) + onRegister(name: string, callback: any) { + if (typeof callback === 'function') + this.changeListeners.push({ name, callback }) } /** * Unregister a callback from the extension list changes. * @param {string} name The name of the listener to remove. */ - offRegister(name) { - const index = this.#changeListeners.findIndex(l => l.name === name) - if (index > -1) this.#changeListeners.splice(index, 1) + offRegister(name: string) { + const index = this.changeListeners.findIndex((l) => l.name === name) + if (index > -1) this.changeListeners.splice(index, 1) } - #emitChange() { - for (const l of this.#changeListeners) { + emitChange() { + for (const l of this.changeListeners) { l.callback(this) } } } -export default ExtensionPoint \ No newline at end of file +export default ExtensionPoint diff --git a/electron/core/plugin-manager/execution/Plugin.js b/web/plugin/Plugin.ts similarity index 72% rename from electron/core/plugin-manager/execution/Plugin.js rename to web/plugin/Plugin.ts index 9d8f25420..bf24c4f2e 100644 --- a/electron/core/plugin-manager/execution/Plugin.js +++ b/web/plugin/Plugin.ts @@ -1,4 +1,4 @@ -import { callExport } from "./import-manager" +import { callExport } from './import-manager' /** * A slimmed down representation of a plugin for the renderer. @@ -25,7 +25,15 @@ class Plugin { /** @type {string} Plugin's logo. */ icon - constructor(name, url, activationPoints, active, description, version, icon) { + constructor( + name?: string, + url?: string, + activationPoints?: any[], + active?: boolean, + description?: string, + version?: string, + icon?: string + ) { this.name = name this.url = url this.activationPoints = activationPoints @@ -39,9 +47,9 @@ class Plugin { * Trigger an exported callback on the plugin's main file. * @param {string} exp exported callback to trigger. */ - triggerExport(exp) { - callExport(this.url, exp, this.name) + triggerExport(exp: string) { + if (this.url && this.name) callExport(this.url, exp, this.name) } } -export default Plugin \ No newline at end of file +export default Plugin diff --git a/electron/core/plugin-manager/execution/activation-manager.js b/web/plugin/activation-manager.ts similarity index 74% rename from electron/core/plugin-manager/execution/activation-manager.js rename to web/plugin/activation-manager.ts index 01eeed2a2..f1fd1d288 100644 --- a/electron/core/plugin-manager/execution/activation-manager.js +++ b/web/plugin/activation-manager.ts @@ -1,16 +1,16 @@ -import Activation from "./Activation.js" - +import Activation from './Activation' +import Plugin from './Plugin' /** * This object contains a register of plugin registrations to an activation points, and the means to work with them. * @namespace activationPoints */ -/** +/** * @constant {Array.} activationRegister * @private * Store of activations used by the consumer */ -const activationRegister = [] +const activationRegister: any[] = [] /** * Register a plugin with its activation points (as defined in its manifest). @@ -18,18 +18,22 @@ const activationRegister = [] * @returns {void} * @alias activationPoints.register */ -export function register(plugin) { - if (!Array.isArray(plugin.activationPoints)) throw new Error( - `Plugin ${plugin.name || 'without name'} does not have any activation points set up in its manifest.` - ) +export function register(plugin: Plugin) { + if (!Array.isArray(plugin.activationPoints)) + throw new Error( + `Plugin ${ + plugin.name || 'without name' + } does not have any activation points set up in its manifest.` + ) for (const ap of plugin.activationPoints) { // Ensure plugin is not already registered to activation point - const duplicate = activationRegister.findIndex(act => - act.plugin === plugin.name && act.activationPoint === ap + const duplicate = activationRegister.findIndex( + (act) => act.plugin === plugin.name && act.activationPoint === ap ) // Create new activation and add it to the register - if (duplicate < 0) activationRegister.push(new Activation(plugin.name, ap, plugin.url)) + if (duplicate < 0 && plugin.name && plugin.url) + activationRegister.push(new Activation(plugin.name, ap, plugin.url)) } } @@ -37,10 +41,10 @@ export function register(plugin) { * Trigger all activations registered to the given activation point. See {@link Plugin}. * This will call the function with the same name as the activation point on the path specified in the plugin. * @param {string} activationPoint Name of the activation to trigger - * @returns {Promise.} Resolves to true when the activations are complete. + * @returns {Promise.} Resolves to true when the activations are complete. * @alias activationPoints.trigger */ -export async function trigger(activationPoint) { +export async function trigger(activationPoint: string) { // Make sure all triggers are complete before returning await Promise.all( // Trigger each relevant activation point from the register and return an array of trigger promises @@ -60,7 +64,7 @@ export async function trigger(activationPoint) { * @returns {void} * @alias activationPoints.remove */ -export function remove(plugin) { +export function remove(plugin: any) { let i = activationRegister.length while (i--) { if (activationRegister[i].plugin === plugin) { @@ -85,4 +89,4 @@ export function clear() { */ export function get() { return [...activationRegister] -} \ No newline at end of file +} diff --git a/electron/core/plugin-manager/execution/extension-manager.js b/web/plugin/extension-manager.ts similarity index 80% rename from electron/core/plugin-manager/execution/extension-manager.js rename to web/plugin/extension-manager.ts index 48b2e893a..790840f01 100644 --- a/electron/core/plugin-manager/execution/extension-manager.js +++ b/web/plugin/extension-manager.ts @@ -3,14 +3,14 @@ * @namespace extensionPoints */ -import ExtensionPoint from "./ExtensionPoint.js" +import ExtensionPoint from './ExtensionPoint' -/** +/** * @constant {Object.} extensionPoints * @private * Register of extension points created by the consumer */ -const _extensionPoints = {} +const _extensionPoints: Record = {} /** * Create new extension point and add it to the registry. @@ -18,7 +18,7 @@ const _extensionPoints = {} * @returns {void} * @alias extensionPoints.add */ -export function add(name) { +export function add(name: string) { _extensionPoints[name] = new ExtensionPoint(name) } @@ -28,7 +28,7 @@ export function add(name) { * @returns {void} * @alias extensionPoints.remove */ -export function remove(name) { +export function remove(name: string) { delete _extensionPoints[name] } @@ -41,7 +41,7 @@ export function remove(name) { * @returns {void} * @alias extensionPoints.register */ -export function register(ep, extension, response, priority) { +export function register(ep: string, extension: string, response: any, priority: number) { if (!_extensionPoints[ep]) add(ep) if (_extensionPoints[ep].register) { _extensionPoints[ep].register(extension, response, priority) @@ -53,7 +53,7 @@ export function register(ep, extension, response, priority) { * @param {RegExp} name Matcher for the name of the extension to remove. * @alias extensionPoints.unregisterAll */ -export function unregisterAll(name) { +export function unregisterAll(name: string) { for (const ep in _extensionPoints) _extensionPoints[ep].unregister(name) } @@ -63,8 +63,8 @@ export function unregisterAll(name) { * @returns {Object. | ExtensionPoint} Found extension points * @alias extensionPoints.get */ -export function get(ep) { - return (ep ? _extensionPoints[ep] : { ..._extensionPoints }) +export function get(ep?: string) { + return ep ? _extensionPoints[ep] : { ..._extensionPoints } } /** @@ -75,10 +75,11 @@ export function get(ep) { * @returns {Array} Result of Promise.all or Promise.allSettled depending on exitOnError * @alias extensionPoints.execute */ -export function execute(name, input) { - if (!_extensionPoints[name] || !_extensionPoints[name].execute) throw new Error( - `The extension point "${name}" is not a valid extension point` - ) +export function execute(name: string, input: any) { + if (!_extensionPoints[name] || !_extensionPoints[name].execute) + throw new Error( + `The extension point "${name}" is not a valid extension point` + ) return _extensionPoints[name].execute(input) } @@ -90,9 +91,10 @@ export function execute(name, input) { * @returns {Promise.<*>} Result of the last extension that was called * @alias extensionPoints.executeSerial */ -export function executeSerial(name, input) { - if (!_extensionPoints[name] || !_extensionPoints[name].executeSerial) throw new Error( - `The extension point "${name}" is not a valid extension point` - ) +export function executeSerial(name: string, input: any) { + if (!_extensionPoints[name] || !_extensionPoints[name].executeSerial) + throw new Error( + `The extension point "${name}" is not a valid extension point` + ) return _extensionPoints[name].executeSerial(input) } diff --git a/electron/core/plugin-manager/execution/facade.js b/web/plugin/facade.ts similarity index 74% rename from electron/core/plugin-manager/execution/facade.js rename to web/plugin/facade.ts index 1059b743f..1ccdb428c 100644 --- a/electron/core/plugin-manager/execution/facade.js +++ b/web/plugin/facade.ts @@ -5,8 +5,8 @@ * @namespace plugins */ -import Plugin from "./Plugin"; -import { register } from "./activation-manager"; +import Plugin from './Plugin' +import { register } from './activation-manager' /** * @typedef {Object.} installOptions The {@link https://www.npmjs.com/package/pacote|pacote options} @@ -21,23 +21,23 @@ import { register } from "./activation-manager"; * @returns {Promise. | false>} plugin as defined by the main process. Has property cancelled set to true if installation was cancelled in the main process. * @alias plugins.install */ -export async function install(plugins) { - if (typeof window === "undefined") { - return; +export async function install(plugins: any[]) { + if (typeof window === 'undefined') { + return } // eslint-disable-next-line no-undef - const plgList = await window.pluggableElectronIpc.install(plugins); - if (plgList.cancelled) return false; - return plgList.map((plg) => { + const plgList = await window.pluggableElectronIpc.install(plugins) + if (plgList.cancelled) return false + return plgList.map((plg: any) => { const plugin = new Plugin( plg.name, plg.url, plg.activationPoints, plg.active - ); - register(plugin); - return plugin; - }); + ) + register(plugin) + return plugin + }) } /** @@ -47,12 +47,12 @@ export async function install(plugins) { * @returns {Promise.} Whether uninstalling the plugins was successful. * @alias plugins.uninstall */ -export function uninstall(plugins, reload = true) { - if (typeof window === "undefined") { - return; +export function uninstall(plugins: string[], reload = true) { + if (typeof window === 'undefined') { + return } // eslint-disable-next-line no-undef - return window.pluggableElectronIpc.uninstall(plugins, reload); + return window.pluggableElectronIpc.uninstall(plugins, reload) } /** @@ -61,17 +61,18 @@ export function uninstall(plugins, reload = true) { * @alias plugins.getActive */ export async function getActive() { - if (typeof window === "undefined") { - return; + if (typeof window === 'undefined') { + return [] } // eslint-disable-next-line no-undef - const plgList = await window.pluggableElectronIpc?.getActive() ?? - await import( - // eslint-disable-next-line no-undef - /* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}` - ).then((data) => data.default.filter((e) => e.supportCloudNative)); + const plgList = + (await window.pluggableElectronIpc?.getActive()) ?? + (await import( + // eslint-disable-next-line no-undef + /* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}` + ).then((data) => data.default.filter((e: any) => e.supportCloudNative))) return plgList.map( - (plugin) => + (plugin: any) => new Plugin( plugin.name, plugin.url, @@ -81,7 +82,7 @@ export async function getActive() { plugin.version, plugin.icon ) - ); + ) } /** @@ -90,12 +91,12 @@ export async function getActive() { * @alias plugins.registerActive */ export async function registerActive() { - if (typeof window === "undefined") { - return; + if (typeof window === 'undefined') { + return } // eslint-disable-next-line no-undef const plgList = await getActive() - plgList.forEach((plugin) => + plgList.forEach((plugin: Plugin) => register( new Plugin( plugin.name, @@ -104,7 +105,7 @@ export async function registerActive() { plugin.active ) ) - ); + ) } /** @@ -114,21 +115,21 @@ export async function registerActive() { * @returns {Promise.>} Updated plugin as defined by the main process. * @alias plugins.update */ -export async function update(plugins, reload = true) { - if (typeof window === "undefined") { - return; +export async function update(plugins: Plugin[], reload = true) { + if (typeof window === 'undefined') { + return } // eslint-disable-next-line no-undef - const plgList = await window.pluggableElectronIpc.update(plugins, reload); + const plgList = await window.pluggableElectronIpc.update(plugins, reload) return plgList.map( - (plugin) => + (plugin: any) => new Plugin( plugin.name, plugin.url, plugin.activationPoints, plugin.active ) - ); + ) } /** @@ -137,12 +138,12 @@ export async function update(plugins, reload = true) { * @returns {Object.} Object with plugins as keys and new version if update is available or false as values. * @alias plugins.updatesAvailable */ -export function updatesAvailable(plugin) { - if (typeof window === "undefined") { - return; +export function updatesAvailable(plugin: Plugin) { + if (typeof window === 'undefined') { + return } // eslint-disable-next-line no-undef - return window.pluggableElectronIpc.updatesAvailable(plugin); + return window.pluggableElectronIpc.updatesAvailable(plugin) } /** @@ -152,11 +153,11 @@ export function updatesAvailable(plugin) { * @returns {Promise.} Updated plugin as defined by the main process. * @alias plugins.toggleActive */ -export async function toggleActive(plugin, active) { - if (typeof window === "undefined") { - return; +export async function toggleActive(plugin: Plugin, active: boolean) { + if (typeof window === 'undefined') { + return } // eslint-disable-next-line no-undef - const plg = await window.pluggableElectronIpc.toggleActive(plugin, active); - return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active); + const plg = await window.pluggableElectronIpc.toggleActive(plugin, active) + return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active) } diff --git a/electron/core/plugin-manager/execution/import-manager.js b/web/plugin/import-manager.ts similarity index 85% rename from electron/core/plugin-manager/execution/import-manager.js rename to web/plugin/import-manager.ts index 0d25297a2..b9b9fb9e4 100644 --- a/electron/core/plugin-manager/execution/import-manager.js +++ b/web/plugin/import-manager.ts @@ -3,7 +3,7 @@ import { register, execute, executeSerial, -} from "./extension-manager.js"; +} from "./extension-manager"; /** * Used to import a plugin entry point. * Ensure your bundler does no try to resolve this import as the plugins are not known at build time. @@ -16,14 +16,14 @@ import { * @private * @type {importer} */ -export let importer; +export let importer: any; /** * @private * Set the plugin importer function. * @param {importer} callback Callback to import plugins. */ -export function setImporter(callback) { +export function setImporter(callback: any) { importer = callback; } @@ -31,14 +31,14 @@ export function setImporter(callback) { * @private * @type {Boolean|null} */ -export let presetEPs; +export let presetEPs: boolean|null; /** * @private * Define how extension points are accessed. * @param {Boolean|null} peps Whether extension points are predefined. */ -export function definePresetEps(peps) { +export function definePresetEps(peps: boolean|null) { presetEPs = peps === null || peps === true ? peps : false; } @@ -49,7 +49,7 @@ export function definePresetEps(peps) { * @param {string} exp Export to call * @param {string} [plugin] @see Activation */ -export async function callExport(url, exp, plugin) { +export async function callExport(url: string, exp: string, plugin: string) { if (!importer) throw new Error("Importer callback has not been set"); const main = await importer(url); diff --git a/electron/core/plugin-manager/execution/index.js b/web/plugin/index.ts similarity index 70% rename from electron/core/plugin-manager/execution/index.js rename to web/plugin/index.ts index 15cc65240..49aede97a 100644 --- a/electron/core/plugin-manager/execution/index.js +++ b/web/plugin/index.ts @@ -1,9 +1,9 @@ -import { definePresetEps, setImporter } from "./import-manager.js"; +import { definePresetEps, setImporter } from "./import-manager"; -export * as extensionPoints from "./extension-manager.js"; -export * as activationPoints from "./activation-manager.js"; -export * as plugins from "./facade.js"; -export { default as ExtensionPoint } from "./ExtensionPoint.js"; +export * as extensionPoints from "./extension-manager"; +export * as activationPoints from "./activation-manager"; +export * as plugins from "./facade"; +export { default as ExtensionPoint } from "./ExtensionPoint"; // eslint-disable-next-line no-undef if (typeof window !== "undefined" && !window.pluggableElectronIpc) @@ -19,7 +19,7 @@ if (typeof window !== "undefined" && !window.pluggableElectronIpc) * can be created on the fly(false) or should not be provided through the input at all (null). * @returns {void} */ -export function setup(options) { +export function setup(options: any) { setImporter(options.importer); definePresetEps(options.presetEPs); } diff --git a/web/screens/Settings/CorePlugins/PluginsCatalog.tsx b/web/screens/Settings/CorePlugins/PluginsCatalog.tsx index 3eb2ab7f9..c5c094252 100644 --- a/web/screens/Settings/CorePlugins/PluginsCatalog.tsx +++ b/web/screens/Settings/CorePlugins/PluginsCatalog.tsx @@ -5,10 +5,7 @@ import { Button, Switch } from '@uikit' import Loader from '@containers/Loader' import { formatPluginsName } from '@utils/converter' -import { - plugins, - extensionPoints, -} from '@/../../electron/core/plugin-manager/execution/index' +import { plugins, extensionPoints } from '@plugin' import { executeSerial } from '@services/pluginService' import { DataService } from '@janhq/core' import useGetAppVersion from '@hooks/useGetAppVersion' @@ -27,19 +24,19 @@ const PluginCatalog = () => { */ useEffect(() => { if (!window.electronAPI) { - return; + return } if (!version) return // Load plugin manifest from plugin if any if (extensionPoints.get(DataService.GetPluginManifest)) { - executeSerial(DataService.GetPluginManifest).then((data) => { + executeSerial(DataService.GetPluginManifest).then((data: any) => { setPluginCatalog( data.filter( (e: any) => !e.requiredVersion || e.requiredVersion.replace(/[.^]/g, '') <= - version.replaceAll('.', '') + version.replaceAll('.', '') ) ) }) @@ -53,7 +50,7 @@ const PluginCatalog = () => { (e: any) => !e.requiredVersion || e.requiredVersion.replace(/[.^]/g, '') <= - version.replaceAll('.', '') + version.replaceAll('.', '') ) ) ) @@ -74,7 +71,7 @@ const PluginCatalog = () => { if (extensionPoints.get('experimentComponent')) { const components = await Promise.all( - extensionPoints.execute('experimentComponent') + extensionPoints.execute('experimentComponent', {}) ) components.forEach((e) => { if (experimentRef.current) { diff --git a/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx b/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx index fbf5dc625..43aba559a 100644 --- a/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx +++ b/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { execute } from '../../../../../electron/core/plugin-manager/execution/extension-manager' +import { execute } from '@plugin/extension-manager' type Props = { pluginName: string @@ -22,7 +22,10 @@ const PreferencePlugins = (props: Props) => { if (timeout) { clearTimeout(timeout) } - timeout = setTimeout(() => execute(PluginService.OnPreferencesUpdate), 100) + timeout = setTimeout( + () => execute(PluginService.OnPreferencesUpdate, {}), + 100 + ) } return ( diff --git a/web/screens/Settings/index.tsx b/web/screens/Settings/index.tsx index 2368eec20..1f0c23e5f 100644 --- a/web/screens/Settings/index.tsx +++ b/web/screens/Settings/index.tsx @@ -13,10 +13,7 @@ import { twMerge } from 'tailwind-merge' import { formatPluginsName } from '@utils/converter' -import { - plugins, - extensionPoints, -} from '@/../../electron/core/plugin-manager/execution/index' +import { extensionPoints } from '@plugin' const staticMenu = ['Appearance'] @@ -40,7 +37,7 @@ const SettingsScreen = () => { const getActivePluginPreferences = async () => { if (extensionPoints.get('PluginPreferences')) { const data = await Promise.all( - extensionPoints.execute('PluginPreferences') + extensionPoints.execute('PluginPreferences', {}) ) setPreferenceItems(Array.isArray(data) ? data : []) Promise.all( diff --git a/web/services/pluginService.ts b/web/services/pluginService.ts index d885e6d8d..8250776a6 100644 --- a/web/services/pluginService.ts +++ b/web/services/pluginService.ts @@ -2,10 +2,9 @@ import { extensionPoints, plugins, -} from '../../electron/core/plugin-manager/execution/index' +} from '@plugin' import { CoreService, - DataService, InferenceService, ModelManagementService, StoreService, diff --git a/web/tsconfig.json b/web/tsconfig.json index be06835f6..42170f12a 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -29,7 +29,9 @@ "@models/*": ["./models/*"], "@hooks/*": ["./hooks/*"], "@containers/*": ["./containers/*"], - "@screens/*": ["./screens/*"] + "@screens/*": ["./screens/*"], + "@plugin/*": ["./plugin/*"], + "@plugin": ["./plugin"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], diff --git a/web/types/index.d.ts b/web/types/index.d.ts index bb21c2a21..52c22a49a 100644 --- a/web/types/index.d.ts +++ b/web/types/index.d.ts @@ -7,5 +7,6 @@ declare global { electronAPI?: any | undefined; corePlugin?: any | undefined; coreAPI?: any | undefined; + pluggableElectronIpc?: any | undefined; } }