From 0fdc1f1cff1c2bd83c3685771adab61cf1d4f4ae Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Tue, 28 Jul 2026 12:07:28 +0700 Subject: [PATCH] fix: plugin API types + JS options validation --- lib/plugins/src/_internal/external-plugins.ts | 44 ++++++++++++++++++- lib/plugins/src/apis/discord.ts | 33 +++++++------- lib/plugins/src/types.ts | 15 +++++-- types/globals.consumers.ts | 20 +++++++-- 4 files changed, 87 insertions(+), 25 deletions(-) diff --git a/lib/plugins/src/_internal/external-plugins.ts b/lib/plugins/src/_internal/external-plugins.ts index 21a045a..09fcc37 100644 --- a/lib/plugins/src/_internal/external-plugins.ts +++ b/lib/plugins/src/_internal/external-plugins.ts @@ -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' { diff --git a/lib/plugins/src/apis/discord.ts b/lib/plugins/src/apis/discord.ts index 396284f..0f02aa4 100644 --- a/lib/plugins/src/apis/discord.ts +++ b/lib/plugins/src/apis/discord.ts @@ -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 diff --git a/lib/plugins/src/types.ts b/lib/plugins/src/types.ts index 8660856..8c520b8 100644 --- a/lib/plugins/src/types.ts +++ b/lib/plugins/src/types.ts @@ -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 {} +> extends UnscopedPreInitPluginApi { + components: typeof import('@revenge-mod/components') + discord: InitPluginApiDiscord +} /** * The unscoped plugin API. This API is available as a global for plugins. diff --git a/types/globals.consumers.ts b/types/globals.consumers.ts index 246d827..be06852 100644 --- a/types/globals.consumers.ts +++ b/types/globals.consumers.ts @@ -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( - manifest: PluginManifest, options: PluginOptions, - ): void + ): PluginOptions + + /** + * 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'