chore: format

This commit is contained in:
PalmDevs 2025-06-03 16:24:58 +07:00
parent c92adf856f
commit 2b905801c3
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
103 changed files with 1389 additions and 755 deletions

View file

@ -3,4 +3,7 @@ import type { Asset, AssetId } from './types'
export const _customs = new Set<Asset>()
export const _overrides = new WeakMap<Asset, Asset>()
export const _metas = new WeakMap<Asset, [assetId: AssetId, moduleId: Metro.ModuleID]>()
export const _metas = new WeakMap<
Asset,
[assetId: AssetId, moduleId: Metro.ModuleID]
>()

View file

@ -5,7 +5,6 @@ import { getStorage } from '@revenge-mod/storage'
import { createLogger } from '@revenge-mod/utils/logger'
import { mergeDeep } from '@revenge-mod/utils/objects'
import { proxify } from '@revenge-mod/utils/proxy'
import type { Metro } from '@revenge-mod/modules/types'
import type { Asset } from './types'

View file

@ -1,16 +1,15 @@
import { ReactNative } from '@revenge-mod/react'
import { _customs, _metas, _overrides } from './_internal'
import { cache } from './caches'
import { AssetRegistry } from './preinit'
import type { Metro } from '@revenge-mod/modules/types'
import type { Asset, AssetId, RegisterableAsset } from './types'
export { AssetRegistry, AssetRegistryModuleId } from './preinit'
// iOS cannot display SVGs
let _preferredType: Asset['type'] = ReactNative.Platform.OS === 'ios' ? 'png' : 'svg'
let _preferredType: Asset['type'] =
ReactNative.Platform.OS === 'ios' ? 'png' : 'svg'
/**
* Set the preferred asset type. This is used to determine which asset to use when multiple types are available.
@ -26,7 +25,8 @@ export function setPreferredAssetType(type: Asset['type']) {
*/
export function* getAssets(): Generator<Asset> {
for (const name of Object.keys(cache!))
for (const mid of Object.values(cache[name])) yield AssetRegistry.getAssetByID(__r(mid)) as Asset
for (const mid of Object.values(cache[name]))
yield AssetRegistry.getAssetByID(__r(mid)) as Asset
for (const asset of _customs) yield asset
}
@ -48,7 +48,9 @@ export function getAssetByName(name: string): Asset | undefined {
* @param name The asset name.
* @returns A record keyed by the type of the asset, with the value being the asset itself.
*/
export function getAssetsByName(name: string): Record<Asset['type'], Asset> | undefined {
export function getAssetsByName(
name: string,
): Record<Asset['type'], Asset> | undefined {
const reg = cache[name]
if (!reg) return
@ -89,7 +91,9 @@ export function getAssetIdByName(name: string): AssetId | undefined {
*
* @param asset The asset to get the module ID for.
*/
export function getAssetModuleId(asset: Asset): Metro.ModuleID | -1 | undefined {
export function getAssetModuleId(
asset: Asset,
): Metro.ModuleID | -1 | undefined {
return _metas.get(asset)?.[1]
}
@ -101,7 +105,9 @@ export function getAssetModuleId(asset: Asset): Metro.ModuleID | -1 | undefined
*/
export function registerAsset(asset: RegisterableAsset): AssetId {
if (cache[asset.name]?.[asset.type] !== undefined)
throw new Error(`Asset with name ${asset.name} and type ${asset.type} already exists!`)
throw new Error(
`Asset with name ${asset.name} and type ${asset.type} already exists!`,
)
_customs.add(asset)

View file

@ -2,10 +2,8 @@ import { _initing, _uninits } from '@revenge-mod/modules/_/metro'
import { byName, byProps } from '@revenge-mod/modules/finders/filters'
import { waitForModules } from '@revenge-mod/modules/finders/wait'
import { getModuleDependencies } from '@revenge-mod/modules/metro/utils'
import { _metas, _overrides } from './_internal'
import { cacheAsset } from './caches'
import type { ReactNative } from '@revenge-mod/react/types'
import type { Asset, ReactNativeAsset } from './types'
@ -13,52 +11,57 @@ export let AssetRegistry: ReactNative.AssetsRegistry
export let AssetRegistryModuleId: number
// Tracking/caching assets
const unsubAR = waitForModules(byProps<ReactNative.AssetsRegistry>('registerAsset'), (exports, id) => {
AssetRegistryModuleId = id
AssetRegistry = exports as ReactNative.AssetsRegistry
const unsubAR = waitForModules(
byProps<ReactNative.AssetsRegistry>('registerAsset'),
(exports, id) => {
AssetRegistryModuleId = id
AssetRegistry = exports as ReactNative.AssetsRegistry
// There are two matching exports. One is the original, and one is a re-export.
// The original asset-registry is simply required by the re-exported one with no changes.
// There are two matching exports. One is the original, and one is a re-export.
// The original asset-registry is simply required by the re-exported one with no changes.
// This is the re-exported registry, because it has dependencies.
// We can begin the caching process here, asset registrar modules have the reexported registry as a dependency.
if (getModuleDependencies(id)!.length) {
unsubAR()
// This is the re-exported registry, because it has dependencies.
// We can begin the caching process here, asset registrar modules have the reexported registry as a dependency.
if (getModuleDependencies(id)!.length) {
unsubAR()
// TODO(assets/patches): conditionally run this if cache does not exist
// More fragile way, but also more performant:
// There is exactly one asset before the reexported asset registry :/
const firstAssetModuleId = id - 1
for (const mId of _uninits) {
if (mId < firstAssetModuleId) continue
// TODO(assets/patches): conditionally run this if cache does not exist
// More fragile way, but also more performant:
// There is exactly one asset before the reexported asset registry :/
const firstAssetModuleId = id - 1
for (const mId of _uninits) {
if (mId < firstAssetModuleId) continue
const deps = getModuleDependencies(mId)!
if (deps.length === 1 && deps[0] === id) __r(mId)
const deps = getModuleDependencies(mId)!
if (deps.length === 1 && deps[0] === id) __r(mId)
}
// We already patched the original asset registry, so we don't need to patch again.
return
}
// We already patched the original asset registry, so we don't need to patch again.
return
}
const orig = exports.registerAsset
exports.registerAsset = (asset: Asset) => {
const result = orig(asset as ReactNativeAsset)
const orig = exports.registerAsset
exports.registerAsset = (asset: Asset) => {
const result = orig(asset as ReactNativeAsset)
// Cache packager assets only
if ((asset as ReactNativeAsset).__packager_asset) {
cacheAsset(asset, _initing)
// In-memory cache
_metas.set(asset, [result, _initing])
}
// Cache packager assets only
if ((asset as ReactNativeAsset).__packager_asset) {
cacheAsset(asset, _initing)
// In-memory cache
_metas.set(asset, [result, _initing])
return result
}
return result
}
})
},
)
// Asset overrides
const unsubRAS = waitForModules(
byName<{
addCustomSourceTransformer: (transformer: (arg: { asset: Asset }) => ReactNativeAsset) => void
addCustomSourceTransformer: (
transformer: (arg: { asset: Asset }) => ReactNativeAsset,
) => void
}>('resolveAssetSource'),
rAS => {
unsubRAS()

View file

@ -4,7 +4,8 @@ export type Asset = ReactNativeAsset | RevengeAsset
export type AssetId = number
export type ReactNativeAsset = ReactNative.AssetsRegistry.PackagerAsset
export interface RevengeAsset extends Pick<ReactNativeAsset, 'name' | 'width' | 'height' | 'type'> {
export interface RevengeAsset
extends Pick<ReactNativeAsset, 'name' | 'width' | 'height' | 'type'> {
uri: string
}

View file

@ -6,80 +6,91 @@ import {
relativeDep,
withoutProps,
} from '@revenge-mod/modules/finders/filters'
import { lookupModule, lookupModules } from '@revenge-mod/modules/finders/lookup'
import {
lookupModule,
lookupModules,
} from '@revenge-mod/modules/finders/lookup'
import { ReactJsxRuntimeModuleId, ReactModuleId } from '@revenge-mod/react'
import { proxify } from '@revenge-mod/utils/proxy'
import { DispatcherModuleId } from './common/flux'
import type { DiscordModules } from './types'
export let ActionSheetActionCreators: DiscordModules.Actions.ActionSheetActionCreators = proxify(
() => {
const [module] = lookupModule(
export let ActionSheetActionCreators: DiscordModules.Actions.ActionSheetActionCreators =
proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<DiscordModules.Actions.ActionSheetActionCreators>(
'hideActionSheet',
),
byDependencies([
undefined,
ReactModuleId,
ReactJsxRuntimeModuleId,
DispatcherModuleId,
relativeDep(1),
relativeDep(2),
2,
]),
),
{
includeUninitialized: true,
},
)
if (module) return (ActionSheetActionCreators = module)
},
{
hint: 'object',
},
)!
export let AlertActionCreators: DiscordModules.Actions.AlertActionCreators =
proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<DiscordModules.Actions.AlertActionCreators>(
'openAlert',
'dismissAlert',
),
byDependencies([
ReactModuleId,
ReactJsxRuntimeModuleId,
undefined,
DispatcherModuleId,
relativeDep(1),
undefined,
2,
]),
),
)
if (module) return (AlertActionCreators = module)
},
{
hint: 'object',
},
)!
export let ToastActionCreators: DiscordModules.Actions.ToastActionCreators =
proxify(() => {
// Many other modules share the same dependencies, the second yielded should be the correct module.
// [Dispatcher, ImportTracker]
const generator = lookupModules(
preferExports(
byProps<DiscordModules.Actions.ActionSheetActionCreators>('hideActionSheet'),
byDependencies([
undefined,
ReactModuleId,
ReactJsxRuntimeModuleId,
DispatcherModuleId,
relativeDep(1),
relativeDep(2),
2,
]),
every(
byProps<DiscordModules.Actions.ToastActionCreators>('open'),
withoutProps('init'),
),
byDependencies([DispatcherModuleId, 2]),
),
{
includeUninitialized: true,
},
)
if (module) return (ActionSheetActionCreators = module)
},
{
hint: 'object',
},
)!
export let AlertActionCreators: DiscordModules.Actions.AlertActionCreators = proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<DiscordModules.Actions.AlertActionCreators>('openAlert', 'dismissAlert'),
byDependencies([
ReactModuleId,
ReactJsxRuntimeModuleId,
undefined,
DispatcherModuleId,
relativeDep(1),
undefined,
2,
]),
),
)
if (module) return (AlertActionCreators = module)
},
{
hint: 'object',
},
)!
export let ToastActionCreators: DiscordModules.Actions.ToastActionCreators = proxify(() => {
// Many other modules share the same dependencies, the second yielded should be the correct module.
// [Dispatcher, ImportTracker]
const generator = lookupModules(
preferExports(
every(byProps<DiscordModules.Actions.ToastActionCreators>('open'), withoutProps('init')),
byDependencies([DispatcherModuleId, 2]),
),
{
includeUninitialized: true,
},
)
for (const [module] of generator) if (module.open.length === 1) return (ToastActionCreators = module)
})!
for (const [module] of generator)
if (module.open.length === 1) return (ToastActionCreators = module)
})!

View file

@ -1,5 +1,4 @@
import {
type Filter,
byDependencies,
byProps,
createFilterGenerator,
@ -9,7 +8,7 @@ import {
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
import { waitForModules } from '@revenge-mod/modules/finders/wait'
import { noopFalse } from '@revenge-mod/utils/callbacks'
import type { Filter } from '@revenge-mod/modules/finders/filters'
import type { Metro } from '@revenge-mod/modules/types'
import type { DiscordModules } from '../types'
@ -18,7 +17,14 @@ import type { DiscordModules } from '../types'
export const [Dispatcher, DispatcherModuleId] = lookupModule(
preferExports(
byProps<DiscordModules.Flux.Dispatcher>('_interceptors'),
byDependencies([relativeDep(1), undefined, undefined, undefined, undefined, 2]),
byDependencies([
relativeDep(1),
undefined,
undefined,
undefined,
undefined,
2,
]),
),
{
includeUninitialized: true,
@ -37,7 +43,10 @@ export const Stores = new Proxy(_stores, {
get: (_, name) => _stores[name as string],
})
export function getStore<T>(name: string, callback: (store: DiscordModules.Flux.Store<T>) => void) {
export function getStore<T>(
name: string,
callback: (store: DiscordModules.Flux.Store<T>) => void,
) {
const store = Stores[name]
if (store) {
callback(store)
@ -47,9 +56,12 @@ export function getStore<T>(name: string, callback: (store: DiscordModules.Flux.
return waitForModules(byStoreName<T>(name), callback)
}
export type ByStoreName = <T>(name: string) => Filter<DiscordModules.Flux.Store<T>>
export type ByStoreName = <T>(
name: string,
) => Filter<DiscordModules.Flux.Store<T>>
export const byStoreName = createFilterGenerator<[name: string]>(
([name], _, exports) => exports.getName?.length === 0 && exports.getName() === name,
([name], _, exports) =>
exports.getName?.length === 0 && exports.getName() === name,
([name]) => `revenge.discord.byStoreName(${name})`,
) as ByStoreName

View file

@ -1,15 +1,18 @@
import { byName, byProps } from '@revenge-mod/modules/finders/filters'
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
import type { Metro } from '@revenge-mod/modules/types'
import type { DiscordModules } from '../types'
export { AppStartPerformance } from '../preinit'
export * as Flux from './flux'
// ../discord_common/js/shared/Logger.tsx
export const [Logger] = lookupModule(byName<DiscordModules.Logger>('Logger')) as [DiscordModules.Logger, Metro.ModuleID]
export const [Logger] = lookupModule(
byName<DiscordModules.Logger>('Logger'),
) as [DiscordModules.Logger, Metro.ModuleID]
// ../discord_common/js/packages/tokens/native.tsx
export const [Tokens, TokensModuleId] = lookupModule(byProps('RawColor')) as [any, Metro.ModuleID]
export const [Tokens, TokensModuleId] = lookupModule(byProps('RawColor')) as [
any,
Metro.ModuleID,
]

View file

@ -7,11 +7,12 @@ import {
relativeDep,
} from '@revenge-mod/modules/finders/filters'
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
import { ReactJsxRuntimeModuleId, ReactModuleId, ReactNativeModuleId } from '@revenge-mod/react'
import {
ReactJsxRuntimeModuleId,
ReactModuleId,
ReactNativeModuleId,
} from '@revenge-mod/react'
import { proxify } from '@revenge-mod/utils/proxy'
import type { DiscordModules } from './types'
// design/native.tsx
@ -22,7 +23,15 @@ export let Design: Design = proxify(
const [module] = lookupModule(
preferExports(
byProps<Design>('TableRow', 'Button'),
byDependencies(looseDeps([relativeDep(1), undefined, relativeDep(2), relativeDep(3), 2])),
byDependencies(
looseDeps([
relativeDep(1),
undefined,
relativeDep(2),
relativeDep(3),
2,
]),
),
),
{
includeUninitialized: true,
@ -45,7 +54,9 @@ export let FormSwitch: DiscordModules.Components.FormSwitch = proxify(() => {
const [module] = lookupModule(
preferExports(
bySingleProp<{ FormSwitch: DiscordModules.Components.FormSwitch }>('FormSwitch'),
bySingleProp<{ FormSwitch: DiscordModules.Components.FormSwitch }>(
'FormSwitch',
),
byDependencies([
[relativeDep(1), relativeDep(2), undefined, relativeDep(3)],
ReactModuleId,

View file

@ -1,8 +1,6 @@
import { _initExts, _uapi } from '@revenge-mod/plugins/_'
import { defineLazyProperty } from '@revenge-mod/utils/objects'
import { Logger } from './common'
import type { UnscopedInitPluginApi } from '@revenge-mod/plugins/types'
import type { PluginApiDiscord } from './types/revenge'
@ -10,10 +8,18 @@ const uapi = _uapi as UnscopedInitPluginApi
const discord = (uapi.discord = {} as PluginApiDiscord)
// TODO: Rolldown issue: Bug requires us to do { ...require('...') } instead of just require('...')
defineLazyProperty(discord, 'actions', () => ({ ...require('@revenge-mod/discord/actions') }))
defineLazyProperty(discord, 'common', () => ({ ...require('@revenge-mod/discord/common') }))
defineLazyProperty(discord, 'design', () => ({ ...require('@revenge-mod/discord/design') }))
defineLazyProperty(discord, 'native', () => ({ ...require('@revenge-mod/discord/native') }))
defineLazyProperty(discord, 'actions', () => ({
...require('@revenge-mod/discord/actions'),
}))
defineLazyProperty(discord, 'common', () => ({
...require('@revenge-mod/discord/common'),
}))
defineLazyProperty(discord, 'design', () => ({
...require('@revenge-mod/discord/design'),
}))
defineLazyProperty(discord, 'native', () => ({
...require('@revenge-mod/discord/native'),
}))
defineLazyProperty(discord, 'modules', () => ({
settings: {
...require('@revenge-mod/discord/modules/settings'),
@ -22,5 +28,9 @@ defineLazyProperty(discord, 'modules', () => ({
}))
_initExts.push((api, { manifest: { id } }) =>
defineLazyProperty(api, 'logger', () => new Logger(`Revenge > Plugins (${id})`)),
defineLazyProperty(
api,
'logger',
() => new Logger(`Revenge > Plugins (${id})`),
),
)

View file

@ -1,4 +1,8 @@
import type { SettingsItem, SettingsModulesLoadedSubscription, SettingsSection } from '.'
import type {
SettingsItem,
SettingsModulesLoadedSubscription,
SettingsSection,
} from '.'
export const _data = [
// Sections to splice in the SettingsOverviewScreen
@ -6,6 +10,10 @@ export const _data = [
// SETTING_RENDERER_CONFIG settings
{},
false,
] as [sections: Record<string, SettingsSection>, config: Record<string, SettingsItem>, loaded: boolean]
] as [
sections: Record<string, SettingsSection>,
config: Record<string, SettingsItem>,
loaded: boolean,
]
export const _subs = new Set<SettingsModulesLoadedSubscription>()

View file

@ -1,5 +1,4 @@
import { _data, _subs } from './_internal'
import type { DiscordModules } from '../../types'
export type SettingsItem = DiscordModules.Modules.Settings.SettingsItem
@ -22,7 +21,9 @@ export function isSettingsModulesLoaded() {
* @returns A function to unsubscribe from the event.
* @throws Throws an error if the settings modules are already loaded. Check with {@link isSettingsModulesLoaded} first.
*/
export function onSettingsModulesLoaded(subcription: SettingsModulesLoadedSubscription) {
export function onSettingsModulesLoaded(
subcription: SettingsModulesLoadedSubscription,
) {
if (_data[2]) throw new Error('Settings modules already loaded')
_subs.add(subcription)

View file

@ -1,13 +1,21 @@
import { byDependencies, byProps, looseDeps, preferExports, relativeDep } from '@revenge-mod/modules/finders/filters'
import {
byDependencies,
byProps,
looseDeps,
preferExports,
relativeDep,
} from '@revenge-mod/modules/finders/filters'
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
import { ReactJsxRuntimeModuleId, ReactModuleId, ReactNativeModuleId } from '@revenge-mod/react'
import {
ReactJsxRuntimeModuleId,
ReactModuleId,
ReactNativeModuleId,
} from '@revenge-mod/react'
import { proxify } from '@revenge-mod/utils/proxy'
import type { DiscordModules } from '../../types'
export type SettingListRenderer = DiscordModules.Modules.Settings.SettingListRenderer
export type SettingListRenderer =
DiscordModules.Modules.Settings.SettingListRenderer
export let SettingListRenderer: SettingListRenderer = proxify(
() => {

View file

@ -11,9 +11,15 @@ import type { DiscordNativeModules } from './types/native'
const nmp = nativeModuleProxy
export const CacheModule = nmp.NativeCacheModule as DiscordNativeModules.CacheModule
export const FileModule = nmp.NativeFileModule as DiscordNativeModules.FileModule
export const ClientInfoModule = nmp.NativeClientInfoModule as DiscordNativeModules.ClientInfoModule
export const DeviceModule = nmp.NativeDeviceModule as DiscordNativeModules.DeviceModule
export const BundleUpdaterManager = nmp.BundleUpdaterManager as DiscordNativeModules.BundleUpdaterManager
export const ThemeModule = nmp.NativeThemeModule as DiscordNativeModules.ThemeModule
export const CacheModule =
nmp.NativeCacheModule as DiscordNativeModules.CacheModule
export const FileModule =
nmp.NativeFileModule as DiscordNativeModules.FileModule
export const ClientInfoModule =
nmp.NativeClientInfoModule as DiscordNativeModules.ClientInfoModule
export const DeviceModule =
nmp.NativeDeviceModule as DiscordNativeModules.DeviceModule
export const BundleUpdaterManager =
nmp.BundleUpdaterManager as DiscordNativeModules.BundleUpdaterManager
export const ThemeModule =
nmp.NativeThemeModule as DiscordNativeModules.ThemeModule

View file

@ -1,6 +1,10 @@
import { byDependencies, byProps, preferExports, relativeDep } from '@revenge-mod/modules/finders/filters'
import {
byDependencies,
byProps,
preferExports,
relativeDep,
} from '@revenge-mod/modules/finders/filters'
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
import type { Metro } from '@revenge-mod/modules/types'
import type { DiscordModules } from './types'

View file

@ -51,12 +51,20 @@ export namespace DiscordModules {
export interface Dispatcher {
_actionHandlers: unknown
_interceptors?: ((payload: DispatcherPayload) => undefined | boolean)[]
_interceptors?: ((
payload: DispatcherPayload,
) => undefined | boolean)[]
_currentDispatchActionType: undefined | string
_processingWaitQueue: boolean
_subscriptions: Record<string, Set<(payload: DispatcherPayload) => void>>
_subscriptions: Record<
string,
Set<(payload: DispatcherPayload) => void>
>
_waitQueue: unknown[]
addDependencies(node1: DispatcherDependency, node2: DispatcherDependency): void
addDependencies(
node1: DispatcherDependency,
node2: DispatcherDependency,
): void
dispatch(payload: DispatcherPayload): Promise<void>
flushWaitQueue(): void
isDispatching(): boolean
@ -65,19 +73,29 @@ export namespace DiscordModules {
actionHandler: Record<string, (e: DispatcherPayload) => void>,
storeDidChange: (e: DispatcherPayload) => boolean,
): string
setInterceptor(interceptor?: (payload: DispatcherPayload) => undefined | boolean): void
setInterceptor(
interceptor?: (
payload: DispatcherPayload,
) => undefined | boolean,
): void
/**
* Subscribes to an action type
* @param actionType The action type to subscribe to
* @param callback The callback to call when the action is dispatched
*/
subscribe(actionType: string, callback: (payload: DispatcherPayload) => void): void
subscribe(
actionType: string,
callback: (payload: DispatcherPayload) => void,
): void
/**
* Unsubscribes from an action type
* @param actionType The action type to unsubscribe from
* @param callback The callback to remove
*/
unsubscribe(actionType: string, callback: (payload: DispatcherPayload) => void): void
unsubscribe(
actionType: string,
callback: (payload: DispatcherPayload) => void,
): void
wait(cb: () => void): void
}
}
@ -88,7 +106,10 @@ export namespace DiscordModules {
export interface AppStartPerformance {
mark(...args: AppStartPerformance.MarkArgs): void
markAndLog(logger: InstanceType<DiscordModules.Logger>, ...args: AppStartPerformance.MarkArgs): void
markAndLog(
logger: InstanceType<DiscordModules.Logger>,
...args: AppStartPerformance.MarkArgs
): void
[index: string]: unknown
}
@ -132,7 +153,9 @@ export namespace DiscordModules {
| 'eyebrow'
export type TextStyleSheet = Record<TextVariant, RNTextProps>
export type CreateStylesFunction = <const S extends Record<string, TextStyle | ViewStyle | ImageStyle>>(
export type CreateStylesFunction = <
const S extends Record<string, TextStyle | ViewStyle | ImageStyle>,
>(
styles: S,
) => () => S
}
@ -303,7 +326,8 @@ export namespace DiscordModules {
label?: string
}
export interface TextAreaProps extends Omit<TextInputProps, 'multiline'> {}
export interface TextAreaProps
extends Omit<TextInputProps, 'multiline'> {}
export type TextInput = FC<TextInputProps>
export type TextField = FC<TextFieldProps>
@ -326,7 +350,8 @@ export namespace DiscordModules {
export type ActionSheet = FC<ActionSheetProps>
export interface ActionSheetCloseButtonProps extends Pick<ComponentProps<IconButton>, 'variant' | 'onPress'> {}
export interface ActionSheetCloseButtonProps
extends Pick<ComponentProps<IconButton>, 'variant' | 'onPress'> {}
export type ActionSheetCloseButton = FC<ActionSheetCloseButtonProps>
@ -374,7 +399,8 @@ export namespace DiscordModules {
Icon: TableRowIcon
}
export interface TableSwitchRowProps extends Omit<TableRowProps, 'trailing'> {
export interface TableSwitchRowProps
extends Omit<TableRowProps, 'trailing'> {
accessibilityHint?: string
value: boolean
onValueChange(value: boolean): void
@ -496,7 +522,11 @@ export namespace DiscordModules {
export interface SettingsListProps {
ListHeaderComponent?: ComponentType
sections: Array<{ label?: string | ReactNode; settings: string[]; subLabel?: string | ReactNode }>
sections: Array<{
label?: string | ReactNode
settings: string[]
subLabel?: string | ReactNode
}>
}
export type SettingsList = FC<SettingsListProps>

View file

@ -42,7 +42,10 @@ export namespace DiscordNativeModules {
* @param encoding The encoding to read the asset with
* @returns
*/
readAsset: (nameOrUri: string, encoding: 'base64' | 'utf8') => Promise<string>
readAsset: (
nameOrUri: string,
encoding: 'base64' | 'utf8',
) => Promise<string>
/**
* Allowed URI schemes on Android: `file://`, `content://` ([See here](https://developer.android.com/reference/android/content/ContentResolver#accepts-the-following-uri-schemes:_3))
*/
@ -52,7 +55,11 @@ export namespace DiscordNativeModules {
* @param encoding Set to `base64` in order to encode response
*/
readFile(path: string, encoding: 'base64' | 'utf8'): Promise<string>
saveFileToGallery?(uri: string, fileName: string, fileType: 'PNG' | 'JPEG'): Promise<string>
saveFileToGallery?(
uri: string,
fileName: string,
fileType: 'PNG' | 'JPEG',
): Promise<string>
/**
* @param storageDir Either `cache` or `documents`.
* @param path Path in `storageDir`, parents are recursively created.
@ -71,7 +78,10 @@ export namespace DiscordNativeModules {
* @param storageDir Either `cache` or `documents`
* @param path Path to the file to be removed
*/
removeFile(storageDir: 'cache' | 'documents', path: string): Promise<boolean>
removeFile(
storageDir: 'cache' | 'documents',
path: string,
): Promise<boolean>
/**
* Clear the folder from the path given
* (!) On Android, this only clears all *files* and not subdirectories!
@ -79,7 +89,10 @@ export namespace DiscordNativeModules {
* @param path Path to the folder to be cleared
* @returns Whether the clearance succeeded
*/
clearFolder(storageDir: 'cache' | 'documents', path: string): Promise<boolean>
clearFolder(
storageDir: 'cache' | 'documents',
path: string,
): Promise<boolean>
getConstants: () => {
/**
* The path the `documents` storage dir (see {@link writeFile}) represents.

View file

@ -1,11 +1,19 @@
import { byDependencies, byProps, preferExports, relativeDep } from '@revenge-mod/modules/finders/filters'
import {
byDependencies,
byProps,
preferExports,
relativeDep,
} from '@revenge-mod/modules/finders/filters'
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
import { proxify } from '@revenge-mod/utils/proxy'
export let nodeUtil: typeof import('node:util') = proxify(
() => {
const [module] = lookupModule(
preferExports(byProps<typeof nodeUtil>('inspect'), byDependencies([relativeDep(1), [], []])),
preferExports(
byProps<typeof nodeUtil>('inspect'),
byDependencies([relativeDep(1), [], []]),
),
{
includeUninitialized: true,
},

View file

@ -1,43 +1,54 @@
import { byDependencies, byProps, every, preferExports, relativeDep } from '@revenge-mod/modules/finders/filters'
import {
byDependencies,
byProps,
every,
preferExports,
relativeDep,
} from '@revenge-mod/modules/finders/filters'
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
import { ReactModuleId, ReactNativeModuleId } from '@revenge-mod/react'
import { destructure, proxify } from '@revenge-mod/utils/proxy'
let ClipboardModule: typeof import('@react-native-clipboard/clipboard') = proxify(
() => {
// ID: 4790
let ClipboardModule: typeof import('@react-native-clipboard/clipboard') =
proxify(
() => {
// ID: 4790
// r6 = 4790;
// r5 = [4791, 4792];
// r6 = 4790;
// r5 = [4791, 4792];
// r6 = 4791;
// r5 = [175, 4792];
// r6 = 4791;
// r5 = [175, 4792];
// r6 = 4792;
// r5 = [27, 4793];
// r6 = 4792;
// r5 = [27, 4793];
const [module] = lookupModule(
preferExports(
byProps<typeof ClipboardModule>('useClipboard'),
every(
byDependencies([relativeDep(1), relativeDep(2)]),
byDependencies([
[ReactModuleId, relativeDep(2, true)],
[ReactNativeModuleId, relativeDep(3, true)],
]),
const [module] = lookupModule(
preferExports(
byProps<typeof ClipboardModule>('useClipboard'),
every(
byDependencies([relativeDep(1), relativeDep(2)]),
byDependencies([
[ReactModuleId, relativeDep(2, true)],
[ReactNativeModuleId, relativeDep(3, true)],
]),
),
),
),
{
includeUninitialized: true,
},
)
{
includeUninitialized: true,
},
)
if (module) return (ClipboardModule = module)
},
{
hint: 'object',
},
)!
if (module) return (ClipboardModule = module)
},
{
hint: 'object',
},
)!
export const { useClipboard } = destructure(ClipboardModule, { hint: 'function' })
export const { default: Clipboard } = destructure(ClipboardModule, { hint: 'object' })
export const { useClipboard } = destructure(ClipboardModule, {
hint: 'function',
})
export const { default: Clipboard } = destructure(ClipboardModule, {
hint: 'object',
})

View file

@ -1,47 +1,71 @@
import { byDependencies, byName, byProps, looseDeps, preferExports } from '@revenge-mod/modules/finders/filters'
import {
byDependencies,
byName,
byProps,
looseDeps,
preferExports,
} from '@revenge-mod/modules/finders/filters'
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
import { proxify } from '@revenge-mod/utils/proxy'
const [, _createClassModuleId] = lookupModule(byName('_createClass'))
const [, _classCallCheckModuleId] = lookupModule(byName('_classCallCheck'))
export let ReactNavigationNative: typeof import('@react-navigation/native') = proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<typeof ReactNavigationNative>('useLinkTo'),
byDependencies(looseDeps([[], looseDeps([[_createClassModuleId, _classCallCheckModuleId]])])),
),
{
includeUninitialized: true,
},
)
export let ReactNavigationNative: typeof import('@react-navigation/native') =
proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<typeof ReactNavigationNative>('useLinkTo'),
byDependencies(
looseDeps([
[],
looseDeps([
[_createClassModuleId, _classCallCheckModuleId],
]),
]),
),
),
{
includeUninitialized: true,
},
)
if (module) return (ReactNavigationNative = module)
},
{
hint: 'object',
},
)!
if (module) return (ReactNavigationNative = module)
},
{
hint: 'object',
},
)!
export let ReactNavigationStack: typeof import('@react-navigation/stack') = proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<typeof ReactNavigationStack>('createStackNavigator'),
byDependencies(looseDeps([[], looseDeps([[_createClassModuleId, _classCallCheckModuleId]])])),
),
{
includeUninitialized: true,
},
)
export let ReactNavigationStack: typeof import('@react-navigation/stack') =
proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<typeof ReactNavigationStack>(
'createStackNavigator',
),
byDependencies(
looseDeps([
[],
looseDeps([
[_createClassModuleId, _classCallCheckModuleId],
]),
]),
),
),
{
includeUninitialized: true,
},
)
if (module) return (ReactNavigationStack = module)
},
{
hint: 'object',
},
)!
if (module) return (ReactNavigationStack = module)
},
{
hint: 'object',
},
)!
export interface ReactNavigationParamList {
[Page: string]: any

View file

@ -1,9 +1,16 @@
import { byDependencies, byProps, preferExports } from '@revenge-mod/modules/finders/filters'
import {
byDependencies,
byProps,
preferExports,
} from '@revenge-mod/modules/finders/filters'
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
import {
ReactJsxRuntimeModuleId,
ReactModuleId,
ReactNativeModuleId,
} from '@revenge-mod/react'
import { proxify } from '@revenge-mod/utils/proxy'
import { ReactJsxRuntimeModuleId, ReactModuleId, ReactNativeModuleId } from '@revenge-mod/react'
export let FlashList: typeof import('@shopify/flash-list') = proxify(
() => {
const [module] = lookupModule(

View file

@ -1,5 +1,4 @@
import { isModuleExportBad } from '../metro/utils'
import type { If } from '@revenge-mod/utils/types'
import type { Metro } from '../types'
import type { Filter } from './filters'
@ -13,7 +12,9 @@ export interface RunFilterOptions {
skipDefault?: boolean
}
export type RunFilterReturnExportsOptions<ReturnNamespace extends boolean = boolean> = RunFilterOptions &
export type RunFilterReturnExportsOptions<
ReturnNamespace extends boolean = boolean,
> = RunFilterOptions &
If<
ReturnNamespace,
{
@ -44,14 +45,18 @@ const FilterResultFlags = {
Namespace: 3,
}
export type FilterResultFlag = (typeof FilterResultFlags)[keyof typeof FilterResultFlags]
export type FilterResultFlag =
(typeof FilterResultFlags)[keyof typeof FilterResultFlags]
// The reason this returns a flag is because flags are never falsy, while exports may be falsy when using ID-only filters (eg. `byDependencies`).
// Currently, we only have options that are relevant for checking exports
// If we add more options later on, do NOT forget about
// adding them here, and passing them in the lookup* functions
export function runFilter(filter: Filter<any, false>, id: Metro.ModuleID): FilterResultFlag | undefined
export function runFilter(
filter: Filter<any, false>,
id: Metro.ModuleID,
): FilterResultFlag | undefined
export function runFilter(
filter: Filter,
@ -81,7 +86,11 @@ export function runFilter(
}
const defaultExport = exports.default
if (!options?.skipDefault && !isModuleExportBad(defaultExport) && filter(id, defaultExport)) {
if (
!options?.skipDefault &&
!isModuleExportBad(defaultExport) &&
filter(id, defaultExport)
) {
// TODO(modules/finders/caches::default)
return FilterResultFlags.Default
}
@ -92,6 +101,7 @@ export function exportsFromFilterResultFlag(
exports: Metro.ModuleExports,
options?: RunFilterReturnExportsOptions,
) {
if (flag === FilterResultFlags.Default && !options?.returnNamespace) return exports.default
if (flag === FilterResultFlags.Default && !options?.returnNamespace)
return exports.default
return exports
}

View file

@ -1,15 +1,22 @@
import { _inits } from '../metro/_internal'
import { getModuleDependencies, initializedModuleHasBadExports, isModuleInitialized } from '../metro/utils'
import {
getModuleDependencies,
initializedModuleHasBadExports,
isModuleInitialized,
} from '../metro/utils'
import type { If, LogicalOr } from '@revenge-mod/utils/types'
import type { Metro } from '../types'
export type FilterResult<F> = F extends Filter<infer R, boolean> ? R : never
export type IsFilterWithExports<F> = F extends Filter<any, infer WE> ? WE : never
export type IsFilterWithExports<F> = F extends Filter<any, infer WE>
? WE
: never
export interface Filter<_Inferable = any, WithExports extends boolean = boolean> {
export interface Filter<
_Inferable = any,
WithExports extends boolean = boolean,
> {
(
...args: If<
WithExports,
@ -41,7 +48,11 @@ export interface Filter<_Inferable = any, WithExports extends boolean = boolean>
* @see {@link byProps} for an example on custom-typed filters.
*/
export function createFilterGenerator<A extends any[]>(
filter: (args: A, id: Metro.ModuleID, exports: Metro.ModuleExports) => boolean,
filter: (
args: A,
id: Metro.ModuleID,
exports: Metro.ModuleExports,
) => boolean,
keyFor: (args: A) => string,
): (...args: A) => Filter<object, true>
@ -55,7 +66,8 @@ export function createFilterGenerator<A extends any[]>(
keyFor: (args: A) => string,
): (...args: any[]) => Filter {
return (...args: A) => {
const filter = (id: Metro.ModuleID, exports?: Metro.ModuleExports) => f(args, id, exports)
const filter = (id: Metro.ModuleID, exports?: Metro.ModuleExports) =>
f(args, id, exports)
filter.key = keyFor(args)
return filter
}
@ -94,7 +106,10 @@ export const byProps = createFilterGenerator<Parameters<ByProps>>(
props => `revenge.props(${props.join(',')})`,
) as ByProps
export type WithoutProps = <T extends Record<string, any>>(prop: string, ...props: string[]) => Filter<T, true>
export type WithoutProps = <T extends Record<string, any>>(
prop: string,
...props: string[]
) => Filter<T, true>
/**
* Filter modules by their exports having none of the specified properties.
@ -112,7 +127,9 @@ export const withoutProps = createFilterGenerator<Parameters<WithoutProps>>(
props => `revenge.withoutProps(${props.join(',')})`,
) as WithoutProps
export type BySingleProp = <T extends Record<string, any>>(prop: keyof T) => Filter<T, true>
export type BySingleProp = <T extends Record<string, any>>(
prop: keyof T,
) => Filter<T, true>
/**
* Filter modules by their exports having only the specified property.
@ -127,11 +144,16 @@ export type BySingleProp = <T extends Record<string, any>>(prop: keyof T) => Fil
*/
export const bySingleProp = createFilterGenerator<Parameters<BySingleProp>>(
// We don't use Reflect.ownKeys here because __esModule is not enumerable, and we don't want to include it in the check
([prop], _, exports) => typeof exports === 'object' && Object.keys(exports).length === 1 && prop in exports,
([prop], _, exports) =>
typeof exports === 'object' &&
Object.keys(exports).length === 1 &&
prop in exports,
([prop]) => `revenge.singleProp(${prop})`,
) as BySingleProp
export type ByName = <T extends object = object>(name: string) => Filter<T, true>
export type ByName = <T extends object = object>(
name: string,
) => Filter<T, true>
/**
* Filter modules by their exports having the specified name.
@ -174,11 +196,19 @@ export interface RelativeDependency {
}
export interface ComparableDependencyMap
extends Array<Metro.ModuleID | RelativeDependency | undefined | ComparableDependencyMap> {
extends Array<
| Metro.ModuleID
| RelativeDependency
| undefined
| ComparableDependencyMap
> {
l?: boolean
}
type ComputedComparableDependencyMap = Exclude<ComparableDependencyMap, Array<RelativeDependency>>
type ComputedComparableDependencyMap = Exclude<
ComparableDependencyMap,
Array<RelativeDependency>
>
type ByDependencies = <T>(deps: ComparableDependencyMap) => Filter<T, false>
@ -229,22 +259,39 @@ export function looseDeps(deps: ComparableDependencyMap) {
* @param id The dependency ID to mark as relative.
* @param root Whether this dependency should be relatively to the root module ID. Useless for single-depth comparisons.
*/
export function relativeDep(id: Metro.ModuleID, root?: boolean): RelativeDependency {
export function relativeDep(
id: Metro.ModuleID,
root?: boolean,
): RelativeDependency {
const o = Object(id) as RelativeDependency
if (root) o.r = true
return o
}
function compareDeps(rootOf: Metro.ModuleID, compare: ComparableDependencyMap, loose = false): boolean {
function compareDeps(
rootOf: Metro.ModuleID,
compare: ComparableDependencyMap,
loose = false,
): boolean {
const stack: Array<
[of: Metro.ModuleID, deps: Metro.ModuleID[], compare: ComparableDependencyMap, loose?: boolean]
[
of: Metro.ModuleID,
deps: Metro.ModuleID[],
compare: ComparableDependencyMap,
loose?: boolean,
]
> = []
stack.push([rootOf, getModuleDependencies(rootOf)!, compare, loose])
while (stack.length) {
const [of, deps, compare, loose] = stack.pop()!
if (loose ? deps.length < compare.length : deps.length !== compare.length) return false
if (
loose
? deps.length < compare.length
: deps.length !== compare.length
)
return false
const ccmp: ComputedComparableDependencyMap = []
for (let i = 0; i < compare.length; i++) {
@ -252,7 +299,8 @@ function compareDeps(rootOf: Metro.ModuleID, compare: ComparableDependencyMap, l
if (cmp === undefined) continue
if (Array.isArray(cmp)) ccmp[i] = cmp
else if (typeof cmp === 'object') ccmp[i] = cmp.r ? rootOf + cmp.valueOf() : of + cmp.valueOf()
else if (typeof cmp === 'object')
ccmp[i] = cmp.r ? rootOf + cmp.valueOf() : of + cmp.valueOf()
else ccmp[i] = cmp
}
@ -280,7 +328,8 @@ function genDepsKey(deps: ComparableDependencyMap): string {
else if (Array.isArray(dep)) {
if (dep.l) key += '#'
key += `[${genDepsKey(dep)}],`
} else key += `${(dep as RelativeDependency).r ? `r${dep.valueOf()}` : dep.valueOf()},`
} else
key += `${(dep as RelativeDependency).r ? `r${dep.valueOf()}` : dep.valueOf()},`
return key.substring(0, key.length - 1)
}
@ -289,14 +338,20 @@ export type Every = {
<F1 extends Filter, F2 extends Filter>(
f1: F1,
f2: F2,
): Filter<FilterResult<F1> & FilterResult<F2>, LogicalOr<IsFilterWithExports<F1>, IsFilterWithExports<F2>>>
): Filter<
FilterResult<F1> & FilterResult<F2>,
LogicalOr<IsFilterWithExports<F1>, IsFilterWithExports<F2>>
>
<F1 extends Filter, F2 extends Filter, F3 extends Filter>(
f1: F1,
f2: F2,
f3: F3,
): Filter<
FilterResult<F1> & FilterResult<F2> & FilterResult<F3>,
LogicalOr<LogicalOr<IsFilterWithExports<F1>, IsFilterWithExports<F2>>, IsFilterWithExports<F3>>
LogicalOr<
LogicalOr<IsFilterWithExports<F1>, IsFilterWithExports<F2>>,
IsFilterWithExports<F3>
>
>
(...filters: Filter[]): Filter
}
@ -331,14 +386,19 @@ export type Some = {
<F1 extends Filter, F2 extends Filter>(
f1: F1,
f2: F2,
): Filter<FilterResult<F1> | FilterResult<F2>, IsFilterWithExports<F1> | IsFilterWithExports<F2>>
): Filter<
FilterResult<F1> | FilterResult<F2>,
IsFilterWithExports<F1> | IsFilterWithExports<F2>
>
<F1 extends Filter, F2 extends Filter, F3 extends Filter>(
f1: F1,
f2: F2,
f3: F3,
): Filter<
FilterResult<F1> | FilterResult<F2> | FilterResult<F3>,
IsFilterWithExports<F1> | IsFilterWithExports<F2> | IsFilterWithExports<F3>
| IsFilterWithExports<F1>
| IsFilterWithExports<F2>
| IsFilterWithExports<F3>
>
(...filters: Filter[]): Filter
}
@ -392,10 +452,13 @@ export type ModuleStateAware = <IF extends Filter>(
* ))
* ```
*/
export const moduleStateAware = createFilterGenerator<Parameters<ModuleStateAware>>(
export const moduleStateAware = createFilterGenerator<
Parameters<ModuleStateAware>
>(
([initializedFilter, uninitializedFilter], id, exports) => {
if (isModuleInitialized(id)) {
if (!initializedModuleHasBadExports(id)) return initializedFilter(id, exports)
if (!initializedModuleHasBadExports(id))
return initializedFilter(id, exports)
return false
}

View file

@ -1,22 +1,24 @@
import { noopFalse } from '@revenge-mod/utils/callbacks'
import { type LookupModulesOptions, lookupModule, lookupModuleByImportedPath } from './lookup'
import { type WaitForModulesOptions, waitForModuleByImportedPath, waitForModules } from './wait'
import type { MaybeDefaultExportMatched } from '../types'
import type { Metro } from '../types'
import { lookupModule, lookupModuleByImportedPath } from './lookup'
import { waitForModuleByImportedPath, waitForModules } from './wait'
import type { MaybeDefaultExportMatched, Metro } from '../types'
import type { RunFilterReturnExportsOptions } from './_internal'
import type { Filter, FilterResult } from './filters'
import type { LookupModulesOptions } from './lookup'
import type { WaitForModulesOptions } from './wait'
export type GetModuleOptions<
ReturnNamespace extends boolean = boolean,
IncludeUninitialized extends boolean = boolean,
> = WaitForModulesOptions<ReturnNamespace> & LookupModulesOptions<ReturnNamespace, IncludeUninitialized>
> = WaitForModulesOptions<ReturnNamespace> &
LookupModulesOptions<ReturnNamespace, IncludeUninitialized>
export type GetModuleResult<
F extends Filter,
O extends GetModuleOptions,
> = O extends RunFilterReturnExportsOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>
> = O extends RunFilterReturnExportsOptions<true>
? MaybeDefaultExportMatched<FilterResult<F>>
: FilterResult<F>
export type GetModuleCallback<T> = (exports: T, id: Metro.ModuleID) => any
@ -48,11 +50,21 @@ export function getModule<F extends Filter>(
): GetModuleUnsubscribeFunction
export function getModule<
F extends O extends GetModuleOptions<boolean, true> ? Filter<any, false> : Filter,
F extends O extends GetModuleOptions<boolean, true>
? Filter<any, false>
: Filter,
O extends GetModuleOptions,
>(filter: F, callback: GetModuleCallback<FilterResult<F>>, options: O): GetModuleUnsubscribeFunction
>(
filter: F,
callback: GetModuleCallback<FilterResult<F>>,
options: O,
): GetModuleUnsubscribeFunction
export function getModule(filter: Filter, callback: GetModuleCallback<any>, options?: GetModuleOptions) {
export function getModule(
filter: Filter,
callback: GetModuleCallback<any>,
options?: GetModuleOptions,
) {
const [exports, id] = lookupModule(filter, options!)
if (id != null) {
callback(exports, id)
@ -85,7 +97,10 @@ export function getModule(filter: Filter, callback: GetModuleCallback<any>, opti
* })
* ```
*/
export function getModuleByImportedPath<T>(path: string, callback: GetModuleCallback<T>): GetModuleUnsubscribeFunction {
export function getModuleByImportedPath<T>(
path: string,
callback: GetModuleCallback<T>,
): GetModuleUnsubscribeFunction {
const [exports, id] = lookupModuleByImportedPath(path)
if (id != null) {
callback(exports, id)

View file

@ -1,19 +1,17 @@
import { _inits, _metas, _paths, _uninits } from '../metro/_internal'
import {
type RunFilterOptions,
type RunFilterReturnExportsOptions,
exportsFromFilterResultFlag,
runFilter,
} from './_internal'
import { _inits, _paths, _uninits } from '../metro/_internal'
import { getInitializedModuleExports } from '../metro/utils'
import { exportsFromFilterResultFlag, runFilter } from './_internal'
import type { If } from '@revenge-mod/utils/types'
import type { MaybeDefaultExportMatched } from '../types'
import type { Metro } from '../types'
import type { MaybeDefaultExportMatched, Metro } from '../types'
import type {
RunFilterOptions,
RunFilterReturnExportsOptions,
} from './_internal'
import type { Filter, FilterResult } from './filters'
export interface BaseLookupModulesOptions<IncludeUninitialized extends boolean = boolean> extends RunFilterOptions {
export interface BaseLookupModulesOptions<
IncludeUninitialized extends boolean = boolean,
> extends RunFilterOptions {
/**
* Whether to include initialized modules in the lookup.
*
@ -35,9 +33,13 @@ export interface BaseLookupModulesOptions<IncludeUninitialized extends boolean =
export type LookupModulesOptions<
ReturnNamespace extends boolean = boolean,
IncludeUninitialized extends boolean = boolean,
> = RunFilterReturnExportsOptions<ReturnNamespace> & BaseLookupModulesOptions<IncludeUninitialized>
> = RunFilterReturnExportsOptions<ReturnNamespace> &
BaseLookupModulesOptions<IncludeUninitialized>
export type LookupModulesResult<F extends Filter, O extends LookupModulesOptions> = [
export type LookupModulesResult<
F extends Filter,
O extends LookupModulesOptions,
> = [
exports: O extends RunFilterReturnExportsOptions<true>
? MaybeDefaultExportMatched<FilterResult<F>>
: FilterResult<F>,
@ -46,7 +48,11 @@ export type LookupModulesResult<F extends Filter, O extends LookupModulesOptions
export type LookupModuleIdsOptions<
IncludeUninitialized extends boolean = boolean,
IncludeAll extends If<IncludeUninitialized, false, boolean> = If<IncludeUninitialized, false, boolean>,
IncludeAll extends If<IncludeUninitialized, false, boolean> = If<
IncludeUninitialized,
false,
boolean
>,
> = RunFilterOptions &
(
| (BaseLookupModulesOptions<IncludeUninitialized> & {
@ -82,10 +88,14 @@ export type LookupModuleIdsOptions<
* for (const exports of lookup) console.log(exports)
* ```
*/
export function lookupModules<F extends Filter>(filter: F): Generator<LookupModulesResult<F, never>, undefined>
export function lookupModules<F extends Filter>(
filter: F,
): Generator<LookupModulesResult<F, never>, undefined>
export function lookupModules<
F extends O extends BaseLookupModulesOptions<true> ? Filter<any, false> : Filter,
F extends O extends BaseLookupModulesOptions<true>
? Filter<any, false>
: Filter,
O extends LookupModulesOptions,
>(filter: F, options: O): Generator<LookupModulesResult<F, O>, undefined>
@ -94,7 +104,8 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
for (const id of _inits) {
const exports = getInitializedModuleExports(id)
const flag = runFilter(filter, id, exports, options)
if (flag) yield [exportsFromFilterResultFlag(flag, exports, options), id]
if (flag)
yield [exportsFromFilterResultFlag(flag, exports, options), id]
}
if (options?.includeUninitialized)
@ -103,7 +114,11 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
// Run the filter again to ensure we have the correct exports
const exports = __r(id)
const flag = runFilter(filter, id, exports, options)
if (flag) yield [exportsFromFilterResultFlag(flag, exports, options), id]
if (flag)
yield [
exportsFromFilterResultFlag(flag, exports, options),
id,
]
else warnDeveloperAboutAPartialFilterMatch(id, filter.key)
}
}
@ -122,7 +137,9 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
* const React = lookupModule(byProps<typeof import('react')>('createElement'))
* ```
*/
export function lookupModule<F extends Filter>(filter: F): LookupModulesResult<F, never> | []
export function lookupModule<F extends Filter>(
filter: F,
): LookupModulesResult<F, never> | []
export function lookupModule<F extends Filter, O extends LookupModulesOptions>(
filter: F,
@ -134,7 +151,8 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
for (const id of _inits) {
const exports = getInitializedModuleExports(id)
const flag = runFilter(filter, id, exports, options)
if (flag) return [exportsFromFilterResultFlag(flag, exports, options), id]
if (flag)
return [exportsFromFilterResultFlag(flag, exports, options), id]
}
if (options?.includeUninitialized)
@ -143,7 +161,11 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
const exports = __r(id)
// Run the filter again to ensure we have the correct exports
const flag = runFilter(filter, id, exports, options)
if (flag) return [exportsFromFilterResultFlag(flag, exports, options), id]
if (flag)
return [
exportsFromFilterResultFlag(flag, exports, options),
id,
]
warnDeveloperAboutAPartialFilterMatch(id, filter.key)
}
@ -163,7 +185,9 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
* const [{ default: Logger }] = lookupModuleByImportedPath<{ default: typeof DiscordModules.Logger }>('modules/debug/Logger.tsx')
* ```
*/
export function lookupModuleByImportedPath<T = any>(path: string): [exports: T, id: Metro.ModuleID] | [] {
export function lookupModuleByImportedPath<T = any>(
path: string,
): [exports: T, id: Metro.ModuleID] | [] {
const id = _paths.get(path)
if (id == null) return []
return [getInitializedModuleExports(id), id]
@ -174,7 +198,10 @@ export function lookupModuleByImportedPath<T = any>(path: string): [exports: T,
* This will cause modules to be initialized unnecessarily, and may cause issues.
* @internal
*/
function warnDeveloperAboutAPartialFilterMatch(id: Metro.ModuleID, key: string) {
function warnDeveloperAboutAPartialFilterMatch(
id: Metro.ModuleID,
key: string,
) {
console.warn(
`[revenge.modules.finders.lookup] Module ${id} matched ${key} during uninitialized lookup, but did not match the full filter.`,
)

View file

@ -1,12 +1,19 @@
import { onAnyModuleInitialized, onModuleFinishedImporting } from '../metro/subscriptions'
import { getInitializedModuleExports, initializedModuleHasBadExports } from '../metro/utils'
import { type RunFilterReturnExportsOptions, exportsFromFilterResultFlag, runFilter } from './_internal'
import type { MaybeDefaultExportMatched } from '../types'
import type { Metro } from '../types'
import {
onAnyModuleInitialized,
onModuleFinishedImporting,
} from '../metro/subscriptions'
import {
getInitializedModuleExports,
initializedModuleHasBadExports,
} from '../metro/utils'
import { exportsFromFilterResultFlag, runFilter } from './_internal'
import type { MaybeDefaultExportMatched, Metro } from '../types'
import type { RunFilterReturnExportsOptions } from './_internal'
import type { Filter, FilterResult } from './filters'
export interface BaseWaitForModulesOptions<IncludeAll extends boolean = boolean> {
export interface BaseWaitForModulesOptions<
IncludeAll extends boolean = boolean,
> {
/**
* Whether to include all modules, including ones with bad exports.
*
@ -22,12 +29,15 @@ export type WaitForModulesCallback<T> = (exports: T, id: Metro.ModuleID) => any
export type WaitForModulesOptions<
ReturnNamespace extends boolean = boolean,
IncludeBadExports extends boolean = boolean,
> = RunFilterReturnExportsOptions<ReturnNamespace> & BaseWaitForModulesOptions<IncludeBadExports>
> = RunFilterReturnExportsOptions<ReturnNamespace> &
BaseWaitForModulesOptions<IncludeBadExports>
export type WaitForModulesResult<
F extends Filter,
O extends WaitForModulesOptions,
> = O extends RunFilterReturnExportsOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>
> = O extends RunFilterReturnExportsOptions<true>
? MaybeDefaultExportMatched<FilterResult<F>>
: FilterResult<F>
/**
* Wait for modules to initialize. **Callback won't be called if the module is already initialized!**
@ -55,7 +65,9 @@ export function waitForModules<F extends Filter>(
): WaitForModulesUnsubscribeFunction
export function waitForModules<
F extends O extends WaitForModulesOptions<boolean, true> ? Filter<any, false> : Filter,
F extends O extends WaitForModulesOptions<boolean, true>
? Filter<any, false>
: Filter,
O extends WaitForModulesOptions,
>(
filter: F,
@ -63,17 +75,29 @@ export function waitForModules<
options: O,
): WaitForModulesUnsubscribeFunction
export function waitForModules(filter: Filter, callback: WaitForModulesCallback<any>, options?: WaitForModulesOptions) {
export function waitForModules(
filter: Filter,
callback: WaitForModulesCallback<any>,
options?: WaitForModulesOptions,
) {
return onAnyModuleInitialized(
options?.includeAll
? (id, exports) => {
const flag = runFilter(filter, id, exports, options)
if (flag) callback(exportsFromFilterResultFlag(flag, exports, options), id)
if (flag)
callback(
exportsFromFilterResultFlag(flag, exports, options),
id,
)
}
: (id, exports) => {
if (initializedModuleHasBadExports(id)) return
const flag = runFilter(filter, id, exports, options)
if (flag) callback(exportsFromFilterResultFlag(flag, exports, options), id)
if (flag)
callback(
exportsFromFilterResultFlag(flag, exports, options),
id,
)
},
)
}

View file

@ -1,6 +1,5 @@
import { _execInitSubs, _execReqSubs } from './subscriptions/_internal'
import { isModuleExportsBad } from './utils'
import type { Metro } from '../types'
/** Initializing ID */
@ -19,7 +18,12 @@ export const _metas = new Map<
export function patchMetroDefine(metroDefine: Metro.DefineFn) {
return ((origFactory, id, deps) => {
// deps won't be undefined last time I checked
const metadata = [deps!, false] as typeof _metas extends Map<any, infer V> ? V : never
const metadata = [deps!, false] as typeof _metas extends Map<
any,
infer V
>
? V
: never
_metas.set(id, metadata)
_uninits.add(id)
@ -33,7 +37,15 @@ export function patchMetroDefine(metroDefine: Metro.DefineFn) {
_execReqSubs(id)
try {
origFactory(global, req, importDefault, importAll, module, exports, deps)
origFactory(
global,
req,
importDefault,
importAll,
module,
exports,
deps,
)
metadata[1] = true
// Add the module to the initialized set only if the factory doesn't error or the exports aren't bad

View file

@ -1,9 +1,7 @@
import { _initing, _paths } from './_internal'
import { _execPathSubs } from './subscriptions/_internal'
import { byProps } from '../finders/filters'
import { waitForModules } from '../finders/wait'
import { _initing, _paths } from './_internal'
import { _execPathSubs } from './subscriptions/_internal'
const unsubFFI = waitForModules(byProps('fileFinishedImporting'), exports => {
unsubFFI()

View file

@ -1,5 +1,9 @@
import type { ModuleFinishedImportingCallback, ModuleFirstRequiredCallback, ModuleInitializedCallback } from '.'
import type { Metro } from '../../types'
import type {
ModuleFinishedImportingCallback,
ModuleFirstRequiredCallback,
ModuleInitializedCallback,
} from '.'
export const _reqAll = new Set<ModuleFirstRequiredCallback>()
export const _req = new Map<Metro.ModuleID, Set<ModuleFirstRequiredCallback>>()
@ -15,7 +19,10 @@ export function _execReqSubs(id: Metro.ModuleID) {
}
}
export function _execInitSubs(id: Metro.ModuleID, exports: Metro.ModuleExports) {
export function _execInitSubs(
id: Metro.ModuleID,
exports: Metro.ModuleExports,
) {
for (const cb of _initAll) cb(id, exports)
if (_init.has(id)) {
for (const cb of _init.get(id)!) cb(id, exports)

View file

@ -1,11 +1,15 @@
import { _init, _initAll, _path, _req, _reqAll } from './_internal'
import type { Metro } from '../../types'
import type { initializedModuleHasBadExports } from '../utils'
export type ModuleFirstRequiredCallback = (id: Metro.ModuleID) => void
export type ModuleInitializedCallback = (id: Metro.ModuleID, exports: Metro.ModuleExports) => void
export type ModuleFinishedImportingCallback = (id: Metro.ModuleID, path: string) => void
export type ModuleInitializedCallback = (
id: Metro.ModuleID,
exports: Metro.ModuleExports,
) => void
export type ModuleFinishedImportingCallback = (
id: Metro.ModuleID,
path: string,
) => void
/**
* Registers a callback to be called when any module is initialized.
@ -35,7 +39,10 @@ export function onAnyModuleInitialized(callback: ModuleInitializedCallback) {
* @param callback The callback to be called.
* @returns A function that unregisters the callback.
*/
export function onModuleInitialized(id: Metro.ModuleID, callback: ModuleInitializedCallback) {
export function onModuleInitialized(
id: Metro.ModuleID,
callback: ModuleInitializedCallback,
) {
let set = _init.get(id)
if (!set) {
set = new Set()
@ -54,7 +61,9 @@ export function onModuleInitialized(id: Metro.ModuleID, callback: ModuleInitiali
* @param callback The callback to be called.
* @returns A function that unregisters the callback.
*/
export function onModuleFinishedImporting(callback: ModuleFinishedImportingCallback) {
export function onModuleFinishedImporting(
callback: ModuleFinishedImportingCallback,
) {
_path.add(callback)
return () => _path.delete(callback)
}
@ -69,7 +78,9 @@ export function onModuleFinishedImporting(callback: ModuleFinishedImportingCallb
* @param callback The callback to be called.
* @returns A function that unregisters the callback.
*/
export function onAnyModuleFirstRequired(callback: ModuleFirstRequiredCallback) {
export function onAnyModuleFirstRequired(
callback: ModuleFirstRequiredCallback,
) {
_reqAll.add(callback)
return () => _reqAll.delete(callback)
}
@ -85,7 +96,10 @@ export function onAnyModuleFirstRequired(callback: ModuleFirstRequiredCallback)
* @param callback The callback to be called.
* @returns A function that unregisters the callback.
*/
export function onModuleFirstRequired(id: Metro.ModuleID, callback: ModuleFirstRequiredCallback) {
export function onModuleFirstRequired(
id: Metro.ModuleID,
callback: ModuleFirstRequiredCallback,
) {
let set = _req.get(id)
if (!set) {
set = new Set()

View file

@ -1,6 +1,5 @@
import { isProxy } from '@revenge-mod/utils/proxy'
import { _inits, _metas, _uninits } from './_internal'
import type { Metro } from '../types'
/**
@ -29,7 +28,9 @@ export function initializedModuleHasBadExports(id: Metro.ModuleID): boolean {
* Returns the dependencies of a module.
* @param id The module ID.
*/
export function getModuleDependencies(id: Metro.ModuleID): Metro.DependencyMap | undefined {
export function getModuleDependencies(
id: Metro.ModuleID,
): Metro.DependencyMap | undefined {
return _metas.get(id)?.[0]
}
@ -45,7 +46,9 @@ export function isModuleInitialized(id: Metro.ModuleID): boolean {
* Returns the exports of an initialized module.
* @param id The module ID.
*/
export function getInitializedModuleExports(id: Metro.ModuleID): Metro.ModuleExports | undefined {
export function getInitializedModuleExports(
id: Metro.ModuleID,
): Metro.ModuleExports | undefined {
return _metas.get(id)?.[2]?.exports
}
@ -53,7 +56,9 @@ export function getInitializedModuleExports(id: Metro.ModuleID): Metro.ModuleExp
* Returns whether a particular module export is bad. This is used for filter functions to check whether an export is filterable.
* @param exp The export to check.
*/
export function isModuleExportBad(exp: Metro.ModuleExports[PropertyKey]): boolean {
export function isModuleExportBad(
exp: Metro.ModuleExports[PropertyKey],
): boolean {
return (
// Nullish?
exp == null ||
@ -78,7 +83,8 @@ export function isModuleExportsBad(exports: Metro.ModuleExports): boolean {
// - String, Boolean, Symbol, BigInt exports are not useful (who would do `module.exports = ...`?)
!(typeof exports === 'object' || typeof exports === 'function') ||
// Checking if the object is empty
(exports.__proto__ === Object.prototype && !Reflect.ownKeys(exports).length) ||
(exports.__proto__ === Object.prototype &&
!Reflect.ownKeys(exports).length) ||
// Can't run isProxy() on this because this isn't your typical proxy:
// https://github.com/facebook/react-native/blob/master/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp
exports === nativeModuleProxy

View file

@ -55,7 +55,11 @@ export namespace Metro {
export type RequireFn = (id: ModuleID) => ModuleExports
export type DefineFn = (factory: FactoryFn, moduleId: ModuleID, dependencyMap?: DependencyMap | undefined) => void
export type DefineFn = (
factory: FactoryFn,
moduleId: ModuleID,
dependencyMap?: DependencyMap | undefined,
) => void
export type ModuleDefiner = (moduleId: ModuleID) => void

View file

@ -1,11 +1,9 @@
import {
createPatchedFunctionProxy,
type HookNode,
type PatchedFunctionProxyState,
patchedFunctionProxyStates,
unproxy,
} from '../_internal'
import type { HookNode, PatchedFunctionProxyState } from '../_internal'
import type {
AfterHook,
BeforeHook,

View file

@ -1,11 +1,9 @@
import {
createPatchedFunctionProxy,
type HookNode,
type PatchedFunctionProxyState,
patchedFunctionProxyStates,
unproxy,
} from '../_internal'
import type { HookNode, PatchedFunctionProxyState } from '../_internal'
import type { BeforeHook, FiniteDomain, UnknownFunction } from '../types'
function unpatchBefore<T extends UnknownFunction>(

View file

@ -1,13 +1,14 @@
import {
createPatchedFunctionProxy,
type FunctionProxyState,
type InsteadHookNode,
type PatchedFunctionProxyState,
patchedFunctionProxyHandler,
patchedFunctionProxyStates,
unproxy,
} from '../_internal'
import type {
FunctionProxyState,
InsteadHookNode,
PatchedFunctionProxyState,
} from '../_internal'
import type {
AbstractNewable,
Callable,

View file

@ -1,11 +1,13 @@
import { getStorage, type StorageOptions } from '@revenge-mod/storage'
import { getStorage } from '@revenge-mod/storage'
import { defineLazyProperty } from '@revenge-mod/utils/objects'
import { allSettled, sleepReject } from '@revenge-mod/utils/promises'
import { _uapi as uapi } from './apis'
import { PluginFlags as Flag, PluginsStorageDirectory, PluginStatus as Status } from './constants'
import {
PluginFlags as Flag,
PluginsStorageDirectory,
PluginStatus as Status,
} from './constants'
import type { StorageOptions } from '@revenge-mod/storage'
import type { AnyObject } from '@revenge-mod/utils/types'
import type {
InitPluginApi,
@ -19,9 +21,15 @@ import type {
export const _uapi = uapi
export const _preInitExts: Array<(api: PreInitPluginApi, plugin: InternalPlugin) => void> = []
export const _initExts: Array<(api: InitPluginApi, plugin: InternalPlugin) => void> = []
export const _startExts: Array<(api: PluginApi, plugin: InternalPlugin) => void> = []
export const _preInitExts: Array<
(api: PreInitPluginApi, plugin: InternalPlugin) => void
> = []
export const _initExts: Array<
(api: InitPluginApi, plugin: InternalPlugin) => void
> = []
export const _startExts: Array<
(api: PluginApi, plugin: InternalPlugin) => void
> = []
export const _plugins = new Map<PluginManifest['id'], InternalPlugin>()
export const _metas = new Map<
@ -41,7 +49,8 @@ export function registerPlugin<S extends AnyObject = AnyObject>(
iflags: number,
) {
// TODO(plugins): verify plugin manifest
if (_plugins.has(manifest.id)) throw new Error(`Plugin with ID "${manifest.id}" already registered`)
if (_plugins.has(manifest.id))
throw new Error(`Plugin with ID "${manifest.id}" already registered`)
const plugin: InternalPlugin = {
_c: [],
@ -95,17 +104,19 @@ function preparePluginPreInit(plugin: InternalPlugin) {
meta[3] = PluginApiLevel.PreInit
}
function preparePluginInit(plugin: InternalPlugin, storageOptions?: StorageOptions<AnyObject>) {
function preparePluginInit(
plugin: InternalPlugin,
storageOptions?: StorageOptions<AnyObject>,
) {
const meta = _metas.get(plugin.manifest.id)!
const api = meta[0] as InitPluginApi
defineLazyProperty(api, 'storage', () => getStorage(
`${PluginsStorageDirectory}/${plugin.manifest.id}.json`,
{
defineLazyProperty(api, 'storage', () =>
getStorage(`${PluginsStorageDirectory}/${plugin.manifest.id}.json`, {
...storageOptions,
directory: 'documents',
},
))
}),
)
for (const ext of _initExts) ext(api, plugin)
@ -126,10 +137,13 @@ function preparePluginStart(plugin: InternalPlugin) {
async function disablePlugin(plugin: InternalPlugin) {
const iflags = _metas.get(plugin.manifest.id)![2] ?? 0
if (iflags & InternalPluginFlags.Essential)
throw new Error(`Plugin "${plugin.manifest.id}" is essential and cannot be disabled`)
throw new Error(
`Plugin "${plugin.manifest.id}" is essential and cannot be disabled`,
)
// If plugin is not stopped, and is also not stopping, we need to stop it
if (plugin.status && !(plugin.status & Status.Stopping)) await stopPlugin(plugin)
if (plugin.status && !(plugin.status & Status.Stopping))
await stopPlugin(plugin)
// TODO(plugins): write to storage
plugin.flags &= ~Flag.Enabled
@ -153,9 +167,12 @@ export async function preInitPlugin(plugin: InternalPlugin) {
preparePluginPreInit(plugin)
if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${id}" is not enabled`)
if (!(plugin.flags & Flag.Enabled))
throw new Error(`Plugin "${id}" is not enabled`)
if (plugin.status & (Status.PreIniting | Status.PreInited))
throw new Error(`Plugin preInit lifecycle for "${id}" is already running`)
throw new Error(
`Plugin preInit lifecycle for "${id}" is already running`,
)
plugin.status |= Status.PreIniting
@ -182,7 +199,8 @@ export async function initPlugin(plugin: InternalPlugin) {
const meta = _metas.get(id)!
const [, promises, , apiLevel] = meta
if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${id}" is not enabled`)
if (!(plugin.flags & Flag.Enabled))
throw new Error(`Plugin "${id}" is not enabled`)
if (plugin.status & (Status.Initing | Status.Inited))
throw new Error(`Plugin init lifecycle for "${id}" is already running`)
@ -212,7 +230,8 @@ export async function startPlugin(plugin: InternalPlugin) {
const meta = _metas.get(id)!
const [, promises, , apiLevel] = meta
if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${id}" is not enabled`)
if (!(plugin.flags & Flag.Enabled))
throw new Error(`Plugin "${id}" is not enabled`)
if (plugin.status & (Status.Starting | Status.Started))
throw new Error(`Plugin start lifecycle for "${id}" is already running`)
@ -245,24 +264,32 @@ export async function stopPlugin(plugin: InternalPlugin) {
const meta = _metas.get(id)!
const [, promises, iflags, apiLevel] = meta
if (iflags & InternalPluginFlags.Essential) throw new Error(`Plugin "${id}" is essential and cannot be stopped`)
if (iflags & InternalPluginFlags.Essential)
throw new Error(`Plugin "${id}" is essential and cannot be stopped`)
if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${id}" is not enabled`)
if (plugin.status & Status.Stopping) throw new Error(`Plugin "${id}" is stopping`)
if (!(plugin.flags & Flag.Enabled))
throw new Error(`Plugin "${id}" is not enabled`)
if (plugin.status & Status.Stopping)
throw new Error(`Plugin "${id}" is stopping`)
// If the plugin is running its lifecycles, we need to wait for it to finish, then we'll stop it
// We want to wait at max 5 seconds for the lifecycles to finish
if (plugin.status & (Status.PreIniting | Status.Initing | Status.Starting))
await Promise.race([
Promise.all(promises),
sleepReject(MaxWaitTime, 'Plugin lifecycles timed out, force stopping'),
sleepReject(
MaxWaitTime,
'Plugin lifecycles timed out, force stopping',
),
])
.then(finished => {
// If the lifecycles don't finish in 5 seconds, a reload is probably required to unapply the changes
if (!finished) plugin.flags |= Flag.ReloadRequired
})
.catch(e => handlePluginError(e, plugin))
else if (!(plugin.status & (Status.PreInited | Status.Inited | Status.Started)))
else if (
!(plugin.status & (Status.PreInited | Status.Inited | Status.Started))
)
throw new Error(`Plugin "${id}" is not running`)
if (apiLevel < PluginApiLevel.PreInit) preparePluginPreInit(plugin)
@ -275,7 +302,10 @@ export async function stopPlugin(plugin: InternalPlugin) {
if (lifecycles.stop)
await Promise.race([
lifecycles.stop(meta[0] as PluginApi),
sleepReject(MaxWaitTime, 'Plugin stop lifecycle timed out, force stopping'),
sleepReject(
MaxWaitTime,
'Plugin stop lifecycle timed out, force stopping',
),
])
} catch (e) {
await handlePluginError(e, plugin)

View file

@ -10,9 +10,15 @@ export interface PluginApiExternals {
export const externals = {} as PluginApiExternals
// TODO: Rolldown issue: Bug requires us to do { ...require('...') } instead of just require('...')
defineLazyProperty(externals, 'Browserify', () => ({ ...require('@revenge-mod/externals/browserify') }))
defineLazyProperty(externals, 'Browserify', () => ({
...require('@revenge-mod/externals/browserify'),
}))
defineLazyProperty(externals, 'ReactNativeClipboard', () => ({
...require('@revenge-mod/externals/react-native-clipboard'),
}))
defineLazyProperty(externals, 'ReactNavigation', () => ({ ...require('@revenge-mod/externals/react-navigation') }))
defineLazyProperty(externals, 'Shopify', () => ({ ...require('@revenge-mod/externals/shopify') }))
defineLazyProperty(externals, 'ReactNavigation', () => ({
...require('@revenge-mod/externals/react-navigation'),
}))
defineLazyProperty(externals, 'Shopify', () => ({
...require('@revenge-mod/externals/shopify'),
}))

View file

@ -1,7 +1,14 @@
import * as PatcherLibrary from '@revenge-mod/patcher'
import type { UnscopedInitPluginApi, UnscopedPluginApi, UnscopedPreInitPluginApi } from '../types'
import type {
UnscopedInitPluginApi,
UnscopedPluginApi,
UnscopedPreInitPluginApi,
} from '../types'
export const _uapi: UnscopedPreInitPluginApi | UnscopedInitPluginApi | UnscopedPluginApi = {
export const _uapi:
| UnscopedPreInitPluginApi
| UnscopedInitPluginApi
| UnscopedPluginApi = {
modules: require('./modules').modules,
plugins: require('./plugins').plugins,
patcher: PatcherLibrary,

View file

@ -4,7 +4,8 @@ export interface PluginApiModules {
native: PluginApiModulesNative
}
export type PluginApiModulesNative = typeof import('@revenge-mod/modules/native')
export type PluginApiModulesNative =
typeof import('@revenge-mod/modules/native')
export type PluginApiModulesMetro =
// biome-ignore format: Don't

View file

@ -1,14 +1,10 @@
import { onRunApplication } from '@revenge-mod/react/native'
import { _uapi } from './apis'
import * as AssetsLibrary from '@revenge-mod/assets'
import { onRunApplication } from '@revenge-mod/react/native'
import { _plugins, initPlugin, startPlugin } from './_internal'
import { _uapi } from './apis'
import { externals } from './apis/externals'
import { react } from './apis/react'
import { _plugins, initPlugin, startPlugin } from './_internal'
import { PluginFlags } from './constants'
import type { UnscopedInitPluginApi } from './types'
// Setup init plugin APIs
@ -17,7 +13,8 @@ uapi.assets = AssetsLibrary
uapi.externals = externals
uapi.react = react
for (const plugin of _plugins.values()) if (plugin.flags & PluginFlags.Enabled) initPlugin(plugin)
for (const plugin of _plugins.values())
if (plugin.flags & PluginFlags.Enabled) initPlugin(plugin)
const unsub = onRunApplication(() => {
unsub()
@ -26,5 +23,6 @@ const unsub = onRunApplication(() => {
// const UILibrary = require('@revenge-mod/ui')
// uapi.ui = UiLibrary
for (const plugin of _plugins.values()) if (plugin.flags & PluginFlags.Enabled) startPlugin(plugin)
for (const plugin of _plugins.values())
if (plugin.flags & PluginFlags.Enabled) startPlugin(plugin)
})

View file

@ -1,4 +1,5 @@
import { _plugins, preInitPlugin } from './_internal'
import { PluginFlags } from './constants'
for (const plugin of _plugins.values()) if (plugin.flags & PluginFlags.Enabled) preInitPlugin(plugin)
for (const plugin of _plugins.values())
if (plugin.flags & PluginFlags.Enabled) preInitPlugin(plugin)

View file

@ -1,12 +1,10 @@
import type { PluginFlags, PluginStatus } from './constants'
import type { Storage, StorageOptions } from '@revenge-mod/storage'
import type { AnyObject } from '@revenge-mod/utils/types'
import type { PluginApiExternals } from './apis/externals'
import type { PluginApiModules } from './apis/modules'
import type { PluginApiPlugins } from './apis/plugins'
import type { PluginApiReact } from './apis/react'
import type { PluginFlags, PluginStatus } from './constants'
/**
* The unscoped plugin API (very limited). This API is available as a global for plugins.
@ -54,7 +52,8 @@ export interface PreInitPluginApi {
* The plugin API (limited).
* Available in the `init` phase.
*/
export interface InitPluginApi<S extends AnyObject = AnyObject> extends PreInitPluginApi {
export interface InitPluginApi<S extends AnyObject = AnyObject>
extends PreInitPluginApi {
unscoped: UnscopedInitPluginApi
/**
* The plugin storage.
@ -71,7 +70,8 @@ export interface InitPluginApi<S extends AnyObject = AnyObject> extends PreInitP
* The plugin API.
* Available in the `start` and `stop` phase.
*/
export interface PluginApi<S extends AnyObject = AnyObject> extends InitPluginApi<S> {
export interface PluginApi<S extends AnyObject = AnyObject>
extends InitPluginApi<S> {
unscoped: UnscopedPluginApi
}
@ -114,7 +114,8 @@ export interface PluginManifest {
icon?: string
}
export interface PluginOptions<S extends AnyObject = AnyObject> extends PluginLifecycles<S> {
export interface PluginOptions<S extends AnyObject = AnyObject>
extends PluginLifecycles<S> {
storage?: Omit<StorageOptions<S>, 'directory'>
}

View file

@ -1,6 +1,5 @@
import { byProps } from '@revenge-mod/modules/finders/filters'
import { waitForModules } from '@revenge-mod/modules/finders/wait'
import type { Metro } from '@revenge-mod/modules/types'
export let ReactModuleId: Metro.ModuleID
@ -13,25 +12,34 @@ export let ReactJsxRuntime: typeof import('react/jsx-runtime')
let RtCount = 0
const unsubRt = waitForModules(byProps<typeof React>('useState'), (exports, id) => {
if (RtCount++ === 2) return unsubRt()
const unsubRt = waitForModules(
byProps<typeof React>('useState'),
(exports, id) => {
if (RtCount++ === 2) return unsubRt()
ReactModuleId = id
React = exports
})
ReactModuleId = id
React = exports
},
)
const unsubRN = waitForModules(byProps<typeof ReactNative>('AppRegistry'), (exports, id) => {
unsubRN()
const unsubRN = waitForModules(
byProps<typeof ReactNative>('AppRegistry'),
(exports, id) => {
unsubRN()
ReactNativeModuleId = id
ReactNative = exports
})
ReactNativeModuleId = id
ReactNative = exports
},
)
let RJsxRCount = 0
const unsubRJsxR = waitForModules(byProps<typeof ReactJsxRuntime>('jsxs'), (exports, id) => {
if (RJsxRCount++ === 2) return unsubRJsxR()
const unsubRJsxR = waitForModules(
byProps<typeof ReactJsxRuntime>('jsxs'),
(exports, id) => {
if (RJsxRCount++ === 2) return unsubRJsxR()
ReactJsxRuntimeModuleId = id
ReactJsxRuntime = exports
})
ReactJsxRuntimeModuleId = id
ReactJsxRuntime = exports
},
)

View file

@ -1,7 +1,5 @@
import { _ara, _bra } from './patches'
import type { AppRegistry } from 'react-native'
/**
* Registers a callback to be run when a call to {@link AppRegistry.runApplication} is made.
*

View file

@ -1,9 +1,7 @@
import { after, before } from '@revenge-mod/patcher'
import { ReactNative } from '..'
import { byProps } from '@revenge-mod/modules/finders/filters'
import { waitForModules } from '@revenge-mod/modules/finders/wait'
import { after, before } from '@revenge-mod/patcher'
import { ReactNative } from '..'
import type { RunApplicationCallback } from '.'
export const _bra = new Set<RunApplicationCallback>()

View file

@ -4,7 +4,6 @@ import { FileModule } from '@revenge-mod/discord/native'
import { React } from '@revenge-mod/react'
import { mergeDeep } from '@revenge-mod/utils/objects'
import { useReRender } from '@revenge-mod/utils/react'
import type { AnyObject, DeepPartial } from '@revenge-mod/utils/types'
/**
@ -13,7 +12,10 @@ import type { AnyObject, DeepPartial } from '@revenge-mod/utils/types'
* @param path Path relative to the directory.
* @param directory Directory to use. Can be either 'cache' or 'documents'.
*/
export function getStorage<T extends AnyObject = AnyObject>(path: string, options?: StorageOptions<T>): Storage<T> {
export function getStorage<T extends AnyObject = AnyObject>(
path: string,
options?: StorageOptions<T>,
): Storage<T> {
const { CacheDirPath, DocumentsDirPath } = FileModule.getConstants()
const directory = options?.directory ?? 'documents'
@ -71,7 +73,12 @@ export function getStorage<T extends AnyObject = AnyObject>(path: string, option
for (const sub of subs) sub(cache)
return cache
} catch (e) {
console.error('Failed to parse storage file (most likely corrupted)', directory, path, e)
console.error(
'Failed to parse storage file (most likely corrupted)',
directory,
path,
e,
)
}
}
},
@ -85,7 +92,12 @@ export function getStorage<T extends AnyObject = AnyObject>(path: string, option
for (const sub of subs) sub(value)
} catch (e) {
console.error('Failed to write storage file', directory, path, e)
console.error(
'Failed to write storage file',
directory,
path,
e,
)
}
},
}
@ -110,7 +122,9 @@ export interface StorageOptions<T> {
load?: boolean
}
export type UseStorageFilter<T extends AnyObject> = (newValue: DeepPartial<T>) => any
export type UseStorageFilter<T extends AnyObject> = (
newValue: DeepPartial<T>,
) => any
export interface Storage<T extends AnyObject> {
/**

11
lib/types/types.d.ts vendored
View file

@ -5,7 +5,10 @@ import type { Metro } from '@revenge-mod/modules/types'
/// HERMES
declare global {
function setTimeout(cb: (...args: unknown[]) => unknown, timeout?: number): number
function setTimeout(
cb: (...args: unknown[]) => unknown,
timeout?: number,
): number
/**
* Calls the garbage collector
*/
@ -28,13 +31,17 @@ declare global {
/// HERMES PROMISES
declare global {
// biome-ignore lint/correctness/noUnusedVariables: Type parameter names must match
interface Promise<T> {
/// PROMISE POLYFILLS FROM: https://github.com/then/promise
/// AND: https://github.com/facebook/hermes/blob/main/lib/InternalBytecode/01-Promise.js
_h: 0 | 1 | 2
}
type HermesPromiseRejectionHandler = (promise: Promise<any>, error: any) => void
type HermesPromiseRejectionHandler = (
promise: Promise<any>,
error: any,
) => void
interface PromiseConstructor {
_m: HermesPromiseRejectionHandler

View file

@ -1,4 +1,7 @@
export function debounce<F extends (...args: any[]) => any>(func: F, timeout: number) {
export function debounce<F extends (...args: any[]) => any>(
func: F,
timeout: number,
) {
let timer: Parameters<typeof clearTimeout>[0]
return (...args: Parameters<F>) =>
new Promise(rs => {

View file

@ -1,16 +1,24 @@
import { getAssetByName, getAssetModuleId } from '@revenge-mod/assets'
import { TokensModuleId } from '@revenge-mod/discord/common'
import { byDependencies, byProps } from '@revenge-mod/modules/finders/filters'
import { lookupModule } from '@revenge-mod/modules/finders/lookup'
import { ReactJsxRuntimeModuleId, ReactModuleId, ReactNativeModuleId } from '@revenge-mod/react'
import {
ReactJsxRuntimeModuleId,
ReactModuleId,
ReactNativeModuleId,
} from '@revenge-mod/react'
import type { FC } from 'react'
// [React, ReactJsxRuntime, (Tokens), (BaseIconImage), (Asset), 2]
// Asset = relativeDep(1)
const IconComponentFilter = [ReactModuleId, ReactJsxRuntimeModuleId, TokensModuleId, undefined, undefined, 2]
const IconComponentFilter = [
ReactModuleId,
ReactJsxRuntimeModuleId,
TokensModuleId,
undefined,
undefined,
2,
]
// Generated IconComponents that have multiple assets, there is no specific length, so the filter has to be generated on-the-fly.
// [React, ReactNative, ReactJsxRuntime, (Tokens), (BaseIconImage), (...Assets), 2]
@ -24,7 +32,10 @@ const MultiIconComponentFilterBase = [
]
// TODO(utils/discord/lookupGeneratedIconComponent): Make this into a filter?
export function lookupGeneratedIconComponent<N extends string>(compName: N, ...assetNames: string[]) {
export function lookupGeneratedIconComponent<N extends string>(
compName: N,
...assetNames: string[]
) {
const icon = lookupModule(byProps(compName))[0]?.[compName]
if (icon) return icon

View file

@ -23,7 +23,9 @@ export function yielded<T>(generator: Generator<T>, skip = 0): T {
if (skip)
for (let i = 0; i < skip; i++)
if (generator.next().done)
throw new Error(`Generator has no more values to drop (dropped ${i + 1} out of ${skip})`)
throw new Error(
`Generator has no more values to drop (dropped ${i + 1} out of ${skip})`,
)
return generator.next().value
}

View file

@ -3,7 +3,8 @@ export function createLogger(tag: string) {
log: (message: string) => console.log(`[${tag}] ${message}`),
warn: (message: string) => console.warn(`[${tag}] ${message}`),
error: (message: string) => console.error(`[${tag}] ${message}`),
wtf: (message: string) => console.error(`\u001b[31m[${tag}] ${message}\u001b[0m`),
wtf: (message: string) =>
console.error(`\u001b[31m[${tag}] ${message}\u001b[0m`),
} satisfies BasicLogger
}

View file

@ -31,8 +31,12 @@ export type InterceptPropertyCallback = (target: any, value: any) => any
* @param callback The callback to call when the property is set, can return a non-null value to set the property to.
* @returns The function to unintercept the property.
*/
export function interceptProperty(prop: PropertyKey, callback: (target: object, value: any) => any) {
if (_intercepting.has(prop)) throw new Error(`Property ${String(prop)} is already being intercepted`)
export function interceptProperty(
prop: PropertyKey,
callback: (target: object, value: any) => any,
) {
if (_intercepting.has(prop))
throw new Error(`Property ${String(prop)} is already being intercepted`)
const proto = Object.prototype
_intercepting.set(prop, Object.getOwnPropertyDescriptor(proto, prop))
@ -102,7 +106,11 @@ export function mergeDeep(target: AnyObject, source: AnyObject) {
* @param property The property key to define.
* @param loader The function that will be called to load the property value when accessed.
*/
export function defineLazyProperty<T extends object, K extends keyof T>(target: T, property: K, loader: () => T[K]) {
export function defineLazyProperty<T extends object, K extends keyof T>(
target: T,
property: K,
loader: () => T[K],
) {
Object.defineProperty(target, property, {
configurable: true,
get() {

View file

@ -1,4 +1,6 @@
export function allSettled(promises: Promise<any>[]): Promise<PromiseSettledResult<any>[]> {
export function allSettled(
promises: Promise<any>[],
): Promise<PromiseSettledResult<any>[]> {
const mapped = promises.map(p =>
p instanceof Promise
? p

View file

@ -45,7 +45,10 @@ export function getProxyTarget(obj: object) {
// Heavily inspired by Wintry's lazy utils, but more optimized and stripped down, with a few fixes.
// https://github.com/pylixonly/wintry/blob/main/src/utils/lazy.ts
const _metas = new WeakMap<object, [factory: () => unknown, cacheable: boolean, cache?: unknown]>()
const _metas = new WeakMap<
object,
[factory: () => unknown, cacheable: boolean, cache?: unknown]
>()
const _handler = {
...Object.fromEntries(
@ -64,7 +67,12 @@ const _handler = {
if (typeof val === 'function')
return new Proxy(val, {
// If thisArg happens to be a proxified value, we will use the target object instead
apply: (fn, thisArg, args) => Reflect.apply(fn, thisArg === recv ? target : thisArg, args),
apply: (fn, thisArg, args) =>
Reflect.apply(
fn,
thisArg === recv ? target : thisArg,
args,
),
})
return val
@ -73,7 +81,8 @@ const _handler = {
// TypeError: getOwnPropertyDescriptor trap result is not configurable but target property '...' is configurable or non-existent
getOwnPropertyDescriptor: (hint, p) => {
const d = Reflect.getOwnPropertyDescriptor(unproxifyFromHint(hint)!, p)
if (d && !Reflect.getOwnPropertyDescriptor(hint, p)) Object.defineProperty(hint, p, d)
if (d && !Reflect.getOwnPropertyDescriptor(hint, p))
Object.defineProperty(hint, p, d)
return d
},
} as ProxyHandler<object>
@ -151,7 +160,8 @@ export function proxify<T>(signal: () => T, options?: ProxifyOptions): T {
*/
export function unproxify<T extends object>(proxified: T): T {
const hint = getProxyTarget(proxified)
if (!hint) throw new TypeError(`${typeof proxified} is not a proxified value`)
if (!hint)
throw new TypeError(`${typeof proxified} is not a proxified value`)
return unproxifyFromHint(hint)
}
@ -188,16 +198,24 @@ function unproxifyFromHint(hint: object) {
* z // TypeError: Cannot destructure and proxify null (reading 'z')
* ```
*/
export function destructure<T extends object>(proxified: T, options?: ProxifyOptions): T {
export function destructure<T extends object>(
proxified: T,
options?: ProxifyOptions,
): T {
return new Proxy({} as T, {
get: (_, p) =>
proxify(() => {
// @ts-expect-error
const v = unproxify(proxified)[p]
if (v == null) throw new TypeError(`Cannot destructure and proxify ${v} (reading '${String(p)}')`)
if (v == null)
throw new TypeError(
`Cannot destructure and proxify ${v} (reading '${String(p)}')`,
)
if (typeof v === 'function' || typeof v === 'object') return v
throw new TypeError(`Cannot destructure and proxify a primitive (reading '${String(p)}')`)
throw new TypeError(
`Cannot destructure and proxify a primitive (reading '${String(p)}')`,
)
}, options),
})
}

View file

@ -1,8 +1,7 @@
import { React } from '@revenge-mod/react'
import { type SearchFilter, findInTree } from './trees'
import { findInTree } from './trees'
import type { ReactNode } from 'react'
import type { SearchFilter } from './trees'
export function useIsFirstRender() {
let firstRender = false
@ -21,7 +20,10 @@ export function useReRender() {
const ReactFiberWalkable = new Set(['props', 'children', 'child', 'sibling'])
export function findInReactFiber(tree: Extract<ReactNode, { props: unknown }>, filter: SearchFilter): any {
export function findInReactFiber(
tree: Extract<ReactNode, { props: unknown }>,
filter: SearchFilter,
): any {
return findInTree(tree, filter, {
walkable: ReactFiberWalkable,
})

View file

@ -18,7 +18,11 @@ export interface FindInTreeOptions {
maxDepth?: number
}
export function findInTree(tree: SearchTree, filter: SearchFilter, opts?: FindInTreeOptions): any | undefined {
export function findInTree(
tree: SearchTree,
filter: SearchFilter,
opts?: FindInTreeOptions,
): any | undefined {
type StackItem = [node: any, depth: number]
const stack: StackItem[] = [[tree, 0]]
const maxDepth = opts?.maxDepth ?? 100
@ -37,7 +41,8 @@ export function findInTree(tree: SearchTree, filter: SearchFilter, opts?: FindIn
if (typeof item !== 'object' || item == null) continue
if (!isArray && opts?.walkable?.size && !opts.walkable?.has(key)) continue
if (!isArray && opts?.walkable?.size && !opts.walkable?.has(key))
continue
if (!isArray && opts?.ignore?.has(key)) continue
stack.push([item, depth + 1])

View file

@ -1,7 +1,11 @@
export type Nullish = null | undefined
export type If<T, Then, Else> = T extends true ? Then : Else
export type AnyObject = Record<any, any>
export type LogicalOr<T1, T2> = T1 extends true ? true : T2 extends true ? true : false
export type LogicalOr<T1, T2> = T1 extends true
? true
: T2 extends true
? true
: false
export type DeepPartial<T> = {
[K in keyof T]?: T[K] extends AnyObject ? DeepPartial<T[K]> : T[K]
}

View file

@ -29,7 +29,14 @@
"build": "bun --bun run scripts/build.ts",
"dev": "bun --bun run scripts/dev.ts"
},
"trustedDependencies": ["@biomejs/biome", "@parcel/watcher", "@swc/core"],
"trustedDependencies": [
"@biomejs/biome",
"@parcel/watcher",
"@swc/core"
],
"type": "module",
"workspaces": ["lib/*", "plugins/*"]
"workspaces": [
"lib/*",
"plugins/*"
]
}

View file

@ -1,12 +1,12 @@
import { parse } from 'path'
import { transform } from '@swc/core'
import { $, main } from 'bun'
import chalk from 'chalk'
import { exists, mkdir, readdir, rm, writeFile } from 'fs/promises'
import { type OutputChunk, type RolldownPlugin, rolldown } from 'rolldown'
import { parse } from 'path'
import { rolldown } from 'rolldown'
import { aliasPlugin, importGlobPlugin } from 'rolldown/experimental'
import pkg from '../package.json'
import type { OutputChunk, RolldownPlugin } from 'rolldown'
const ShimsDir = `${import.meta.dir}/../shims`
const AssetsDir = `${import.meta.dir}/../src/assets`
@ -45,8 +45,12 @@ export default async function build(dev = false, log = true) {
},
define: {
__BUILD_VERSION__: JSON.stringify(pkg.version),
__BUILD_COMMIT__: JSON.stringify((await $`git rev-parse HEAD`.text()).trim().substring(0, 7)),
__BUILD_BRANCH__: JSON.stringify((await $`git rev-parse --abbrev-ref HEAD`.text()).trim()),
__BUILD_COMMIT__: JSON.stringify(
(await $`git rev-parse HEAD`.text()).trim().substring(0, 7),
),
__BUILD_BRANCH__: JSON.stringify(
(await $`git rev-parse --abbrev-ref HEAD`.text()).trim(),
),
__BUILD_ENV__: JSON.stringify(dev ? 'development' : 'production'),
__BUILD_FLAG_LOG_PROMISE_REJECTIONS__: String(dev),
},
@ -71,11 +75,24 @@ export default async function build(dev = false, log = true) {
'-Wno-undefined-variable',
],
before() {
if (log) console.debug(chalk.cyanBright('\u{1F5CE} JS compilation finished...'))
if (log) console.debug(chalk.gray('\u{1F5CE} Compiling bytecode...'))
if (log)
console.debug(
chalk.cyanBright(
'\u{1F5CE} JS compilation finished...',
),
)
if (log)
console.debug(
chalk.gray('\u{1F5CE} Compiling bytecode...'),
)
},
after() {
if (log) console.debug(chalk.cyanBright('\u{1F5CE} Bytecode compilation finished'))
if (log)
console.debug(
chalk.cyanBright(
'\u{1F5CE} Bytecode compilation finished',
),
)
},
}),
],
@ -122,7 +139,7 @@ function swcPlugin() {
},
},
env: {
// https://github.com/discord/hermes/blob/main/doc/Features.md
// https://github.com/facebook/hermes/blob/main/doc/Features.md
targets: 'fully supports es6',
include: [
'transform-async-generator-functions',
@ -152,14 +169,23 @@ function swcPlugin() {
} satisfies RolldownPlugin
}
function hermesCPlugin({ after, before, flags }: { flags?: string[]; before?: () => void; after?: () => void } = {}) {
function hermesCPlugin({
after,
before,
flags,
}: {
flags?: string[]
before?: () => void
after?: () => void
} = {}) {
const paths = {
win32: 'hermesc.exe',
darwin: 'hermesc',
linux: 'hermesc',
}
if (!(process.platform in paths)) throw new Error(`Unsupported platform: ${process.platform}`)
if (!(process.platform in paths))
throw new Error(`Unsupported platform: ${process.platform}`)
const binPath = paths[process.platform as keyof typeof paths]
@ -201,7 +227,8 @@ function hermesCPlugin({ after, before, flags }: { flags?: string[]; before?: ()
}
async function generateAssets() {
if (!(await exists(GeneratedAssetsDir))) await mkdir(GeneratedAssetsDir, { recursive: true })
if (!(await exists(GeneratedAssetsDir)))
await mkdir(GeneratedAssetsDir, { recursive: true })
const promises: Promise<void>[] = []

View file

@ -1,8 +1,8 @@
import os from 'os'
import { join } from 'path'
import watcher from '@parcel/watcher'
import { debounce } from '@revenge-mod/utils/callbacks'
import chalk from 'chalk'
import os from 'os'
import { join } from 'path'
import pkg from '../package.json'
import build from './build'
@ -13,9 +13,18 @@ console.info(chalk.redBright(`\nRevenge ${chalk.white(`v${pkg.version}`)}\n`))
const cwdify = (path: string) => join(process.cwd(), path)
const Sources = ['src', 'lib', 'plugins'].map(cwdify)
const ExitTriggers = ['scripts', 'shims', 'package.json', 'bun.lock', 'tsconfig.json'].map(cwdify)
const ExitTriggers = [
'scripts',
'shims',
'package.json',
'bun.lock',
'tsconfig.json',
].map(cwdify)
const debouncedBuildDev = debounce(() => ((needRebuild = false), build(true).catch(console.error)), 250)
const debouncedBuildDev = debounce(
() => ((needRebuild = false), build(true).catch(console.error)),
250,
)
let needRebuild = true
@ -23,25 +32,40 @@ watcher.subscribe(process.cwd(), (err, events) => {
if (err) return console.error(err)
if (events.some(it => ExitTriggers.some(se => it.path.startsWith(se)))) {
console.error(chalk.redBright('\u26A0 Scripts, packages, shims, or tsconfig.json has changed, exiting!'))
console.error(
chalk.redBright(
'\u26A0 Scripts, packages, shims, or tsconfig.json has changed, exiting!',
),
)
process.exit()
}
needRebuild ||= events.some(it => Sources.some(src => it.path.startsWith(src)))
needRebuild ||= events.some(it =>
Sources.some(src => it.path.startsWith(src)),
)
})
const server = Bun.serve({
hostname: lanHost ? '0.0.0.0' : '127.0.0.1',
async fetch(req, srv) {
if (needRebuild) await debouncedBuildDev()
console.debug(chalk.gray(`\u{1F79B} Receiving request from ${srv.requestIP(req)!.address}`))
console.debug(
chalk.gray(
`\u{1F79B} Receiving request from ${srv.requestIP(req)!.address}`,
),
)
return new Response(Bun.file('./dist/revenge.bundle'))
},
port: 4040,
})
if (lanHost) console.info(chalk.gray('\u24D8 Listening on all interfaces...'))
else console.info(chalk.gray('\u24D8 Listening locally... Use --lan to listen on all interfaces'))
else
console.info(
chalk.gray(
'\u24D8 Listening locally... Use --lan to listen on all interfaces',
),
)
if (!prod) console.info(chalk.gray('\u24D8 Use --prod to build for production'))
else console.info(chalk.gray('\u24D8 Building for production...'))

View file

@ -1,13 +1,22 @@
import { ReactJsxRuntime } from '@revenge-mod/react'
import { isProxified, unproxify } from '@revenge-mod/utils/proxy'
import type { ElementType } from 'react'
const { jsx: _jsx, jsxs: _jsxs, Fragment } = ReactJsxRuntime
const jsx = ((type, ...args) =>
_jsx(isProxified(type as object) ? (unproxify(type as object) as ElementType) : type, ...args)) as typeof _jsx
_jsx(
isProxified(type as object)
? (unproxify(type as object) as ElementType)
: type,
...args,
)) as typeof _jsx
const jsxs = ((type, ...args) =>
_jsxs(isProxified(type as object) ? (unproxify(type as object) as ElementType) : type, ...args)) as typeof _jsxs
_jsxs(
isProxified(type as object)
? (unproxify(type as object) as ElementType)
: type,
...args,
)) as typeof _jsxs
export { jsx, jsxs, Fragment }

View file

@ -1,12 +1,13 @@
import { FormSwitch as Switch } from '@revenge-mod/discord/design'
import { ReactNative } from '@revenge-mod/react'
import type { DiscordModules } from '@revenge-mod/discord/types'
/**
* A switch component that is styled to match Discord's configuration
*/
export default function FormSwitch(props: DiscordModules.Components.FormSwitchProps) {
export default function FormSwitch(
props: DiscordModules.Components.FormSwitchProps,
) {
return (
<ReactNative.View style={props.disabled && styles.disabled}>
<Switch {...props} />

View file

@ -1,10 +1,10 @@
import { Design } from '@revenge-mod/discord/design'
import type { DiscordModules } from '@revenge-mod/discord/types'
import { ReactNative } from '@revenge-mod/react'
import type { DiscordModules } from '@revenge-mod/discord/types'
export default function Page(props: DiscordModules.Components.StackProps) {
return (
<Design.Stack style={styles.page} spacing={24} {...props}>
<Design.Stack spacing={24} style={styles.page} {...props}>
{props.children}
</Design.Stack>
)

View file

@ -1,17 +1,18 @@
import { Design } from '@revenge-mod/discord/design'
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
import type { DiscordModules } from '@revenge-mod/discord/types'
const MagnifyingGlassIcon = lookupGeneratedIconComponent('MagnifyingGlassIcon')
export default function SearchInput(props: DiscordModules.Components.TextInputProps) {
export default function SearchInput(
props: DiscordModules.Components.TextInputProps,
) {
return (
<Design.TextInput
leadingIcon={MagnifyingGlassIcon}
size="md"
placeholder="Search"
returnKeyType="search"
size="md"
{...props}
/>
)

View file

@ -1,13 +1,20 @@
import { getAssetIdByName } from '@revenge-mod/assets'
import { Design } from '@revenge-mod/discord/design'
import type { DiscordModules } from '@revenge-mod/discord/types'
export default function TableRowAssetIcon(props: TableRowAssetIconProps) {
return <Design.TableRow.Icon source={props.name ? getAssetIdByName(props.name)! : props.id!} {...props} />
return (
<Design.TableRow.Icon
source={props.name ? getAssetIdByName(props.name)! : props.id!}
{...props}
/>
)
}
export type TableRowAssetIconProps = Omit<DiscordModules.Components.TableRowIconProps, 'source'> &
export type TableRowAssetIconProps = Omit<
DiscordModules.Components.TableRowIconProps,
'source'
> &
(
| {
name: string

View file

@ -4,16 +4,23 @@ import { interceptProperty } from '@revenge-mod/utils/objects'
// By doing this, we are assuming __METRO_GLOBAL_PREFIX__ is an empty string and globalThis.__d isn't already set.
// But both should be true either way.
const unintercept = interceptProperty('__d', (_, mDefine) => (unintercept(), patchMetroDefine(mDefine)))
const unintercept = interceptProperty(
'__d',
(_, mDefine) => (unintercept(), patchMetroDefine(mDefine)),
)
import { onModuleFirstRequired, onModuleInitialized } from '@revenge-mod/modules/metro/subscriptions'
import {
onModuleFirstRequired,
onModuleInitialized,
} from '@revenge-mod/modules/metro/subscriptions'
import { getErrorStack } from '@revenge-mod/utils/errors'
const IndexModuleId = 0
onModuleFirstRequired(IndexModuleId, function onIndexRequired() {
runCatching(() => {
if (__BUILD_FLAG_LOG_PROMISE_REJECTIONS__) require('./patches/log-promise-rejections')
if (__BUILD_FLAG_LOG_PROMISE_REJECTIONS__)
require('./patches/log-promise-rejections')
// Initialize preinit libraries
require('@revenge-mod/modules/preinit')
@ -44,12 +51,10 @@ function runCatching(fn: () => void) {
} catch (e) {
// TODO(init): Move to use native provided alert function, which will accept a string stack trace
// Above will reduce the need for runCatching() to exist, as the code won't be able to be deduped any further
const { ClientInfoModule, DeviceModule } = require('@revenge-mod/discord/native') as
// biome-ignore format: Don't format this please
typeof import('@revenge-mod/discord/native')
const { ClientInfoModule, DeviceModule } =
require('@revenge-mod/discord/native') as typeof import('@revenge-mod/discord/native')
alert(
// biome-ignore lint/style/useTemplate: I want this to be readable, thank you
`Failed to load Revenge (${__BUILD_VERSION__}-${__BUILD_COMMIT__}-${__BUILD_BRANCH__} (${__BUILD_ENV__}))\n` +
`Discord: ${ClientInfoModule.Version} (${ClientInfoModule.Build})\n` +
`Device: ${DeviceModule.deviceManufacturer} ${DeviceModule.deviceModel}\n\n` +

View file

@ -16,7 +16,10 @@ Promise._m = (promise, err) => {
if (err)
setTimeout(
() => {
if (promise._h === 0) logger.error(`Unhandled promise rejection: ${getErrorStack(err)}`)
if (promise._h === 0)
logger.error(
`Unhandled promise rejection: ${getErrorStack(err)}`,
)
},
ErrorTypeWhitelist.some(it => err instanceof it) ? 0 : 2000,
// The time is completely arbitary. I've picked what Hermes chose.

View file

@ -1,7 +1,5 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting, UserSettingsSections } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const AssetBrowserSetting: SettingsItem = {
@ -11,7 +9,8 @@ const AssetBrowserSetting: SettingsItem = {
title: () => 'Asset Browser',
screen: {
route: UserSettingsSections[MobileSetting.ASSET_BROWSER],
getComponent: () => require('../screens/AssetBrowserSettingScreen').default,
getComponent: () =>
require('../screens/AssetBrowserSettingScreen').default,
},
}

View file

@ -1,17 +1,11 @@
import { React } from '@revenge-mod/react'
import { AlertActionCreators } from '@revenge-mod/discord/actions'
import { Design } from '@revenge-mod/discord/design'
import { nodeUtil } from '@revenge-mod/externals/browserify'
import { React } from '@revenge-mod/react'
import { getErrorStack } from '@revenge-mod/utils/errors'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { api } from '..'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const ALERT_KEY = 'evaluate-javascript'
@ -21,7 +15,8 @@ const EvaluateJavaScriptSetting: SettingsItem = {
IconComponent: () => <TableRowAssetIcon name="FileIcon" />,
title: () => 'Evaluate JavaScript',
useDescription: () => 'Runs a JavaScript code snippet.',
onPress: () => AlertActionCreators.openAlert(ALERT_KEY, <EvaluateJavaScriptAlert />),
onPress: () =>
AlertActionCreators.openAlert(ALERT_KEY, <EvaluateJavaScriptAlert />),
type: 'pressable',
}
@ -47,60 +42,21 @@ function EvaluateJavaScriptAlert() {
return (
<AlertModal
title="Evaluate JavaScript"
extraContent={
<Stack>
<TextArea
autoFocus
label="Code"
size="md"
placeholder="ReactNative.NativeModules.BundleUpdaterManager.reload()"
onChange={v => (code.current = v)}
/>
<TableRowGroup>
<TableSwitchRow
label="Await result"
subLabel="Wait for the result of the code before displaying it."
value={awaitResult}
onValueChange={setAwaitResult}
/>
<TableSwitchRow
label="Show hidden"
subLabel="Show hidden properties of the object."
value={showHidden}
onValueChange={setShowHidden}
/>
<TableRow
label="Inspect depth"
subLabel="The depth of the object to inspect."
trailing={<Text>{inspectDepth}</Text>}
/>
<Slider
startIcon={<Text>1</Text>}
endIcon={<Text>10</Text>}
step={1}
minimumValue={1}
maximumValue={10}
value={inspectDepth}
onValueChange={setInspectDepth}
/>
</TableRowGroup>
</Stack>
}
actions={
<Stack>
<Button
text="Evaluate"
variant="primary"
loading={loading}
// Async arrow functions are not supported
onPress={async function onPress() {
setLoading(true)
try {
if (!api) {
alert('Unable to provide plugin API. Running snippet in a second...')
await new Promise(rs => setTimeout(rs, 1000))
alert(
'Unable to provide plugin API. Running snippet in a second...',
)
await new Promise(rs =>
setTimeout(rs, 1000),
)
} else {
// @ts-expect-error
globalThis.revenge = api.unscoped
@ -113,10 +69,15 @@ function EvaluateJavaScriptAlert() {
const res = globalThis.eval?.(code.current)
alert(
nodeUtil.inspect(awaitResult && res instanceof Promise ? await res : res, {
depth: inspectDepth,
showHidden,
}),
nodeUtil.inspect(
awaitResult && res instanceof Promise
? await res
: res,
{
depth: inspectDepth,
showHidden,
},
),
)
// @ts-expect-error
@ -129,10 +90,53 @@ function EvaluateJavaScriptAlert() {
setLoading(false)
AlertActionCreators.dismissAlert(ALERT_KEY)
}}
text="Evaluate"
// Async arrow functions are not supported
variant="primary"
/>
<AlertActionButton text="Cancel" variant="secondary" />
</Stack>
}
extraContent={
<Stack>
<TextArea
autoFocus
label="Code"
onChange={v => (code.current = v)}
placeholder="ReactNative.NativeModules.BundleUpdaterManager.reload()"
size="md"
/>
<TableRowGroup>
<TableSwitchRow
label="Await result"
onValueChange={setAwaitResult}
subLabel="Wait for the result of the code before displaying it."
value={awaitResult}
/>
<TableSwitchRow
label="Show hidden"
onValueChange={setShowHidden}
subLabel="Show hidden properties of the object."
value={showHidden}
/>
<TableRow
label="Inspect depth"
subLabel="The depth of the object to inspect."
trailing={<Text>{inspectDepth}</Text>}
/>
<Slider
endIcon={<Text>10</Text>}
maximumValue={10}
minimumValue={1}
onValueChange={setInspectDepth}
startIcon={<Text>1</Text>}
step={1}
value={inspectDepth}
/>
</TableRowGroup>
</Stack>
}
title="Evaluate JavaScript"
/>
)
}

View file

@ -1,20 +1,19 @@
import { React } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { api } from '..'
import { MobileSetting } from '../constants'
import { DevToolsContext } from '../react-devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const ReactDevToolsAutoConnectSetting: SettingsItem = {
parent: MobileSetting.REVENGE_DEVELOPER,
IconComponent: () => <TableRowAssetIcon name="LinkIcon" />,
title: () => 'Auto-connect to React DevTools',
useDescription: () => 'Automatically connect to React DevTools during startup.',
useDescription: () =>
'Automatically connect to React DevTools during startup.',
usePredicate: () => DevToolsContext.available,
useValue: () => api.storage.use(s => s.devtools?.autoConnect !== undefined)!.devtools.autoConnect,
useValue: () =>
api.storage.use(s => s.devtools?.autoConnect !== undefined)!.devtools
.autoConnect,
onValueChange: v => api.storage.set({ devtools: { autoConnect: v } }),
type: 'toggle',
}

View file

@ -1,10 +1,10 @@
import { React } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import { DevToolsContext, connectToDevTools, useIsDevToolsOpen } from '../react-devtools'
import {
connectToDevTools,
DevToolsContext,
useIsDevToolsOpen,
} from '../react-devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const ReactDevToolsConnectSetting: SettingsItem = {

View file

@ -1,10 +1,6 @@
import { React } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import { disconnectFromDevTools, useIsDevToolsOpen } from '../react-devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const ReactDevToolsDisconnectSetting: SettingsItem = {

View file

@ -1,10 +1,6 @@
import { React } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import { DevToolsContext } from '../react-devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const ReactDevToolsVersionSetting: SettingsItem = {

View file

@ -1,7 +1,5 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting, UserSettingsSections } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RevengeDeveloperSetting: SettingsItem = {
@ -11,7 +9,8 @@ const RevengeDeveloperSetting: SettingsItem = {
title: () => 'Developer',
screen: {
route: UserSettingsSections[MobileSetting.REVENGE_DEVELOPER],
getComponent: () => require('../screens/RevengeDeveloperSettingScreen').default,
getComponent: () =>
require('../screens/RevengeDeveloperSettingScreen').default,
},
}

View file

@ -1,9 +1,9 @@
import {
isSettingsModulesLoaded,
onSettingsModulesLoaded,
} from '@revenge-mod/discord/modules/settings'
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
import { _data } from '@revenge-mod/discord/_/modules/settings'
import { isSettingsModulesLoaded, onSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings'
import type { PluginApi } from '@revenge-mod/plugins/types'
interface Storage {
@ -40,7 +40,10 @@ registerPlugin<Storage>(
else onSettingsModulesLoaded(() => require('./register').register())
api.storage.get().then(settings => {
if (settings.devtools.autoConnect) import('./react-devtools').then(it => it.connectToDevTools())
if (settings.devtools.autoConnect)
import('./react-devtools').then(it =>
it.connectToDevTools(),
)
})
},
},

View file

@ -20,11 +20,16 @@ export const DevToolsContext: {
type Subscription = (event: 1 | 2 | 3, err?: unknown) => void
const subscriptions: Set<Subscription> = new Set()
const CircleXIcon = lookupGeneratedIconComponent('CircleXIcon', 'CircleXIcon-secondary', 'CircleXIcon-primary')
const CircleXIcon = lookupGeneratedIconComponent(
'CircleXIcon',
'CircleXIcon-secondary',
'CircleXIcon-primary',
)
subscriptions.add((e, err) => {
if (e === 3) {
const actualError = (err as { message: string }).message ?? getErrorStack(err)
const actualError =
(err as { message: string }).message ?? getErrorStack(err)
api.logger.error('React DevTools error:', actualError)
@ -39,7 +44,9 @@ subscriptions.add((e, err) => {
export function connectToDevTools() {
if (!DevToolsContext.available || DevToolsContext.ws) return
const websocket = (DevToolsContext.ws = new WebSocket(`ws://${DevToolsContext.address}`))
const websocket = (DevToolsContext.ws = new WebSocket(
`ws://${DevToolsContext.address}`,
))
websocket.addEventListener('open', () => {
DevToolsContext.open = true

View file

@ -1,7 +1,9 @@
import { addSettingsItemToSection, registerSettingsItems } from '@revenge-mod/discord/modules/settings'
import {
addSettingsItemToSection,
registerSettingsItems,
} from '@revenge-mod/discord/modules/settings'
import { api } from '.'
import { MobileSetting } from './constants'
import AssetBrowserSetting from './definitions/AssetBrowserSetting'
import EvaluateJavaScriptSetting from './definitions/EvaluateJavaScriptSetting'
import ReactDevToolsAutoConnectSetting from './definitions/ReactDevToolsAutoConnectSetting'
@ -10,16 +12,16 @@ import ReactDevToolsDisconnectSetting from './definitions/ReactDevToolsDisconnec
import ReactDevToolsVersionSetting from './definitions/ReactDevToolsVersionSetting'
import RevengeDeveloperSetting from './definitions/RevengeDeveloperSetting'
import { api } from '.'
export function register() {
api.cleanup(
registerSettingsItems({
[MobileSetting.REVENGE_DEVELOPER]: RevengeDeveloperSetting,
[MobileSetting.REACT_DEVTOOLS_VERSION]: ReactDevToolsVersionSetting,
[MobileSetting.REACT_DEVTOOLS_AUTO_CONNECT]: ReactDevToolsAutoConnectSetting,
[MobileSetting.REACT_DEVTOOLS_AUTO_CONNECT]:
ReactDevToolsAutoConnectSetting,
[MobileSetting.REACT_DEVTOOLS_CONNECT]: ReactDevToolsConnectSetting,
[MobileSetting.REACT_DEVTOOLS_DISCONNECT]: ReactDevToolsDisconnectSetting,
[MobileSetting.REACT_DEVTOOLS_DISCONNECT]:
ReactDevToolsDisconnectSetting,
[MobileSetting.EVALUATE_JAVASCRIPT]: EvaluateJavaScriptSetting,
[MobileSetting.ASSET_BROWSER]: AssetBrowserSetting,
}),

View file

@ -1,15 +1,12 @@
import { getAssetId, getAssetModuleId, getAssets } from '@revenge-mod/assets'
import { AlertActionCreators } from '@revenge-mod/discord/actions'
import { Design } from '@revenge-mod/discord/design'
import { Clipboard } from '@revenge-mod/externals/react-native-clipboard'
import { FlashList } from '@revenge-mod/externals/shopify'
import { React, ReactNative } from '@revenge-mod/react'
import Page from '~/components/Page'
import SearchInput from '~/components/SearchInput'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { getAssetId, getAssetModuleId, getAssets } from '@revenge-mod/assets'
import type { Asset, AssetId } from '@revenge-mod/assets/types'
import type { Metro } from '@revenge-mod/modules/types'
@ -28,7 +25,15 @@ const UndisplayableFallback = {
export default function AssetBrowserSettingScreen() {
const [search, setSearch] = React.useState('')
const assets = React.useMemo(
() => [...getAssets()].map(asset => [getAssetId(asset)!, getAssetModuleId(asset)!, asset] as const),
() =>
[...getAssets()].map(
asset =>
[
getAssetId(asset)!,
getAssetModuleId(asset)!,
asset,
] as const,
),
[getAssets],
)
@ -36,26 +41,28 @@ export default function AssetBrowserSettingScreen() {
() =>
!search.length
? assets
: assets.filter(([, , asset]) => asset.name.toLowerCase().includes(search.toLowerCase())),
: assets.filter(([, , asset]) =>
asset.name.toLowerCase().includes(search.toLowerCase()),
),
[assets, search],
)
return (
<Page spacing={16}>
<SearchInput size="md" onChange={(v: string) => setSearch(v)} />
<SearchInput onChange={(v: string) => setSearch(v)} size="md" />
<FlashList.FlashList
data={filteredAssets}
estimatedItemSize={80}
keyExtractor={([id]) => id.toString()}
renderItem={({ item: [id, moduleId, asset], index }) => (
<AssetDisplay
start={!index}
asset={asset}
end={index === assets.length - 1}
id={id}
moduleId={moduleId}
asset={asset}
start={!index}
/>
)}
estimatedItemSize={80}
/>
</Page>
)
@ -83,26 +90,30 @@ function AssetDisplay({
return (
<TableRow
start={start}
end={end}
variant={isDisplayable ? 'default' : 'danger'}
label={asset.name}
subLabel={metadata.map(([name, value]) => `${name}: ${value}`).join(' • ')}
icon={
isDisplayable ? (
<Image source={id} style={styles.smallPreview} />
) : (
<TableRowAssetIcon
variant="danger"
name={
asset.type in UndisplayableFallback
? UndisplayableFallback[asset.type as keyof typeof UndisplayableFallback]
? UndisplayableFallback[
asset.type as keyof typeof UndisplayableFallback
]
: UndisplayableFallback.default
}
variant="danger"
/>
)
}
label={asset.name}
onPress={() => openAssetDisplayAlert(asset, id, metadata)}
start={start}
subLabel={metadata
.map(([name, value]) => `${name}: ${value}`)
.join(' • ')}
variant={isDisplayable ? 'default' : 'danger'}
/>
)
}
@ -117,7 +128,7 @@ function openAssetDisplayAlert(
AlertActionCreators.openAlert(
'asset-display',
<AlertModal
title={asset.name}
actions={<AlertActionButton text="Close" variant="secondary" />}
extraContent={
<>
{isDisplayable ? (
@ -130,25 +141,30 @@ function openAssetDisplayAlert(
<TableRow
key={id}
label={name}
subLabel={value}
onPress={() => Clipboard.setString(value)}
subLabel={value}
/>
))}
</TableRowGroup>
<Text variant="text-xs/semibold" color="text-danger">
Note: Asset IDs and module IDs are not consistent between app launches and app versions
respectively and should only be used when absolutely needed.
<Text color="text-danger" variant="text-xs/semibold">
Note: Asset IDs and module IDs are not consistent
between app launches and app versions respectively and
should only be used when absolutely needed.
</Text>
</>
}
actions={<AlertActionButton text="Close" variant="secondary" />}
title={asset.name}
/>,
)
}
function PreviewUnavailable({ type }: { type: string }) {
return (
<Text variant="text-sm/medium" color="text-danger" style={styles.centeredText}>
<Text
color="text-danger"
style={styles.centeredText}
variant="text-sm/medium"
>
Asset type {type.toUpperCase()} is not supported for preview.
</Text>
)

View file

@ -1,9 +1,8 @@
import { ToastActionCreators } from '@revenge-mod/discord/actions'
import { Design } from '@revenge-mod/discord/design'
import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/renderer'
import { React, ReactNative } from '@revenge-mod/react'
import { ReactNative } from '@revenge-mod/react'
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
import { api } from '..'
import { MobileSetting } from '../constants'
import { DevToolsContext, useIsDevToolsOpen } from '../react-devtools'
@ -34,7 +33,10 @@ export default function RevengeDeveloperSettingScreen() {
},
{
label: 'Tools',
settings: [MobileSetting.EVALUATE_JAVASCRIPT, MobileSetting.ASSET_BROWSER],
settings: [
MobileSetting.EVALUATE_JAVASCRIPT,
MobileSetting.ASSET_BROWSER,
],
},
]}
/>
@ -49,12 +51,13 @@ export function ReactDevToolsAddressSetting() {
return (
<ReactNative.View style={{ paddingHorizontal: 16, paddingTop: 16 }}>
<Design.TextInput
defaultValue={
settings?.devtools.address ?? DevToolsContext.address
}
editable={!open}
isDisabled={open}
leadingText="ws://"
defaultValue={settings?.devtools.address ?? DevToolsContext.address}
label="React DevTools"
onChange={text => (DevToolsContext.address = text)}
leadingText="ws://"
onBlur={() => {
api.storage
.set({
@ -70,6 +73,7 @@ export function ReactDevToolsAddressSetting() {
})
})
}}
onChange={text => (DevToolsContext.address = text)}
returnKeyType="done"
/>
</ReactNative.View>

View file

@ -1,9 +1,5 @@
import { React } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
// @ts-expect-error

View file

@ -1,12 +1,10 @@
import { React, ReactNative } from '@revenge-mod/react'
import { ReactNative } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const { major, minor, patch, prerelease } = ReactNative.Platform.constants.reactNativeVersion
const { major, minor, patch, prerelease } =
ReactNative.Platform.constants.reactNativeVersion
const version = `${major}.${minor}.${patch}${prerelease ? `-${prerelease}` : ''}`
const ReactNativeVersionSetting: SettingsItem = {

View file

@ -1,9 +1,6 @@
import { React } from '@revenge-mod/react'
import { MobileSetting } from '../constants'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const ReactVersionSetting: SettingsItem = {

View file

@ -1,10 +1,6 @@
import { BundleUpdaterManager } from '@revenge-mod/discord/native'
import { React } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const ReloadAppSetting: SettingsItem = {

View file

@ -1,5 +1,4 @@
import { MobileSetting, UserSettingsSections } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RevengeCustomPageSetting: SettingsItem = {
@ -9,7 +8,8 @@ const RevengeCustomPageSetting: SettingsItem = {
unsearchable: true,
screen: {
route: UserSettingsSections[MobileSetting.REVENGE_CUSTOM_PAGE],
getComponent: () => require('../screens/RevengeCustomPageScreen').default,
getComponent: () =>
require('../screens/RevengeCustomPageScreen').default,
},
}

View file

@ -1,10 +1,7 @@
import { React, ReactNative } from '@revenge-mod/react'
import { ReactNative } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { DiscordServerURL } from '~/constants'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RevengeDiscordServerSetting: SettingsItem = {

View file

@ -1,7 +1,5 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting, UserSettingsSections } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RevengeFontsSetting: SettingsItem = {
@ -11,7 +9,8 @@ const RevengeFontsSetting: SettingsItem = {
title: () => 'Fonts',
screen: {
route: UserSettingsSections[MobileSetting.REVENGE_PLUGINS],
getComponent: () => require('../screens/RevengePluginsSettingScreen').default,
getComponent: () =>
require('../screens/RevengePluginsSettingScreen').default,
},
}

View file

@ -1,15 +1,14 @@
import { React, ReactNative } from '@revenge-mod/react'
import { ReactNative } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { GitHubOrganizationURL } from '~/constants'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RevengeGitHubOrganizationSetting: SettingsItem = {
parent: MobileSetting.REVENGE,
IconComponent: () => <TableRowAssetIcon name="img_account_sync_github_white" />,
IconComponent: () => (
<TableRowAssetIcon name="img_account_sync_github_white" />
),
title: () => 'GitHub',
onPress: () => ReactNative.Linking.openURL(GitHubOrganizationURL),
type: 'pressable',

View file

@ -1,14 +1,14 @@
import { React } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RevengeNotImplementedSetting: SettingsItem = {
parent: null,
IconComponent: () => <TableRowAssetIcon name="CircleXIcon-primary" variant="danger" />,
IconComponent: () => (
<TableRowAssetIcon name="CircleXIcon-primary" variant="danger" />
),
title: () => 'Not Implemented',
useDescription: () => 'This feature is not implemented yet. Check back later!',
useDescription: () =>
'This feature is not implemented yet. Check back later!',
variant: 'danger',
type: 'pressable',
unsearchable: true,

View file

@ -1,10 +1,7 @@
import { _plugins } from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting, UserSettingsSections } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RevengePluginsSetting: SettingsItem = {
@ -14,12 +11,14 @@ const RevengePluginsSetting: SettingsItem = {
title: () => 'Plugins',
useTrailing: () => {
let count = 0
for (const plugin of _plugins.values()) if (plugin.flags & PluginFlags.Enabled) count++
for (const plugin of _plugins.values())
if (plugin.flags & PluginFlags.Enabled) count++
return `${count} enabled`
},
screen: {
route: UserSettingsSections[MobileSetting.REVENGE_PLUGINS],
getComponent: () => require('../screens/RevengePluginsSettingScreen').default,
getComponent: () =>
require('../screens/RevengePluginsSettingScreen').default,
},
}

View file

@ -1,8 +1,6 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import RevengeIcon from '~assets/RevengeIcon'
import { MobileSetting, UserSettingsSections } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RevengeSetting: SettingsItem = {
@ -10,7 +8,8 @@ const RevengeSetting: SettingsItem = {
type: 'route',
IconComponent: () => <TableRowAssetIcon id={RevengeIcon} />,
title: () => 'Revenge',
useTrailing: () => `${__BUILD_VERSION__}-${__BUILD_COMMIT__}-${__BUILD_BRANCH__}`,
useTrailing: () =>
`${__BUILD_VERSION__}-${__BUILD_COMMIT__}-${__BUILD_BRANCH__}`,
screen: {
route: UserSettingsSections[MobileSetting.REVENGE],
getComponent: () => require('../screens/RevengeSettingScreen').default,

View file

@ -1,7 +1,5 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting, UserSettingsSections } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RevengeThemesSetting: SettingsItem = {
@ -11,7 +9,8 @@ const RevengeThemesSetting: SettingsItem = {
title: () => 'Themes',
screen: {
route: UserSettingsSections[MobileSetting.REVENGE_THEMES],
getComponent: () => require('../screens/RevengeThemesSettingScreen').default,
getComponent: () =>
require('../screens/RevengeThemesSettingScreen').default,
},
}

View file

@ -1,15 +1,14 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import RevengeIcon from '~assets/RevengeIcon'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RevengeVersionSetting: SettingsItem = {
parent: MobileSetting.REVENGE,
IconComponent: () => <TableRowAssetIcon id={RevengeIcon} />,
title: () => 'Revenge',
useDescription: () => `${__BUILD_VERSION__}-${__BUILD_COMMIT__}-${__BUILD_BRANCH__} (${__BUILD_ENV__})`,
useDescription: () =>
`${__BUILD_VERSION__}-${__BUILD_COMMIT__}-${__BUILD_BRANCH__} (${__BUILD_ENV__})`,
type: 'static',
}

View file

@ -1,14 +1,11 @@
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
import { _data, _subs } from '@revenge-mod/discord/_/modules/settings'
import { onSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings'
import { byName, byProps } from '@revenge-mod/modules/finders/filters'
import { waitForModules } from '@revenge-mod/modules/finders/wait'
import { _data, _subs } from '@revenge-mod/discord/_/modules/settings'
import { type SettingsItem, onSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings'
import { after } from '@revenge-mod/patcher'
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
import type { FC } from 'react'
onSettingsModulesLoaded(() => require('./register'))
@ -30,29 +27,39 @@ registerPlugin(
SettingRendererConfig => {
unsubForRendererConfig()
logger.info('Settings modules loaded, running subscriptions and patching...')
logger.info(
'Settings modules loaded, running subscriptions and patching...',
)
for (const sub of _subs)
try {
sub()
} catch (e) {
logger.error('Failed to run settings modules subscription', e)
logger.error(
'Failed to run settings modules subscription',
e,
)
}
// We don't ever need to call this again
_subs.clear()
_data[2] = true
let ORIGINAL_RENDERER_CONFIG = SettingRendererConfig.SETTING_RENDERER_CONFIG
let ORIGINAL_RENDERER_CONFIG =
SettingRendererConfig.SETTING_RENDERER_CONFIG
Object.defineProperty(SettingRendererConfig, 'SETTING_RENDERER_CONFIG', {
get: () =>
({
...ORIGINAL_RENDERER_CONFIG,
..._data[1],
}) as Record<string, SettingsItem>,
set: v => (ORIGINAL_RENDERER_CONFIG = v),
})
Object.defineProperty(
SettingRendererConfig,
'SETTING_RENDERER_CONFIG',
{
get: () =>
({
...ORIGINAL_RENDERER_CONFIG,
..._data[1],
}) as Record<string, SettingsItem>,
set: v => (ORIGINAL_RENDERER_CONFIG = v),
},
)
},
)
@ -76,14 +83,17 @@ registerPlugin(
}
// Check if we even have custom sections
const firstCustomSection = customSections[Object.keys(customSections)[0]]
const firstCustomSection =
customSections[Object.keys(customSections)[0]]
if (!firstCustomSection) return tree
// Check if sections are already spliced
const firstCustomItem = firstCustomSection.settings[0]
if (
sections.findIndex(section =>
section.settings.some(setting => setting === firstCustomItem),
section.settings.some(
setting => setting === firstCustomItem,
),
) !== -1
)
return tree

View file

@ -1,7 +1,8 @@
import { registerSettingsItems, registerSettingsSection } from '@revenge-mod/discord/modules/settings'
import {
registerSettingsItems,
registerSettingsSection,
} from '@revenge-mod/discord/modules/settings'
import { MobileSetting } from './constants'
import HermesVersionSetting from './definitions/HermesVersionSetting'
import ReactNativeVersionSetting from './definitions/ReactNativeVersionSetting'
// import RevengeThemesSetting from './definitions/RevengeThemesSetting'
@ -22,7 +23,8 @@ registerSettingsItems({
// [MobileSetting.REVENGE_THEMES]: RevengeThemesSetting,
// [MobileSetting.REVENGE_FONTS]: RevengeFontsSetting,
[MobileSetting.REVENGE_CUSTOM_PAGE]: RevengeCustomPageSetting,
[MobileSetting.REVENGE_GITHUB_ORGANIZATION]: RevengeGitHubOrganizationSetting,
[MobileSetting.REVENGE_GITHUB_ORGANIZATION]:
RevengeGitHubOrganizationSetting,
[MobileSetting.REVENGE_DISCORD_SERVER]: RevengeDiscordServerSetting,
[MobileSetting.RELOAD_APP]: ReloadAppSetting,
[MobileSetting.REVENGE_VERSION]: RevengeVersionSetting,

View file

@ -1,8 +1,6 @@
import { React } from '@revenge-mod/react'
import type { StackScreenProps } from '@react-navigation/stack'
import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation'
import type { MobileSetting, UserSettingsSections } from '../constants'
export default function RevengeCustomPageScreen({

View file

@ -1,5 +1,4 @@
import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/renderer'
import { MobileSetting } from '../constants'
export default function RevengeFontsSettingScreen() {

View file

@ -1,25 +1,23 @@
import { getAssetIdByName } from '@revenge-mod/assets'
import { Tokens } from '@revenge-mod/discord/common'
import { Design } from '@revenge-mod/discord/design'
import { FlashList } from '@revenge-mod/externals/shopify'
import {
type InternalPlugin,
InternalPluginFlags,
_metas,
_plugins,
enablePlugin,
InternalPluginFlags,
initPlugin,
preInitPlugin,
startPlugin,
} from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
import { React, ReactNative } from '@revenge-mod/react'
import { useReRender } from '@revenge-mod/utils/react'
import FormSwitch from '~/components/FormSwitch'
import Page from '~/components/Page'
import SearchInput from '~/components/SearchInput'
import { getAssetIdByName } from '@revenge-mod/assets'
import { Tokens } from '@revenge-mod/discord/common'
import { PluginFlags } from '@revenge-mod/plugins/constants'
import { useReRender } from '@revenge-mod/utils/react'
import type { InternalPlugin } from '@revenge-mod/plugins/_'
const { Image, useWindowDimensions } = ReactNative
const { Card, Text, Stack } = Design
@ -31,7 +29,10 @@ export default function RevengePluginsSettingScreen() {
const numColumns = Math.floor((width - 16) / 448)
const plugins = React.useMemo(
() => [..._plugins.values()].map(plugin => [plugin, _metas.get(plugin.manifest.id)![2]] as const),
() =>
[..._plugins.values()].map(
plugin => [plugin, _metas.get(plugin.manifest.id)![2]] as const,
),
[_plugins],
)
@ -51,16 +52,16 @@ export default function RevengePluginsSettingScreen() {
return (
<Page spacing={16}>
<SearchInput size="md" onChange={(v: string) => setSearch(v)} />
<SearchInput onChange={(v: string) => setSearch(v)} size="md" />
<FlashList.MasonryFlashList
data={filteredPlugins}
numColumns={numColumns}
estimatedItemSize={108}
numColumns={numColumns}
renderItem={({ item: [plugin, iflags], columnIndex }) => (
<InstalledPluginCard
iflags={iflags}
key={plugin.manifest.id}
plugin={plugin}
iflags={iflags}
rightGap={columnIndex + 1 < numColumns}
/>
)}
@ -103,7 +104,11 @@ function InstalledPluginCard({
plugin,
iflags,
rightGap,
}: { plugin: InternalPlugin; iflags: number; rightGap?: boolean }) {
}: {
plugin: InternalPlugin
iflags: number
rightGap?: boolean
}) {
const {
manifest: { name, description, author, icon },
flags,
@ -116,14 +121,23 @@ function InstalledPluginCard({
return (
<Card style={[styles.card, rightGap && styles.rightGap]}>
<Stack direction="horizontal" style={[styles.grow, styles.topContainer]}>
<Stack spacing={8} direction="horizontal" style={[styles.topContainer, styles.autoSize]}>
<Image source={getAssetIdByName(icon ?? 'PuzzlePieceIcon')!} style={styles.icon} />
<Stack
direction="horizontal"
style={[styles.grow, styles.topContainer]}
>
<Stack
direction="horizontal"
spacing={8}
style={[styles.topContainer, styles.autoSize]}
>
<Image
source={getAssetIdByName(icon ?? 'PuzzlePieceIcon')!}
style={styles.icon}
/>
<Text variant="heading-lg/semibold">{name}</Text>
</Stack>
<FormSwitch
disabled={essential}
value={enabled}
// biome-ignore lint/complexity/useArrowFunction: Async arrows are not supported
onValueChange={async function (v) {
if (v) {
@ -139,10 +153,15 @@ function InstalledPluginCard({
// make an event based system for this, so we can register a listener for when plugins are disabled
// and check its flags afterwards
}}
value={enabled}
/>
</Stack>
<Stack spacing={4} style={[styles.alignedContainer, styles.grow]}>
<Text style={styles.grow} variant="heading-md/medium" color="text-muted">
<Text
color="text-muted"
style={styles.grow}
variant="heading-md/medium"
>
by {author}
</Text>
<Text style={styles.grow} variant="text-md/medium">

View file

@ -18,8 +18,12 @@ export default function RevengeSettingScreen() {
<Design.Text variant="text-xs/medium">
You are using the next version of Revenge!
</Design.Text>
<Design.Text variant="text-xs/semibold" color="text-danger">
This version is experimental and may be unstable.
<Design.Text
color="text-danger"
variant="text-xs/semibold"
>
This version is experimental and may be
unstable.
</Design.Text>
</>
),

View file

@ -1,5 +1,4 @@
import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/renderer'
import { MobileSetting } from '../constants'
export default function RevengeThemesSettingScreen() {

View file

@ -1,12 +1,9 @@
import { AlertActionCreators } from '@revenge-mod/discord/actions'
import { Stores, getStore } from '@revenge-mod/discord/common/flux'
import { getStore, Stores } from '@revenge-mod/discord/common/flux'
import { Design } from '@revenge-mod/discord/design'
import { byProps } from '@revenge-mod/modules/finders/filters'
import { getModule } from '@revenge-mod/modules/finders/get'
import { instead } from '@revenge-mod/patcher'
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
@ -24,15 +21,24 @@ registerPlugin(
function reinitDEStore() {
cleanup(
getStore<{ initialize(): void }>('DeveloperExperimentStore', store => {
logger.log('Reinitializing DeveloperExperimentStore to apply changes...')
getStore<{ initialize(): void }>(
'DeveloperExperimentStore',
store => {
logger.log(
'Reinitializing DeveloperExperimentStore to apply changes...',
)
setTimeout(() => {
const unpatch = instead(Object, 'defineProperties', () => {})
store.initialize()
unpatch()
})
}),
setTimeout(() => {
const unpatch = instead(
Object,
'defineProperties',
() => {},
)
store.initialize()
unpatch()
})
},
),
)
}
@ -41,7 +47,12 @@ registerPlugin(
logger.log('Patching UserStoreUtils...')
cleanup(
instead(UserStoreUtils, 'isStaffEnv', ([user]) => user === Stores.UserStore.getCurrentUser()),
instead(
UserStoreUtils,
'isStaffEnv',
([user]) =>
user === Stores.UserStore.getCurrentUser(),
),
)
reinitDEStore()
@ -59,9 +70,11 @@ function showNoticeAlert() {
AlertActionCreators.openAlert(
'staff-settings-notice',
<Design.AlertModal
title="Notice"
actions={
<Design.AlertActionButton text="Got it" variant="secondary" />
}
content="Close and open the Settings page again to apply changes."
actions={<Design.AlertActionButton variant="secondary" text="Got it" />}
title="Notice"
/>,
)
}

Some files were not shown because too many files have changed in this diff Show more