feat!: allow plugin API extensions to get runtime options, and allow API extensions to pass options, refactor api.storage to API extensions

This commit is contained in:
PalmDevs 2025-06-16 09:51:30 +07:00
parent bf9e723e73
commit fb8cc2d897
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
10 changed files with 118 additions and 69 deletions

View file

@ -1,19 +1,12 @@
import { TypedEventEmitter } from '@revenge-mod/discord/common/utils'
import { getStorage } from '@revenge-mod/storage'
import { defineLazyProperty } from '@revenge-mod/utils/objects'
import { allSettled, sleepReject } from '@revenge-mod/utils/promises'
import { _uapi as uapi } from './apis'
import {
PluginFlags as Flag,
PluginsStorageDirectory,
PluginStatus as Status,
} from './constants'
import type { StorageOptions } from '@revenge-mod/storage'
import type { AnyObject } from '@revenge-mod/utils/types'
import { PluginFlags as Flag, PluginStatus as Status } from './constants'
import type {
InitPluginApi,
Plugin,
PluginApi,
PluginApiExtensionOptions,
PluginCleanup,
PluginManifest,
PluginOptions,
@ -23,7 +16,7 @@ import type {
export const _uapi = uapi
export const _emitter = new TypedEventEmitter<{
register: [InternalPlugin]
register: [InternalPlugin, PluginOptions<any>]
disabled: [InternalPlugin]
enabled: [InternalPlugin]
// Add preInit API extensions
@ -50,9 +43,9 @@ export const _metas = new Map<
]
>()
export function registerPlugin<S extends AnyObject = AnyObject>(
export function registerPlugin<E extends PluginApiExtensionOptions>(
manifest: PluginManifest,
options: PluginOptions<S>,
options: PluginOptions<E>,
flags: number,
iflags: number,
) {
@ -62,7 +55,6 @@ export function registerPlugin<S extends AnyObject = AnyObject>(
const plugin: InternalPlugin = {
_c: [],
_s: options.storage,
errors: [],
manifest,
lifecycles: {
@ -81,7 +73,7 @@ export function registerPlugin<S extends AnyObject = AnyObject>(
_metas.set(manifest.id, [undefined, [], iflags, PluginApiLevel.None])
_plugins.set(manifest.id, plugin)
_emitter.emit('register', plugin)
_emitter.emit('register', plugin, options)
}
function handlePluginError(e: unknown, plugin: InternalPlugin) {
@ -118,26 +110,15 @@ function preparePluginPreInit(plugin: InternalPlugin) {
function preparePluginInit(plugin: InternalPlugin) {
const meta = _metas.get(plugin.manifest.id)!
const api = meta[0] as InitPluginApi
defineLazyProperty(api, 'storage', () =>
getStorage(`${PluginsStorageDirectory}/${plugin.manifest.id}.json`, {
...plugin._s,
directory: 'documents',
}),
)
_emitter.emit('init', plugin, api)
_emitter.emit('init', plugin, meta[0] as InitPluginApi)
meta[3] = PluginApiLevel.Init
}
function preparePluginStart(plugin: InternalPlugin) {
const meta = _metas.get(plugin.manifest.id)!
const api = meta[0] as PluginApi
// api.settings = ...
_emitter.emit('start', plugin, api)
_emitter.emit('start', plugin, meta[0] as PluginApi)
meta[3] = PluginApiLevel.Start
}
@ -381,8 +362,7 @@ export async function stopPlugin(plugin: InternalPlugin) {
}
}
export interface InternalPlugin extends Plugin {
_s?: StorageOptions<AnyObject>
export interface InternalPlugin extends Plugin<any> {
_c: PluginCleanup[]
}

View file

@ -17,10 +17,6 @@ for (const plugin of _plugins.values())
const unsub = onRunApplication(() => {
unsub()
// TODO(plugins/start): init start APIs
// const UILibrary = require('@revenge-mod/ui')
// uapi.ui = UiLibrary
for (const plugin of _plugins.values())
if (plugin.flags & PluginFlags.Enabled) startPlugin(plugin)
})

View file

@ -1,16 +1,19 @@
import type { Storage, StorageOptions } from '@revenge-mod/storage'
import type { AnyObject } from '@revenge-mod/utils/types'
import type { FunctionComponent } from 'react'
import type { PluginApiModules } from './apis/modules'
import type { PluginApiPlugins } from './apis/plugins'
import type { PluginApiReact } from './apis/react'
import type { PluginFlags, PluginStatus } from './constants'
// biome-ignore lint/suspicious/noEmptyInterface: To be extended by actual extensions
export interface PluginApiExtensionOptions {}
/**
* The unscoped plugin API (very limited). This API is available as a global for plugins.
* Available in the `preInit` phase.
*/
export interface UnscopedPreInitPluginApi {
export interface UnscopedPreInitPluginApi<
O extends PluginApiExtensionOptions = PluginApiExtensionOptions,
> {
modules: PluginApiModules
patcher: typeof import('@revenge-mod/patcher')
plugins: PluginApiPlugins
@ -21,7 +24,9 @@ export interface UnscopedPreInitPluginApi {
* The unscoped plugin API (limited). This API is available as a global for plugins.
* Available in the `init` phase.
*/
export interface UnscopedInitPluginApi extends UnscopedPreInitPluginApi {
export interface UnscopedInitPluginApi<
O extends PluginApiExtensionOptions = PluginApiExtensionOptions,
> extends UnscopedPreInitPluginApi<O> {
assets: typeof import('@revenge-mod/assets')
react: PluginApiReact
}
@ -30,7 +35,9 @@ export interface UnscopedInitPluginApi extends UnscopedPreInitPluginApi {
* 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 {
export interface UnscopedPluginApi<
O extends PluginApiExtensionOptions = PluginApiExtensionOptions,
> extends UnscopedInitPluginApi<O> {
// ui: typeof import('@revenge-mod/ui')
}
@ -41,7 +48,9 @@ export type PluginCleanupApi = (...fns: PluginCleanup[]) => void
* The plugin API (very limited).
* Available in the `preInit` phase.
*/
export interface PreInitPluginApi {
export interface PreInitPluginApi<
O extends PluginApiExtensionOptions = PluginApiExtensionOptions,
> {
unscoped: UnscopedPreInitPluginApi
cleanup: PluginCleanupApi
plugin: Plugin
@ -51,26 +60,19 @@ export interface PreInitPluginApi {
* The plugin API (limited).
* Available in the `init` phase.
*/
export interface InitPluginApi<S extends AnyObject = AnyObject>
extends PreInitPluginApi {
export interface InitPluginApi<
O extends PluginApiExtensionOptions = PluginApiExtensionOptions,
> extends PreInitPluginApi<O> {
unscoped: UnscopedInitPluginApi
/**
* The plugin storage.
*
* Note that the instance is only created when the plugin accesses the API.
* This is to prevent unnecessary storage instances from being created.
*
* To preload storage, simply call `api.storage.get()`.
*/
storage: Storage<S>
}
/**
* The plugin API.
* Available in the `start` and `stop` phase.
*/
export interface PluginApi<S extends AnyObject = AnyObject>
extends InitPluginApi<S> {
export interface PluginApi<
O extends PluginApiExtensionOptions = PluginApiExtensionOptions,
> extends InitPluginApi<O> {
unscoped: UnscopedPluginApi
}
@ -113,42 +115,46 @@ export interface PluginManifest {
icon?: string
}
export interface PluginOptions<S extends AnyObject = AnyObject>
extends PluginLifecycles<S> {
SettingsComponent?: PluginSettingsComponent<S>
storage?: Omit<StorageOptions<S>, 'directory'>
export interface PluginOptions<
O extends PluginApiExtensionOptions = PluginApiExtensionOptions,
> extends PluginLifecycles<O> {
SettingsComponent?: PluginSettingsComponent<O>
}
export interface PluginLifecycles<S extends AnyObject = AnyObject> {
export interface PluginLifecycles<
O extends PluginApiExtensionOptions = PluginApiExtensionOptions,
> {
/**
* Runs as soon as possible with very limited APIs.
* Before the index module (module 0)'s factory is run.
*
* @param api Plugin API (very limited).
*/
preInit?: (api: PreInitPluginApi) => any
preInit?: (api: PreInitPluginApi<O>) => any
/**
* Runs as soon as all important modules are initialized.
* After the index module (module 0)'s factory is run.
*
* @param api Plugin API (limited).
*/
init?: (api: InitPluginApi<S>) => any
init?: (api: InitPluginApi<O>) => any
/**
* Runs when the plugin can be started with all APIs available.
*
* @param api Plugin API.
*/
start?: (api: PluginApi<S>) => any
start?: (api: PluginApi<O>) => any
/**
* Runs when the plugin is stopped.
*
* @param api Plugin API.
*/
stop?: (api: PluginApi<S>) => any
stop?: (api: PluginApi<O>) => any
}
export interface Plugin<S extends AnyObject = AnyObject> {
export interface Plugin<
O extends PluginApiExtensionOptions = PluginApiExtensionOptions,
> {
// TODO(plugins): support plugin bundles
// /**
// * The plugin bundle this plugin belongs to.
@ -161,7 +167,7 @@ export interface Plugin<S extends AnyObject = AnyObject> {
/**
* The plugin lifecycles.
*/
lifecycles: PluginLifecycles<S>
lifecycles: PluginLifecycles<O>
/**
* The plugin flags.
@ -181,7 +187,7 @@ export interface Plugin<S extends AnyObject = AnyObject> {
/**
* The plugin settings page.
*/
SettingsComponent?: PluginSettingsComponent<S>
SettingsComponent?: PluginSettingsComponent<O>
/**
* Disable the plugin. This will also stop the plugin if it is running.
@ -193,5 +199,6 @@ export interface Plugin<S extends AnyObject = AnyObject> {
stop(): Promise<void>
}
export type PluginSettingsComponent<S extends AnyObject = AnyObject> =
FunctionComponent<{ api: PluginApi<S> }>
export interface PluginSettingsComponent<
O extends PluginApiExtensionOptions = PluginApiExtensionOptions,
> extends FunctionComponent<{ api: PluginApi<O> }> {}

View file

@ -5,6 +5,12 @@
"exports": {
".": {
"default": "./src/index.ts"
},
"./preinit": {
"default": "./src/preinit.ts"
},
"./init": {
"default": "./src/init.ts"
}
}
}

View file

@ -108,7 +108,7 @@ export function getStorage<T extends AnyObject = AnyObject>(
return storage
}
export interface StorageOptions<T> {
export interface StorageOptions<T extends AnyObject = AnyObject> {
/**
* The directory of the storage file.
*/

13
lib/storage/src/init.ts Normal file
View file

@ -0,0 +1,13 @@
import { _emitter } from '@revenge-mod/plugins/_'
import { PluginsStorageDirectory } from '@revenge-mod/plugins/constants'
import { defineLazyProperty } from '@revenge-mod/utils/objects'
import { getStorage } from '.'
_emitter.on('init', ({ _s: storageOptions, manifest: { id } }, api) =>
defineLazyProperty(api, 'storage', () =>
getStorage(`${PluginsStorageDirectory}/${id}.json`, {
...storageOptions,
directory: 'documents',
}),
),
)

View file

@ -0,0 +1,3 @@
import { _emitter } from '@revenge-mod/plugins/_'
_emitter.on('register', (plugin, options) => (plugin._s = options.storage))

42
lib/storage/src/types.ts Normal file
View file

@ -0,0 +1,42 @@
import type {
PluginApiExtensionOptions,
PluginManifest,
PluginOptions,
} from '@revenge-mod/plugins/types'
import type { AnyObject } from '@revenge-mod/utils/types'
import type { Storage, StorageOptions } from '.'
declare module '@revenge-mod/plugins/types' {
export interface PluginApiExtensionOptions {
storage: AnyObject
}
export interface PluginOptions<O extends PluginApiExtensionOptions> {
storage?: Omit<StorageOptions<O['storage']>, 'directory'>
}
export interface InitPluginApi<O extends PluginApiExtensionOptions> {
/**
* The plugin storage.
*
* Note that the instance is only created when the plugin accesses the API.
* This is to prevent unnecessary storage instances from being created.
*
* To preload storage, simply call `api.storage.get()`.
*/
storage: Storage<O['storage']>
}
}
declare module '@revenge-mod/plugins/_' {
export function registerPlugin<E extends PluginApiExtensionOptions>(
manifest: PluginManifest,
options: PluginOptions<E>,
flags: number,
iflags: number,
): void
export interface InternalPlugin {
_s?: StorageOptions<AnyObject>
}
}

View file

@ -26,6 +26,7 @@ onModuleFirstRequired(IndexModuleId, function onIndexRequired() {
require('@revenge-mod/modules/preinit')
require('@revenge-mod/react/preinit')
require('@revenge-mod/assets/preinit')
require('@revenge-mod/storage/preinit')
require('@revenge-mod/discord/preinit')
// Run all preinit plugins
@ -35,6 +36,7 @@ onModuleFirstRequired(IndexModuleId, function onIndexRequired() {
onModuleInitialized(IndexModuleId, function onIndexInitialized() {
runCatching(() => {
// Initialize init libraries
require('@revenge-mod/storage/init')
require('@revenge-mod/externals/init')
require('@revenge-mod/discord/init')
require('@revenge-mod/components/init')

View file

@ -21,7 +21,7 @@ const defaultStorage: Storage = {
},
}
registerPlugin<Storage>(
registerPlugin<{ storage: Storage }>(
{
id: 'revenge.settings.developer',
name: 'Developer Settings',
@ -57,4 +57,4 @@ registerPlugin<Storage>(
)
// Expose to EvaluateJavaScriptSetting
export let api: PluginApi<Storage>
export let api: PluginApi<{ storage: Storage }>