refactor: plugin manager and execution as ts (#504)

* refactor: plugin manager and execution as ts

* chore: refactoring
This commit is contained in:
Louis 2023-11-01 09:48:28 +07:00 committed by GitHub
parent edeb82ae84
commit 37c36363d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 518 additions and 1725 deletions

View File

@ -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" }])
})

View File

@ -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()
})
})

View File

@ -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([])
}
)
})

View File

@ -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/)
})
})

View File

@ -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)
})
})

View File

@ -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,
})
})
})

View File

@ -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()
})
})

View File

@ -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.<string>} plg The specifiers used to locate the packages (from NPM or local file)
* @returns {Promise<boolean>} Whether to proceed with the plugin installation
*/

View File

@ -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,
})
})
})

View File

@ -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
}

View File

@ -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.<Plugin> */
let activePlugins
/** @type Array.<Plugin> */
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)
})
})

View File

@ -1,23 +1,23 @@
const { ipcRenderer, contextBridge } = require("electron"); const { ipcRenderer, contextBridge } = require("electron");
function useFacade() { export function useFacade() {
const interfaces = { const interfaces = {
install(plugins) { install(plugins: any[]) {
return ipcRenderer.invoke("pluggable:install", plugins); return ipcRenderer.invoke("pluggable:install", plugins);
}, },
uninstall(plugins, reload) { uninstall(plugins: any[], reload: boolean) {
return ipcRenderer.invoke("pluggable:uninstall", plugins, reload); return ipcRenderer.invoke("pluggable:uninstall", plugins, reload);
}, },
getActive() { getActive() {
return ipcRenderer.invoke("pluggable:getActivePlugins"); return ipcRenderer.invoke("pluggable:getActivePlugins");
}, },
update(plugins, reload) { update(plugins: any[], reload: boolean) {
return ipcRenderer.invoke("pluggable:update", plugins, reload); return ipcRenderer.invoke("pluggable:update", plugins, reload);
}, },
updatesAvailable(plugin) { updatesAvailable(plugin: any) {
return ipcRenderer.invoke("pluggable:updatesAvailable", plugin); return ipcRenderer.invoke("pluggable:updatesAvailable", plugin);
}, },
toggleActive(plugin, active) { toggleActive(plugin: any, active: boolean) {
return ipcRenderer.invoke("pluggable:togglePluginActive", plugin, active); return ipcRenderer.invoke("pluggable:togglePluginActive", plugin, active);
}, },
}; };
@ -28,5 +28,3 @@ function useFacade() {
return interfaces; return interfaces;
} }
module.exports = useFacade;

View File

@ -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");
}

View File

@ -1,11 +1,23 @@
import { readFileSync } from "fs" import { readFileSync } from "fs";
import { protocol } from 'electron' import { protocol } from "electron";
import { normalize } from "path" import { normalize } from "path";
import Plugin from "./Plugin" import Plugin from "./plugin";
import { getAllPlugins, removePlugin, persistPlugins, installPlugins, getPlugin, getActivePlugins, addPlugin } from "./store" import {
import { pluginsPath as storedPluginsPath, setPluginsPath, getPluginsFile, setConfirmInstall } from './globals' getAllPlugins,
import router from "./router" 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. * Sets up the required communication between the main and renderer processes.
@ -17,24 +29,24 @@ import router from "./router"
* @returns {pluginManager|Object} A set of functions used to manage the plugin lifecycle if usePlugins is provided. * @returns {pluginManager|Object} A set of functions used to manage the plugin lifecycle if usePlugins is provided.
* @function * @function
*/ */
export function init(options) { export function init(options: any) {
if (!Object.prototype.hasOwnProperty.call(options, 'useFacade') || options.useFacade) { if (
// Store the confirmInstall function !Object.prototype.hasOwnProperty.call(options, "useFacade") ||
setConfirmInstall(options.confirmInstall) options.useFacade
) {
// Enable IPC to be used by the facade // Enable IPC to be used by the facade
router() router();
} }
// Create plugins protocol to serve plugins to renderer // Create plugins protocol to serve plugins to renderer
registerPluginProtocol() registerPluginProtocol();
// perform full setup if pluginsPath is provided // perform full setup if pluginsPath is provided
if (options.pluginsPath) { 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 * @returns {boolean} Whether the protocol registration was successful
*/ */
function registerPluginProtocol() { function registerPluginProtocol() {
return protocol.registerFileProtocol('plugin', (request, callback) => { return protocol.registerFileProtocol("plugin", (request, callback) => {
const entry = request.url.substr(8) const entry = request.url.substr(8);
const url = normalize(storedPluginsPath + entry) const url = normalize(storedPluginsPath + entry);
callback({ path: url }) callback({ path: url });
}) });
} }
/** /**
@ -56,34 +68,38 @@ function registerPluginProtocol() {
* @param {string} pluginsPath Path to the plugins folder. Required if not yet set up. * @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. * @returns {pluginManager} A set of functions used to manage the plugin lifecycle.
*/ */
export function usePlugins(pluginsPath) { export function usePlugins(pluginsPath: string) {
if (!pluginsPath) throw Error('A path to the plugins folder is required to use Pluggable Electron') if (!pluginsPath)
throw Error(
"A path to the plugins folder is required to use Pluggable Electron"
);
// Store the path to the plugins folder // Store the path to the plugins folder
setPluginsPath(pluginsPath) setPluginsPath(pluginsPath);
// Remove any registered plugins // Remove any registered plugins
for (const plugin of getAllPlugins()) { for (const plugin of getAllPlugins()) {
removePlugin(plugin.name, false) if (plugin.name) removePlugin(plugin.name, false);
} }
// Read plugin list from plugins folder // Read plugin list from plugins folder
const plugins = JSON.parse(readFileSync(getPluginsFile())) const plugins = JSON.parse(readFileSync(getPluginsFile(), "utf-8"));
try { try {
// Create and store a Plugin instance for each plugin in list // Create and store a Plugin instance for each plugin in list
for (const p in plugins) { for (const p in plugins) {
loadPlugin(plugins[p]) loadPlugin(plugins[p]);
} }
persistPlugins() persistPlugins();
} catch (error) { } catch (error) {
// Throw meaningful error if plugin loading fails // Throw meaningful error if plugin loading fails
throw new Error('Could not successfully rebuild list of installed plugins.\n' throw new Error(
+ error "Could not successfully rebuild list of installed plugins.\n" +
+ '\nPlease check the plugins.json file in the plugins folder.') error +
"\nPlease check the plugins.json file in the plugins folder."
);
} }
// Return the plugin lifecycle functions // Return the plugin lifecycle functions
return getStore() return getStore();
} }
/** /**
@ -92,16 +108,24 @@ export function usePlugins(pluginsPath) {
* @private * @private
* @param {Object} plg Plugin info * @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 // 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) { 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) addPlugin(plugin, false);
plugin.subscribe('pe-persist', persistPlugins) plugin.subscribe("pe-persist", persistPlugins);
} }
/** /**
@ -110,7 +134,9 @@ function loadPlugin(plg) {
*/ */
export function getStore() { export function getStore() {
if (!storedPluginsPath) { 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 { return {
@ -119,5 +145,5 @@ export function getStore() {
getAllPlugins, getAllPlugins,
getActivePlugins, getActivePlugins,
removePlugin, removePlugin,
} };
} }

View File

@ -1,9 +1,9 @@
import { rmdir } from "fs/promises" import { rmdir } from "fs/promises";
import { resolve, join } from "path" import { resolve, join } from "path";
import { manifest, extract } from "pacote" import { manifest, extract } from "pacote";
import Arborist from '@npmcli/arborist' 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. * An NPM package that can be used as a Pluggable Electron plugin.
@ -21,30 +21,39 @@ class Plugin {
* @property {string} description The description of plugin as defined in the manifest. * @property {string} description The description of plugin as defined in the manifest.
* @property {string} icon The icon 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<string>;
main?: string;
description?: string;
icon?: string;
/** @private */ /** @private */
_active = false _active = false;
/** /**
* @private * @private
* @property {Object.<string, Function>} #listeners A list of callbacks to be executed when the Plugin is updated. * @property {Object.<string, Function>} #listeners A list of callbacks to be executed when the Plugin is updated.
*/ */
#listeners = {} listeners: Record<string, (obj: any) => void> = {};
/** /**
* Set installOptions with defaults for options that have not been provided. * Set installOptions with defaults for options that have not been provided.
* @param {string} [origin] Original specification provided to fetch the package. * @param {string} [origin] Original specification provided to fetch the package.
* @param {Object} [options] Options provided to pacote when fetching the manifest. * @param {Object} [options] Options provided to pacote when fetching the manifest.
*/ */
constructor(origin, options = {}) { constructor(origin?: string, options = {}) {
const defaultOpts = { const defaultOpts = {
version: false, version: false,
fullMetadata: false, fullMetadata: false,
Arborist Arborist,
} };
this.origin = origin this.origin = origin;
this.installOptions = { ...defaultOpts, ...options } this.installOptions = { ...defaultOpts, ...options };
} }
/** /**
@ -52,7 +61,10 @@ class Plugin {
* @type {string} * @type {string}
*/ */
get specifier() { 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} * @type {boolean}
*/ */
get active() { get active() {
return this._active return this._active;
} }
/** /**
* Set Package details based on it's manifest * Set Package details based on it's manifest
* @returns {Promise.<Boolean>} Resolves to true when the action completed * @returns {Promise.<Boolean>} Resolves to true when the action completed
*/ */
async #getManifest() { async getManifest() {
// Get the package's manifest (package.json object) // Get the package's manifest (package.json object)
try { 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 // set the Package properties based on the it's manifest
this.name = mnf.name this.name = mnf.name;
this.version = mnf.version this.version = mnf.version;
this.activationPoints = mnf.activationPoints || null this.activationPoints = mnf.activationPoints
this.main = mnf.main ? (mnf.activationPoints as string[])
this.description = mnf.description : undefined;
this.icon = mnf.icon this.main = mnf.main;
this.description = mnf.description;
this.icon = mnf.icon as any;
} catch (error) { } 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() { async _install() {
try { try {
// import the manifest details // import the manifest details
await this.#getManifest() await this.getManifest();
// Install the package in a child folder of the given folder // 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)) 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 // Set the url using the custom plugins protocol
this.url = `plugin://${this.name}/${this.main}` this.url = `plugin://${this.name}/${this.main}`;
this.#emitUpdate()
this.emitUpdate();
} catch (err) { } catch (err) {
// Ensure the plugin is not stored and the folder is removed if the installation fails // Ensure the plugin is not stored and the folder is removed if the installation fails
this.setActive(false) this.setActive(false);
throw err throw err;
} }
return [this] return [this];
} }
/** /**
@ -122,24 +140,24 @@ class Plugin {
* @param {string} name name of the callback to register * @param {string} name name of the callback to register
* @param {callback} cb The function to execute on update * @param {callback} cb The function to execute on update
*/ */
subscribe(name, cb) { subscribe(name: string, cb: () => void) {
this.#listeners[name] = cb this.listeners[name] = cb;
} }
/** /**
* Remove subscription * Remove subscription
* @param {string} name name of the callback to remove * @param {string} name name of the callback to remove
*/ */
unsubscribe(name) { unsubscribe(name: string) {
delete this.#listeners[name] delete this.listeners[name];
} }
/** /**
* Execute listeners * Execute listeners
*/ */
#emitUpdate() { emitUpdate() {
for (const cb in this.#listeners) { for (const cb in this.listeners) {
this.#listeners[cb].call(null, this) this.listeners[cb].call(null, this);
} }
} }
@ -149,13 +167,13 @@ class Plugin {
* @returns {boolean} Whether an update was performed. * @returns {boolean} Whether an update was performed.
*/ */
async update(version = false) { async update(version = false) {
if (this.isUpdateAvailable()) { if (await this.isUpdateAvailable()) {
this.installOptions.version = version this.installOptions.version = version;
await this._install(false) await this._install();
return true return true;
} }
return false return false;
} }
/** /**
@ -163,8 +181,10 @@ class Plugin {
* @returns the latest available version if a new version is available or false if not. * @returns the latest available version if a new version is available or false if not.
*/ */
async isUpdateAvailable() { async isUpdateAvailable() {
const mnf = await manifest(this.origin) if (this.origin) {
return mnf.version !== this.version ? mnf.version : false const mnf = await manifest(this.origin);
return mnf.version !== this.version ? mnf.version : false;
}
} }
/** /**
@ -172,10 +192,10 @@ class Plugin {
* @returns {Promise} * @returns {Promise}
*/ */
async uninstall() { async uninstall() {
const plgPath = resolve(pluginsPath, this.name) const plgPath = resolve(pluginsPath ?? "", this.name ?? "");
await rmdir(plgPath, { recursive: true }) await rmdir(plgPath, { recursive: true });
this.#emitUpdate() this.emitUpdate();
} }
/** /**
@ -183,11 +203,11 @@ class Plugin {
* @param {boolean} active State to set _active to * @param {boolean} active State to set _active to
* @returns {Plugin} This plugin * @returns {Plugin} This plugin
*/ */
setActive(active) { setActive(active: boolean) {
this._active = active this._active = active;
this.#emitUpdate() this.emitUpdate();
return this return this;
} }
} }
export default Plugin export default Plugin;

View File

@ -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<string, Plugin> = {};
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;
}

View File

@ -8,9 +8,9 @@
* @prop {removePlugin} removePlugin * @prop {removePlugin} removePlugin
*/ */
import { writeFileSync } from "fs" import { writeFileSync } from "fs";
import Plugin from "./Plugin" import Plugin from "./plugin";
import { getPluginsFile } from './globals' import { getPluginsFile } from "./globals";
/** /**
* @module store * @module store
@ -21,7 +21,7 @@ import { getPluginsFile } from './globals'
* Register of installed plugins * Register of installed plugins
* @type {Object.<string, Plugin>} plugin - List of installed plugins * @type {Object.<string, Plugin>} plugin - List of installed plugins
*/ */
const plugins = {} const plugins: Record<string, Plugin> = {};
/** /**
* Get a plugin from the stored plugins. * Get a plugin from the stored plugins.
@ -29,12 +29,12 @@ const plugins = {}
* @returns {Plugin} Retrieved plugin * @returns {Plugin} Retrieved plugin
* @alias pluginManager.getPlugin * @alias pluginManager.getPlugin
*/ */
export function getPlugin(name) { export function getPlugin(name: string) {
if (!Object.prototype.hasOwnProperty.call(plugins, name)) { 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.<Plugin>} All plugin objects * @returns {Array.<Plugin>} All plugin objects
* @alias pluginManager.getAllPlugins * @alias pluginManager.getAllPlugins
*/ */
export function getAllPlugins() { return Object.values(plugins) } export function getAllPlugins() {
return Object.values(plugins);
}
/** /**
* Get list of active plugin objects. * Get list of active plugin objects.
@ -50,7 +52,7 @@ export function getAllPlugins() { return Object.values(plugins) }
* @alias pluginManager.getActivePlugins * @alias pluginManager.getActivePlugins
*/ */
export function 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 * @returns {boolean} Whether the delete was successful
* @alias pluginManager.removePlugin * @alias pluginManager.removePlugin
*/ */
export function removePlugin(name, persist = true) { export function removePlugin(name: string, persist = true) {
const del = delete plugins[name] const del = delete plugins[name];
if (persist) persistPlugins() if (persist) persistPlugins();
return del return del;
} }
/** /**
@ -72,11 +74,11 @@ export function removePlugin(name, persist = true) {
* @param {boolean} persist Whether to save the changes to plugins to file * @param {boolean} persist Whether to save the changes to plugins to file
* @returns {void} * @returns {void}
*/ */
export function addPlugin(plugin, persist = true) { export function addPlugin(plugin: Plugin, persist = true) {
plugins[plugin.name] = plugin if (plugin.name) plugins[plugin.name] = plugin;
if (persist) { if (persist) {
persistPlugins() persistPlugins();
plugin.subscribe('pe-persist', persistPlugins) plugin.subscribe("pe-persist", persistPlugins);
} }
} }
@ -85,11 +87,11 @@ export function addPlugin(plugin, persist = true) {
* @returns {void} * @returns {void}
*/ */
export function persistPlugins() { export function persistPlugins() {
const persistData = {} const persistData: Record<string, Plugin> = {};
for (const name in plugins) { 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.<Array.<Plugin>>} New plugin * @returns {Promise.<Array.<Plugin>>} New plugin
* @alias pluginManager.installPlugins * @alias pluginManager.installPlugins
*/ */
export async function installPlugins(plugins, store = true) { export async function installPlugins(plugins: any, store = true) {
const installed = [] const installed: Plugin[] = [];
for (const plg of plugins) { for (const plg of plugins) {
// Set install options and activation based on input type // Set install options and activation based on input type
const isObject = typeof plg === 'object' const isObject = typeof plg === "object";
const spec = isObject ? [plg.specifier, plg] : [plg] const spec = isObject ? [plg.specifier, plg] : [plg];
const activate = isObject ? plg.activate !== false : true const activate = isObject ? plg.activate !== false : true;
// Install and possibly activate plugin // Install and possibly activate plugin
const plugin = new Plugin(...spec) const plugin = new Plugin(...spec);
await plugin._install() await plugin._install();
if (activate) plugin.setActive(true) if (activate) plugin.setActive(true);
// Add plugin to store if needed // Add plugin to store if needed
if (store) addPlugin(plugin) if (store) addPlugin(plugin);
installed.push(plugin) installed.push(plugin);
} }
// Return list of all installed plugins // Return list of all installed plugins
return installed return installed;
} }
/** /**

View File

@ -9,7 +9,7 @@ import {
import { readdirSync, writeFileSync } from "fs"; import { readdirSync, writeFileSync } from "fs";
import { resolve, join, extname } from "path"; import { resolve, join, extname } from "path";
import { rmdir, unlink, createWriteStream } from "fs"; 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 { setupMenu } from "./utils/menu";
import { dispose } from "./utils/disposable"; import { dispose } from "./utils/disposable";

View File

@ -74,6 +74,8 @@
"devDependencies": { "devDependencies": {
"@electron/notarize": "^2.1.0", "@electron/notarize": "^2.1.0",
"@playwright/test": "^1.38.1", "@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/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3", "@typescript-eslint/parser": "^6.7.3",
"electron": "26.2.1", "electron": "26.2.1",

View File

@ -1,6 +1,5 @@
// Make Pluggable Electron's facade available to the renderer on window.plugins // Make Pluggable Electron's facade available to the renderer on window.plugins
//@ts-ignore import { useFacade } from "./core/plugin/facade";
const useFacade = require("../core/plugin-manager/facade");
useFacade(); useFacade();
//@ts-ignore //@ts-ignore
const { contextBridge, ipcRenderer } = require("electron"); const { contextBridge, ipcRenderer } = require("electron");

View File

@ -1,9 +1,6 @@
'use client' 'use client'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { import { plugins, extensionPoints } from '@plugin'
plugins,
extensionPoints,
} from '@/../../electron/core/plugin-manager/execution/index'
import { import {
ChartPieIcon, ChartPieIcon,
CommandLineIcon, CommandLineIcon,
@ -13,7 +10,7 @@ import {
import { MagnifyingGlassIcon } from '@heroicons/react/20/solid' import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'
import classNames from 'classnames' import classNames from 'classnames'
import { DataService, PluginService, preferences } from '@janhq/core' 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 LoadingIndicator from './LoadingIndicator'
import { executeSerial } from '@services/pluginService' 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. * Loads the plugin catalog module from a CDN and sets it as the plugin catalog state.
*/ */
useEffect(() => { useEffect(() => {
executeSerial(DataService.GetPluginManifest).then((data) => { executeSerial(DataService.GetPluginManifest).then((data: any) => {
setPluginCatalog(data) setPluginCatalog(data)
}) })
}, []) }, [])
@ -52,7 +49,7 @@ export const Preferences = () => {
if (extensionPoints.get('experimentComponent')) { if (extensionPoints.get('experimentComponent')) {
const components = await Promise.all( const components = await Promise.all(
extensionPoints.execute('experimentComponent') extensionPoints.execute('experimentComponent', {})
) )
if (components.length > 0) { if (components.length > 0) {
setIsTestAvailable(true) setIsTestAvailable(true)
@ -67,7 +64,7 @@ export const Preferences = () => {
if (extensionPoints.get('PluginPreferences')) { if (extensionPoints.get('PluginPreferences')) {
const data = await Promise.all( const data = await Promise.all(
extensionPoints.execute('PluginPreferences') extensionPoints.execute('PluginPreferences', {})
) )
setPreferenceItems(Array.isArray(data) ? data : []) setPreferenceItems(Array.isArray(data) ? data : [])
Promise.all( Promise.all(
@ -149,7 +146,7 @@ export const Preferences = () => {
} }
if (extensionPoints.get(PluginService.OnPreferencesUpdate)) if (extensionPoints.get(PluginService.OnPreferencesUpdate))
timeout = setTimeout( timeout = setTimeout(
() => execute(PluginService.OnPreferencesUpdate), () => execute(PluginService.OnPreferencesUpdate, {}),
100 100
) )
} }

View File

@ -7,15 +7,14 @@ import JotaiWrapper from '@helpers/JotaiWrapper'
import { ModalWrapper } from '@helpers/ModalWrapper' import { ModalWrapper } from '@helpers/ModalWrapper'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import CompactLogo from '@containers/Logo/CompactLogo' import CompactLogo from '@containers/Logo/CompactLogo'
import { import { setup, plugins, activationPoints, extensionPoints } from '@plugin'
setup,
plugins,
activationPoints,
extensionPoints,
} from '../../../electron/core/plugin-manager/execution/index'
import EventListenerWrapper from '@helpers/EventListenerWrapper' import EventListenerWrapper from '@helpers/EventListenerWrapper'
import { setupCoreServices } from '@services/coreService' import { setupCoreServices } from '@services/coreService'
import { executeSerial, isCorePluginInstalled, setupBasePlugins } from '@services/pluginService' import {
executeSerial,
isCorePluginInstalled,
setupBasePlugins,
} from '@services/pluginService'
const Providers = (props: PropsWithChildren) => { const Providers = (props: PropsWithChildren) => {
const [setupCore, setSetupCore] = useState(false) const [setupCore, setSetupCore] = useState(false)

View File

@ -9,12 +9,15 @@ import {
updateConversationAtom, updateConversationAtom,
updateConversationWaitingForResponseAtom, updateConversationWaitingForResponseAtom,
} from './atoms/Conversation.atom' } 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 { debounce } from 'lodash'
import { setDownloadStateAtom, setDownloadStateSuccessAtom } from "./atoms/DownloadState.atom"; import {
import { downloadedModelAtom } from "./atoms/DownloadedModel.atom"; setDownloadStateAtom,
import { ModelManagementService } from "@janhq/core"; setDownloadStateSuccessAtom,
import { getDownloadedModels } from "../hooks/useGetDownloadedModels"; } 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 let currentConversation: Conversation | undefined = undefined
@ -25,7 +28,6 @@ const debouncedUpdateConversation = debounce(
1000 1000
) )
export default function EventHandler({ children }: { children: ReactNode }) { export default function EventHandler({ children }: { children: ReactNode }) {
const addNewMessage = useSetAtom(addNewMessageAtom) const addNewMessage = useSetAtom(addNewMessageAtom)
const updateMessage = useSetAtom(updateMessageAtom) const updateMessage = useSetAtom(updateMessageAtom)
@ -34,9 +36,9 @@ export default function EventHandler({ children }: { children: ReactNode }) {
const { getConversationById } = useGetUserConversations() const { getConversationById } = useGetUserConversations()
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom) const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
const setDownloadState = useSetAtom(setDownloadStateAtom); const setDownloadState = useSetAtom(setDownloadStateAtom)
const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom); const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom)
const setDownloadedModels = useSetAtom(downloadedModelAtom); const setDownloadedModels = useSetAtom(downloadedModelAtom)
async function handleNewMessageResponse(message: NewMessageResponse) { async function handleNewMessageResponse(message: NewMessageResponse) {
if (message.conversationId) { if (message.conversationId) {
@ -97,18 +99,21 @@ export default function EventHandler({ children }: { children: ReactNode }) {
} }
function handleDownloadUpdate(state: any) { function handleDownloadUpdate(state: any) {
if (!state) return; if (!state) return
setDownloadState(state); setDownloadState(state)
} }
function handleDownloadSuccess(state: any) { function handleDownloadSuccess(state: any) {
if (state && state.fileName && state.success === true) { if (state && state.fileName && state.success === true) {
setDownloadStateSuccess(state.fileName); setDownloadStateSuccess(state.fileName)
executeSerial(ModelManagementService.UpdateFinishedDownloadAt, state.fileName).then(() => { executeSerial(
ModelManagementService.UpdateFinishedDownloadAt,
state.fileName
).then(() => {
getDownloadedModels().then((models) => { 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.OnNewMessageResponse, handleNewMessageResponse)
events.on(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate) events.on(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate)
events.on( events.on(
"OnMessageResponseFinished", 'OnMessageResponseFinished',
// EventName.OnMessageResponseFinished, // EventName.OnMessageResponseFinished,
handleMessageResponseFinished handleMessageResponseFinished
) )
events.on(EventName.OnDownloadUpdate, handleDownloadUpdate); events.on(EventName.OnDownloadUpdate, handleDownloadUpdate)
events.on(EventName.OnDownloadSuccess, handleDownloadSuccess); events.on(EventName.OnDownloadSuccess, handleDownloadSuccess)
} }
}, []) }, [])
@ -131,12 +136,12 @@ export default function EventHandler({ children }: { children: ReactNode }) {
events.off(EventName.OnNewMessageResponse, handleNewMessageResponse) events.off(EventName.OnNewMessageResponse, handleNewMessageResponse)
events.off(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate) events.off(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate)
events.off( events.off(
"OnMessageResponseFinished", 'OnMessageResponseFinished',
// EventName.OnMessageResponseFinished, // EventName.OnMessageResponseFinished,
handleMessageResponseFinished handleMessageResponseFinished
) )
events.off(EventName.OnDownloadUpdate, handleDownloadUpdate); events.off(EventName.OnDownloadUpdate, handleDownloadUpdate)
events.off(EventName.OnDownloadSuccess, handleDownloadSuccess); events.off(EventName.OnDownloadSuccess, handleDownloadSuccess)
} }
}, []) }, [])
return <>{children}</> return <>{children}</>

View File

@ -2,7 +2,7 @@ import { useEffect } from 'react'
import { ModelManagementService } from '@janhq/core' import { ModelManagementService } from '@janhq/core'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom' import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom'
import { extensionPoints } from '../../electron/core/plugin-manager/execution' import { extensionPoints } from '@plugin'
import { executeSerial } from '@services/pluginService' import { executeSerial } from '@services/pluginService'
export function useGetDownloadedModels() { export function useGetDownloadedModels() {

View File

@ -1,5 +1,5 @@
import { ModelManagementService } from '@janhq/core' 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() { export default function useGetModelById() {
const getModelById = async ( const getModelById = async (

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { extensionPoints } from '../../electron/core/plugin-manager/execution' import { extensionPoints } from '@plugin'
import { SystemMonitoringService } from '@janhq/core' import { SystemMonitoringService } from '@janhq/core'
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { totalRamAtom } from '@helpers/atoms/SystemBar.atom' import { totalRamAtom } from '@helpers/atoms/SystemBar.atom'

View File

@ -1,4 +1,4 @@
import { callExport } from "./import-manager.js" import { callExport } from "./import-manager"
class Activation { class Activation {
/** @type {string} Name of the registered plugin. */ /** @type {string} Name of the registered plugin. */
@ -13,7 +13,7 @@ class Activation {
/** @type {boolean} Whether the activation has been activated. */ /** @type {boolean} Whether the activation has been activated. */
activated activated
constructor(plugin, activationPoint, url) { constructor(plugin: string, activationPoint: string, url: string) {
this.plugin = plugin this.plugin = plugin
this.activationPoint = activationPoint this.activationPoint = activationPoint
this.url = url this.url = url

View File

@ -18,15 +18,15 @@ class ExtensionPoint {
* @type {Array.<Extension>} The list of all extensions registered with this extension point. * @type {Array.<Extension>} The list of all extensions registered with this extension point.
* @private * @private
*/ */
_extensions = [] _extensions: any[] = []
/** /**
* @type {Array.<Object>} A list of functions to be executed when the list of extensions changes. * @type {Array.<Object>} A list of functions to be executed when the list of extensions changes.
* @private * @private
*/ */
#changeListeners = [] changeListeners: any[] = []
constructor(name) { constructor(name: string) {
this.name = name this.name = name
} }
@ -39,8 +39,8 @@ class ExtensionPoint {
* @param {number} [priority] Order priority for execution used for executing in serial. * @param {number} [priority] Order priority for execution used for executing in serial.
* @returns {void} * @returns {void}
*/ */
register(name, response, priority = 0) { register(name: string, response: any, priority: number = 0) {
const index = this._extensions.findIndex(p => p.priority > priority) const index = this._extensions.findIndex((p) => p.priority > priority)
const newExt = { name, response, priority } const newExt = { name, response, priority }
if (index > -1) { if (index > -1) {
this._extensions.splice(index, 0, newExt) this._extensions.splice(index, 0, newExt)
@ -48,7 +48,7 @@ class ExtensionPoint {
this._extensions.push(newExt) 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. * @param {RegExp } name Matcher for the name of the extension to remove.
* @returns {void} * @returns {void}
*/ */
unregister(name) { unregister(name: string) {
const index = this._extensions.findIndex(ext => ext.name.match(name)) const index = this._extensions.findIndex((ext) => ext.name.match(name))
if (index > -1) this._extensions.splice(index, 1) if (index > -1) this._extensions.splice(index, 1)
this.#emitChange() this.emitChange()
} }
/** /**
@ -69,7 +69,7 @@ class ExtensionPoint {
*/ */
clear() { clear() {
this._extensions = [] this._extensions = []
this.#emitChange() this.emitChange()
} }
/** /**
@ -77,8 +77,8 @@ class ExtensionPoint {
* @param {string} name Name of the extension to return * @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. * @returns {Object|Callback|undefined} The response of the extension. If this is a function the function is returned, not its response.
*/ */
get(name) { get(name: string) {
const ep = this._extensions.find(ext => ext.name === name) const ep = this._extensions.find((ext) => ext.name === name)
return ep && ep.response 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. * @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. * @returns {Array} List of responses from the extensions.
*/ */
execute(input) { execute(input: any) {
return this._extensions.map(p => { return this._extensions.map((p) => {
if (typeof p.response === 'function') { if (typeof p.response === 'function') {
return p.response(input) return p.response(input)
} else { } else {
@ -105,7 +105,7 @@ class ExtensionPoint {
* @param {*} input Input to be provided as a parameter to the 1st callback * @param {*} input Input to be provided as a parameter to the 1st callback
* @returns {Promise.<*>} Result of the last extension that was called * @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) => { return await this._extensions.reduce(async (throughput, p) => {
let tp = await throughput let tp = await throughput
if (typeof p.response === 'function') { if (typeof p.response === 'function') {
@ -122,21 +122,22 @@ class ExtensionPoint {
* @param {string} name Name of the listener needed if it is to be removed. * @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. * @param {Function} callback The callback function to trigger on a change.
*/ */
onRegister(name, callback) { onRegister(name: string, callback: any) {
if (typeof callback === 'function') this.#changeListeners.push({ name, callback }) if (typeof callback === 'function')
this.changeListeners.push({ name, callback })
} }
/** /**
* Unregister a callback from the extension list changes. * Unregister a callback from the extension list changes.
* @param {string} name The name of the listener to remove. * @param {string} name The name of the listener to remove.
*/ */
offRegister(name) { offRegister(name: string) {
const index = this.#changeListeners.findIndex(l => l.name === name) const index = this.changeListeners.findIndex((l) => l.name === name)
if (index > -1) this.#changeListeners.splice(index, 1) if (index > -1) this.changeListeners.splice(index, 1)
} }
#emitChange() { emitChange() {
for (const l of this.#changeListeners) { for (const l of this.changeListeners) {
l.callback(this) l.callback(this)
} }
} }

View File

@ -1,4 +1,4 @@
import { callExport } from "./import-manager" import { callExport } from './import-manager'
/** /**
* A slimmed down representation of a plugin for the renderer. * A slimmed down representation of a plugin for the renderer.
@ -25,7 +25,15 @@ class Plugin {
/** @type {string} Plugin's logo. */ /** @type {string} Plugin's logo. */
icon 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.name = name
this.url = url this.url = url
this.activationPoints = activationPoints this.activationPoints = activationPoints
@ -39,8 +47,8 @@ class Plugin {
* Trigger an exported callback on the plugin's main file. * Trigger an exported callback on the plugin's main file.
* @param {string} exp exported callback to trigger. * @param {string} exp exported callback to trigger.
*/ */
triggerExport(exp) { triggerExport(exp: string) {
callExport(this.url, exp, this.name) if (this.url && this.name) callExport(this.url, exp, this.name)
} }
} }

View File

@ -1,5 +1,5 @@
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. * This object contains a register of plugin registrations to an activation points, and the means to work with them.
* @namespace activationPoints * @namespace activationPoints
@ -10,7 +10,7 @@ import Activation from "./Activation.js"
* @private * @private
* Store of activations used by the consumer * Store of activations used by the consumer
*/ */
const activationRegister = [] const activationRegister: any[] = []
/** /**
* Register a plugin with its activation points (as defined in its manifest). * Register a plugin with its activation points (as defined in its manifest).
@ -18,18 +18,22 @@ const activationRegister = []
* @returns {void} * @returns {void}
* @alias activationPoints.register * @alias activationPoints.register
*/ */
export function register(plugin) { export function register(plugin: Plugin) {
if (!Array.isArray(plugin.activationPoints)) throw new Error( if (!Array.isArray(plugin.activationPoints))
`Plugin ${plugin.name || 'without name'} does not have any activation points set up in its manifest.` 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) { for (const ap of plugin.activationPoints) {
// Ensure plugin is not already registered to activation point // Ensure plugin is not already registered to activation point
const duplicate = activationRegister.findIndex(act => const duplicate = activationRegister.findIndex(
act.plugin === plugin.name && act.activationPoint === ap (act) => act.plugin === plugin.name && act.activationPoint === ap
) )
// Create new activation and add it to the register // 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))
} }
} }
@ -40,7 +44,7 @@ export function register(plugin) {
* @returns {Promise.<Boolean>} Resolves to true when the activations are complete. * @returns {Promise.<Boolean>} Resolves to true when the activations are complete.
* @alias activationPoints.trigger * @alias activationPoints.trigger
*/ */
export async function trigger(activationPoint) { export async function trigger(activationPoint: string) {
// Make sure all triggers are complete before returning // Make sure all triggers are complete before returning
await Promise.all( await Promise.all(
// Trigger each relevant activation point from the register and return an array of trigger promises // 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} * @returns {void}
* @alias activationPoints.remove * @alias activationPoints.remove
*/ */
export function remove(plugin) { export function remove(plugin: any) {
let i = activationRegister.length let i = activationRegister.length
while (i--) { while (i--) {
if (activationRegister[i].plugin === plugin) { if (activationRegister[i].plugin === plugin) {

View File

@ -3,14 +3,14 @@
* @namespace extensionPoints * @namespace extensionPoints
*/ */
import ExtensionPoint from "./ExtensionPoint.js" import ExtensionPoint from './ExtensionPoint'
/** /**
* @constant {Object.<string, ExtensionPoint>} extensionPoints * @constant {Object.<string, ExtensionPoint>} extensionPoints
* @private * @private
* Register of extension points created by the consumer * Register of extension points created by the consumer
*/ */
const _extensionPoints = {} const _extensionPoints: Record<string, any> = {}
/** /**
* Create new extension point and add it to the registry. * Create new extension point and add it to the registry.
@ -18,7 +18,7 @@ const _extensionPoints = {}
* @returns {void} * @returns {void}
* @alias extensionPoints.add * @alias extensionPoints.add
*/ */
export function add(name) { export function add(name: string) {
_extensionPoints[name] = new ExtensionPoint(name) _extensionPoints[name] = new ExtensionPoint(name)
} }
@ -28,7 +28,7 @@ export function add(name) {
* @returns {void} * @returns {void}
* @alias extensionPoints.remove * @alias extensionPoints.remove
*/ */
export function remove(name) { export function remove(name: string) {
delete _extensionPoints[name] delete _extensionPoints[name]
} }
@ -41,7 +41,7 @@ export function remove(name) {
* @returns {void} * @returns {void}
* @alias extensionPoints.register * @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]) add(ep)
if (_extensionPoints[ep].register) { if (_extensionPoints[ep].register) {
_extensionPoints[ep].register(extension, response, priority) _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. * @param {RegExp} name Matcher for the name of the extension to remove.
* @alias extensionPoints.unregisterAll * @alias extensionPoints.unregisterAll
*/ */
export function unregisterAll(name) { export function unregisterAll(name: string) {
for (const ep in _extensionPoints) _extensionPoints[ep].unregister(name) for (const ep in _extensionPoints) _extensionPoints[ep].unregister(name)
} }
@ -63,8 +63,8 @@ export function unregisterAll(name) {
* @returns {Object.<ExtensionPoint> | ExtensionPoint} Found extension points * @returns {Object.<ExtensionPoint> | ExtensionPoint} Found extension points
* @alias extensionPoints.get * @alias extensionPoints.get
*/ */
export function get(ep) { export function get(ep?: string) {
return (ep ? _extensionPoints[ep] : { ..._extensionPoints }) 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 * @returns {Array} Result of Promise.all or Promise.allSettled depending on exitOnError
* @alias extensionPoints.execute * @alias extensionPoints.execute
*/ */
export function execute(name, input) { export function execute(name: string, input: any) {
if (!_extensionPoints[name] || !_extensionPoints[name].execute) throw new Error( if (!_extensionPoints[name] || !_extensionPoints[name].execute)
`The extension point "${name}" is not a valid extension point` throw new Error(
) `The extension point "${name}" is not a valid extension point`
)
return _extensionPoints[name].execute(input) return _extensionPoints[name].execute(input)
} }
@ -90,9 +91,10 @@ export function execute(name, input) {
* @returns {Promise.<*>} Result of the last extension that was called * @returns {Promise.<*>} Result of the last extension that was called
* @alias extensionPoints.executeSerial * @alias extensionPoints.executeSerial
*/ */
export function executeSerial(name, input) { export function executeSerial(name: string, input: any) {
if (!_extensionPoints[name] || !_extensionPoints[name].executeSerial) throw new Error( if (!_extensionPoints[name] || !_extensionPoints[name].executeSerial)
`The extension point "${name}" is not a valid extension point` throw new Error(
) `The extension point "${name}" is not a valid extension point`
)
return _extensionPoints[name].executeSerial(input) return _extensionPoints[name].executeSerial(input)
} }

View File

@ -5,8 +5,8 @@
* @namespace plugins * @namespace plugins
*/ */
import Plugin from "./Plugin"; import Plugin from './Plugin'
import { register } from "./activation-manager"; import { register } from './activation-manager'
/** /**
* @typedef {Object.<string, any>} installOptions The {@link https://www.npmjs.com/package/pacote|pacote options} * @typedef {Object.<string, any>} installOptions The {@link https://www.npmjs.com/package/pacote|pacote options}
@ -21,23 +21,23 @@ import { register } from "./activation-manager";
* @returns {Promise.<Array.<Plugin> | false>} plugin as defined by the main process. Has property cancelled set to true if installation was cancelled in the main process. * @returns {Promise.<Array.<Plugin> | 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 * @alias plugins.install
*/ */
export async function install(plugins) { export async function install(plugins: any[]) {
if (typeof window === "undefined") { if (typeof window === 'undefined') {
return; return
} }
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc.install(plugins); const plgList = await window.pluggableElectronIpc.install(plugins)
if (plgList.cancelled) return false; if (plgList.cancelled) return false
return plgList.map((plg) => { return plgList.map((plg: any) => {
const plugin = new Plugin( const plugin = new Plugin(
plg.name, plg.name,
plg.url, plg.url,
plg.activationPoints, plg.activationPoints,
plg.active plg.active
); )
register(plugin); register(plugin)
return plugin; return plugin
}); })
} }
/** /**
@ -47,12 +47,12 @@ export async function install(plugins) {
* @returns {Promise.<boolean>} Whether uninstalling the plugins was successful. * @returns {Promise.<boolean>} Whether uninstalling the plugins was successful.
* @alias plugins.uninstall * @alias plugins.uninstall
*/ */
export function uninstall(plugins, reload = true) { export function uninstall(plugins: string[], reload = true) {
if (typeof window === "undefined") { if (typeof window === 'undefined') {
return; return
} }
// eslint-disable-next-line no-undef // 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 * @alias plugins.getActive
*/ */
export async function getActive() { export async function getActive() {
if (typeof window === "undefined") { if (typeof window === 'undefined') {
return; return []
} }
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc?.getActive() ?? const plgList =
await import( (await window.pluggableElectronIpc?.getActive()) ??
// eslint-disable-next-line no-undef (await import(
/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}` // eslint-disable-next-line no-undef
).then((data) => data.default.filter((e) => e.supportCloudNative)); /* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`
).then((data) => data.default.filter((e: any) => e.supportCloudNative)))
return plgList.map( return plgList.map(
(plugin) => (plugin: any) =>
new Plugin( new Plugin(
plugin.name, plugin.name,
plugin.url, plugin.url,
@ -81,7 +82,7 @@ export async function getActive() {
plugin.version, plugin.version,
plugin.icon plugin.icon
) )
); )
} }
/** /**
@ -90,12 +91,12 @@ export async function getActive() {
* @alias plugins.registerActive * @alias plugins.registerActive
*/ */
export async function registerActive() { export async function registerActive() {
if (typeof window === "undefined") { if (typeof window === 'undefined') {
return; return
} }
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const plgList = await getActive() const plgList = await getActive()
plgList.forEach((plugin) => plgList.forEach((plugin: Plugin) =>
register( register(
new Plugin( new Plugin(
plugin.name, plugin.name,
@ -104,7 +105,7 @@ export async function registerActive() {
plugin.active plugin.active
) )
) )
); )
} }
/** /**
@ -114,21 +115,21 @@ export async function registerActive() {
* @returns {Promise.<Array.<Plugin>>} Updated plugin as defined by the main process. * @returns {Promise.<Array.<Plugin>>} Updated plugin as defined by the main process.
* @alias plugins.update * @alias plugins.update
*/ */
export async function update(plugins, reload = true) { export async function update(plugins: Plugin[], reload = true) {
if (typeof window === "undefined") { if (typeof window === 'undefined') {
return; return
} }
// eslint-disable-next-line no-undef // 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( return plgList.map(
(plugin) => (plugin: any) =>
new Plugin( new Plugin(
plugin.name, plugin.name,
plugin.url, plugin.url,
plugin.activationPoints, plugin.activationPoints,
plugin.active plugin.active
) )
); )
} }
/** /**
@ -137,12 +138,12 @@ export async function update(plugins, reload = true) {
* @returns {Object.<string | false>} Object with plugins as keys and new version if update is available or false as values. * @returns {Object.<string | false>} Object with plugins as keys and new version if update is available or false as values.
* @alias plugins.updatesAvailable * @alias plugins.updatesAvailable
*/ */
export function updatesAvailable(plugin) { export function updatesAvailable(plugin: Plugin) {
if (typeof window === "undefined") { if (typeof window === 'undefined') {
return; return
} }
// eslint-disable-next-line no-undef // 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.<Plugin>} Updated plugin as defined by the main process. * @returns {Promise.<Plugin>} Updated plugin as defined by the main process.
* @alias plugins.toggleActive * @alias plugins.toggleActive
*/ */
export async function toggleActive(plugin, active) { export async function toggleActive(plugin: Plugin, active: boolean) {
if (typeof window === "undefined") { if (typeof window === 'undefined') {
return; return
} }
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const plg = await window.pluggableElectronIpc.toggleActive(plugin, active); const plg = await window.pluggableElectronIpc.toggleActive(plugin, active)
return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active); return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active)
} }

View File

@ -3,7 +3,7 @@ import {
register, register,
execute, execute,
executeSerial, executeSerial,
} from "./extension-manager.js"; } from "./extension-manager";
/** /**
* Used to import a plugin entry point. * 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. * 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 * @private
* @type {importer} * @type {importer}
*/ */
export let importer; export let importer: any;
/** /**
* @private * @private
* Set the plugin importer function. * Set the plugin importer function.
* @param {importer} callback Callback to import plugins. * @param {importer} callback Callback to import plugins.
*/ */
export function setImporter(callback) { export function setImporter(callback: any) {
importer = callback; importer = callback;
} }
@ -31,14 +31,14 @@ export function setImporter(callback) {
* @private * @private
* @type {Boolean|null} * @type {Boolean|null}
*/ */
export let presetEPs; export let presetEPs: boolean|null;
/** /**
* @private * @private
* Define how extension points are accessed. * Define how extension points are accessed.
* @param {Boolean|null} peps Whether extension points are predefined. * @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; presetEPs = peps === null || peps === true ? peps : false;
} }
@ -49,7 +49,7 @@ export function definePresetEps(peps) {
* @param {string} exp Export to call * @param {string} exp Export to call
* @param {string} [plugin] @see Activation * @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"); if (!importer) throw new Error("Importer callback has not been set");
const main = await importer(url); const main = await importer(url);

View File

@ -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 extensionPoints from "./extension-manager";
export * as activationPoints from "./activation-manager.js"; export * as activationPoints from "./activation-manager";
export * as plugins from "./facade.js"; export * as plugins from "./facade";
export { default as ExtensionPoint } from "./ExtensionPoint.js"; export { default as ExtensionPoint } from "./ExtensionPoint";
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
if (typeof window !== "undefined" && !window.pluggableElectronIpc) 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). * can be created on the fly(false) or should not be provided through the input at all (null).
* @returns {void} * @returns {void}
*/ */
export function setup(options) { export function setup(options: any) {
setImporter(options.importer); setImporter(options.importer);
definePresetEps(options.presetEPs); definePresetEps(options.presetEPs);
} }

View File

@ -5,10 +5,7 @@ import { Button, Switch } from '@uikit'
import Loader from '@containers/Loader' import Loader from '@containers/Loader'
import { formatPluginsName } from '@utils/converter' import { formatPluginsName } from '@utils/converter'
import { import { plugins, extensionPoints } from '@plugin'
plugins,
extensionPoints,
} from '@/../../electron/core/plugin-manager/execution/index'
import { executeSerial } from '@services/pluginService' import { executeSerial } from '@services/pluginService'
import { DataService } from '@janhq/core' import { DataService } from '@janhq/core'
import useGetAppVersion from '@hooks/useGetAppVersion' import useGetAppVersion from '@hooks/useGetAppVersion'
@ -27,19 +24,19 @@ const PluginCatalog = () => {
*/ */
useEffect(() => { useEffect(() => {
if (!window.electronAPI) { if (!window.electronAPI) {
return; return
} }
if (!version) return if (!version) return
// Load plugin manifest from plugin if any // Load plugin manifest from plugin if any
if (extensionPoints.get(DataService.GetPluginManifest)) { if (extensionPoints.get(DataService.GetPluginManifest)) {
executeSerial(DataService.GetPluginManifest).then((data) => { executeSerial(DataService.GetPluginManifest).then((data: any) => {
setPluginCatalog( setPluginCatalog(
data.filter( data.filter(
(e: any) => (e: any) =>
!e.requiredVersion || !e.requiredVersion ||
e.requiredVersion.replace(/[.^]/g, '') <= e.requiredVersion.replace(/[.^]/g, '') <=
version.replaceAll('.', '') version.replaceAll('.', '')
) )
) )
}) })
@ -53,7 +50,7 @@ const PluginCatalog = () => {
(e: any) => (e: any) =>
!e.requiredVersion || !e.requiredVersion ||
e.requiredVersion.replace(/[.^]/g, '') <= e.requiredVersion.replace(/[.^]/g, '') <=
version.replaceAll('.', '') version.replaceAll('.', '')
) )
) )
) )
@ -74,7 +71,7 @@ const PluginCatalog = () => {
if (extensionPoints.get('experimentComponent')) { if (extensionPoints.get('experimentComponent')) {
const components = await Promise.all( const components = await Promise.all(
extensionPoints.execute('experimentComponent') extensionPoints.execute('experimentComponent', {})
) )
components.forEach((e) => { components.forEach((e) => {
if (experimentRef.current) { if (experimentRef.current) {

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { execute } from '../../../../../electron/core/plugin-manager/execution/extension-manager' import { execute } from '@plugin/extension-manager'
type Props = { type Props = {
pluginName: string pluginName: string
@ -22,7 +22,10 @@ const PreferencePlugins = (props: Props) => {
if (timeout) { if (timeout) {
clearTimeout(timeout) clearTimeout(timeout)
} }
timeout = setTimeout(() => execute(PluginService.OnPreferencesUpdate), 100) timeout = setTimeout(
() => execute(PluginService.OnPreferencesUpdate, {}),
100
)
} }
return ( return (

View File

@ -13,10 +13,7 @@ import { twMerge } from 'tailwind-merge'
import { formatPluginsName } from '@utils/converter' import { formatPluginsName } from '@utils/converter'
import { import { extensionPoints } from '@plugin'
plugins,
extensionPoints,
} from '@/../../electron/core/plugin-manager/execution/index'
const staticMenu = ['Appearance'] const staticMenu = ['Appearance']
@ -40,7 +37,7 @@ const SettingsScreen = () => {
const getActivePluginPreferences = async () => { const getActivePluginPreferences = async () => {
if (extensionPoints.get('PluginPreferences')) { if (extensionPoints.get('PluginPreferences')) {
const data = await Promise.all( const data = await Promise.all(
extensionPoints.execute('PluginPreferences') extensionPoints.execute('PluginPreferences', {})
) )
setPreferenceItems(Array.isArray(data) ? data : []) setPreferenceItems(Array.isArray(data) ? data : [])
Promise.all( Promise.all(

View File

@ -2,10 +2,9 @@
import { import {
extensionPoints, extensionPoints,
plugins, plugins,
} from '../../electron/core/plugin-manager/execution/index' } from '@plugin'
import { import {
CoreService, CoreService,
DataService,
InferenceService, InferenceService,
ModelManagementService, ModelManagementService,
StoreService, StoreService,

View File

@ -29,7 +29,9 @@
"@models/*": ["./models/*"], "@models/*": ["./models/*"],
"@hooks/*": ["./hooks/*"], "@hooks/*": ["./hooks/*"],
"@containers/*": ["./containers/*"], "@containers/*": ["./containers/*"],
"@screens/*": ["./screens/*"] "@screens/*": ["./screens/*"],
"@plugin/*": ["./plugin/*"],
"@plugin": ["./plugin"]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],

View File

@ -7,5 +7,6 @@ declare global {
electronAPI?: any | undefined; electronAPI?: any | undefined;
corePlugin?: any | undefined; corePlugin?: any | undefined;
coreAPI?: any | undefined; coreAPI?: any | undefined;
pluggableElectronIpc?: any | undefined;
} }
} }