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