From 2e455cf2c511813db2cf12a6f73e1aee3b7ce06a Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Sun, 20 Jul 2025 23:06:56 +0700 Subject: [PATCH] feat: add ability to decorate plugin APIs, move private properties to WeakMap stores --- lib/components/package.json | 5 +- lib/components/src/init.ts | 9 - lib/discord/package.json | 3 - lib/discord/src/init.ts | 44 ----- lib/discord/src/types/index.ts | 1 + lib/externals/package.json | 5 +- lib/externals/src/init.ts | 23 --- lib/plugins/package.json | 2 +- lib/plugins/src/_internal/decorators.ts | 36 ++++ lib/plugins/src/_internal/dependency-graph.ts | 78 ++++++++ .../src/{_internal.ts => _internal/index.ts} | 185 ++++++++++-------- lib/plugins/src/init.ts | 12 +- lib/plugins/src/preinit.ts | 7 +- lib/plugins/src/start.ts | 7 +- lib/plugins/src/types.ts | 41 +++- lib/storage/package.json | 8 +- lib/storage/src/_internal.ts | 6 + lib/storage/src/index.ts | 12 +- lib/storage/src/init.ts | 40 ---- lib/storage/src/preinit.ts | 5 - lib/storage/src/types.ts | 24 +-- lib/utils/package.json | 8 +- lib/utils/src/init.ts | 7 - lib/utils/src/preinit.ts | 34 ---- lib/utils/src/types.ts | 26 +++ src/init.ts | 5 - src/plugins/init/api.assets/index.ts | 23 +++ src/plugins/init/api.components/index.ts | 27 +++ src/plugins/init/api.discord/index.ts | 60 ++++++ src/plugins/init/api.externals/index.ts | 40 ++++ .../components/FilterAndSortActionSheet.tsx | 6 +- .../init/settings/components/PluginCard.tsx | 4 +- .../definitions/RevengePluginsSetting.tsx | 7 +- src/plugins/init/settings/register-routes.tsx | 4 +- .../screens/RevengePluginsSettingScreen.tsx | 10 +- .../preinit/api.storage/define-storage-use.ts | 30 +++ src/plugins/preinit/api.storage/index.ts | 48 +++++ src/plugins/preinit/api.utils/index.ts | 33 ++++ src/preinit.ts | 2 - 39 files changed, 595 insertions(+), 332 deletions(-) delete mode 100644 lib/components/src/init.ts delete mode 100644 lib/discord/src/init.ts delete mode 100644 lib/externals/src/init.ts create mode 100644 lib/plugins/src/_internal/decorators.ts create mode 100644 lib/plugins/src/_internal/dependency-graph.ts rename lib/plugins/src/{_internal.ts => _internal/index.ts} (68%) create mode 100644 lib/storage/src/_internal.ts delete mode 100644 lib/storage/src/init.ts delete mode 100644 lib/storage/src/preinit.ts delete mode 100644 lib/utils/src/init.ts delete mode 100644 lib/utils/src/preinit.ts create mode 100644 src/plugins/init/api.assets/index.ts create mode 100644 src/plugins/init/api.components/index.ts create mode 100644 src/plugins/init/api.discord/index.ts create mode 100644 src/plugins/init/api.externals/index.ts create mode 100644 src/plugins/preinit/api.storage/define-storage-use.ts create mode 100644 src/plugins/preinit/api.storage/index.ts create mode 100644 src/plugins/preinit/api.utils/index.ts diff --git a/lib/components/package.json b/lib/components/package.json index 0119d01..7e4d42f 100644 --- a/lib/components/package.json +++ b/lib/components/package.json @@ -18,9 +18,6 @@ "./TableRowAssetIcon": { "default": "./src/TableRowAssetIcon.tsx" }, - "./init": { - "default": "./src/init.ts" - }, "./types": { "types": "./src/types.ts" }, @@ -28,4 +25,4 @@ "default": "./src/_internal.ts" } } -} +} \ No newline at end of file diff --git a/lib/components/src/init.ts b/lib/components/src/init.ts deleted file mode 100644 index 2aae937..0000000 --- a/lib/components/src/init.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { pUnscopedApi } from '@revenge-mod/plugins/_' -import { defineLazyProperty } from '@revenge-mod/utils/object' -import type { UnscopedInitPluginApi } from '@revenge-mod/plugins/types' - -const uapi = pUnscopedApi as UnscopedInitPluginApi - -defineLazyProperty(uapi, 'components', () => { - return require('.') -}) diff --git a/lib/discord/package.json b/lib/discord/package.json index d648d4f..e135c84 100644 --- a/lib/discord/package.json +++ b/lib/discord/package.json @@ -24,9 +24,6 @@ "./native": { "default": "./src/native.ts" }, - "./init": { - "default": "./src/init.ts" - }, "./preinit": { "default": "./src/preinit.ts" }, diff --git a/lib/discord/src/init.ts b/lib/discord/src/init.ts deleted file mode 100644 index 92313a8..0000000 --- a/lib/discord/src/init.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { pEmitter, pUnscopedApi } from '@revenge-mod/plugins/_' -import { - defineLazyProperties, - defineLazyProperty, -} from '@revenge-mod/utils/object' -import { Logger } from './common' -import type { UnscopedInitPluginApi } from '@revenge-mod/plugins/types' -import type { PluginApiDiscord } from './types/revenge' - -defineLazyProperties( - ((pUnscopedApi as UnscopedInitPluginApi).discord = {} as PluginApiDiscord), - { - actions: () => { - return require('@revenge-mod/discord/actions') - }, - common: () => { - return require('@revenge-mod/discord/common') - }, - design: () => { - return require('@revenge-mod/discord/design') - }, - native: () => { - return require('@revenge-mod/discord/native') - }, - modules: () => - defineLazyProperties({} as PluginApiDiscord.Modules, { - mainTabsV2: () => { - return require('@revenge-mod/discord/modules/main_tabs_v2') - }, - settings: () => ({ - ...require('@revenge-mod/discord/modules/settings'), - renderer: require('@revenge-mod/discord/modules/settings/renderer'), - }), - }), - }, -) - -pEmitter.on('init', ({ manifest: { id } }, api) => - defineLazyProperty( - api, - 'logger', - () => new Logger(`Revenge > Plugins (${id})`), - ), -) diff --git a/lib/discord/src/types/index.ts b/lib/discord/src/types/index.ts index ab2b7da..5741be7 100644 --- a/lib/discord/src/types/index.ts +++ b/lib/discord/src/types/index.ts @@ -17,6 +17,7 @@ import type { export * from './native' export * from './polyfills' +export * from './revenge' export namespace DiscordModules { export namespace Flux { diff --git a/lib/externals/package.json b/lib/externals/package.json index 7296702..4259ec9 100644 --- a/lib/externals/package.json +++ b/lib/externals/package.json @@ -6,9 +6,6 @@ "./browserify": { "default": "./src/browserify.ts" }, - "./init": { - "default": "./src/init.ts" - }, "./react-native-clipboard": { "default": "./src/react-native-clipboard.ts" }, @@ -22,4 +19,4 @@ "types": "./src/types.ts" } } -} +} \ No newline at end of file diff --git a/lib/externals/src/init.ts b/lib/externals/src/init.ts deleted file mode 100644 index 59d4844..0000000 --- a/lib/externals/src/init.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { pUnscopedApi } from '@revenge-mod/plugins/_' -import { defineLazyProperties } from '@revenge-mod/utils/object' -import type { UnscopedInitPluginApi } from '@revenge-mod/plugins/types' -import type { PluginApiExternals } from './types' - -defineLazyProperties( - ((pUnscopedApi as UnscopedInitPluginApi).externals = - {} as PluginApiExternals), - { - Browserify: () => { - return require('@revenge-mod/externals/browserify') - }, - ReactNativeClipboard: () => { - return require('@revenge-mod/externals/react-native-clipboard') - }, - ReactNavigation: () => { - return require('@revenge-mod/externals/react-navigation') - }, - Shopify: () => { - return require('@revenge-mod/externals/shopify') - }, - }, -) diff --git a/lib/plugins/package.json b/lib/plugins/package.json index e9adf05..ab1c4bf 100644 --- a/lib/plugins/package.json +++ b/lib/plugins/package.json @@ -19,7 +19,7 @@ "types": "./src/types.ts" }, "./_": { - "default": "./src/_internal.ts" + "default": "./src/_internal/index.ts" } } } \ No newline at end of file diff --git a/lib/plugins/src/_internal/decorators.ts b/lib/plugins/src/_internal/decorators.ts new file mode 100644 index 0000000..e944a78 --- /dev/null +++ b/lib/plugins/src/_internal/decorators.ts @@ -0,0 +1,36 @@ +import type { + InitPluginApi, + Plugin, + PluginApi, + PreInitPluginApi, +} from '../types' +import type { InternalPluginMeta } from '.' + +export type PluginApiDecoratorStore< + A extends PreInitPluginApi | InitPluginApi | PluginApi, +> = [decorator: Parameters[0], handleError: (e: unknown) => void] + +export const pDecorators: { + preInit: PluginApiDecoratorStore[] + init: PluginApiDecoratorStore[] + start: PluginApiDecoratorStore[] +} = { + preInit: [], + init: [], + start: [], +} + +export function decoratePluginApi( + store: PluginApiDecoratorStore< + PreInitPluginApi | InitPluginApi | PluginApi + >[], + plugin: Plugin, + meta: InternalPluginMeta, +) { + for (const [decorator, handleError] of store) + try { + decorator(plugin as Plugin, meta.options) + } catch (e) { + handleError(e) + } +} diff --git a/lib/plugins/src/_internal/dependency-graph.ts b/lib/plugins/src/_internal/dependency-graph.ts new file mode 100644 index 0000000..9efd967 --- /dev/null +++ b/lib/plugins/src/_internal/dependency-graph.ts @@ -0,0 +1,78 @@ +import { pList, pMetadata } from '../_internal' +import { PluginFlags } from '../constants' +import type { AnyPlugin } from '../_internal' + +/// PLUGIN DEPENDENCY GRAPHING + +// We don't store the graph as a tree, but rather as a set of nodes. + +// Root nodes are plugins that have dependencies, but no dependents. These plugins are the starting points of the dependency graph. +// Leaf nodes are plugins that have no dependencies, but may have dependents. These plugins are the end points of the dependency graph. + +// Start order: Single nodes (no dependencies & dependents) -> Leaf nodes (no dependencies, maybe dependents) -> Root nodes (with dependencies, no dependents) +// This way we can ensure that all dependencies are started before the plugin itself. + +export const pRootNodes = new Set() +export const pLeafOrSingleNodes = new Set() + +// Ordered list of plugins to be started +export const pListOrdered = new Set() +// Pending plugins to be computed +export const pPending = new Set() + +export function computePendingNodes() { + for (const plugin of pPending) resolvePluginGraph(plugin) + pPending.clear() + + for (const plugin of pLeafOrSingleNodes) pListOrdered.add(plugin) + pLeafOrSingleNodes.clear() + + const stack = [...pRootNodes] + pRootNodes.clear() + + while (stack.length) { + const plugin = stack.pop()! + if (pListOrdered.has(plugin)) continue + + if (plugin.manifest.dependencies?.length) { + for (const dep of plugin.manifest.dependencies) { + const depPlugin = pList.get(dep.id) + if (depPlugin && !pListOrdered.has(depPlugin)) + stack.push(depPlugin) + } + } else pListOrdered.add(plugin) + } +} + +export function resolvePluginGraph(plugin: AnyPlugin) { + const { manifest } = plugin + + if (manifest.dependencies?.length) { + // Optimisitically add to root nodes (if there are dependents, it will be removed later) + pRootNodes.add(plugin) + + for (const { id } of manifest.dependencies) { + const dep = pList.get(id) + if (!dep) { + // TODO: Once external plugins are implemented, we will have to check the external plugin registry here as well + // External plugin registry should ideally be Record + // Then we register the plugin here and do dep = pList.get(id) again + + throw new Error( + `Plugin "${manifest.id}" depends on unregistered plugin "${id}"`, + ) + } + + if (!(dep.flags & PluginFlags.Enabled)) + throw new Error( + `Plugin "${manifest.id}" depends on disabled plugin "${id}"`, + ) + + const depMeta = pMetadata.get(dep)! + depMeta.dependents.push(plugin) + + // Not a root node if it has dependencies + if (dep.manifest.dependencies?.length) pRootNodes.delete(dep) + } + } else pLeafOrSingleNodes.add(plugin) +} diff --git a/lib/plugins/src/_internal.ts b/lib/plugins/src/_internal/index.ts similarity index 68% rename from lib/plugins/src/_internal.ts rename to lib/plugins/src/_internal/index.ts index bc8bd72..9e2d4bd 100644 --- a/lib/plugins/src/_internal.ts +++ b/lib/plugins/src/_internal/index.ts @@ -1,8 +1,14 @@ import { TypedEventEmitter } from '@revenge-mod/discord/common/utils' import { getErrorStack } from '@revenge-mod/utils/error' import { allSettled, sleepReject } from '@revenge-mod/utils/promise' -import { pUnscopedApi as uapi } from './apis' -import { PluginFlags as Flag, PluginStatus as Status } from './constants' +import { pUnscopedApi as uapi } from '../apis' +import { + PluginFlags as Flag, + PluginFlags, + PluginStatus as Status, +} from '../constants' +import { decoratePluginApi, pDecorators } from './decorators' +import { pLeafOrSingleNodes, pPending } from './dependency-graph' import type { InitPluginApi, Plugin, @@ -12,37 +18,34 @@ import type { PluginManifest, PluginOptions, PreInitPluginApi, -} from './types' +} from '../types' + +export type AnyPlugin = Plugin export const pUnscopedApi = uapi - export const pEmitter = new TypedEventEmitter<{ - register: [InternalPlugin, PluginOptions] - disabled: [InternalPlugin] - enabled: [InternalPlugin] - // Add preInit API extensions - preInit: [InternalPlugin, PreInitPluginApi] - preInited: [InternalPlugin] - // Add init API extensions - init: [InternalPlugin, InitPluginApi] - inited: [InternalPlugin] - // Add start API extensions - start: [InternalPlugin, PluginApi] - started: [InternalPlugin] - stopped: [InternalPlugin] - errored: [InternalPlugin, unknown] + register: [AnyPlugin, PluginOptions, update?: true] + disabled: [AnyPlugin] + enabled: [AnyPlugin] + preInited: [AnyPlugin] + inited: [AnyPlugin] + started: [AnyPlugin] + stopped: [AnyPlugin] + errored: [AnyPlugin, unknown] }>() -export const pList = new Map() -export const pMetadata = new Map< - PluginManifest['id'], - { - api: PreInitPluginApi | InitPluginApi | PluginApi | undefined - promises: Promise[] - iflags: number - apiLevel: number - } ->() +export const pList = new Map() +export const pMetadata = new WeakMap() + +export interface InternalPluginMeta { + handleError: (e: unknown) => Promise + promises: Promise[] + cleanups: PluginCleanup[] + iflags: number + apiLevel: number + dependents: AnyPlugin[] + options: PluginOptions +} export function registerPlugin( manifest: PluginManifest, @@ -54,8 +57,7 @@ export function registerPlugin( if (pList.has(manifest.id)) throw new Error(`Plugin with ID "${manifest.id}" already registered`) - const plugin: InternalPlugin = { - _c: [], + const plugin: AnyPlugin = { errors: [], manifest, lifecycles: { @@ -69,25 +71,39 @@ export function registerPlugin( flags, disable: () => disablePlugin(plugin), stop: () => stopPlugin(plugin), + api: undefined, } - pMetadata.set(manifest.id, { - api: undefined, + const meta: InternalPluginMeta = { + cleanups: [], promises: [], iflags, apiLevel: PluginApiLevel.None, - }) + dependents: [], + handleError: e => handlePluginError(e, plugin), + options, + } + + pMetadata.set(plugin, meta) pList.set(manifest.id, plugin) + if (iflags & InternalPluginFlags.ImplicitDependency) + pLeafOrSingleNodes.add(plugin) + // Only add to pending if the plugin is enabled + else if (flags & PluginFlags.Enabled) pPending.add(plugin) + pEmitter.emit('register', plugin, options) } -function handlePluginError(e: unknown, plugin: InternalPlugin) { +async function handlePluginError(e: unknown, plugin: AnyPlugin) { plugin.errors.push(e) plugin.flags |= Flag.Errored - const { api, iflags } = pMetadata.get(plugin.manifest.id)! + const { iflags } = pMetadata.get(plugin)! - ;(api as InitPluginApi).logger.error('Plugin encountered an error', e) + ;(plugin.api as InitPluginApi).logger.error( + 'Plugin encountered an error', + e, + ) nativeLoggingHook( `\u001b[31mPlugin "${plugin.manifest.id}" encountered an error: ${getErrorStack(e)}\u001b[0m`, 2, @@ -95,47 +111,61 @@ function handlePluginError(e: unknown, plugin: InternalPlugin) { pEmitter.emit('errored', plugin, e) - if (!(iflags & InternalPluginFlags.Essential)) return plugin.disable() + if (!(iflags & InternalPluginFlags.Essential)) await plugin.disable() } -function preparePluginPreInit(plugin: InternalPlugin) { - const meta = pMetadata.get(plugin.manifest.id)! +function preparePluginPreInit(plugin: AnyPlugin) { + const meta = pMetadata.get(plugin)! // Clear errors from previous runs plugin.errors = [] plugin.status &= ~Flag.Errored - const api = (meta.api = { + plugin.api = { cleanup: (...items) => { - plugin._c.push(...items) + meta.cleanups.push(...items) }, plugin, unscoped: pUnscopedApi, - }) + decorate: decorator => { + pDecorators.preInit.push([decorator, meta.handleError]) + }, + } satisfies PreInitPluginApi + + decoratePluginApi(pDecorators.preInit, plugin, meta) - pEmitter.emit('preInit', plugin, api) meta.apiLevel = PluginApiLevel.PreInit } -function preparePluginInit(plugin: InternalPlugin) { - const meta = pMetadata.get(plugin.manifest.id)! +function preparePluginInit(plugin: AnyPlugin) { + const meta = pMetadata.get(plugin)! + const api = plugin.api as InitPluginApi + api.decorate = decorator => { + pDecorators.init.push([decorator, meta.handleError]) + } + + decoratePluginApi(pDecorators.init, plugin, meta) - pEmitter.emit('init', plugin, meta.api as InitPluginApi) meta.apiLevel = PluginApiLevel.Init } -function preparePluginStart(plugin: InternalPlugin) { - const meta = pMetadata.get(plugin.manifest.id)! +function preparePluginStart(plugin: AnyPlugin) { + const meta = pMetadata.get(plugin)! + const api = plugin.api as PluginApi + api.decorate = decorator => { + pDecorators.start.push([decorator, meta.handleError]) + } + + decoratePluginApi(pDecorators.start, plugin, meta) - pEmitter.emit('start', plugin, meta.api as PluginApi) meta.apiLevel = PluginApiLevel.Start } -async function disablePlugin(plugin: InternalPlugin) { +async function disablePlugin(plugin: AnyPlugin) { if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${plugin.manifest.id}" is not enabled`) - const iflags = pMetadata.get(plugin.manifest.id)!.iflags ?? 0 + const iflags = pMetadata.get(plugin)!.iflags ?? 0 if (iflags & InternalPluginFlags.Essential) throw new Error( `Plugin "${plugin.manifest.id}" is essential and cannot be disabled`, @@ -151,7 +181,7 @@ async function disablePlugin(plugin: InternalPlugin) { pEmitter.emit('disabled', plugin) } -export function enablePlugin(plugin: InternalPlugin, late: boolean) { +export function enablePlugin(plugin: AnyPlugin, late: boolean) { if (plugin.flags & Flag.Enabled) throw new Error(`Plugin "${plugin.manifest.id}" is already enabled`) @@ -162,7 +192,7 @@ export function enablePlugin(plugin: InternalPlugin, late: boolean) { pEmitter.emit('enabled', plugin) } -export async function preInitPlugin(plugin: InternalPlugin) { +export async function preInitPlugin(plugin: AnyPlugin) { const { manifest: { id }, lifecycles, @@ -170,8 +200,8 @@ export async function preInitPlugin(plugin: InternalPlugin) { try { if (!lifecycles.preInit) return - const meta = pMetadata.get(id)! - const { promises } = meta + const meta = pMetadata.get(plugin)! + const { promises, handleError } = meta if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${id}" is not enabled`) @@ -185,7 +215,7 @@ export async function preInitPlugin(plugin: InternalPlugin) { plugin.status |= Status.PreIniting try { - const prom = lifecycles.preInit(meta.api as PreInitPluginApi) + const prom = lifecycles.preInit(plugin.api as PreInitPluginApi) promises.push(prom) await prom @@ -193,14 +223,14 @@ export async function preInitPlugin(plugin: InternalPlugin) { plugin.status |= Status.PreInited plugin.status &= ~Status.PreIniting } catch (e) { - await handlePluginError(e, plugin) + await handleError(e) } } finally { pEmitter.emit('preInited', plugin) } } -export async function initPlugin(plugin: InternalPlugin) { +export async function initPlugin(plugin: AnyPlugin) { const { manifest: { id }, lifecycles, @@ -209,8 +239,8 @@ export async function initPlugin(plugin: InternalPlugin) { try { if (!lifecycles.init) return - const meta = pMetadata.get(id)! - const { promises, apiLevel } = meta + const meta = pMetadata.get(plugin)! + const { promises, apiLevel, handleError } = meta if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${id}" is not enabled`) @@ -225,7 +255,7 @@ export async function initPlugin(plugin: InternalPlugin) { plugin.status |= Status.Initing try { - const prom = lifecycles.init(meta.api as InitPluginApi) + const prom = lifecycles.init(plugin.api as InitPluginApi) promises.push(prom) await prom @@ -233,14 +263,14 @@ export async function initPlugin(plugin: InternalPlugin) { plugin.status |= Status.Inited plugin.status &= ~Status.Initing } catch (e) { - await handlePluginError(e, plugin) + await handleError(e) } } finally { pEmitter.emit('inited', plugin) } } -export async function startPlugin(plugin: InternalPlugin) { +export async function startPlugin(plugin: AnyPlugin) { const { manifest: { id }, lifecycles, @@ -249,8 +279,8 @@ export async function startPlugin(plugin: InternalPlugin) { try { if (!lifecycles.start) return - const meta = pMetadata.get(id)! - const { promises, apiLevel } = meta + const meta = pMetadata.get(plugin)! + const { promises, apiLevel, handleError } = meta if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${id}" is not enabled`) @@ -266,7 +296,7 @@ export async function startPlugin(plugin: InternalPlugin) { plugin.status |= Status.Starting try { - const prom = lifecycles.start(meta.api as PluginApi) + const prom = lifecycles.start(plugin.api as PluginApi) promises.push(prom) await prom @@ -274,7 +304,7 @@ export async function startPlugin(plugin: InternalPlugin) { plugin.status |= Status.Started plugin.status &= ~Status.Starting } catch (e) { - await handlePluginError(e, plugin) + await handleError(e) } } finally { pEmitter.emit('started', plugin) @@ -283,12 +313,12 @@ export async function startPlugin(plugin: InternalPlugin) { const MaxWaitTime = 5000 -export async function stopPlugin(plugin: InternalPlugin) { +export async function stopPlugin(plugin: AnyPlugin) { const { manifest: { id }, lifecycles, } = plugin - const meta = pMetadata.get(id)! + const meta = pMetadata.get(plugin)! const { promises, iflags, apiLevel } = meta if (iflags & InternalPluginFlags.Essential) @@ -331,7 +361,7 @@ export async function stopPlugin(plugin: InternalPlugin) { if (!lifecycles.stop) return await Promise.race([ - lifecycles.stop(meta.api as PluginApi), + lifecycles.stop(plugin.api as PluginApi), sleepReject( MaxWaitTime, 'Plugin stop lifecycle timed out, force stopping', @@ -350,7 +380,7 @@ export async function stopPlugin(plugin: InternalPlugin) { try { const results = await allSettled( - plugin._c.map(cleanup => cleanup()), + meta.cleanups.map(cleanup => cleanup()), ) await Promise.all( @@ -365,12 +395,12 @@ export async function stopPlugin(plugin: InternalPlugin) { } // Reset APIs - meta.api = undefined + plugin.api = undefined meta.apiLevel = PluginApiLevel.None // Clear temp data meta.promises = [] - plugin._c = [] + meta.cleanups = [] // Reset status plugin.status = 0 @@ -379,10 +409,6 @@ export async function stopPlugin(plugin: InternalPlugin) { } } -export interface InternalPlugin extends Plugin { - _c: PluginCleanup[] -} - export const InternalPluginFlags = { /** * Marks the plugin as internal. @@ -392,6 +418,11 @@ export const InternalPluginFlags = { * Marks the plugin as essential. This means it should not be removed, disabled, or stopped by normal means. */ Essential: 1 << 1, + /** + * Marks the plugin as a leaf node in the plugin graph, implicitly making it a dependency for every plugin (as they are executed first). + * This means it has no dependencies, but may have dependents, and it will be started first. + */ + ImplicitDependency: 1 << 2, } const PluginApiLevel = { diff --git a/lib/plugins/src/init.ts b/lib/plugins/src/init.ts index 9f4fcc0..531d9c6 100644 --- a/lib/plugins/src/init.ts +++ b/lib/plugins/src/init.ts @@ -1,12 +1,8 @@ -import * as AssetsLibrary from '@revenge-mod/assets' -import { initPlugin, pList } from './_internal' -import { pUnscopedApi } from './apis' +import { initPlugin } from './_internal' +import { computePendingNodes, pListOrdered } from './_internal/dependency-graph' import { PluginFlags } from './constants' -import type { UnscopedInitPluginApi } from './types' -// Setup init plugin APIs -const uapi = pUnscopedApi as UnscopedInitPluginApi -uapi.assets = AssetsLibrary +computePendingNodes() -for (const plugin of pList.values()) +for (const plugin of pListOrdered) if (plugin.flags & PluginFlags.Enabled) initPlugin(plugin) diff --git a/lib/plugins/src/preinit.ts b/lib/plugins/src/preinit.ts index f196c9a..78632ad 100644 --- a/lib/plugins/src/preinit.ts +++ b/lib/plugins/src/preinit.ts @@ -1,5 +1,8 @@ -import { pList, preInitPlugin } from './_internal' +import { preInitPlugin } from './_internal' +import { computePendingNodes, pListOrdered } from './_internal/dependency-graph' import { PluginFlags } from './constants' -for (const plugin of pList.values()) +computePendingNodes() + +for (const plugin of pListOrdered) if (plugin.flags & PluginFlags.Enabled) preInitPlugin(plugin) diff --git a/lib/plugins/src/start.ts b/lib/plugins/src/start.ts index a23956c..b6f8696 100644 --- a/lib/plugins/src/start.ts +++ b/lib/plugins/src/start.ts @@ -1,5 +1,8 @@ -import { pList, startPlugin } from '@revenge-mod/plugins/_' +import { startPlugin } from './_internal' +import { computePendingNodes, pListOrdered } from './_internal/dependency-graph' import { PluginFlags } from './constants' -for (const plugin of pList.values()) +computePendingNodes() + +for (const plugin of pListOrdered) if (plugin.flags & PluginFlags.Enabled) startPlugin(plugin) diff --git a/lib/plugins/src/types.ts b/lib/plugins/src/types.ts index 924c9bb..f88af40 100644 --- a/lib/plugins/src/types.ts +++ b/lib/plugins/src/types.ts @@ -12,7 +12,7 @@ export interface PluginApiExtensionsOptions {} * Available in the `preInit` phase. */ export interface UnscopedPreInitPluginApi< - // biome-ignore lint/correctness/noUnusedVariables: This is used for extensions + // biome-ignore lint/correctness/noUnusedVariables: This is for plugin API extensions O extends PluginApiExtensionsOptions = PluginApiExtensionsOptions, > { modules: PluginApiModules @@ -48,9 +48,11 @@ export type PluginCleanupApi = (...fns: PluginCleanup[]) => void * Available in the `preInit` phase. */ export interface PreInitPluginApi< - // biome-ignore lint/correctness/noUnusedVariables: This is used for extensions O extends PluginApiExtensionsOptions = PluginApiExtensionsOptions, > { + decorate( + decorator: (plugin: Plugin, options: O) => void, + ): void unscoped: UnscopedPreInitPluginApi cleanup: PluginCleanupApi plugin: Plugin @@ -60,9 +62,11 @@ export interface PreInitPluginApi< * The plugin API (limited). * Available in the `init` phase. */ +// @ts-expect-error export interface InitPluginApi< O extends PluginApiExtensionsOptions = PluginApiExtensionsOptions, > extends PreInitPluginApi { + decorate(decorator: (plugin: Plugin, options: O) => void): void unscoped: UnscopedInitPluginApi } @@ -70,9 +74,11 @@ export interface InitPluginApi< * The plugin API. * Available in the `start` and `stop` phase. */ +// @ts-expect-error export interface PluginApi< O extends PluginApiExtensionsOptions = PluginApiExtensionsOptions, > extends InitPluginApi { + decorate(decorator: (plugin: Plugin, options: O) => void): void unscoped: UnscopedPluginApi } @@ -113,6 +119,22 @@ export interface PluginManifest { * The icon of the plugin. */ icon?: string + /** + * The dependencies of the plugin. + */ + dependencies?: PluginDependency[] +} + +export interface PluginDependency { + /** + * The ID of this dependency. + */ + id: string + // TODO(plugins): support plugin bundles + // /** + // * The bundle of this dependency. + // */ + // bundle: PluginBundle } export interface PluginOptions< @@ -154,6 +176,7 @@ export interface PluginLifecycles< export interface Plugin< O extends PluginApiExtensionsOptions = PluginApiExtensionsOptions, + S extends keyof PluginApiInStage = keyof PluginApiInStage, > { // TODO(plugins): support plugin bundles // /** @@ -197,6 +220,20 @@ export interface Plugin< * Stop the plugin. */ stop(): Promise + + /** + * The plugin API. + * + * Not recommended to use this directly. + */ + api: PluginApiInStage[S] +} + +type PluginApiInStage = { + Register: undefined + PreInit: PreInitPluginApi + Init: InitPluginApi + Start: PluginApi } export interface PluginSettingsComponent< diff --git a/lib/storage/package.json b/lib/storage/package.json index 0d1165a..2efd716 100644 --- a/lib/storage/package.json +++ b/lib/storage/package.json @@ -5,12 +5,6 @@ "exports": { ".": { "default": "./src/index.ts" - }, - "./preinit": { - "default": "./src/preinit.ts" - }, - "./init": { - "default": "./src/init.ts" } } -} +} \ No newline at end of file diff --git a/lib/storage/src/_internal.ts b/lib/storage/src/_internal.ts new file mode 100644 index 0000000..53a3577 --- /dev/null +++ b/lib/storage/src/_internal.ts @@ -0,0 +1,6 @@ +import type { Storage, StorageSubscription } from '.' + +export const instanceToSubscriptions = new WeakMap< + Storage, + Set> +>() diff --git a/lib/storage/src/index.ts b/lib/storage/src/index.ts index 021d491..e3bc711 100644 --- a/lib/storage/src/index.ts +++ b/lib/storage/src/index.ts @@ -3,6 +3,7 @@ import { FileModule } from '@revenge-mod/discord/native' import { getErrorStack } from '@revenge-mod/utils/error' import { mergeDeep } from '@revenge-mod/utils/object' +import { instanceToSubscriptions } from './_internal' import type { AnyObject, DeepPartial } from '@revenge-mod/utils/types' export type StorageSubscription = ( @@ -20,7 +21,8 @@ export function Storage( const dirPath = directory === 'cache' ? CacheDirPath : DocumentsDirPath const fullPath = `${dirPath}/${path}` - this._s = new Set>() + const subscriptions = new Set>() + instanceToSubscriptions.set(this, subscriptions) this.loaded = false this.cache = options?.default @@ -41,7 +43,7 @@ export function Storage( this.loaded = true try { const cache = (this.cache = JSON.parse(contents)) - for (const sub of this._s) sub(cache) + for (const sub of subscriptions) sub(cache) return cache } catch (e) { nativeLoggingHook( @@ -60,7 +62,7 @@ export function Storage( const contents = JSON.stringify(this.cache) await FileModule.writeFile(directory, path, contents, 'utf8') - for (const sub of this._s) sub(value) + for (const sub of subscriptions) sub(value) } catch (e) { nativeLoggingHook( `Failed to write storage (<${directory}>/${path}): ${getErrorStack(e)}`, @@ -114,10 +116,6 @@ export type UseStorageFilter = ( ) => any export interface Storage { - /** - * @internal - */ - _s: Set> /** * Whether the storage has been loaded. If the storage is not loaded, `storage.cache` may be `undefined`. * If you have `options.default` set, you can use this property to check if `storage.cache` is the default value or not. diff --git a/lib/storage/src/init.ts b/lib/storage/src/init.ts deleted file mode 100644 index c6d8956..0000000 --- a/lib/storage/src/init.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { pEmitter } from '@revenge-mod/plugins/_' -import { PluginsStorageDirectory } from '@revenge-mod/plugins/constants' -import { defineLazyProperty } from '@revenge-mod/utils/object' -import { useReRender } from '@revenge-mod/utils/react' -import { useEffect } from 'react' -import { getStorage, Storage } from '.' -import type { StorageSubscription } from '.' - -// Actual implementation of Storage#use -const SP: Storage = Storage.prototype -SP.use = function (filter) { - if (!this.cache) this.get() - - const reRender = useReRender() - - useEffect(() => { - const sub: StorageSubscription = filter - ? v => { - if (filter(v)) reRender() - } - : reRender - - this._s.add(sub) - - return () => { - this._s.delete(sub) - } - }, []) - - return this.cache -} - -pEmitter.on('init', ({ _s: storageOptions, manifest: { id } }, api) => - defineLazyProperty(api, 'storage', () => - getStorage(`${PluginsStorageDirectory}/${id}.json`, { - ...storageOptions, - directory: 'documents', - }), - ), -) diff --git a/lib/storage/src/preinit.ts b/lib/storage/src/preinit.ts deleted file mode 100644 index 2f5891c..0000000 --- a/lib/storage/src/preinit.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { pEmitter } from '@revenge-mod/plugins/_' - -pEmitter.on('register', (plugin, options) => { - plugin._s = options.storage -}) diff --git a/lib/storage/src/types.ts b/lib/storage/src/types.ts index 928c316..c89ee09 100644 --- a/lib/storage/src/types.ts +++ b/lib/storage/src/types.ts @@ -1,18 +1,13 @@ -import type { - PluginApiExtensionsOptions, - 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 PluginApiExtensionsOptions { - storage: AnyObject + storage?: AnyObject } export interface PluginOptions { - storage?: Omit, 'directory'> + storage?: Omit>, 'directory'> } export interface InitPluginApi { @@ -24,19 +19,6 @@ declare module '@revenge-mod/plugins/types' { * * To preload storage, simply call `api.storage.get()`. */ - storage: Storage - } -} - -declare module '@revenge-mod/plugins/_' { - export function registerPlugin( - manifest: PluginManifest, - options: PluginOptions, - flags: number, - iflags: number, - ): void - - export interface InternalPlugin { - _s?: StorageOptions + storage: Storage> } } diff --git a/lib/utils/package.json b/lib/utils/package.json index 7a9d939..2369078 100644 --- a/lib/utils/package.json +++ b/lib/utils/package.json @@ -35,12 +35,6 @@ }, "./types": { "types": "./src/types.ts" - }, - "./preinit": { - "default": "./src/preinit.ts" - }, - "./init": { - "default": "./src/init.ts" } } -} +} \ No newline at end of file diff --git a/lib/utils/src/init.ts b/lib/utils/src/init.ts deleted file mode 100644 index 2b13a35..0000000 --- a/lib/utils/src/init.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { pUnscopedApi } from '@revenge-mod/plugins/_' -import type { PluginApiUtils } from './preinit' - -const utils = pUnscopedApi.utils as PluginApiUtils - -utils.discord = require('@revenge-mod/utils/discord') -utils.react = require('@revenge-mod/utils/react') diff --git a/lib/utils/src/preinit.ts b/lib/utils/src/preinit.ts deleted file mode 100644 index 9233831..0000000 --- a/lib/utils/src/preinit.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { pUnscopedApi } from '@revenge-mod/plugins/_' - -pUnscopedApi.utils = { - callback: require('@revenge-mod/utils/callback'), - error: require('@revenge-mod/utils/error'), - object: require('@revenge-mod/utils/object'), - promise: require('@revenge-mod/utils/promise'), - proxy: require('@revenge-mod/utils/proxy'), - tree: require('@revenge-mod/utils/tree'), -} - -export interface PreInitPluginApiUtils { - callback: typeof import('@revenge-mod/utils/callback') - error: typeof import('@revenge-mod/utils/error') - object: typeof import('@revenge-mod/utils/object') - promise: typeof import('@revenge-mod/utils/promise') - proxy: typeof import('@revenge-mod/utils/proxy') - tree: typeof import('@revenge-mod/utils/tree') -} - -export interface PluginApiUtils extends PreInitPluginApiUtils { - react: typeof import('@revenge-mod/utils/react') - discord: typeof import('@revenge-mod/utils/discord') -} - -declare module '@revenge-mod/plugins/types' { - export interface UnscopedPreInitPluginApi { - utils: PreInitPluginApiUtils - } - - export interface UnscopedInitPluginApi { - utils: PluginApiUtils - } -} diff --git a/lib/utils/src/types.ts b/lib/utils/src/types.ts index 68a9a27..d550a55 100644 --- a/lib/utils/src/types.ts +++ b/lib/utils/src/types.ts @@ -12,3 +12,29 @@ export type DeepPartial = { export type ExtractPredicate = T extends (arg: any) => arg is infer R ? R : never + +/// PLUGIN API EXTENSIONS + +export interface PreInitPluginApiUtils { + callback: typeof import('@revenge-mod/utils/callback') + error: typeof import('@revenge-mod/utils/error') + object: typeof import('@revenge-mod/utils/object') + promise: typeof import('@revenge-mod/utils/promise') + proxy: typeof import('@revenge-mod/utils/proxy') + tree: typeof import('@revenge-mod/utils/tree') +} + +export interface PluginApiUtils extends PreInitPluginApiUtils { + react: typeof import('@revenge-mod/utils/react') + discord: typeof import('@revenge-mod/utils/discord') +} + +declare module '@revenge-mod/plugins/types' { + export interface UnscopedPreInitPluginApi { + utils: PreInitPluginApiUtils + } + + export interface UnscopedInitPluginApi { + utils: PluginApiUtils + } +} diff --git a/src/init.ts b/src/init.ts index 277743e..0ae64a3 100644 --- a/src/init.ts +++ b/src/init.ts @@ -1,10 +1,5 @@ // Initialize init libraries -import '@revenge-mod/utils/init' import '@revenge-mod/react/init' -import '@revenge-mod/storage/init' -import '@revenge-mod/externals/init' -import '@revenge-mod/discord/init' -import '@revenge-mod/components/init' // Run all init plugins import '~/plugins/init' diff --git a/src/plugins/init/api.assets/index.ts b/src/plugins/init/api.assets/index.ts new file mode 100644 index 0000000..9bf1376 --- /dev/null +++ b/src/plugins/init/api.assets/index.ts @@ -0,0 +1,23 @@ +import * as assets from '@revenge-mod/assets' +import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' +import { PluginFlags } from '@revenge-mod/plugins/constants' + +registerPlugin( + { + id: 'revenge.api.assets', + name: 'Assets API', + description: '@revenge-mod/assets API for plugins.', + author: 'Revenge', + icon: 'PollsIcon', + }, + { + init({ unscoped }) { + unscoped.assets = assets + }, + }, + PluginFlags.Enabled, + // biome-ignore format: Don't format this + InternalPluginFlags.Internal | + InternalPluginFlags.Essential | + InternalPluginFlags.ImplicitDependency, +) diff --git a/src/plugins/init/api.components/index.ts b/src/plugins/init/api.components/index.ts new file mode 100644 index 0000000..6f4a59d --- /dev/null +++ b/src/plugins/init/api.components/index.ts @@ -0,0 +1,27 @@ +import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' +import { PluginFlags } from '@revenge-mod/plugins/constants' +import { defineLazyProperties } from '@revenge-mod/utils/object' + +registerPlugin( + { + id: 'revenge.api.components', + name: 'Components API', + description: '@revenge-mod/components API for plugins.', + author: 'Revenge', + icon: 'PollsIcon', + }, + { + init({ unscoped }) { + defineLazyProperties(unscoped, { + components: () => { + return require('@revenge-mod/components') + }, + }) + }, + }, + PluginFlags.Enabled, + // biome-ignore format: Don't format this + InternalPluginFlags.Internal | + InternalPluginFlags.Essential | + InternalPluginFlags.ImplicitDependency, +) diff --git a/src/plugins/init/api.discord/index.ts b/src/plugins/init/api.discord/index.ts new file mode 100644 index 0000000..e9e6363 --- /dev/null +++ b/src/plugins/init/api.discord/index.ts @@ -0,0 +1,60 @@ +import { Logger } from '@revenge-mod/discord/common' +import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' +import { PluginFlags } from '@revenge-mod/plugins/constants' +import { + defineLazyProperties, + defineLazyProperty, +} from '@revenge-mod/utils/object' +import type { PluginApiDiscord } from '@revenge-mod/discord/types' + +registerPlugin( + { + id: 'revenge.api.discord', + name: 'Discord API', + description: '@revenge-mod/discord API for plugins.', + author: 'Revenge', + icon: 'PollsIcon', + }, + { + init({ unscoped, decorate }) { + decorate(plugin => { + defineLazyProperty( + plugin.api, + 'logger', + () => + new Logger(`Revenge > Plugins (${plugin.manifest.id})`), + ) + }) + + defineLazyProperties((unscoped.discord = {} as PluginApiDiscord), { + actions: () => { + return require('@revenge-mod/discord/actions') + }, + common: () => { + return require('@revenge-mod/discord/common') + }, + design: () => { + return require('@revenge-mod/discord/design') + }, + native: () => { + return require('@revenge-mod/discord/native') + }, + modules: () => + defineLazyProperties({} as PluginApiDiscord.Modules, { + mainTabsV2: () => { + return require('@revenge-mod/discord/modules/main_tabs_v2') + }, + settings: () => ({ + ...require('@revenge-mod/discord/modules/settings'), + renderer: require('@revenge-mod/discord/modules/settings/renderer'), + }), + }), + }) + }, + }, + PluginFlags.Enabled, + // biome-ignore format: Don't format this + InternalPluginFlags.Internal | + InternalPluginFlags.Essential | + InternalPluginFlags.ImplicitDependency, +) diff --git a/src/plugins/init/api.externals/index.ts b/src/plugins/init/api.externals/index.ts new file mode 100644 index 0000000..6499139 --- /dev/null +++ b/src/plugins/init/api.externals/index.ts @@ -0,0 +1,40 @@ +import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' +import { PluginFlags } from '@revenge-mod/plugins/constants' +import { defineLazyProperties } from '@revenge-mod/utils/object' +import type { PluginApiExternals } from '@revenge-mod/externals/types' + +registerPlugin( + { + id: 'revenge.api.externals', + name: 'Externals API', + description: '@revenge-mod/externals API for plugins.', + author: 'Revenge', + icon: 'PollsIcon', + }, + { + init({ unscoped }) { + defineLazyProperties( + (unscoped.externals = {} as PluginApiExternals), + { + Browserify: () => { + return require('@revenge-mod/externals/browserify') + }, + ReactNativeClipboard: () => { + return require('@revenge-mod/externals/react-native-clipboard') + }, + ReactNavigation: () => { + return require('@revenge-mod/externals/react-navigation') + }, + Shopify: () => { + return require('@revenge-mod/externals/shopify') + }, + }, + ) + }, + }, + PluginFlags.Enabled, + // biome-ignore format: Don't format this + InternalPluginFlags.Internal | + InternalPluginFlags.Essential | + InternalPluginFlags.ImplicitDependency, +) diff --git a/src/plugins/init/settings/components/FilterAndSortActionSheet.tsx b/src/plugins/init/settings/components/FilterAndSortActionSheet.tsx index 275104e..42d4e8f 100644 --- a/src/plugins/init/settings/components/FilterAndSortActionSheet.tsx +++ b/src/plugins/init/settings/components/FilterAndSortActionSheet.tsx @@ -2,14 +2,14 @@ import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon' import { Design } from '@revenge-mod/discord/design' import { useState } from 'react' import type { AssetId } from '@revenge-mod/assets/types' -import type { InternalPlugin } from '@revenge-mod/plugins/_' +import type { AnyPlugin } from '@revenge-mod/plugins/_' export interface FilterAndSortActionSheetProps { filters: Record< string, { icon: AssetId - filter: (p: InternalPlugin, iflags: number) => boolean + filter: (p: AnyPlugin, iflags: number) => boolean } > filter: string[] @@ -18,7 +18,7 @@ export interface FilterAndSortActionSheetProps { setMatchAll: (matchAll: boolean) => void sorts: Record< string, - [icon: AssetId, (a: InternalPlugin, b: InternalPlugin) => number] + [icon: AssetId, (a: AnyPlugin, b: AnyPlugin) => number] > sort: string setSort: (fn: string) => void diff --git a/src/plugins/init/settings/components/PluginCard.tsx b/src/plugins/init/settings/components/PluginCard.tsx index 53f852b..21f01f9 100644 --- a/src/plugins/init/settings/components/PluginCard.tsx +++ b/src/plugins/init/settings/components/PluginCard.tsx @@ -20,7 +20,7 @@ import { setEnablePluginTooltipVisible, setEssentialPluginTooltipVisible, } from './TooltipProvider' -import type { InternalPlugin } from '@revenge-mod/plugins/_' +import type { AnyPlugin } from '@revenge-mod/plugins/_' import type { View } from 'react-native' const { Card, Text, Stack, IconButton, createStyles } = Design @@ -34,7 +34,7 @@ export function InstalledPluginCard({ enabled, rightGap, }: { - plugin: InternalPlugin + plugin: AnyPlugin iflags: number enabled: boolean rightGap?: boolean diff --git a/src/plugins/init/settings/definitions/RevengePluginsSetting.tsx b/src/plugins/init/settings/definitions/RevengePluginsSetting.tsx index 81ff0bc..0892634 100644 --- a/src/plugins/init/settings/definitions/RevengePluginsSetting.tsx +++ b/src/plugins/init/settings/definitions/RevengePluginsSetting.tsx @@ -8,7 +8,7 @@ import { useReRender } from '@revenge-mod/utils/react' import { useEffect } from 'react' import { RouteNames, Setting } from '../constants' import type { SettingsItem } from '@revenge-mod/discord/modules/settings' -import type { InternalPlugin } from '@revenge-mod/plugins/_' +import type { AnyPlugin } from '@revenge-mod/plugins/_' const RevengePluginsSetting: SettingsItem = { parent: null, @@ -58,8 +58,8 @@ const { AlertActionButton, AlertModal, Text } = Design pEmitter.on('started', showReloadRequiredAlertIfNeeded) pEmitter.on('stopped', showReloadRequiredAlertIfNeeded) -function showReloadRequiredAlertIfNeeded(plugin: InternalPlugin) { - if (plugin.flags & PluginFlags.ReloadRequired) { +function showReloadRequiredAlertIfNeeded(plugin: AnyPlugin) { + if (plugin.flags & PluginFlags.ReloadRequired) AlertActionCreators.openAlert( 'plugin-reload-required', , ) - } } export default RevengePluginsSetting diff --git a/src/plugins/init/settings/register-routes.tsx b/src/plugins/init/settings/register-routes.tsx index 41fb5f5..36655e8 100644 --- a/src/plugins/init/settings/register-routes.tsx +++ b/src/plugins/init/settings/register-routes.tsx @@ -1,6 +1,6 @@ import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon' import { registerSettingsItem } from '@revenge-mod/discord/modules/settings' -import { pEmitter, pMetadata } from '@revenge-mod/plugins/_' +import { pEmitter } from '@revenge-mod/plugins/_' import { lazy, useLayoutEffect } from 'react' import { Setting } from './constants' import type { StackScreenProps } from '@react-navigation/stack' @@ -13,7 +13,7 @@ const NavigatorHeaderWithIcon = lazy( pEmitter.on('started', plugin => { if (plugin.SettingsComponent) { - const api = pMetadata.get(plugin.manifest.id)!.api as PluginApi + const api = plugin.api as PluginApi function PluginSettings({ navigation, diff --git a/src/plugins/init/settings/screens/RevengePluginsSettingScreen.tsx b/src/plugins/init/settings/screens/RevengePluginsSettingScreen.tsx index a9f26bc..3404dea 100644 --- a/src/plugins/init/settings/screens/RevengePluginsSettingScreen.tsx +++ b/src/plugins/init/settings/screens/RevengePluginsSettingScreen.tsx @@ -22,7 +22,7 @@ import { import { useUpdateOnPluginStatesChange } from '../definitions/RevengePluginsSetting' import type { RouteProp } from '@react-navigation/core' import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation' -import type { InternalPlugin } from '@revenge-mod/plugins/_' +import type { AnyPlugin } from '@revenge-mod/plugins/_' import type { FilterAndSortActionSheetProps } from '../components/FilterAndSortActionSheet' import type { RouteNames, Setting } from '../constants' @@ -108,11 +108,7 @@ function Screen() { const allPlugins = useMemo( () => [...pList.values()].map( - plugin => - [ - plugin, - pMetadata.get(plugin.manifest.id)!.iflags, - ] as const, + plugin => [plugin, pMetadata.get(plugin)!.iflags] as const, ), [], ) @@ -194,7 +190,7 @@ function Screen() { function PluginMasonryFlashList({ plugins, }: { - plugins: (readonly [InternalPlugin, number])[] + plugins: (readonly [AnyPlugin, number])[] }) { const { width, height } = useWindowDimensions() const numColumns = Math.floor((width - 16) / 448) diff --git a/src/plugins/preinit/api.storage/define-storage-use.ts b/src/plugins/preinit/api.storage/define-storage-use.ts new file mode 100644 index 0000000..b78dfc8 --- /dev/null +++ b/src/plugins/preinit/api.storage/define-storage-use.ts @@ -0,0 +1,30 @@ +import { Storage } from '@revenge-mod/storage' +import { useReRender } from '@revenge-mod/utils/react' +import { useEffect } from 'react' +import { instanceToSubscriptions } from '../../../../lib/storage/src/_internal' +import type { StorageSubscription } from '@revenge-mod/storage' + +// Actual implementation of Storage#use +const SP: Storage = Storage.prototype +SP.use = function (filter) { + if (!this.cache) this.get() + + const reRender = useReRender() + const subscriptions = instanceToSubscriptions.get(this)! + + useEffect(() => { + const sub: StorageSubscription = filter + ? v => { + if (filter(v)) reRender() + } + : reRender + + subscriptions.add(sub) + + return () => { + subscriptions.delete(sub) + } + }, [filter, reRender, subscriptions.add, subscriptions.delete]) + + return this.cache +} diff --git a/src/plugins/preinit/api.storage/index.ts b/src/plugins/preinit/api.storage/index.ts new file mode 100644 index 0000000..e7cd4d8 --- /dev/null +++ b/src/plugins/preinit/api.storage/index.ts @@ -0,0 +1,48 @@ +import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' +import { + PluginFlags, + PluginsStorageDirectory, +} from '@revenge-mod/plugins/constants' +import { getStorage } from '@revenge-mod/storage' +import { defineLazyProperty } from '@revenge-mod/utils/object' +import type { AnyPlugin } from '@revenge-mod/plugins/_' +import type { StorageOptions } from '@revenge-mod/storage' + +const storageOptions = new WeakMap() + +registerPlugin( + { + id: 'revenge.api.storage', + name: 'Storage API', + description: '@revenge-mod/storage API for plugins.', + author: 'Revenge', + icon: 'PollsIcon', + }, + { + preInit({ decorate }) { + decorate((plugin, { storage }) => { + if (storage) storageOptions.set(plugin, storage) + }) + }, + init({ decorate }) { + require('./define-storage-use') + + decorate(plugin => { + defineLazyProperty(plugin.api, 'storage', () => + getStorage( + `${PluginsStorageDirectory}/${plugin.manifest.id}.json`, + { + ...storageOptions.get(plugin), + directory: 'documents', + }, + ), + ) + }) + }, + }, + PluginFlags.Enabled, + // biome-ignore format: Don't format this + InternalPluginFlags.Internal | + InternalPluginFlags.Essential | + InternalPluginFlags.ImplicitDependency, +) diff --git a/src/plugins/preinit/api.utils/index.ts b/src/plugins/preinit/api.utils/index.ts new file mode 100644 index 0000000..8e8dd11 --- /dev/null +++ b/src/plugins/preinit/api.utils/index.ts @@ -0,0 +1,33 @@ +import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_' +import { PluginFlags } from '@revenge-mod/plugins/constants' + +registerPlugin( + { + id: 'revenge.api.utils', + name: 'Utils API', + description: '@revenge-mod/utils API for plugins.', + author: 'Revenge', + icon: 'PollsIcon', + }, + { + preInit({ unscoped }) { + unscoped.utils = { + callback: require('@revenge-mod/utils/callback'), + error: require('@revenge-mod/utils/error'), + object: require('@revenge-mod/utils/object'), + promise: require('@revenge-mod/utils/promise'), + proxy: require('@revenge-mod/utils/proxy'), + tree: require('@revenge-mod/utils/tree'), + } + }, + init({ unscoped: { utils } }) { + utils.discord = require('@revenge-mod/utils/discord') + utils.react = require('@revenge-mod/utils/react') + }, + }, + PluginFlags.Enabled, + // biome-ignore format: Don't format this + InternalPluginFlags.Internal | + InternalPluginFlags.Essential | + InternalPluginFlags.ImplicitDependency, +) diff --git a/src/preinit.ts b/src/preinit.ts index f076b9e..0ef03e3 100644 --- a/src/preinit.ts +++ b/src/preinit.ts @@ -19,10 +19,8 @@ onModuleFirstRequired(IndexModuleId, function onIndexRequired() { nativeLoggingHook(`\u001b[31m--- PREINIT STAGE ---\u001b[0m`, 1) // Initialize preinit libraries - require('@revenge-mod/utils/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