refactor: plugin manager and execution as ts (#504)
* refactor: plugin manager and execution as ts * chore: refactoring
This commit is contained in:
parent
edeb82ae84
commit
37c36363d8
@ -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" }])
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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([])
|
||||
}
|
||||
)
|
||||
})
|
||||
@ -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/)
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
*/
|
||||
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -1,23 +1,23 @@
|
||||
const { ipcRenderer, contextBridge } = require("electron");
|
||||
|
||||
function useFacade() {
|
||||
export function useFacade() {
|
||||
const interfaces = {
|
||||
install(plugins) {
|
||||
install(plugins: any[]) {
|
||||
return ipcRenderer.invoke("pluggable:install", plugins);
|
||||
},
|
||||
uninstall(plugins, reload) {
|
||||
uninstall(plugins: any[], reload: boolean) {
|
||||
return ipcRenderer.invoke("pluggable:uninstall", plugins, reload);
|
||||
},
|
||||
getActive() {
|
||||
return ipcRenderer.invoke("pluggable:getActivePlugins");
|
||||
},
|
||||
update(plugins, reload) {
|
||||
update(plugins: any[], reload: boolean) {
|
||||
return ipcRenderer.invoke("pluggable:update", plugins, reload);
|
||||
},
|
||||
updatesAvailable(plugin) {
|
||||
updatesAvailable(plugin: any) {
|
||||
return ipcRenderer.invoke("pluggable:updatesAvailable", plugin);
|
||||
},
|
||||
toggleActive(plugin, active) {
|
||||
toggleActive(plugin: any, active: boolean) {
|
||||
return ipcRenderer.invoke("pluggable:togglePluginActive", plugin, active);
|
||||
},
|
||||
};
|
||||
@ -28,5 +28,3 @@ function useFacade() {
|
||||
|
||||
return interfaces;
|
||||
}
|
||||
|
||||
module.exports = useFacade;
|
||||
36
electron/core/plugin/globals.ts
Normal file
36
electron/core/plugin/globals.ts
Normal 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");
|
||||
}
|
||||
@ -1,40 +1,52 @@
|
||||
import { readFileSync } from "fs"
|
||||
import { protocol } from 'electron'
|
||||
import { normalize } from "path"
|
||||
import { readFileSync } from "fs";
|
||||
import { protocol } from "electron";
|
||||
import { normalize } from "path";
|
||||
|
||||
import Plugin from "./Plugin"
|
||||
import { getAllPlugins, removePlugin, persistPlugins, installPlugins, getPlugin, getActivePlugins, addPlugin } from "./store"
|
||||
import { pluginsPath as storedPluginsPath, setPluginsPath, getPluginsFile, setConfirmInstall } from './globals'
|
||||
import router from "./router"
|
||||
import Plugin from "./plugin";
|
||||
import {
|
||||
getAllPlugins,
|
||||
removePlugin,
|
||||
persistPlugins,
|
||||
installPlugins,
|
||||
getPlugin,
|
||||
getActivePlugins,
|
||||
addPlugin,
|
||||
} from "./store";
|
||||
import {
|
||||
pluginsPath as storedPluginsPath,
|
||||
setPluginsPath,
|
||||
getPluginsFile,
|
||||
} from "./globals";
|
||||
import router from "./router";
|
||||
|
||||
/**
|
||||
* Sets up the required communication between the main and renderer processes.
|
||||
* Additionally sets the plugins up using {@link usePlugins} if a pluginsPath is provided.
|
||||
* @param {Object} options configuration for setting up the renderer facade.
|
||||
* @param {confirmInstall} [options.confirmInstall] Function to validate that a plugin should be installed.
|
||||
* @param {confirmInstall} [options.confirmInstall] Function to validate that a plugin should be installed.
|
||||
* @param {Boolean} [options.useFacade=true] Whether to make a facade to the plugins available in the renderer.
|
||||
* @param {string} [options.pluginsPath] Optional path to the plugins folder.
|
||||
* @returns {pluginManager|Object} A set of functions used to manage the plugin lifecycle if usePlugins is provided.
|
||||
* @function
|
||||
*/
|
||||
export function init(options) {
|
||||
if (!Object.prototype.hasOwnProperty.call(options, 'useFacade') || options.useFacade) {
|
||||
// Store the confirmInstall function
|
||||
setConfirmInstall(options.confirmInstall)
|
||||
export function init(options: any) {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(options, "useFacade") ||
|
||||
options.useFacade
|
||||
) {
|
||||
// Enable IPC to be used by the facade
|
||||
router()
|
||||
router();
|
||||
}
|
||||
|
||||
// Create plugins protocol to serve plugins to renderer
|
||||
registerPluginProtocol()
|
||||
registerPluginProtocol();
|
||||
|
||||
// perform full setup if pluginsPath is provided
|
||||
if (options.pluginsPath) {
|
||||
return usePlugins(options.pluginsPath)
|
||||
return usePlugins(options.pluginsPath);
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,11 +55,11 @@ export function init(options) {
|
||||
* @returns {boolean} Whether the protocol registration was successful
|
||||
*/
|
||||
function registerPluginProtocol() {
|
||||
return protocol.registerFileProtocol('plugin', (request, callback) => {
|
||||
const entry = request.url.substr(8)
|
||||
const url = normalize(storedPluginsPath + entry)
|
||||
callback({ path: url })
|
||||
})
|
||||
return protocol.registerFileProtocol("plugin", (request, callback) => {
|
||||
const entry = request.url.substr(8);
|
||||
const url = normalize(storedPluginsPath + entry);
|
||||
callback({ path: url });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -56,34 +68,38 @@ function registerPluginProtocol() {
|
||||
* @param {string} pluginsPath Path to the plugins folder. Required if not yet set up.
|
||||
* @returns {pluginManager} A set of functions used to manage the plugin lifecycle.
|
||||
*/
|
||||
export function usePlugins(pluginsPath) {
|
||||
if (!pluginsPath) throw Error('A path to the plugins folder is required to use Pluggable Electron')
|
||||
export function usePlugins(pluginsPath: string) {
|
||||
if (!pluginsPath)
|
||||
throw Error(
|
||||
"A path to the plugins folder is required to use Pluggable Electron"
|
||||
);
|
||||
// Store the path to the plugins folder
|
||||
setPluginsPath(pluginsPath)
|
||||
setPluginsPath(pluginsPath);
|
||||
|
||||
// Remove any registered plugins
|
||||
for (const plugin of getAllPlugins()) {
|
||||
removePlugin(plugin.name, false)
|
||||
if (plugin.name) removePlugin(plugin.name, false);
|
||||
}
|
||||
|
||||
// Read plugin list from plugins folder
|
||||
const plugins = JSON.parse(readFileSync(getPluginsFile()))
|
||||
const plugins = JSON.parse(readFileSync(getPluginsFile(), "utf-8"));
|
||||
try {
|
||||
// Create and store a Plugin instance for each plugin in list
|
||||
for (const p in plugins) {
|
||||
loadPlugin(plugins[p])
|
||||
loadPlugin(plugins[p]);
|
||||
}
|
||||
persistPlugins()
|
||||
|
||||
persistPlugins();
|
||||
} catch (error) {
|
||||
// Throw meaningful error if plugin loading fails
|
||||
throw new Error('Could not successfully rebuild list of installed plugins.\n'
|
||||
+ error
|
||||
+ '\nPlease check the plugins.json file in the plugins folder.')
|
||||
throw new Error(
|
||||
"Could not successfully rebuild list of installed plugins.\n" +
|
||||
error +
|
||||
"\nPlease check the plugins.json file in the plugins folder."
|
||||
);
|
||||
}
|
||||
|
||||
// Return the plugin lifecycle functions
|
||||
return getStore()
|
||||
return getStore();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -92,16 +108,24 @@ export function usePlugins(pluginsPath) {
|
||||
* @private
|
||||
* @param {Object} plg Plugin info
|
||||
*/
|
||||
function loadPlugin(plg) {
|
||||
function loadPlugin(plg: any) {
|
||||
// Create new plugin, populate it with plg details and save it to the store
|
||||
const plugin = new Plugin()
|
||||
const plugin = new Plugin();
|
||||
|
||||
for (const key in plg) {
|
||||
plugin[key] = plg[key]
|
||||
if (Object.prototype.hasOwnProperty.call(plg, key)) {
|
||||
// Use Object.defineProperty to set the properties as writable
|
||||
Object.defineProperty(plugin, key, {
|
||||
value: plg[key],
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addPlugin(plugin, false)
|
||||
plugin.subscribe('pe-persist', persistPlugins)
|
||||
addPlugin(plugin, false);
|
||||
plugin.subscribe("pe-persist", persistPlugins);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,7 +134,9 @@ function loadPlugin(plg) {
|
||||
*/
|
||||
export function getStore() {
|
||||
if (!storedPluginsPath) {
|
||||
throw new Error('The plugin path has not yet been set up. Please run usePlugins before accessing the store')
|
||||
throw new Error(
|
||||
"The plugin path has not yet been set up. Please run usePlugins before accessing the store"
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
@ -119,5 +145,5 @@ export function getStore() {
|
||||
getAllPlugins,
|
||||
getActivePlugins,
|
||||
removePlugin,
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
import { rmdir } from "fs/promises"
|
||||
import { resolve, join } from "path"
|
||||
import { manifest, extract } from "pacote"
|
||||
import Arborist from '@npmcli/arborist'
|
||||
import { rmdir } from "fs/promises";
|
||||
import { resolve, join } from "path";
|
||||
import { manifest, extract } from "pacote";
|
||||
import * as Arborist from "@npmcli/arborist";
|
||||
|
||||
import { pluginsPath } from "./globals"
|
||||
import { pluginsPath } from "./globals";
|
||||
|
||||
/**
|
||||
/**
|
||||
* An NPM package that can be used as a Pluggable Electron plugin.
|
||||
* Used to hold all the information and functions necessary to handle the plugin lifecycle.
|
||||
*/
|
||||
@ -21,30 +21,39 @@ class Plugin {
|
||||
* @property {string} description The description of plugin as defined in the manifest.
|
||||
* @property {string} icon The icon of plugin as defined in the manifest.
|
||||
*/
|
||||
origin?: string;
|
||||
installOptions: any;
|
||||
name?: string;
|
||||
url?: string;
|
||||
version?: string;
|
||||
activationPoints?: Array<string>;
|
||||
main?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
|
||||
/** @private */
|
||||
_active = false
|
||||
_active = false;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @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.
|
||||
* @param {string} [origin] Original specification provided to fetch the package.
|
||||
* @param {Object} [options] Options provided to pacote when fetching the manifest.
|
||||
*/
|
||||
constructor(origin, options = {}) {
|
||||
constructor(origin?: string, options = {}) {
|
||||
const defaultOpts = {
|
||||
version: false,
|
||||
fullMetadata: false,
|
||||
Arborist
|
||||
}
|
||||
Arborist,
|
||||
};
|
||||
|
||||
this.origin = origin
|
||||
this.installOptions = { ...defaultOpts, ...options }
|
||||
this.origin = origin;
|
||||
this.installOptions = { ...defaultOpts, ...options };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,7 +61,10 @@ class Plugin {
|
||||
* @type {string}
|
||||
*/
|
||||
get specifier() {
|
||||
return this.origin + (this.installOptions.version ? '@' + this.installOptions.version : '')
|
||||
return (
|
||||
this.origin +
|
||||
(this.installOptions.version ? "@" + this.installOptions.version : "")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -60,31 +72,34 @@ class Plugin {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get active() {
|
||||
return this._active
|
||||
return this._active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Package details based on it's manifest
|
||||
* @returns {Promise.<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)
|
||||
try {
|
||||
const mnf = await manifest(this.specifier, this.installOptions)
|
||||
const mnf = await manifest(this.specifier, this.installOptions);
|
||||
|
||||
// set the Package properties based on the it's manifest
|
||||
this.name = mnf.name
|
||||
this.version = mnf.version
|
||||
this.activationPoints = mnf.activationPoints || null
|
||||
this.main = mnf.main
|
||||
this.description = mnf.description
|
||||
this.icon = mnf.icon
|
||||
|
||||
this.name = mnf.name;
|
||||
this.version = mnf.version;
|
||||
this.activationPoints = mnf.activationPoints
|
||||
? (mnf.activationPoints as string[])
|
||||
: undefined;
|
||||
this.main = mnf.main;
|
||||
this.description = mnf.description;
|
||||
this.icon = mnf.icon as any;
|
||||
} catch (error) {
|
||||
throw new Error(`Package ${this.origin} does not contain a valid manifest: ${error}`)
|
||||
throw new Error(
|
||||
`Package ${this.origin} does not contain a valid manifest: ${error}`
|
||||
);
|
||||
}
|
||||
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,26 +110,29 @@ class Plugin {
|
||||
async _install() {
|
||||
try {
|
||||
// import the manifest details
|
||||
await this.#getManifest()
|
||||
await this.getManifest();
|
||||
|
||||
// Install the package in a child folder of the given folder
|
||||
await extract(this.specifier, join(pluginsPath, this.name), this.installOptions)
|
||||
await extract(
|
||||
this.specifier,
|
||||
join(pluginsPath ?? "", this.name ?? ""),
|
||||
this.installOptions
|
||||
);
|
||||
|
||||
if (!Array.isArray(this.activationPoints))
|
||||
throw new Error('The plugin does not contain any activation points')
|
||||
throw new Error("The plugin does not contain any activation points");
|
||||
|
||||
// Set the url using the custom plugins protocol
|
||||
this.url = `plugin://${this.name}/${this.main}`
|
||||
|
||||
this.#emitUpdate()
|
||||
this.url = `plugin://${this.name}/${this.main}`;
|
||||
|
||||
this.emitUpdate();
|
||||
} catch (err) {
|
||||
// Ensure the plugin is not stored and the folder is removed if the installation fails
|
||||
this.setActive(false)
|
||||
throw err
|
||||
this.setActive(false);
|
||||
throw err;
|
||||
}
|
||||
|
||||
return [this]
|
||||
return [this];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -122,24 +140,24 @@ class Plugin {
|
||||
* @param {string} name name of the callback to register
|
||||
* @param {callback} cb The function to execute on update
|
||||
*/
|
||||
subscribe(name, cb) {
|
||||
this.#listeners[name] = cb
|
||||
subscribe(name: string, cb: () => void) {
|
||||
this.listeners[name] = cb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove subscription
|
||||
* @param {string} name name of the callback to remove
|
||||
*/
|
||||
unsubscribe(name) {
|
||||
delete this.#listeners[name]
|
||||
unsubscribe(name: string) {
|
||||
delete this.listeners[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute listeners
|
||||
*/
|
||||
#emitUpdate() {
|
||||
for (const cb in this.#listeners) {
|
||||
this.#listeners[cb].call(null, this)
|
||||
emitUpdate() {
|
||||
for (const cb in this.listeners) {
|
||||
this.listeners[cb].call(null, this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,13 +167,13 @@ class Plugin {
|
||||
* @returns {boolean} Whether an update was performed.
|
||||
*/
|
||||
async update(version = false) {
|
||||
if (this.isUpdateAvailable()) {
|
||||
this.installOptions.version = version
|
||||
await this._install(false)
|
||||
return true
|
||||
if (await this.isUpdateAvailable()) {
|
||||
this.installOptions.version = version;
|
||||
await this._install();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -163,19 +181,21 @@ class Plugin {
|
||||
* @returns the latest available version if a new version is available or false if not.
|
||||
*/
|
||||
async isUpdateAvailable() {
|
||||
const mnf = await manifest(this.origin)
|
||||
return mnf.version !== this.version ? mnf.version : false
|
||||
if (this.origin) {
|
||||
const mnf = await manifest(this.origin);
|
||||
return mnf.version !== this.version ? mnf.version : false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove plugin and refresh renderers.
|
||||
* @returns {Promise}
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async uninstall() {
|
||||
const plgPath = resolve(pluginsPath, this.name)
|
||||
await rmdir(plgPath, { recursive: true })
|
||||
const plgPath = resolve(pluginsPath ?? "", this.name ?? "");
|
||||
await rmdir(plgPath, { recursive: true });
|
||||
|
||||
this.#emitUpdate()
|
||||
this.emitUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -183,11 +203,11 @@ class Plugin {
|
||||
* @param {boolean} active State to set _active to
|
||||
* @returns {Plugin} This plugin
|
||||
*/
|
||||
setActive(active) {
|
||||
this._active = active
|
||||
this.#emitUpdate()
|
||||
return this
|
||||
setActive(active: boolean) {
|
||||
this._active = active;
|
||||
this.emitUpdate();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export default Plugin
|
||||
export default Plugin;
|
||||
97
electron/core/plugin/router.ts
Normal file
97
electron/core/plugin/router.ts
Normal 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;
|
||||
}
|
||||
@ -8,9 +8,9 @@
|
||||
* @prop {removePlugin} removePlugin
|
||||
*/
|
||||
|
||||
import { writeFileSync } from "fs"
|
||||
import Plugin from "./Plugin"
|
||||
import { getPluginsFile } from './globals'
|
||||
import { writeFileSync } from "fs";
|
||||
import Plugin from "./plugin";
|
||||
import { getPluginsFile } from "./globals";
|
||||
|
||||
/**
|
||||
* @module store
|
||||
@ -21,7 +21,7 @@ import { getPluginsFile } from './globals'
|
||||
* Register of installed plugins
|
||||
* @type {Object.<string, Plugin>} plugin - List of installed plugins
|
||||
*/
|
||||
const plugins = {}
|
||||
const plugins: Record<string, Plugin> = {};
|
||||
|
||||
/**
|
||||
* Get a plugin from the stored plugins.
|
||||
@ -29,12 +29,12 @@ const plugins = {}
|
||||
* @returns {Plugin} Retrieved plugin
|
||||
* @alias pluginManager.getPlugin
|
||||
*/
|
||||
export function getPlugin(name) {
|
||||
export function getPlugin(name: string) {
|
||||
if (!Object.prototype.hasOwnProperty.call(plugins, name)) {
|
||||
throw new Error(`Plugin ${name} does not exist`)
|
||||
throw new Error(`Plugin ${name} does not exist`);
|
||||
}
|
||||
|
||||
return plugins[name]
|
||||
return plugins[name];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,7 +42,9 @@ export function getPlugin(name) {
|
||||
* @returns {Array.<Plugin>} All plugin objects
|
||||
* @alias pluginManager.getAllPlugins
|
||||
*/
|
||||
export function getAllPlugins() { return Object.values(plugins) }
|
||||
export function getAllPlugins() {
|
||||
return Object.values(plugins);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of active plugin objects.
|
||||
@ -50,7 +52,7 @@ export function getAllPlugins() { return Object.values(plugins) }
|
||||
* @alias pluginManager.getActivePlugins
|
||||
*/
|
||||
export function getActivePlugins() {
|
||||
return Object.values(plugins).filter(plugin => plugin.active)
|
||||
return Object.values(plugins).filter((plugin) => plugin.active);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -60,10 +62,10 @@ export function getActivePlugins() {
|
||||
* @returns {boolean} Whether the delete was successful
|
||||
* @alias pluginManager.removePlugin
|
||||
*/
|
||||
export function removePlugin(name, persist = true) {
|
||||
const del = delete plugins[name]
|
||||
if (persist) persistPlugins()
|
||||
return del
|
||||
export function removePlugin(name: string, persist = true) {
|
||||
const del = delete plugins[name];
|
||||
if (persist) persistPlugins();
|
||||
return del;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,11 +74,11 @@ export function removePlugin(name, persist = true) {
|
||||
* @param {boolean} persist Whether to save the changes to plugins to file
|
||||
* @returns {void}
|
||||
*/
|
||||
export function addPlugin(plugin, persist = true) {
|
||||
plugins[plugin.name] = plugin
|
||||
export function addPlugin(plugin: Plugin, persist = true) {
|
||||
if (plugin.name) plugins[plugin.name] = plugin;
|
||||
if (persist) {
|
||||
persistPlugins()
|
||||
plugin.subscribe('pe-persist', persistPlugins)
|
||||
persistPlugins();
|
||||
plugin.subscribe("pe-persist", persistPlugins);
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,11 +87,11 @@ export function addPlugin(plugin, persist = true) {
|
||||
* @returns {void}
|
||||
*/
|
||||
export function persistPlugins() {
|
||||
const persistData = {}
|
||||
const persistData: Record<string, Plugin> = {};
|
||||
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
|
||||
* @alias pluginManager.installPlugins
|
||||
*/
|
||||
export async function installPlugins(plugins, store = true) {
|
||||
const installed = []
|
||||
export async function installPlugins(plugins: any, store = true) {
|
||||
const installed: Plugin[] = [];
|
||||
for (const plg of plugins) {
|
||||
// Set install options and activation based on input type
|
||||
const isObject = typeof plg === 'object'
|
||||
const spec = isObject ? [plg.specifier, plg] : [plg]
|
||||
const activate = isObject ? plg.activate !== false : true
|
||||
const isObject = typeof plg === "object";
|
||||
const spec = isObject ? [plg.specifier, plg] : [plg];
|
||||
const activate = isObject ? plg.activate !== false : true;
|
||||
|
||||
// Install and possibly activate plugin
|
||||
const plugin = new Plugin(...spec)
|
||||
await plugin._install()
|
||||
if (activate) plugin.setActive(true)
|
||||
const plugin = new Plugin(...spec);
|
||||
await plugin._install();
|
||||
if (activate) plugin.setActive(true);
|
||||
|
||||
// Add plugin to store if needed
|
||||
if (store) addPlugin(plugin)
|
||||
installed.push(plugin)
|
||||
if (store) addPlugin(plugin);
|
||||
installed.push(plugin);
|
||||
}
|
||||
|
||||
// Return list of all installed plugins
|
||||
return installed
|
||||
return installed;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -126,4 +128,4 @@ export async function installPlugins(plugins, store = true) {
|
||||
* options used to install the plugin with some extra options.
|
||||
* @param {string} specifier the NPM specifier that identifies the package.
|
||||
* @param {boolean} [activate] Whether this plugin should be activated after installation. Defaults to true.
|
||||
*/
|
||||
*/
|
||||
@ -9,7 +9,7 @@ import {
|
||||
import { readdirSync, writeFileSync } from "fs";
|
||||
import { resolve, join, extname } from "path";
|
||||
import { rmdir, unlink, createWriteStream } from "fs";
|
||||
import { init } from "./core/plugin-manager/pluginMgr";
|
||||
import { init } from "./core/plugin/index";
|
||||
import { setupMenu } from "./utils/menu";
|
||||
import { dispose } from "./utils/disposable";
|
||||
|
||||
|
||||
@ -74,6 +74,8 @@
|
||||
"devDependencies": {
|
||||
"@electron/notarize": "^2.1.0",
|
||||
"@playwright/test": "^1.38.1",
|
||||
"@types/npmcli__arborist": "^5.6.4",
|
||||
"@types/pacote": "^11.1.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
"electron": "26.2.1",
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
// Make Pluggable Electron's facade available to the renderer on window.plugins
|
||||
//@ts-ignore
|
||||
const useFacade = require("../core/plugin-manager/facade");
|
||||
import { useFacade } from "./core/plugin/facade";
|
||||
useFacade();
|
||||
//@ts-ignore
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
'use client'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
plugins,
|
||||
extensionPoints,
|
||||
} from '@/../../electron/core/plugin-manager/execution/index'
|
||||
import { plugins, extensionPoints } from '@plugin'
|
||||
import {
|
||||
ChartPieIcon,
|
||||
CommandLineIcon,
|
||||
@ -13,7 +10,7 @@ import {
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'
|
||||
import classNames from 'classnames'
|
||||
import { DataService, PluginService, preferences } from '@janhq/core'
|
||||
import { execute } from '../../../electron/core/plugin-manager/execution/extension-manager'
|
||||
import { execute } from '@plugin/extension-manager'
|
||||
import LoadingIndicator from './LoadingIndicator'
|
||||
import { executeSerial } from '@services/pluginService'
|
||||
|
||||
@ -33,7 +30,7 @@ export const Preferences = () => {
|
||||
* Loads the plugin catalog module from a CDN and sets it as the plugin catalog state.
|
||||
*/
|
||||
useEffect(() => {
|
||||
executeSerial(DataService.GetPluginManifest).then((data) => {
|
||||
executeSerial(DataService.GetPluginManifest).then((data: any) => {
|
||||
setPluginCatalog(data)
|
||||
})
|
||||
}, [])
|
||||
@ -52,7 +49,7 @@ export const Preferences = () => {
|
||||
|
||||
if (extensionPoints.get('experimentComponent')) {
|
||||
const components = await Promise.all(
|
||||
extensionPoints.execute('experimentComponent')
|
||||
extensionPoints.execute('experimentComponent', {})
|
||||
)
|
||||
if (components.length > 0) {
|
||||
setIsTestAvailable(true)
|
||||
@ -67,7 +64,7 @@ export const Preferences = () => {
|
||||
|
||||
if (extensionPoints.get('PluginPreferences')) {
|
||||
const data = await Promise.all(
|
||||
extensionPoints.execute('PluginPreferences')
|
||||
extensionPoints.execute('PluginPreferences', {})
|
||||
)
|
||||
setPreferenceItems(Array.isArray(data) ? data : [])
|
||||
Promise.all(
|
||||
@ -149,7 +146,7 @@ export const Preferences = () => {
|
||||
}
|
||||
if (extensionPoints.get(PluginService.OnPreferencesUpdate))
|
||||
timeout = setTimeout(
|
||||
() => execute(PluginService.OnPreferencesUpdate),
|
||||
() => execute(PluginService.OnPreferencesUpdate, {}),
|
||||
100
|
||||
)
|
||||
}
|
||||
|
||||
@ -7,15 +7,14 @@ import JotaiWrapper from '@helpers/JotaiWrapper'
|
||||
import { ModalWrapper } from '@helpers/ModalWrapper'
|
||||
import { useEffect, useState } from 'react'
|
||||
import CompactLogo from '@containers/Logo/CompactLogo'
|
||||
import {
|
||||
setup,
|
||||
plugins,
|
||||
activationPoints,
|
||||
extensionPoints,
|
||||
} from '../../../electron/core/plugin-manager/execution/index'
|
||||
import { setup, plugins, activationPoints, extensionPoints } from '@plugin'
|
||||
import EventListenerWrapper from '@helpers/EventListenerWrapper'
|
||||
import { setupCoreServices } from '@services/coreService'
|
||||
import { executeSerial, isCorePluginInstalled, setupBasePlugins } from '@services/pluginService'
|
||||
import {
|
||||
executeSerial,
|
||||
isCorePluginInstalled,
|
||||
setupBasePlugins,
|
||||
} from '@services/pluginService'
|
||||
|
||||
const Providers = (props: PropsWithChildren) => {
|
||||
const [setupCore, setSetupCore] = useState(false)
|
||||
|
||||
@ -9,12 +9,15 @@ import {
|
||||
updateConversationAtom,
|
||||
updateConversationWaitingForResponseAtom,
|
||||
} from './atoms/Conversation.atom'
|
||||
import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager'
|
||||
import { executeSerial } from '@plugin/extension-manager'
|
||||
import { debounce } from 'lodash'
|
||||
import { setDownloadStateAtom, setDownloadStateSuccessAtom } from "./atoms/DownloadState.atom";
|
||||
import { downloadedModelAtom } from "./atoms/DownloadedModel.atom";
|
||||
import { ModelManagementService } from "@janhq/core";
|
||||
import { getDownloadedModels } from "../hooks/useGetDownloadedModels";
|
||||
import {
|
||||
setDownloadStateAtom,
|
||||
setDownloadStateSuccessAtom,
|
||||
} from './atoms/DownloadState.atom'
|
||||
import { downloadedModelAtom } from './atoms/DownloadedModel.atom'
|
||||
import { ModelManagementService } from '@janhq/core'
|
||||
import { getDownloadedModels } from '../hooks/useGetDownloadedModels'
|
||||
|
||||
let currentConversation: Conversation | undefined = undefined
|
||||
|
||||
@ -25,7 +28,6 @@ const debouncedUpdateConversation = debounce(
|
||||
1000
|
||||
)
|
||||
|
||||
|
||||
export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
const addNewMessage = useSetAtom(addNewMessageAtom)
|
||||
const updateMessage = useSetAtom(updateMessageAtom)
|
||||
@ -34,9 +36,9 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
const { getConversationById } = useGetUserConversations()
|
||||
|
||||
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
||||
const setDownloadState = useSetAtom(setDownloadStateAtom);
|
||||
const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom);
|
||||
const setDownloadedModels = useSetAtom(downloadedModelAtom);
|
||||
const setDownloadState = useSetAtom(setDownloadStateAtom)
|
||||
const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom)
|
||||
const setDownloadedModels = useSetAtom(downloadedModelAtom)
|
||||
|
||||
async function handleNewMessageResponse(message: NewMessageResponse) {
|
||||
if (message.conversationId) {
|
||||
@ -97,18 +99,21 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
function handleDownloadUpdate(state: any) {
|
||||
if (!state) return;
|
||||
setDownloadState(state);
|
||||
if (!state) return
|
||||
setDownloadState(state)
|
||||
}
|
||||
|
||||
function handleDownloadSuccess(state: any) {
|
||||
if (state && state.fileName && state.success === true) {
|
||||
setDownloadStateSuccess(state.fileName);
|
||||
executeSerial(ModelManagementService.UpdateFinishedDownloadAt, state.fileName).then(() => {
|
||||
setDownloadStateSuccess(state.fileName)
|
||||
executeSerial(
|
||||
ModelManagementService.UpdateFinishedDownloadAt,
|
||||
state.fileName
|
||||
).then(() => {
|
||||
getDownloadedModels().then((models) => {
|
||||
setDownloadedModels(models);
|
||||
});
|
||||
});
|
||||
setDownloadedModels(models)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,12 +122,12 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
events.on(EventName.OnNewMessageResponse, handleNewMessageResponse)
|
||||
events.on(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate)
|
||||
events.on(
|
||||
"OnMessageResponseFinished",
|
||||
'OnMessageResponseFinished',
|
||||
// EventName.OnMessageResponseFinished,
|
||||
handleMessageResponseFinished
|
||||
)
|
||||
events.on(EventName.OnDownloadUpdate, handleDownloadUpdate);
|
||||
events.on(EventName.OnDownloadSuccess, handleDownloadSuccess);
|
||||
events.on(EventName.OnDownloadUpdate, handleDownloadUpdate)
|
||||
events.on(EventName.OnDownloadSuccess, handleDownloadSuccess)
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -131,12 +136,12 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
events.off(EventName.OnNewMessageResponse, handleNewMessageResponse)
|
||||
events.off(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate)
|
||||
events.off(
|
||||
"OnMessageResponseFinished",
|
||||
'OnMessageResponseFinished',
|
||||
// EventName.OnMessageResponseFinished,
|
||||
handleMessageResponseFinished
|
||||
)
|
||||
events.off(EventName.OnDownloadUpdate, handleDownloadUpdate);
|
||||
events.off(EventName.OnDownloadSuccess, handleDownloadSuccess);
|
||||
events.off(EventName.OnDownloadUpdate, handleDownloadUpdate)
|
||||
events.off(EventName.OnDownloadSuccess, handleDownloadSuccess)
|
||||
}
|
||||
}, [])
|
||||
return <>{children}</>
|
||||
|
||||
@ -2,7 +2,7 @@ import { useEffect } from 'react'
|
||||
import { ModelManagementService } from '@janhq/core'
|
||||
import { useAtom } from 'jotai'
|
||||
import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom'
|
||||
import { extensionPoints } from '../../electron/core/plugin-manager/execution'
|
||||
import { extensionPoints } from '@plugin'
|
||||
import { executeSerial } from '@services/pluginService'
|
||||
|
||||
export function useGetDownloadedModels() {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ModelManagementService } from '@janhq/core'
|
||||
import { executeSerial } from '../../electron/core/plugin-manager/execution/extension-manager'
|
||||
import { executeSerial } from '@plugin/extension-manager'
|
||||
|
||||
export default function useGetModelById() {
|
||||
const getModelById = async (
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { extensionPoints } from '../../electron/core/plugin-manager/execution'
|
||||
import { extensionPoints } from '@plugin'
|
||||
import { SystemMonitoringService } from '@janhq/core'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import { totalRamAtom } from '@helpers/atoms/SystemBar.atom'
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { callExport } from "./import-manager.js"
|
||||
import { callExport } from "./import-manager"
|
||||
|
||||
class Activation {
|
||||
/** @type {string} Name of the registered plugin. */
|
||||
@ -13,7 +13,7 @@ class Activation {
|
||||
/** @type {boolean} Whether the activation has been activated. */
|
||||
activated
|
||||
|
||||
constructor(plugin, activationPoint, url) {
|
||||
constructor(plugin: string, activationPoint: string, url: string) {
|
||||
this.plugin = plugin
|
||||
this.activationPoint = activationPoint
|
||||
this.url = url
|
||||
@ -18,29 +18,29 @@ class ExtensionPoint {
|
||||
* @type {Array.<Extension>} The list of all extensions registered with this extension point.
|
||||
* @private
|
||||
*/
|
||||
_extensions = []
|
||||
_extensions: any[] = []
|
||||
|
||||
/**
|
||||
* @type {Array.<Object>} A list of functions to be executed when the list of extensions changes.
|
||||
* @private
|
||||
*/
|
||||
#changeListeners = []
|
||||
changeListeners: any[] = []
|
||||
|
||||
constructor(name) {
|
||||
constructor(name: string) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
/**
|
||||
* Register new extension with this extension point.
|
||||
* The registered response will be executed (if callback) or returned (if object)
|
||||
* The registered response will be executed (if callback) or returned (if object)
|
||||
* when the extension point is executed (see below).
|
||||
* @param {string} name Unique name for the extension.
|
||||
* @param {Object|Callback} response Object to be returned or function to be called by the extension point.
|
||||
* @param {number} [priority] Order priority for execution used for executing in serial.
|
||||
* @returns {void}
|
||||
*/
|
||||
register(name, response, priority = 0) {
|
||||
const index = this._extensions.findIndex(p => p.priority > priority)
|
||||
register(name: string, response: any, priority: number = 0) {
|
||||
const index = this._extensions.findIndex((p) => p.priority > priority)
|
||||
const newExt = { name, response, priority }
|
||||
if (index > -1) {
|
||||
this._extensions.splice(index, 0, newExt)
|
||||
@ -48,7 +48,7 @@ class ExtensionPoint {
|
||||
this._extensions.push(newExt)
|
||||
}
|
||||
|
||||
this.#emitChange()
|
||||
this.emitChange()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -56,11 +56,11 @@ class ExtensionPoint {
|
||||
* @param {RegExp } name Matcher for the name of the extension to remove.
|
||||
* @returns {void}
|
||||
*/
|
||||
unregister(name) {
|
||||
const index = this._extensions.findIndex(ext => ext.name.match(name))
|
||||
unregister(name: string) {
|
||||
const index = this._extensions.findIndex((ext) => ext.name.match(name))
|
||||
if (index > -1) this._extensions.splice(index, 1)
|
||||
|
||||
this.#emitChange()
|
||||
this.emitChange()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,7 +69,7 @@ class ExtensionPoint {
|
||||
*/
|
||||
clear() {
|
||||
this._extensions = []
|
||||
this.#emitChange()
|
||||
this.emitChange()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,8 +77,8 @@ class ExtensionPoint {
|
||||
* @param {string} name Name of the extension to return
|
||||
* @returns {Object|Callback|undefined} The response of the extension. If this is a function the function is returned, not its response.
|
||||
*/
|
||||
get(name) {
|
||||
const ep = this._extensions.find(ext => ext.name === name)
|
||||
get(name: string) {
|
||||
const ep = this._extensions.find((ext) => ext.name === name)
|
||||
return ep && ep.response
|
||||
}
|
||||
|
||||
@ -88,8 +88,8 @@ class ExtensionPoint {
|
||||
* @param {*} input Input to be provided as a parameter to each response if response is a callback.
|
||||
* @returns {Array} List of responses from the extensions.
|
||||
*/
|
||||
execute(input) {
|
||||
return this._extensions.map(p => {
|
||||
execute(input: any) {
|
||||
return this._extensions.map((p) => {
|
||||
if (typeof p.response === 'function') {
|
||||
return p.response(input)
|
||||
} else {
|
||||
@ -105,7 +105,7 @@ class ExtensionPoint {
|
||||
* @param {*} input Input to be provided as a parameter to the 1st callback
|
||||
* @returns {Promise.<*>} Result of the last extension that was called
|
||||
*/
|
||||
async executeSerial(input) {
|
||||
async executeSerial(input: any) {
|
||||
return await this._extensions.reduce(async (throughput, p) => {
|
||||
let tp = await throughput
|
||||
if (typeof p.response === 'function') {
|
||||
@ -122,24 +122,25 @@ class ExtensionPoint {
|
||||
* @param {string} name Name of the listener needed if it is to be removed.
|
||||
* @param {Function} callback The callback function to trigger on a change.
|
||||
*/
|
||||
onRegister(name, callback) {
|
||||
if (typeof callback === 'function') this.#changeListeners.push({ name, callback })
|
||||
onRegister(name: string, callback: any) {
|
||||
if (typeof callback === 'function')
|
||||
this.changeListeners.push({ name, callback })
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a callback from the extension list changes.
|
||||
* @param {string} name The name of the listener to remove.
|
||||
*/
|
||||
offRegister(name) {
|
||||
const index = this.#changeListeners.findIndex(l => l.name === name)
|
||||
if (index > -1) this.#changeListeners.splice(index, 1)
|
||||
offRegister(name: string) {
|
||||
const index = this.changeListeners.findIndex((l) => l.name === name)
|
||||
if (index > -1) this.changeListeners.splice(index, 1)
|
||||
}
|
||||
|
||||
#emitChange() {
|
||||
for (const l of this.#changeListeners) {
|
||||
emitChange() {
|
||||
for (const l of this.changeListeners) {
|
||||
l.callback(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ExtensionPoint
|
||||
export default ExtensionPoint
|
||||
@ -1,4 +1,4 @@
|
||||
import { callExport } from "./import-manager"
|
||||
import { callExport } from './import-manager'
|
||||
|
||||
/**
|
||||
* A slimmed down representation of a plugin for the renderer.
|
||||
@ -25,7 +25,15 @@ class Plugin {
|
||||
/** @type {string} Plugin's logo. */
|
||||
icon
|
||||
|
||||
constructor(name, url, activationPoints, active, description, version, icon) {
|
||||
constructor(
|
||||
name?: string,
|
||||
url?: string,
|
||||
activationPoints?: any[],
|
||||
active?: boolean,
|
||||
description?: string,
|
||||
version?: string,
|
||||
icon?: string
|
||||
) {
|
||||
this.name = name
|
||||
this.url = url
|
||||
this.activationPoints = activationPoints
|
||||
@ -39,9 +47,9 @@ class Plugin {
|
||||
* Trigger an exported callback on the plugin's main file.
|
||||
* @param {string} exp exported callback to trigger.
|
||||
*/
|
||||
triggerExport(exp) {
|
||||
callExport(this.url, exp, this.name)
|
||||
triggerExport(exp: string) {
|
||||
if (this.url && this.name) callExport(this.url, exp, this.name)
|
||||
}
|
||||
}
|
||||
|
||||
export default Plugin
|
||||
export default Plugin
|
||||
@ -1,16 +1,16 @@
|
||||
import Activation from "./Activation.js"
|
||||
|
||||
import Activation from './Activation'
|
||||
import Plugin from './Plugin'
|
||||
/**
|
||||
* This object contains a register of plugin registrations to an activation points, and the means to work with them.
|
||||
* @namespace activationPoints
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* @constant {Array.<Activation>} activationRegister
|
||||
* @private
|
||||
* Store of activations used by the consumer
|
||||
*/
|
||||
const activationRegister = []
|
||||
const activationRegister: any[] = []
|
||||
|
||||
/**
|
||||
* Register a plugin with its activation points (as defined in its manifest).
|
||||
@ -18,18 +18,22 @@ const activationRegister = []
|
||||
* @returns {void}
|
||||
* @alias activationPoints.register
|
||||
*/
|
||||
export function register(plugin) {
|
||||
if (!Array.isArray(plugin.activationPoints)) throw new Error(
|
||||
`Plugin ${plugin.name || 'without name'} does not have any activation points set up in its manifest.`
|
||||
)
|
||||
export function register(plugin: Plugin) {
|
||||
if (!Array.isArray(plugin.activationPoints))
|
||||
throw new Error(
|
||||
`Plugin ${
|
||||
plugin.name || 'without name'
|
||||
} does not have any activation points set up in its manifest.`
|
||||
)
|
||||
for (const ap of plugin.activationPoints) {
|
||||
// Ensure plugin is not already registered to activation point
|
||||
const duplicate = activationRegister.findIndex(act =>
|
||||
act.plugin === plugin.name && act.activationPoint === ap
|
||||
const duplicate = activationRegister.findIndex(
|
||||
(act) => act.plugin === plugin.name && act.activationPoint === ap
|
||||
)
|
||||
|
||||
// Create new activation and add it to the register
|
||||
if (duplicate < 0) activationRegister.push(new Activation(plugin.name, ap, plugin.url))
|
||||
if (duplicate < 0 && plugin.name && plugin.url)
|
||||
activationRegister.push(new Activation(plugin.name, ap, plugin.url))
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,10 +41,10 @@ export function register(plugin) {
|
||||
* Trigger all activations registered to the given activation point. See {@link Plugin}.
|
||||
* This will call the function with the same name as the activation point on the path specified in the plugin.
|
||||
* @param {string} activationPoint Name of the activation to trigger
|
||||
* @returns {Promise.<Boolean>} Resolves to true when the activations are complete.
|
||||
* @returns {Promise.<Boolean>} Resolves to true when the activations are complete.
|
||||
* @alias activationPoints.trigger
|
||||
*/
|
||||
export async function trigger(activationPoint) {
|
||||
export async function trigger(activationPoint: string) {
|
||||
// Make sure all triggers are complete before returning
|
||||
await Promise.all(
|
||||
// Trigger each relevant activation point from the register and return an array of trigger promises
|
||||
@ -60,7 +64,7 @@ export async function trigger(activationPoint) {
|
||||
* @returns {void}
|
||||
* @alias activationPoints.remove
|
||||
*/
|
||||
export function remove(plugin) {
|
||||
export function remove(plugin: any) {
|
||||
let i = activationRegister.length
|
||||
while (i--) {
|
||||
if (activationRegister[i].plugin === plugin) {
|
||||
@ -85,4 +89,4 @@ export function clear() {
|
||||
*/
|
||||
export function get() {
|
||||
return [...activationRegister]
|
||||
}
|
||||
}
|
||||
@ -3,14 +3,14 @@
|
||||
* @namespace extensionPoints
|
||||
*/
|
||||
|
||||
import ExtensionPoint from "./ExtensionPoint.js"
|
||||
import ExtensionPoint from './ExtensionPoint'
|
||||
|
||||
/**
|
||||
/**
|
||||
* @constant {Object.<string, ExtensionPoint>} extensionPoints
|
||||
* @private
|
||||
* 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.
|
||||
@ -18,7 +18,7 @@ const _extensionPoints = {}
|
||||
* @returns {void}
|
||||
* @alias extensionPoints.add
|
||||
*/
|
||||
export function add(name) {
|
||||
export function add(name: string) {
|
||||
_extensionPoints[name] = new ExtensionPoint(name)
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ export function add(name) {
|
||||
* @returns {void}
|
||||
* @alias extensionPoints.remove
|
||||
*/
|
||||
export function remove(name) {
|
||||
export function remove(name: string) {
|
||||
delete _extensionPoints[name]
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ export function remove(name) {
|
||||
* @returns {void}
|
||||
* @alias extensionPoints.register
|
||||
*/
|
||||
export function register(ep, extension, response, priority) {
|
||||
export function register(ep: string, extension: string, response: any, priority: number) {
|
||||
if (!_extensionPoints[ep]) add(ep)
|
||||
if (_extensionPoints[ep].register) {
|
||||
_extensionPoints[ep].register(extension, response, priority)
|
||||
@ -53,7 +53,7 @@ export function register(ep, extension, response, priority) {
|
||||
* @param {RegExp} name Matcher for the name of the extension to remove.
|
||||
* @alias extensionPoints.unregisterAll
|
||||
*/
|
||||
export function unregisterAll(name) {
|
||||
export function unregisterAll(name: string) {
|
||||
for (const ep in _extensionPoints) _extensionPoints[ep].unregister(name)
|
||||
}
|
||||
|
||||
@ -63,8 +63,8 @@ export function unregisterAll(name) {
|
||||
* @returns {Object.<ExtensionPoint> | ExtensionPoint} Found extension points
|
||||
* @alias extensionPoints.get
|
||||
*/
|
||||
export function get(ep) {
|
||||
return (ep ? _extensionPoints[ep] : { ..._extensionPoints })
|
||||
export function get(ep?: string) {
|
||||
return ep ? _extensionPoints[ep] : { ..._extensionPoints }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -75,10 +75,11 @@ export function get(ep) {
|
||||
* @returns {Array} Result of Promise.all or Promise.allSettled depending on exitOnError
|
||||
* @alias extensionPoints.execute
|
||||
*/
|
||||
export function execute(name, input) {
|
||||
if (!_extensionPoints[name] || !_extensionPoints[name].execute) throw new Error(
|
||||
`The extension point "${name}" is not a valid extension point`
|
||||
)
|
||||
export function execute(name: string, input: any) {
|
||||
if (!_extensionPoints[name] || !_extensionPoints[name].execute)
|
||||
throw new Error(
|
||||
`The extension point "${name}" is not a valid extension point`
|
||||
)
|
||||
return _extensionPoints[name].execute(input)
|
||||
}
|
||||
|
||||
@ -90,9 +91,10 @@ export function execute(name, input) {
|
||||
* @returns {Promise.<*>} Result of the last extension that was called
|
||||
* @alias extensionPoints.executeSerial
|
||||
*/
|
||||
export function executeSerial(name, input) {
|
||||
if (!_extensionPoints[name] || !_extensionPoints[name].executeSerial) throw new Error(
|
||||
`The extension point "${name}" is not a valid extension point`
|
||||
)
|
||||
export function executeSerial(name: string, input: any) {
|
||||
if (!_extensionPoints[name] || !_extensionPoints[name].executeSerial)
|
||||
throw new Error(
|
||||
`The extension point "${name}" is not a valid extension point`
|
||||
)
|
||||
return _extensionPoints[name].executeSerial(input)
|
||||
}
|
||||
@ -5,8 +5,8 @@
|
||||
* @namespace plugins
|
||||
*/
|
||||
|
||||
import Plugin from "./Plugin";
|
||||
import { register } from "./activation-manager";
|
||||
import Plugin from './Plugin'
|
||||
import { register } from './activation-manager'
|
||||
|
||||
/**
|
||||
* @typedef {Object.<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.
|
||||
* @alias plugins.install
|
||||
*/
|
||||
export async function install(plugins) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
export async function install(plugins: any[]) {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plgList = await window.pluggableElectronIpc.install(plugins);
|
||||
if (plgList.cancelled) return false;
|
||||
return plgList.map((plg) => {
|
||||
const plgList = await window.pluggableElectronIpc.install(plugins)
|
||||
if (plgList.cancelled) return false
|
||||
return plgList.map((plg: any) => {
|
||||
const plugin = new Plugin(
|
||||
plg.name,
|
||||
plg.url,
|
||||
plg.activationPoints,
|
||||
plg.active
|
||||
);
|
||||
register(plugin);
|
||||
return plugin;
|
||||
});
|
||||
)
|
||||
register(plugin)
|
||||
return plugin
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,12 +47,12 @@ export async function install(plugins) {
|
||||
* @returns {Promise.<boolean>} Whether uninstalling the plugins was successful.
|
||||
* @alias plugins.uninstall
|
||||
*/
|
||||
export function uninstall(plugins, reload = true) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
export function uninstall(plugins: string[], reload = true) {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
return window.pluggableElectronIpc.uninstall(plugins, reload);
|
||||
return window.pluggableElectronIpc.uninstall(plugins, reload)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -61,17 +61,18 @@ export function uninstall(plugins, reload = true) {
|
||||
* @alias plugins.getActive
|
||||
*/
|
||||
export async function getActive() {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
if (typeof window === 'undefined') {
|
||||
return []
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plgList = await window.pluggableElectronIpc?.getActive() ??
|
||||
await import(
|
||||
// eslint-disable-next-line no-undef
|
||||
/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`
|
||||
).then((data) => data.default.filter((e) => e.supportCloudNative));
|
||||
const plgList =
|
||||
(await window.pluggableElectronIpc?.getActive()) ??
|
||||
(await import(
|
||||
// eslint-disable-next-line no-undef
|
||||
/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`
|
||||
).then((data) => data.default.filter((e: any) => e.supportCloudNative)))
|
||||
return plgList.map(
|
||||
(plugin) =>
|
||||
(plugin: any) =>
|
||||
new Plugin(
|
||||
plugin.name,
|
||||
plugin.url,
|
||||
@ -81,7 +82,7 @@ export async function getActive() {
|
||||
plugin.version,
|
||||
plugin.icon
|
||||
)
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -90,12 +91,12 @@ export async function getActive() {
|
||||
* @alias plugins.registerActive
|
||||
*/
|
||||
export async function registerActive() {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plgList = await getActive()
|
||||
plgList.forEach((plugin) =>
|
||||
plgList.forEach((plugin: Plugin) =>
|
||||
register(
|
||||
new Plugin(
|
||||
plugin.name,
|
||||
@ -104,7 +105,7 @@ export async function registerActive() {
|
||||
plugin.active
|
||||
)
|
||||
)
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -114,21 +115,21 @@ export async function registerActive() {
|
||||
* @returns {Promise.<Array.<Plugin>>} Updated plugin as defined by the main process.
|
||||
* @alias plugins.update
|
||||
*/
|
||||
export async function update(plugins, reload = true) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
export async function update(plugins: Plugin[], reload = true) {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plgList = await window.pluggableElectronIpc.update(plugins, reload);
|
||||
const plgList = await window.pluggableElectronIpc.update(plugins, reload)
|
||||
return plgList.map(
|
||||
(plugin) =>
|
||||
(plugin: any) =>
|
||||
new Plugin(
|
||||
plugin.name,
|
||||
plugin.url,
|
||||
plugin.activationPoints,
|
||||
plugin.active
|
||||
)
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -137,12 +138,12 @@ export async function update(plugins, reload = true) {
|
||||
* @returns {Object.<string | false>} Object with plugins as keys and new version if update is available or false as values.
|
||||
* @alias plugins.updatesAvailable
|
||||
*/
|
||||
export function updatesAvailable(plugin) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
export function updatesAvailable(plugin: Plugin) {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
return window.pluggableElectronIpc.updatesAvailable(plugin);
|
||||
return window.pluggableElectronIpc.updatesAvailable(plugin)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,11 +153,11 @@ export function updatesAvailable(plugin) {
|
||||
* @returns {Promise.<Plugin>} Updated plugin as defined by the main process.
|
||||
* @alias plugins.toggleActive
|
||||
*/
|
||||
export async function toggleActive(plugin, active) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
export async function toggleActive(plugin: Plugin, active: boolean) {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const plg = await window.pluggableElectronIpc.toggleActive(plugin, active);
|
||||
return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active);
|
||||
const plg = await window.pluggableElectronIpc.toggleActive(plugin, active)
|
||||
return new Plugin(plg.name, plg.url, plg.activationPoints, plg.active)
|
||||
}
|
||||
@ -3,7 +3,7 @@ import {
|
||||
register,
|
||||
execute,
|
||||
executeSerial,
|
||||
} from "./extension-manager.js";
|
||||
} from "./extension-manager";
|
||||
/**
|
||||
* Used to import a plugin entry point.
|
||||
* Ensure your bundler does no try to resolve this import as the plugins are not known at build time.
|
||||
@ -16,14 +16,14 @@ import {
|
||||
* @private
|
||||
* @type {importer}
|
||||
*/
|
||||
export let importer;
|
||||
export let importer: any;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Set the plugin importer function.
|
||||
* @param {importer} callback Callback to import plugins.
|
||||
*/
|
||||
export function setImporter(callback) {
|
||||
export function setImporter(callback: any) {
|
||||
importer = callback;
|
||||
}
|
||||
|
||||
@ -31,14 +31,14 @@ export function setImporter(callback) {
|
||||
* @private
|
||||
* @type {Boolean|null}
|
||||
*/
|
||||
export let presetEPs;
|
||||
export let presetEPs: boolean|null;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Define how extension points are accessed.
|
||||
* @param {Boolean|null} peps Whether extension points are predefined.
|
||||
*/
|
||||
export function definePresetEps(peps) {
|
||||
export function definePresetEps(peps: boolean|null) {
|
||||
presetEPs = peps === null || peps === true ? peps : false;
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ export function definePresetEps(peps) {
|
||||
* @param {string} exp Export to call
|
||||
* @param {string} [plugin] @see Activation
|
||||
*/
|
||||
export async function callExport(url, exp, plugin) {
|
||||
export async function callExport(url: string, exp: string, plugin: string) {
|
||||
if (!importer) throw new Error("Importer callback has not been set");
|
||||
|
||||
const main = await importer(url);
|
||||
@ -1,9 +1,9 @@
|
||||
import { definePresetEps, setImporter } from "./import-manager.js";
|
||||
import { definePresetEps, setImporter } from "./import-manager";
|
||||
|
||||
export * as extensionPoints from "./extension-manager.js";
|
||||
export * as activationPoints from "./activation-manager.js";
|
||||
export * as plugins from "./facade.js";
|
||||
export { default as ExtensionPoint } from "./ExtensionPoint.js";
|
||||
export * as extensionPoints from "./extension-manager";
|
||||
export * as activationPoints from "./activation-manager";
|
||||
export * as plugins from "./facade";
|
||||
export { default as ExtensionPoint } from "./ExtensionPoint";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
if (typeof window !== "undefined" && !window.pluggableElectronIpc)
|
||||
@ -19,7 +19,7 @@ if (typeof window !== "undefined" && !window.pluggableElectronIpc)
|
||||
* can be created on the fly(false) or should not be provided through the input at all (null).
|
||||
* @returns {void}
|
||||
*/
|
||||
export function setup(options) {
|
||||
export function setup(options: any) {
|
||||
setImporter(options.importer);
|
||||
definePresetEps(options.presetEPs);
|
||||
}
|
||||
@ -5,10 +5,7 @@ import { Button, Switch } from '@uikit'
|
||||
import Loader from '@containers/Loader'
|
||||
import { formatPluginsName } from '@utils/converter'
|
||||
|
||||
import {
|
||||
plugins,
|
||||
extensionPoints,
|
||||
} from '@/../../electron/core/plugin-manager/execution/index'
|
||||
import { plugins, extensionPoints } from '@plugin'
|
||||
import { executeSerial } from '@services/pluginService'
|
||||
import { DataService } from '@janhq/core'
|
||||
import useGetAppVersion from '@hooks/useGetAppVersion'
|
||||
@ -27,19 +24,19 @@ const PluginCatalog = () => {
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!window.electronAPI) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (!version) return
|
||||
|
||||
// Load plugin manifest from plugin if any
|
||||
if (extensionPoints.get(DataService.GetPluginManifest)) {
|
||||
executeSerial(DataService.GetPluginManifest).then((data) => {
|
||||
executeSerial(DataService.GetPluginManifest).then((data: any) => {
|
||||
setPluginCatalog(
|
||||
data.filter(
|
||||
(e: any) =>
|
||||
!e.requiredVersion ||
|
||||
e.requiredVersion.replace(/[.^]/g, '') <=
|
||||
version.replaceAll('.', '')
|
||||
version.replaceAll('.', '')
|
||||
)
|
||||
)
|
||||
})
|
||||
@ -53,7 +50,7 @@ const PluginCatalog = () => {
|
||||
(e: any) =>
|
||||
!e.requiredVersion ||
|
||||
e.requiredVersion.replace(/[.^]/g, '') <=
|
||||
version.replaceAll('.', '')
|
||||
version.replaceAll('.', '')
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -74,7 +71,7 @@ const PluginCatalog = () => {
|
||||
|
||||
if (extensionPoints.get('experimentComponent')) {
|
||||
const components = await Promise.all(
|
||||
extensionPoints.execute('experimentComponent')
|
||||
extensionPoints.execute('experimentComponent', {})
|
||||
)
|
||||
components.forEach((e) => {
|
||||
if (experimentRef.current) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { execute } from '../../../../../electron/core/plugin-manager/execution/extension-manager'
|
||||
import { execute } from '@plugin/extension-manager'
|
||||
|
||||
type Props = {
|
||||
pluginName: string
|
||||
@ -22,7 +22,10 @@ const PreferencePlugins = (props: Props) => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(() => execute(PluginService.OnPreferencesUpdate), 100)
|
||||
timeout = setTimeout(
|
||||
() => execute(PluginService.OnPreferencesUpdate, {}),
|
||||
100
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -13,10 +13,7 @@ import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import { formatPluginsName } from '@utils/converter'
|
||||
|
||||
import {
|
||||
plugins,
|
||||
extensionPoints,
|
||||
} from '@/../../electron/core/plugin-manager/execution/index'
|
||||
import { extensionPoints } from '@plugin'
|
||||
|
||||
const staticMenu = ['Appearance']
|
||||
|
||||
@ -40,7 +37,7 @@ const SettingsScreen = () => {
|
||||
const getActivePluginPreferences = async () => {
|
||||
if (extensionPoints.get('PluginPreferences')) {
|
||||
const data = await Promise.all(
|
||||
extensionPoints.execute('PluginPreferences')
|
||||
extensionPoints.execute('PluginPreferences', {})
|
||||
)
|
||||
setPreferenceItems(Array.isArray(data) ? data : [])
|
||||
Promise.all(
|
||||
|
||||
@ -2,10 +2,9 @@
|
||||
import {
|
||||
extensionPoints,
|
||||
plugins,
|
||||
} from '../../electron/core/plugin-manager/execution/index'
|
||||
} from '@plugin'
|
||||
import {
|
||||
CoreService,
|
||||
DataService,
|
||||
InferenceService,
|
||||
ModelManagementService,
|
||||
StoreService,
|
||||
|
||||
@ -29,7 +29,9 @@
|
||||
"@models/*": ["./models/*"],
|
||||
"@hooks/*": ["./hooks/*"],
|
||||
"@containers/*": ["./containers/*"],
|
||||
"@screens/*": ["./screens/*"]
|
||||
"@screens/*": ["./screens/*"],
|
||||
"@plugin/*": ["./plugin/*"],
|
||||
"@plugin": ["./plugin"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
|
||||
1
web/types/index.d.ts
vendored
1
web/types/index.d.ts
vendored
@ -7,5 +7,6 @@ declare global {
|
||||
electronAPI?: any | undefined;
|
||||
corePlugin?: any | undefined;
|
||||
coreAPI?: any | undefined;
|
||||
pluggableElectronIpc?: any | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user