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 '.' } from '.'
import { pPending } from './dependency-graph' import { pPending } from './dependency-graph'
import { registerRepositoryEvents } from './repositories' import { registerRepositoryEvents } from './repositories'
import type { PluginManifest, PluginOptionsFactory } from '../types' import type {
PluginLifecycles,
PluginManifest,
PluginOptions,
PluginOptionsFactory,
} from '../types'
import type { import type {
AnyPlugin, AnyPlugin,
PluginError, PluginError,
@ -221,9 +226,44 @@ export function confirmInstall(
return callNativeMethod('revenge.plugins.confirmInstall', [token, accepted]) 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 { function createOptionsFactory(script?: string): PluginOptionsFactory {
if (!script) return () => ({}) 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' { 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 { defineLazyProperties } from '@revenge-mod/utils/object'
import { guardIndexInitialized } from '.' import { guardIndexInitialized } from '.'
export interface PluginApiDiscord { export interface PreInitPluginApiDiscord {
actions: PluginApiDiscord.Actions actions: PreInitPluginApiDiscord.Actions
/** /** This API is not available in the `preInit` phase. */
* This submodule is only available after the index module (ID 0) is initialized. common: unknown
* Attempting to access it before then will throw an error. design: PreInitPluginApiDiscord.Design
*/ flux: PreInitPluginApiDiscord.Flux
common: PluginApiDiscord.Common modules: PreInitPluginApiDiscord.Modules
design: PluginApiDiscord.Design native: PreInitPluginApiDiscord.Native
flux: PluginApiDiscord.Flux utils: PreInitPluginApiDiscord.Utils
modules: PluginApiDiscord.Modules
native: PluginApiDiscord.Native
utils: PluginApiDiscord.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 Actions = typeof import('@revenge-mod/discord/actions')
export type Common = typeof import('@revenge-mod/discord/common') export type Common = typeof import('@revenge-mod/discord/common')
export type Design = typeof import('@revenge-mod/discord/design') export type Design = typeof import('@revenge-mod/discord/design')
@ -46,7 +47,7 @@ export namespace PluginApiDiscord {
export const discord = defineLazyProperties( export const discord = defineLazyProperties(
{ {
modules: defineLazyProperties({} as PluginApiDiscord.Modules, { modules: defineLazyProperties({} as PreInitPluginApiDiscord.Modules, {
mainTabsV2: () => { mainTabsV2: () => {
return DiscordModulesMainTabsV2 return DiscordModulesMainTabsV2
}, },
@ -55,7 +56,7 @@ export const discord = defineLazyProperties(
...require('@revenge-mod/discord/modules/settings/renderer'), ...require('@revenge-mod/discord/modules/settings/renderer'),
}), }),
}), }),
utils: defineLazyProperties({} as PluginApiDiscord.Utils, { utils: defineLazyProperties({} as PreInitPluginApiDiscord.Utils, {
finders: () => { finders: () => {
return DiscordUtilsFinders return DiscordUtilsFinders
}, },
@ -63,7 +64,7 @@ export const discord = defineLazyProperties(
subscriptions: DiscordUtilsMetroSubscriptions, subscriptions: DiscordUtilsMetroSubscriptions,
}), }),
}), }),
} as PluginApiDiscord, } as PreInitPluginApiDiscord,
{ {
actions: () => { actions: () => {
return DiscordActions return DiscordActions

View file

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

View file

@ -1,14 +1,28 @@
import type { import type {
PluginApiExtensionsOptions, PluginApiExtensionsOptions,
PluginManifest,
PluginOptions, PluginOptions,
UnscopedInitPluginApi,
UnscopedPluginApi,
UnscopedPreInitPluginApi,
} from '@revenge-mod/plugins/types' } from '@revenge-mod/plugins/types'
declare global { declare global {
/**
* Defines an entrypoint for a Revenge JS plugin.
*/
export function plugin<O extends PluginApiExtensionsOptions>( export function plugin<O extends PluginApiExtensionsOptions>(
manifest: PluginManifest,
options: PluginOptions<O>, 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' export * from './globals'