feat: add ability to decorate plugin APIs, move private properties to WeakMap stores
This commit is contained in:
parent
78614c1cc9
commit
2e455cf2c5
39 changed files with 595 additions and 332 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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('.')
|
||||
})
|
||||
|
|
@ -24,9 +24,6 @@
|
|||
"./native": {
|
||||
"default": "./src/native.ts"
|
||||
},
|
||||
"./init": {
|
||||
"default": "./src/init.ts"
|
||||
},
|
||||
"./preinit": {
|
||||
"default": "./src/preinit.ts"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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})`),
|
||||
),
|
||||
)
|
||||
|
|
@ -17,6 +17,7 @@ import type {
|
|||
|
||||
export * from './native'
|
||||
export * from './polyfills'
|
||||
export * from './revenge'
|
||||
|
||||
export namespace DiscordModules {
|
||||
export namespace Flux {
|
||||
|
|
|
|||
5
lib/externals/package.json
vendored
5
lib/externals/package.json
vendored
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
lib/externals/src/init.ts
vendored
23
lib/externals/src/init.ts
vendored
|
|
@ -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')
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
"types": "./src/types.ts"
|
||||
},
|
||||
"./_": {
|
||||
"default": "./src/_internal.ts"
|
||||
"default": "./src/_internal/index.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
36
lib/plugins/src/_internal/decorators.ts
Normal file
36
lib/plugins/src/_internal/decorators.ts
Normal file
|
|
@ -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<A['decorate']>[0], handleError: (e: unknown) => void]
|
||||
|
||||
export const pDecorators: {
|
||||
preInit: PluginApiDecoratorStore<PreInitPluginApi>[]
|
||||
init: PluginApiDecoratorStore<InitPluginApi>[]
|
||||
start: PluginApiDecoratorStore<PluginApi>[]
|
||||
} = {
|
||||
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<any, any>, meta.options)
|
||||
} catch (e) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
78
lib/plugins/src/_internal/dependency-graph.ts
Normal file
78
lib/plugins/src/_internal/dependency-graph.ts
Normal file
|
|
@ -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<AnyPlugin>()
|
||||
export const pLeafOrSingleNodes = new Set<AnyPlugin>()
|
||||
|
||||
// Ordered list of plugins to be started
|
||||
export const pListOrdered = new Set<AnyPlugin>()
|
||||
// Pending plugins to be computed
|
||||
export const pPending = new Set<AnyPlugin>()
|
||||
|
||||
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<PluginManifest['id'], [PluginManifest, Flags: number, PluginCode: string]>
|
||||
// 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)
|
||||
}
|
||||
|
|
@ -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<any, any>
|
||||
|
||||
export const pUnscopedApi = uapi
|
||||
|
||||
export const pEmitter = new TypedEventEmitter<{
|
||||
register: [InternalPlugin, PluginOptions<any>]
|
||||
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<any>, update?: true]
|
||||
disabled: [AnyPlugin]
|
||||
enabled: [AnyPlugin]
|
||||
preInited: [AnyPlugin]
|
||||
inited: [AnyPlugin]
|
||||
started: [AnyPlugin]
|
||||
stopped: [AnyPlugin]
|
||||
errored: [AnyPlugin, unknown]
|
||||
}>()
|
||||
|
||||
export const pList = new Map<PluginManifest['id'], InternalPlugin>()
|
||||
export const pMetadata = new Map<
|
||||
PluginManifest['id'],
|
||||
{
|
||||
api: PreInitPluginApi | InitPluginApi | PluginApi | undefined
|
||||
promises: Promise<void>[]
|
||||
iflags: number
|
||||
apiLevel: number
|
||||
}
|
||||
>()
|
||||
export const pList = new Map<PluginManifest['id'], AnyPlugin>()
|
||||
export const pMetadata = new WeakMap<AnyPlugin, InternalPluginMeta>()
|
||||
|
||||
export interface InternalPluginMeta {
|
||||
handleError: (e: unknown) => Promise<void>
|
||||
promises: Promise<void>[]
|
||||
cleanups: PluginCleanup[]
|
||||
iflags: number
|
||||
apiLevel: number
|
||||
dependents: AnyPlugin[]
|
||||
options: PluginOptions<any>
|
||||
}
|
||||
|
||||
export function registerPlugin<O extends PluginApiExtensionsOptions>(
|
||||
manifest: PluginManifest,
|
||||
|
|
@ -54,8 +57,7 @@ export function registerPlugin<O extends PluginApiExtensionsOptions>(
|
|||
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<O extends PluginApiExtensionsOptions>(
|
|||
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<any> {
|
||||
_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 = {
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<O, 'PreInit'>, 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<O> {
|
||||
decorate(decorator: (plugin: Plugin<O, 'Init'>, 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<O> {
|
||||
decorate(decorator: (plugin: Plugin<O, 'Start'>, 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<O> = keyof PluginApiInStage<O>,
|
||||
> {
|
||||
// TODO(plugins): support plugin bundles
|
||||
// /**
|
||||
|
|
@ -197,6 +220,20 @@ export interface Plugin<
|
|||
* Stop the plugin.
|
||||
*/
|
||||
stop(): Promise<void>
|
||||
|
||||
/**
|
||||
* The plugin API.
|
||||
*
|
||||
* Not recommended to use this directly.
|
||||
*/
|
||||
api: PluginApiInStage<O>[S]
|
||||
}
|
||||
|
||||
type PluginApiInStage<O extends PluginApiExtensionsOptions> = {
|
||||
Register: undefined
|
||||
PreInit: PreInitPluginApi<O>
|
||||
Init: InitPluginApi<O>
|
||||
Start: PluginApi<O>
|
||||
}
|
||||
|
||||
export interface PluginSettingsComponent<
|
||||
|
|
|
|||
|
|
@ -5,12 +5,6 @@
|
|||
"exports": {
|
||||
".": {
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./preinit": {
|
||||
"default": "./src/preinit.ts"
|
||||
},
|
||||
"./init": {
|
||||
"default": "./src/init.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
lib/storage/src/_internal.ts
Normal file
6
lib/storage/src/_internal.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import type { Storage, StorageSubscription } from '.'
|
||||
|
||||
export const instanceToSubscriptions = new WeakMap<
|
||||
Storage<any>,
|
||||
Set<StorageSubscription<any>>
|
||||
>()
|
||||
|
|
@ -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<T extends AnyObject = AnyObject> = (
|
||||
|
|
@ -20,7 +21,8 @@ export function Storage<T extends AnyObject>(
|
|||
const dirPath = directory === 'cache' ? CacheDirPath : DocumentsDirPath
|
||||
const fullPath = `${dirPath}/${path}`
|
||||
|
||||
this._s = new Set<StorageSubscription<T>>()
|
||||
const subscriptions = new Set<StorageSubscription<T>>()
|
||||
instanceToSubscriptions.set(this, subscriptions)
|
||||
|
||||
this.loaded = false
|
||||
this.cache = options?.default
|
||||
|
|
@ -41,7 +43,7 @@ export function Storage<T extends AnyObject>(
|
|||
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<T extends AnyObject>(
|
|||
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<T extends AnyObject> = (
|
|||
) => any
|
||||
|
||||
export interface Storage<T extends AnyObject> {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_s: Set<StorageSubscription<T>>
|
||||
/**
|
||||
* 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.
|
||||
|
|
|
|||
|
|
@ -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<any> = 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',
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { pEmitter } from '@revenge-mod/plugins/_'
|
||||
|
||||
pEmitter.on('register', (plugin, options) => {
|
||||
plugin._s = options.storage
|
||||
})
|
||||
|
|
@ -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<O extends PluginApiExtensionsOptions> {
|
||||
storage?: Omit<StorageOptions<O['storage']>, 'directory'>
|
||||
storage?: Omit<StorageOptions<NonNullable<O['storage']>>, 'directory'>
|
||||
}
|
||||
|
||||
export interface InitPluginApi<O extends PluginApiExtensionsOptions> {
|
||||
|
|
@ -24,19 +19,6 @@ declare module '@revenge-mod/plugins/types' {
|
|||
*
|
||||
* To preload storage, simply call `api.storage.get()`.
|
||||
*/
|
||||
storage: Storage<O['storage']>
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@revenge-mod/plugins/_' {
|
||||
export function registerPlugin<E extends PluginApiExtensionsOptions>(
|
||||
manifest: PluginManifest,
|
||||
options: PluginOptions<E>,
|
||||
flags: number,
|
||||
iflags: number,
|
||||
): void
|
||||
|
||||
export interface InternalPlugin {
|
||||
_s?: StorageOptions<AnyObject>
|
||||
storage: Storage<NonNullable<O['storage']>>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,12 +35,6 @@
|
|||
},
|
||||
"./types": {
|
||||
"types": "./src/types.ts"
|
||||
},
|
||||
"./preinit": {
|
||||
"default": "./src/preinit.ts"
|
||||
},
|
||||
"./init": {
|
||||
"default": "./src/init.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -12,3 +12,29 @@ export type DeepPartial<T> = {
|
|||
export type ExtractPredicate<T> = 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
23
src/plugins/init/api.assets/index.ts
Normal file
23
src/plugins/init/api.assets/index.ts
Normal file
|
|
@ -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,
|
||||
)
|
||||
27
src/plugins/init/api.components/index.ts
Normal file
27
src/plugins/init/api.components/index.ts
Normal file
|
|
@ -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,
|
||||
)
|
||||
60
src/plugins/init/api.discord/index.ts
Normal file
60
src/plugins/init/api.discord/index.ts
Normal file
|
|
@ -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,
|
||||
)
|
||||
40
src/plugins/init/api.externals/index.ts
Normal file
40
src/plugins/init/api.externals/index.ts
Normal file
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
<AlertModal
|
||||
|
|
@ -87,7 +87,6 @@ function showReloadRequiredAlertIfNeeded(plugin: InternalPlugin) {
|
|||
}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default RevengePluginsSetting
|
||||
|
|
|
|||
|
|
@ -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<any>
|
||||
const api = plugin.api as PluginApi<any>
|
||||
|
||||
function PluginSettings({
|
||||
navigation,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
30
src/plugins/preinit/api.storage/define-storage-use.ts
Normal file
30
src/plugins/preinit/api.storage/define-storage-use.ts
Normal file
|
|
@ -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<any> = 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
|
||||
}
|
||||
48
src/plugins/preinit/api.storage/index.ts
Normal file
48
src/plugins/preinit/api.storage/index.ts
Normal file
|
|
@ -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<AnyPlugin, StorageOptions>()
|
||||
|
||||
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,
|
||||
)
|
||||
33
src/plugins/preinit/api.utils/index.ts
Normal file
33
src/plugins/preinit/api.utils/index.ts
Normal file
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue