fix: plugin API types + JS options validation

This commit is contained in:
PalmDevs 2026-07-28 12:07:28 +07:00
parent a629b2ad44
commit 0fdc1f1cff
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
4 changed files with 87 additions and 25 deletions

View file

@ -21,7 +21,12 @@ import {
} from '.'
import { pPending } from './dependency-graph'
import { registerRepositoryEvents } from './repositories'
import type { PluginManifest, PluginOptionsFactory } from '../types'
import type {
PluginLifecycles,
PluginManifest,
PluginOptions,
PluginOptionsFactory,
} from '../types'
import type {
AnyPlugin,
PluginError,
@ -221,9 +226,44 @@ export function confirmInstall(
return callNativeMethod('revenge.plugins.confirmInstall', [token, accepted])
}
function assertIsFunction(
name: string,
value: unknown,
): asserts value is (...args: any[]) => any {
if (typeof value !== 'function')
throw new Error(`${name} must be a function, got ${typeof value}`)
}
function createOptionsFactory(script?: string): PluginOptionsFactory {
if (!script) return () => ({})
return () => new Function('revenge', `return ${script}`)(pUnscopedApi)
return () => {
const opts = new Function('revenge', 'plugin', `return ${script}`)(
pUnscopedApi,
// See types.consumers.ts
(opts: PluginOptions) => opts,
)()
if (typeof opts !== 'object' || opts === null)
throw new Error('Plugin options must be an object')
if (
opts.SettingsComponent !== undefined &&
typeof opts.SettingsComponent !== 'function'
)
throw new Error(
'SettingsComponent must be a function React component',
)
for (const key of ['preInit', 'init', 'start'] as Array<
keyof PluginLifecycles
>) {
if (opts[key] !== undefined) {
assertIsFunction(key, opts[key])
}
}
return opts
}
}
declare module '@revenge-mod/modules/native' {

View file

@ -9,21 +9,22 @@ import defer * as DiscordUtilsMetroSubscriptions from '@revenge-mod/discord/util
import { defineLazyProperties } from '@revenge-mod/utils/object'
import { guardIndexInitialized } from '.'
export interface PluginApiDiscord {
actions: PluginApiDiscord.Actions
/**
* This submodule is only available after the index module (ID 0) is initialized.
* Attempting to access it before then will throw an error.
*/
common: PluginApiDiscord.Common
design: PluginApiDiscord.Design
flux: PluginApiDiscord.Flux
modules: PluginApiDiscord.Modules
native: PluginApiDiscord.Native
utils: PluginApiDiscord.Utils
export interface PreInitPluginApiDiscord {
actions: PreInitPluginApiDiscord.Actions
/** This API is not available in the `preInit` phase. */
common: unknown
design: PreInitPluginApiDiscord.Design
flux: PreInitPluginApiDiscord.Flux
modules: PreInitPluginApiDiscord.Modules
native: PreInitPluginApiDiscord.Native
utils: PreInitPluginApiDiscord.Utils
}
export namespace PluginApiDiscord {
export interface InitPluginApiDiscord extends PreInitPluginApiDiscord {
common: PreInitPluginApiDiscord.Common
}
export namespace PreInitPluginApiDiscord {
export type Actions = typeof import('@revenge-mod/discord/actions')
export type Common = typeof import('@revenge-mod/discord/common')
export type Design = typeof import('@revenge-mod/discord/design')
@ -46,7 +47,7 @@ export namespace PluginApiDiscord {
export const discord = defineLazyProperties(
{
modules: defineLazyProperties({} as PluginApiDiscord.Modules, {
modules: defineLazyProperties({} as PreInitPluginApiDiscord.Modules, {
mainTabsV2: () => {
return DiscordModulesMainTabsV2
},
@ -55,7 +56,7 @@ export const discord = defineLazyProperties(
...require('@revenge-mod/discord/modules/settings/renderer'),
}),
}),
utils: defineLazyProperties({} as PluginApiDiscord.Utils, {
utils: defineLazyProperties({} as PreInitPluginApiDiscord.Utils, {
finders: () => {
return DiscordUtilsFinders
},
@ -63,7 +64,7 @@ export const discord = defineLazyProperties(
subscriptions: DiscordUtilsMetroSubscriptions,
}),
}),
} as PluginApiDiscord,
} as PreInitPluginApiDiscord,
{
actions: () => {
return DiscordActions

View file

@ -1,5 +1,8 @@
import type { FunctionComponent } from 'react'
import type { PluginApiDiscord } from './apis/discord'
import type {
InitPluginApiDiscord,
PreInitPluginApiDiscord,
} from './apis/discord'
import type { PluginApiExternals } from './apis/externals'
import type { PluginApiModules } from './apis/modules'
import type { PluginApiPlugins } from './apis/plugins'
@ -23,8 +26,9 @@ export interface UnscopedPreInitPluginApi<
react: PluginApiReact
assets: typeof import('@revenge-mod/assets')
externals: PluginApiExternals
components: typeof import('@revenge-mod/components')
discord: PluginApiDiscord
/** This API is not available in the `preInit` phase. */
components: unknown
discord: PreInitPluginApiDiscord
}
/**
@ -33,7 +37,10 @@ export interface UnscopedPreInitPluginApi<
*/
export interface UnscopedInitPluginApi<
O extends PluginApiExtensionsOptions = PluginApiExtensionsOptions,
> extends UnscopedPreInitPluginApi<O> {}
> extends UnscopedPreInitPluginApi<O> {
components: typeof import('@revenge-mod/components')
discord: InitPluginApiDiscord
}
/**
* The unscoped plugin API. This API is available as a global for plugins.

View file

@ -1,14 +1,28 @@
import type {
PluginApiExtensionsOptions,
PluginManifest,
PluginOptions,
UnscopedInitPluginApi,
UnscopedPluginApi,
UnscopedPreInitPluginApi,
} from '@revenge-mod/plugins/types'
declare global {
/**
* Defines an entrypoint for a Revenge JS plugin.
*/
export function plugin<O extends PluginApiExtensionsOptions>(
manifest: PluginManifest,
options: PluginOptions<O>,
): void
): PluginOptions<O>
/**
* The unscoped plugin API. **Use directly only when necessary.**
*
* You should be using the `unscoped` property of the `PluginApi` object instead.
*/
export const revenge:
| UnscopedPreInitPluginApi
| UnscopedInitPluginApi
| UnscopedPluginApi
}
export * from './globals'