feat(lib): add initial impl of plugins
This commit is contained in:
parent
1dab1ead79
commit
020045f2eb
6 changed files with 348 additions and 0 deletions
16
lib/plugins/package.json
Normal file
16
lib/plugins/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "@revenge-mod/plugins",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./init": {
|
||||
"default": "./src/init.ts"
|
||||
},
|
||||
"./_": {
|
||||
"default": "./src/_internal.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
137
lib/plugins/src/_internal.ts
Normal file
137
lib/plugins/src/_internal.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import * as DiscordLibrary from '@revenge-mod/discord'
|
||||
import * as ModulesLibrary from '@revenge-mod/modules'
|
||||
import * as PatcherLibrary from '@revenge-mod/patcher'
|
||||
import * as PluginsLibrary from '@revenge-mod/plugins'
|
||||
import * as ReactLibrary from '@revenge-mod/react'
|
||||
|
||||
const { PluginFlags } = PluginsLibrary
|
||||
|
||||
export const _uapi: PluginsLibrary.UnscopedInitPluginApi | PluginsLibrary.UnscopedPluginApi = {
|
||||
discord: DiscordLibrary,
|
||||
modules: ModulesLibrary,
|
||||
plugins: PluginsLibrary,
|
||||
react: ReactLibrary,
|
||||
patcher: PatcherLibrary,
|
||||
// utils: UtilsLibrary,
|
||||
}
|
||||
|
||||
const _plugins = new Map<PluginsLibrary.PluginManifest['id'], InternalPlugin>()
|
||||
|
||||
export function registerPlugin(
|
||||
manifest: PluginsLibrary.PluginManifest,
|
||||
lifecycles: PluginsLibrary.PluginLifecycles,
|
||||
flags: PluginsLibrary.PluginFlags = 0,
|
||||
) {
|
||||
// TODO(plugins): verify plugin manifest
|
||||
if (_plugins.has(manifest.id)) throw new Error(`Plugin with ID "${manifest.id}" already registered`)
|
||||
|
||||
const handleError = (e: unknown) => {
|
||||
plugin.errors.push(e)
|
||||
plugin.flags |= PluginFlags.Errored
|
||||
return plugin.disable()
|
||||
}
|
||||
|
||||
const plugin: InternalPlugin = {
|
||||
cleanups: [],
|
||||
errors: [],
|
||||
manifest,
|
||||
lifecycles,
|
||||
flags,
|
||||
prepareStart: () => {
|
||||
// const sapi = api as PluginsLibrary.PluginApi
|
||||
// sapi.settings ??= ...
|
||||
},
|
||||
init: async () => {
|
||||
plugin.flags |= PluginFlags.StatusInit
|
||||
plugin.flags |= PluginFlags.StatusActioning
|
||||
|
||||
try {
|
||||
await lifecycles.init?.(api)
|
||||
} catch (e) {
|
||||
handleError(e)
|
||||
}
|
||||
|
||||
// Not in finally block because plugin.disable() already handles
|
||||
plugin.flags &= ~PluginFlags.StatusInit
|
||||
plugin.flags &= ~PluginFlags.StatusActioning
|
||||
},
|
||||
start: async () => {
|
||||
plugin.flags |= PluginFlags.StatusStart
|
||||
plugin.flags |= PluginFlags.StatusActioning
|
||||
|
||||
plugin.prepareStart()
|
||||
|
||||
try {
|
||||
await lifecycles.start?.(api as PluginsLibrary.PluginApi)
|
||||
} catch (e) {
|
||||
handleError(e)
|
||||
}
|
||||
|
||||
// Not in finally block because plugin.disable() already handles
|
||||
plugin.flags &= ~PluginFlags.StatusStart
|
||||
plugin.flags &= ~PluginFlags.StatusActioning
|
||||
},
|
||||
stop: async () => {
|
||||
plugin.flags &= ~PluginFlags.StatusInit
|
||||
plugin.flags &= ~PluginFlags.StatusStart
|
||||
plugin.flags |= PluginFlags.StatusActioning
|
||||
|
||||
// In case the plugin only has init() and stop()
|
||||
plugin.prepareStart()
|
||||
|
||||
try {
|
||||
await lifecycles.stop?.(api as PluginsLibrary.PluginApi)
|
||||
} catch (e) {
|
||||
handleError(e)
|
||||
} finally {
|
||||
// Run cleanups
|
||||
const results = await Promise.allSettled(plugin.cleanups.map(cleanup => cleanup()))
|
||||
for (const result of results) if (result.status === 'rejected') plugin.errors.push(result.reason)
|
||||
|
||||
plugin.flags &= ~PluginFlags.StatusActioning
|
||||
}
|
||||
},
|
||||
enable: async () => {
|
||||
plugin.flags |= PluginFlags.Enabled
|
||||
if (lifecycles.init) plugin.flags |= PluginFlags.ReloadRequired
|
||||
},
|
||||
disable: async () => {
|
||||
plugin.flags &= ~PluginFlags.Enabled
|
||||
// If plugin is initialized/started, we need to stop it
|
||||
if (plugin.flags & (PluginFlags.StatusInit | PluginFlags.StatusStart)) await plugin.stop()
|
||||
},
|
||||
}
|
||||
|
||||
const api: PluginsLibrary.InitPluginApi | PluginsLibrary.PluginApi = {
|
||||
cleanup: (...items) => {
|
||||
plugin.cleanups.push(...items)
|
||||
},
|
||||
logger: new DiscordLibrary.Logger(`Revenge > ${plugin.manifest.name}`),
|
||||
plugin,
|
||||
unscoped: _uapi,
|
||||
}!
|
||||
|
||||
_plugins.set(manifest.id, plugin)
|
||||
}
|
||||
|
||||
export function initPlugins() {
|
||||
const promises: Promise<void>[] = []
|
||||
for (const plugin of _plugins.values()) if (plugin.flags & PluginFlags.Enabled) promises.push(plugin.init())
|
||||
// These Promises never throw, so it is fine
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
export function startPlugins() {
|
||||
const promises: Promise<void>[] = []
|
||||
for (const plugin of _plugins.values()) if (plugin.flags & PluginFlags.Enabled) promises.push(plugin.start())
|
||||
// These Promises never throw, so it is fine
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
export interface InternalPlugin extends PluginsLibrary.Plugin {
|
||||
cleanups: PluginsLibrary.PluginCleanup[]
|
||||
prepareStart(): void
|
||||
init(): Promise<void>
|
||||
start(): Promise<void>
|
||||
stop(): Promise<void>
|
||||
}
|
||||
32
lib/plugins/src/constants.ts
Normal file
32
lib/plugins/src/constants.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export const PluginFlags = {
|
||||
/**
|
||||
* The plugin is enabled.
|
||||
*/
|
||||
Enabled: 1 << 0,
|
||||
/**
|
||||
* The plugin is an internal plugin.
|
||||
*/
|
||||
Internal: 1 << 1,
|
||||
/**
|
||||
* The plugin requires a reload to apply changes.
|
||||
*/
|
||||
ReloadRequired: 1 << 2,
|
||||
/**
|
||||
* The plugin is initialized or initializing.
|
||||
*/
|
||||
StatusInit: 1 << 3,
|
||||
/**
|
||||
* The plugin is started or starting.
|
||||
*/
|
||||
StatusStart: 1 << 4,
|
||||
/**
|
||||
* The plugin is actioning something, such as initializing or starting.
|
||||
*/
|
||||
StatusActioning: 1 << 5,
|
||||
/**
|
||||
* The plugin has errors.
|
||||
*/
|
||||
Errored: 1 << 6,
|
||||
}
|
||||
|
||||
export type PluginFlags = (typeof PluginFlags)[keyof typeof PluginFlags]
|
||||
2
lib/plugins/src/index.ts
Normal file
2
lib/plugins/src/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './constants'
|
||||
export * from './types'
|
||||
23
lib/plugins/src/init.ts
Normal file
23
lib/plugins/src/init.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import * as AssetsLibrary from '@revenge-mod/assets'
|
||||
// import * as UiLibrary from '@revenge-mod/ui'
|
||||
|
||||
import { after } from '@revenge-mod/patcher'
|
||||
import { ReactNative } from '@revenge-mod/react'
|
||||
|
||||
import { _uapi, initPlugins, startPlugins } from './_internal'
|
||||
|
||||
import type { UnscopedPluginApi } from './types'
|
||||
|
||||
// Setup non-limited APIs
|
||||
const uapi = _uapi as UnscopedPluginApi
|
||||
uapi.assets = AssetsLibrary
|
||||
// uapi.ui = UiLibrary
|
||||
|
||||
initPlugins()
|
||||
|
||||
// TODO(plugins/init): move to @revenge-mod/app onAppInitialized
|
||||
const unpatch = after(ReactNative.AppRegistry, 'runApplication', ret => {
|
||||
unpatch()
|
||||
setImmediate(startPlugins)
|
||||
return ret
|
||||
})
|
||||
138
lib/plugins/src/types.ts
Normal file
138
lib/plugins/src/types.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import type { DiscordModules } from '@revenge-mod/discord/types'
|
||||
|
||||
import type { PluginFlags } from './constants'
|
||||
|
||||
/**
|
||||
* The unscoped plugin API (limited). This API is available as a global for plugins.
|
||||
* Available in the `init` phase.
|
||||
*/
|
||||
export interface UnscopedInitPluginApi {
|
||||
discord: typeof import('@revenge-mod/discord')
|
||||
plugins: typeof import('@revenge-mod/plugins')
|
||||
modules: typeof import('@revenge-mod/modules')
|
||||
patcher: typeof import('@revenge-mod/patcher')
|
||||
react: typeof import('@revenge-mod/react')
|
||||
}
|
||||
|
||||
/**
|
||||
* The unscoped plugin API. This API is available as a global for plugins.
|
||||
* Available in the `start` and `stop` phase.
|
||||
*/
|
||||
export interface UnscopedPluginApi extends UnscopedInitPluginApi {
|
||||
assets: typeof import('@revenge-mod/assets')
|
||||
// ui: typeof import('@revenge-mod/ui')
|
||||
// utils: typeof import('@revenge-mod/utils')
|
||||
}
|
||||
|
||||
export type PluginCleanup = () => any
|
||||
export type PluginCleanupApi = (...fns: PluginCleanup[]) => void
|
||||
|
||||
/**
|
||||
* The plugin API (limited).
|
||||
* Available in the `init` phase.
|
||||
*/
|
||||
export interface InitPluginApi {
|
||||
plugin: Plugin
|
||||
logger: InstanceType<DiscordModules.Logger>
|
||||
unscoped: UnscopedInitPluginApi
|
||||
cleanup: PluginCleanupApi
|
||||
}
|
||||
|
||||
/**
|
||||
* The plugin API.
|
||||
* Available in the `start` and `stop` phase.
|
||||
*/
|
||||
export interface PluginApi extends InitPluginApi {
|
||||
// settings: typeof import('@revenge-mod/settings')
|
||||
unscoped: UnscopedPluginApi
|
||||
}
|
||||
|
||||
// TODO(plugins): support plugin bundles
|
||||
// export interface PluginBundle {
|
||||
// /**
|
||||
// * The unique identifier for the plugin bundle.
|
||||
// */
|
||||
// id: string
|
||||
// /**
|
||||
// * The author of the plugin bundle.
|
||||
// */
|
||||
// author: string
|
||||
// }
|
||||
|
||||
export interface PluginManifest {
|
||||
/**
|
||||
* The unique identifier for the plugin.
|
||||
*/
|
||||
id: string
|
||||
/**
|
||||
* The name of the plugin.
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* The description of the plugin.
|
||||
*/
|
||||
description: string
|
||||
/**
|
||||
* The icon of the plugin.
|
||||
*/
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export interface PluginLifecycles {
|
||||
/**
|
||||
* Runs immediately as soon as possible with limited APIs.
|
||||
*
|
||||
* @param api Plugin API (limited).
|
||||
* @param context Plugin context.
|
||||
*/
|
||||
init?: (api: InitPluginApi) => any
|
||||
/**
|
||||
* Runs when the plugin can be started with all APIs available.
|
||||
*
|
||||
* @param api Plugin API.
|
||||
* @param context Plugin context.
|
||||
*/
|
||||
start?: (api: PluginApi) => any
|
||||
/**
|
||||
* Runs when the plugin is stopped.
|
||||
*
|
||||
* @param api Plugin API.
|
||||
* @param context Plugin context.
|
||||
*/
|
||||
stop?: (api: PluginApi) => any
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
// TODO(plugins): support plugin bundles
|
||||
// /**
|
||||
// * The plugin bundle this plugin belongs to.
|
||||
// */
|
||||
// bundle: PluginBundle
|
||||
/**
|
||||
* The plugin manifest.
|
||||
*/
|
||||
manifest: PluginManifest
|
||||
/**
|
||||
* The plugin lifecycles.
|
||||
*/
|
||||
lifecycles: PluginLifecycles
|
||||
|
||||
/**
|
||||
* The plugin flags.
|
||||
*/
|
||||
flags: PluginFlags
|
||||
/**
|
||||
* Errors encountered during the plugin lifecycles.
|
||||
*/
|
||||
errors: unknown[]
|
||||
|
||||
/**
|
||||
* Disable the plugin. This will also stop the plugin if it is running.
|
||||
*/
|
||||
disable(): Promise<void>
|
||||
/**
|
||||
* Enable the plugin.
|
||||
* @internal
|
||||
*/
|
||||
enable(): Promise<void>
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue