feat!: module finders refactor (#6)

- Renamed `byX` filters to `withX`.
  It is more correct. For example: You don't lookup, wait for, or get modules **BY** props. You lookup, wait for, and get modules **WITH** props.
- Renamed `*byImportedPath` to `*withImportedPath`
  For consistency with the above change.
- Renamed `getModule` to `getModules`.
  The API allows getting multiple modules and not just a single one. Types are renamed as well.
  `getModuleByImportedPath` remains the same as it only gets one single module with a unique imported path.
- `withX` filter **helpers** removed.
  They can now be replaced with the new capability-based `and` filter.
- `preferExports` and `moduleStateAware` filters removed.
  They can now be replaced with the new capability-based `and` filter.
- Composite finders renamed.
  `some` -> `or`, `every` -> `and`
- Composite finders takes a capability-based approach.
- Filter generators now need to attach runtime flag to let the system know whether the generated filter can be used to filter modules without proper exports or not.

## Misc.

- Removed useless unsub call in Profile Badges
- Use `queueMicrotask` with `asap`

# Features

## Capability-based approach to composite filters

```js
// Old
byProps(...).withDependencies(...)
// Old, same as: preferExports(byProps(...), byDependencies(...))
every(byDependencies(...), byProps(...))
// Old, ordering matters, this would fail unexpectedly!
every(byProps(...), byDependencies(...))

// New
withProps(...).and(withDependencies(...))
// New (as well), ordering doesn't matter, this has the same result as above
withDependencies(...).and(withProps(...))
```
This commit is contained in:
Palm 2025-08-30 09:19:57 +07:00 • committed by GitHub
parent a7ba4a4391
commit 5f5fc6f2b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 985 additions and 992 deletions

View file

@ -4,17 +4,17 @@
"": {
"name": "@revenge-mod/revenge",
"dependencies": {
"@swc/core": "^1.13.3",
"@swc/core": "^1.13.5",
"chalk": "^5.6.0",
"react-native-click-outside": "^0.1.1",
"rolldown": "1.0.0-beta.34",
},
"devDependencies": {
"@biomejs/biome": "^2.2.0",
"@biomejs/biome": "^2.2.2",
"@parcel/watcher": "^2.5.1",
"@react-native-clipboard/clipboard": "^1.16.3",
"@types/bun": "^1.2.20",
"rolldown-plugin-dts": "^0.15.6",
"rolldown-plugin-dts": "^0.15.9",
},
"peerDependencies": {
"@react-navigation/core": "^7.12.4",

View file

@ -4,9 +4,9 @@ import {
waitForModules,
} from '@revenge-mod/modules/finders'
import {
byDependencies,
byName,
byProps,
withDependencies,
withName,
withProps,
} from '@revenge-mod/modules/finders/filters'
import {
mInitializingId,
@ -20,13 +20,13 @@ import type { Metro } from '@revenge-mod/modules/types'
import type { ReactNative } from '@revenge-mod/react/types'
import type { Asset, PackagerAsset } from './types'
const { relative } = byDependencies
const { relative } = withDependencies
const [, _classCallCheckId] = lookupModule(byName('_classCallCheck'))
const [, _createClassId] = lookupModule(byName('_createClass'))
const [, _classCallCheckId] = lookupModule(withName('_classCallCheck'))
const [, _createClassId] = lookupModule(withName('_createClass'))
// https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Image/AssetSourceResolver.js
const byAssetSourceResolver = byDependencies([
const withAssetSourceResolver = withDependencies([
_classCallCheckId,
_createClassId,
relative(1),
@ -39,7 +39,7 @@ const cachedOnly = { cached: true }
// Tracking/caching assets
const unsubAR = waitForModules(
byProps<ReactNative.AssetsRegistry>('registerAsset'),
withProps<ReactNative.AssetsRegistry>('registerAsset'),
(exports, id) => {
AssetsRegistryModuleId = id
AssetsRegistry = exports as ReactNative.AssetsRegistry
@ -104,7 +104,7 @@ const unsubAR = waitForModules(
*/
export let AssetsRegistryModuleId: Metro.ModuleID | undefined
export let AssetsRegistry: ReactNative.AssetsRegistry = proxify(() => {
for (const [, id] of lookupModules(byDependencies([[]]), {
for (const [, id] of lookupModules(withDependencies([[]]), {
initialize: false,
uninitialized: true,
})) {
@ -112,7 +112,7 @@ export let AssetsRegistry: ReactNative.AssetsRegistry = proxify(() => {
if (deps.length !== 1) continue
// The module next to assets-registry is AssetSourceResolver
if (byAssetSourceResolver(deps[0] + 1)) {
if (withAssetSourceResolver(deps[0] + 1)) {
const module = __r(id)
// ID will be set by the wait below
if (module?.registerAsset) return (AssetsRegistry = module)
@ -124,7 +124,7 @@ export let AssetsRegistry: ReactNative.AssetsRegistry = proxify(() => {
// Asset overrides
const unsubRAS = waitForModules(
byName<{
withName<{
addCustomSourceTransformer: (
transformer: (arg: { asset: Asset }) => PackagerAsset,
) => void

View file

@ -1,10 +1,8 @@
import { lookupModule, lookupModules } from '@revenge-mod/modules/finders'
import {
byDependencies,
byProps,
every,
preferExports,
withDependencies,
withoutProps,
withProps,
} from '@revenge-mod/modules/finders/filters'
import {
ReactJSXRuntimeModuleId,
@ -15,19 +13,18 @@ import { proxify } from '@revenge-mod/utils/proxy'
import { DispatcherModuleId } from './common/flux'
import type { DiscordModules } from './types'
const { relative } = byDependencies
const { relative } = withDependencies
// modules/action_sheet/native/ActionSheetActionCreators.tsx
export let ActionSheetActionCreators: DiscordModules.Actions.ActionSheetActionCreators =
proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<DiscordModules.Actions.ActionSheetActionCreators>(
'hideActionSheet',
'openLazy',
),
byDependencies([
withProps<DiscordModules.Actions.ActionSheetActionCreators>(
'hideActionSheet',
'openLazy',
).and(
withDependencies([
null,
ReactModuleId,
ReactJSXRuntimeModuleId,
@ -54,11 +51,10 @@ export let AlertActionCreators: DiscordModules.Actions.AlertActionCreators =
proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<DiscordModules.Actions.AlertActionCreators>(
'openAlert',
),
byDependencies([
withProps<DiscordModules.Actions.AlertActionCreators>(
'openAlert',
).and(
withDependencies([
[[], relative(1)],
[ReactNativeModuleId, 2],
2,
@ -83,13 +79,9 @@ export let ToastActionCreators: DiscordModules.Actions.ToastActionCreators =
// [Dispatcher, ImportTracker]
const generator = lookupModules(
preferExports(
every(
byProps<DiscordModules.Actions.ToastActionCreators>('open'),
withoutProps('init'),
),
byDependencies([DispatcherModuleId, 2]),
),
withProps<DiscordModules.Actions.ToastActionCreators>('open')
.and(withoutProps('init'))
.and(withDependencies([DispatcherModuleId, 2])),
{
uninitialized: true,
},

View file

@ -1,20 +1,18 @@
import { lookupModule } from '@revenge-mod/modules/finders'
import {
byDependencies,
byProps,
preferExports,
withDependencies,
withProps,
} from '@revenge-mod/modules/finders/filters'
import type { Metro } from '@revenge-mod/modules/types'
import type { DiscordModules } from '../types'
const { relative } = byDependencies
const { relative } = withDependencies
// ../discord_common/js/packages/flux
export const [Dispatcher, DispatcherModuleId] = lookupModule(
preferExports(
byProps<DiscordModules.Flux.Dispatcher>('_interceptors'),
byDependencies([relative(1), null, null, null, null, 2]),
withProps<DiscordModules.Flux.Dispatcher>('_interceptors').and(
withDependencies([relative(1), null, null, null, null, 2]),
),
{
uninitialized: true,

View file

@ -1,9 +1,8 @@
import { lookupModule } from '@revenge-mod/modules/finders'
import {
byDependencies,
byName,
byProps,
preferExports,
withDependencies,
withName,
withProps,
} from '@revenge-mod/modules/finders/filters'
import { proxify } from '@revenge-mod/utils/proxy'
import type { Metro } from '@revenge-mod/modules/types'
@ -13,15 +12,15 @@ export { AppStartPerformance } from '../preinit'
export * as flux from './flux'
export * as utils from './utils'
const { loose, relative } = byDependencies
const { loose, relative } = withDependencies
// ../discord_common/js/packages/logger/Logger.tsx
export const [Logger, LoggerModuleId] = lookupModule(
byName<typeof DiscordModules.Logger>('Logger'),
withName<typeof DiscordModules.Logger>('Logger'),
) as [typeof DiscordModules.Logger, Metro.ModuleID]
// ../discord_common/js/packages/tokens/native.tsx
export const [Tokens, TokensModuleId] = lookupModule(byProps('RawColor')) as [
export const [Tokens, TokensModuleId] = lookupModule(withProps('RawColor')) as [
any,
Metro.ModuleID,
]
@ -43,13 +42,11 @@ export let ConstantsModuleId: Metro.ModuleID | undefined
export let Constants: DiscordModules.Constants = proxify(
() => {
const [module, id] = lookupModule(
preferExports(
byProps<DiscordModules.Constants>('ME'),
withProps<DiscordModules.Constants>('ME').and(
// ID: 1236
// Deps: 26, 1237, 1238, 1239, 1240, ...
// Every module has only one dependency, which is the import tracker
byDependencies(
withDependencies(
loose([
null,
relative.withDependencies(OnlyImportTrackerDep, 1),

View file

@ -1,14 +1,13 @@
import { lookupModule } from '@revenge-mod/modules/finders'
import {
byDependencies,
byName,
preferExports,
withDependencies,
withName,
} from '@revenge-mod/modules/finders/filters'
import { proxify } from '@revenge-mod/utils/proxy'
import type { DiscordModules } from '../types'
const [, _createClassModuleId] = lookupModule(byName('_createClass'))
const [, _classCallCheckModuleId] = lookupModule(byName('_classCallCheck'))
const [, _createClassModuleId] = lookupModule(withName('_createClass'))
const [, _classCallCheckModuleId] = lookupModule(withName('_classCallCheck'))
// ../discord_common/js/shared/utils/TypedEventEmitter.tsx
/**
@ -17,11 +16,10 @@ const [, _classCallCheckModuleId] = lookupModule(byName('_classCallCheck'))
export let TypedEventEmitter: typeof DiscordModules.Utils.TypedEventEmitter =
proxify(() => {
const [module] = lookupModule(
preferExports(
byName<typeof DiscordModules.Utils.TypedEventEmitter>(
'TypedEventEmitter',
),
byDependencies([
withName<typeof DiscordModules.Utils.TypedEventEmitter>(
'TypedEventEmitter',
).and(
withDependencies([
_classCallCheckModuleId,
_createClassModuleId,
[],

View file

@ -1,10 +1,8 @@
import { lookupModule } from '@revenge-mod/modules/finders'
import {
byDependencies,
byProps,
bySingleProp,
preferExports,
some,
withDependencies,
withProps,
withSingleProp,
} from '@revenge-mod/modules/finders/filters'
import {
ReactJSXRuntimeModuleId,
@ -14,7 +12,7 @@ import {
import { proxify } from '@revenge-mod/utils/proxy'
import type { DiscordModules } from './types'
const { loose, relative } = byDependencies
const { loose, relative } = withDependencies
// design/native.tsx
export let Design: Design = proxify(
@ -22,25 +20,23 @@ export let Design: Design = proxify(
// ID: 3236
// [3237, 1366, 3238, 3239, 2, ...];
const [module] = lookupModule(
preferExports(
byProps<Design>('TableRow', 'Button'),
some(
byDependencies(
loose([
null,
null,
null,
relative.withDependencies([2], 1),
2,
null, // 3009
null, // 3010
null, // 3011
null, // 3012
relative.withDependencies([2], 2),
]),
),
withProps<Design>('TableRow', 'Button').and(
withDependencies(
loose([
null,
null,
null,
relative.withDependencies([2], 1),
2,
null, // 3009
null, // 3010
null, // 3011
null, // 3012
relative.withDependencies([2], 2),
]),
).or(
// TODO(PalmDevs): Remove once stable channel is > 295203 (for 295203 and below)
byDependencies(
withDependencies(
loose([relative(1), null, relative(2), relative(3), 2]),
),
),
@ -65,11 +61,10 @@ export let FormSwitch: DiscordModules.Components.FormSwitch = proxify(() => {
// Deps: [47, 48, 35, 49]
const [module] = lookupModule(
preferExports(
bySingleProp<{ FormSwitch: DiscordModules.Components.FormSwitch }>(
'FormSwitch',
),
byDependencies([
withSingleProp<{
FormSwitch: DiscordModules.Components.FormSwitch
}>('FormSwitch').and(
withDependencies([
[relative(1), relative(2), null, relative(3)],
ReactModuleId,
ReactNativeModuleId,

View file

@ -4,13 +4,14 @@ import {
waitForModules,
} from '@revenge-mod/modules/finders'
import {
byName,
createFilterGenerator,
withName,
} from '@revenge-mod/modules/finders/filters'
import { getModuleDependencies } from '@revenge-mod/modules/metro/utils'
import { asap, noop } from '@revenge-mod/utils/callback'
import { cached, cacheFilterResultForId } from '#modules/src/caches'
import { FilterResultFlags } from '#modules/src/finders/_internal'
import { FilterFlag } from '#modules/src/finders/filters/constants'
import type {
Filter,
FilterGenerator,
@ -33,7 +34,7 @@ export const Stores = new Proxy(_stores, {
get: (target, prop: string) =>
target[prop] ??
// @ts-expect-error: This always uses cache
lookupModule(byStoreName(prop), { uninitialized: true })[0],
lookupModule(withStoreName(prop), { uninitialized: true })[0],
})
/**
@ -53,7 +54,7 @@ export function getStore<T>(
return noop
}
return waitForModules(byStoreName<T>(name), callback, { cached: true })
return waitForModules(withStoreName<T>(name), callback, { cached: true })
}
/// STORE FILTERING
@ -70,15 +71,15 @@ export function getStore<T>(
]
*/
const [, _createClassModuleId] = lookupModule(byName('_createClass'))
const [, _classCallCheckModuleId] = lookupModule(byName('_classCallCheck'))
const [, _createClassModuleId] = lookupModule(withName('_createClass'))
const [, _classCallCheckModuleId] = lookupModule(withName('_classCallCheck'))
const [, _possibleConstructorReturnModuleId] = lookupModule(
byName('_possibleConstructorReturn'),
withName('_possibleConstructorReturn'),
)
const [, _bound_getPrototypeOfModuleId] = lookupModule(
byName('bound getPrototypeOf'),
withName('bound getPrototypeOf'),
)
const [, _inheritsModuleId] = lookupModule(byName('_inherits'))
const [, _inheritsModuleId] = lookupModule(withName('_inherits'))
const FluxStoreLeadingDeps = [
_classCallCheckModuleId,
@ -88,14 +89,14 @@ const FluxStoreLeadingDeps = [
_inheritsModuleId,
]
export type ByStore = FilterGenerator<
export type WithStore = FilterGenerator<
<T>() => Filter<DiscordModules.Flux.Store<T>, boolean>
>
/**
* A dynamic filter that matches all Flux stores.
*/
export const byStore = createFilterGenerator(
export const withStore = createFilterGenerator(
(_, id, exports) => {
if (exports) return Boolean(exports._dispatchToken)
else {
@ -108,29 +109,31 @@ export const byStore = createFilterGenerator(
return deps[deps.length - 1] === 2
}
},
() => 'revenge.discord.byStore',
) as ByStore
() => 'revenge.discord.store',
FilterFlag.Dynamic,
) as WithStore
export type ByStoreName = FilterGenerator<
export type WithStoreName = FilterGenerator<
<T>(name: string) => Filter<DiscordModules.Flux.Store<T>, true>
>
/**
* A with-exports filter that matches a Flux store by its name.
*/
export const byStoreName = createFilterGenerator(
export const withStoreName = createFilterGenerator(
([name], _, exports) =>
exports.getName?.length === 0 && exports.getName() === name,
([name]) => `revenge.discord.byStoreName(${name})`,
) as ByStoreName
([name]) => `revenge.discord.storeName(${name})`,
FilterFlag.RequiresExports,
) as WithStoreName
/// STORE CACHING
waitForModules(byStore(), (store, id) => {
waitForModules(withStore(), (store, id) => {
const name = store.getName()
// Cache stores
cacheFilterResultForId(
byStoreName.keyFor([name]),
withStoreName.keyFor([name]),
id,
FilterResultFlags.Default,
)
@ -140,7 +143,7 @@ waitForModules(byStore(), (store, id) => {
cached.then(cached => {
if (!cached)
asap(() => {
const lookup = lookupModules(byStore(), {
const lookup = lookupModules(withStore(), {
uninitialized: true,
})

View file

@ -1,13 +1,12 @@
import { lookupModule } from '@revenge-mod/modules/finders'
import {
byDependencies,
byProps,
preferExports,
withDependencies,
withProps,
} from '@revenge-mod/modules/finders/filters'
import { proxify } from '@revenge-mod/utils/proxy'
import type { NavigationContainerRef } from '@react-navigation/core'
const { loose, relative } = byDependencies
const { loose, relative } = withDependencies
export interface RootNavigationRef {
getRootNavigationRef<
@ -19,9 +18,8 @@ export interface RootNavigationRef {
export let RootNavigationRef: RootNavigationRef = proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<RootNavigationRef>('getRootNavigationRef'),
byDependencies([
withProps<RootNavigationRef>('getRootNavigationRef').and(
withDependencies([
loose([[], loose([relative(1), relative(2)])]),
2,
]),

View file

@ -1,8 +1,7 @@
import { lookupModule } from '@revenge-mod/modules/finders'
import {
byDependencies,
byProps,
preferExports,
withDependencies,
withProps,
} from '@revenge-mod/modules/finders/filters'
import {
ReactJSXRuntimeModuleId,
@ -12,7 +11,7 @@ import {
import { proxify } from '@revenge-mod/utils/proxy'
import type { DiscordModules } from '../../types'
const { loose, relative } = byDependencies
const { loose, relative } = withDependencies
export type SettingListRenderer =
DiscordModules.Modules.Settings.SettingListRenderer
@ -21,9 +20,8 @@ export type SettingListRenderer =
export let SettingListRenderer: SettingListRenderer = proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<SettingListRenderer>('SettingsList'),
byDependencies(
withProps<SettingListRenderer>('SettingsList').and(
withDependencies(
loose([
ReactModuleId,
ReactNativeModuleId,

View file

@ -1,19 +1,17 @@
import { lookupModule } from '@revenge-mod/modules/finders'
import {
byDependencies,
byProps,
preferExports,
withDependencies,
withProps,
} from '@revenge-mod/modules/finders/filters'
import type { Metro } from '@revenge-mod/modules/types'
import type { DiscordModules } from './types'
const { relative } = byDependencies
const { relative } = withDependencies
// ../discord_common/js/packages/app-start-performance/AppStartPerformance.tsx
export const [AppStartPerformance] = lookupModule(
preferExports(
byProps<DiscordModules.AppStartPerformance>('markAndLog'),
byDependencies([relative(-1), null, null, 2]),
withProps<DiscordModules.AppStartPerformance>('markAndLog').and(
withDependencies([relative(-1), null, null, 2]),
),
{
uninitialized: true,

View file

@ -1,12 +1,12 @@
import { waitForModules } from '@revenge-mod/modules/finders'
import { byProps } from '@revenge-mod/modules/finders/filters'
import { withProps } from '@revenge-mod/modules/finders/filters'
import { sConfig, sSubscriptions } from './modules/settings/_internal'
import type { SettingsItem } from './modules/settings'
export let sLoaded = false
const unsubSRC = waitForModules(
byProps<{ SETTING_RENDERER_CONFIG: Record<string, SettingsItem> }>(
withProps<{ SETTING_RENDERER_CONFIG: Record<string, SettingsItem> }>(
'SETTING_RENDERER_CONFIG',
),
SettingRendererConfig => {

View file

@ -1,19 +1,17 @@
import { lookupModule } from '@revenge-mod/modules/finders'
import {
byDependencies,
byProps,
preferExports,
withDependencies,
withProps,
} from '@revenge-mod/modules/finders/filters'
import { proxify } from '@revenge-mod/utils/proxy'
const { loose, relative } = byDependencies
const { loose, relative } = withDependencies
export let nodeUtil: typeof import('node:util') = proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<typeof nodeUtil>('inspect'),
byDependencies([
withProps<typeof nodeUtil>('inspect').and(
withDependencies([
loose([relative(2, true), relative(4, true)]),
[],
[],

View file

@ -1,13 +1,12 @@
import { lookupModule } from '@revenge-mod/modules/finders'
import {
byDependencies,
byProps,
preferExports,
withDependencies,
withProps,
} from '@revenge-mod/modules/finders/filters'
import { ReactModuleId, ReactNativeModuleId } from '@revenge-mod/react'
import { destructure, proxify } from '@revenge-mod/utils/proxy'
const { relative } = byDependencies
const { relative } = withDependencies
let ClipboardModule: typeof import('@react-native-clipboard/clipboard') =
proxify(
@ -24,9 +23,8 @@ let ClipboardModule: typeof import('@react-native-clipboard/clipboard') =
// r5 = [27, 4793];
const [module] = lookupModule(
preferExports(
byProps<typeof ClipboardModule>('useClipboard'),
byDependencies([
withProps<typeof ClipboardModule>('useClipboard').and(
withDependencies([
relative.withDependencies(
[ReactModuleId, relative(2, true)],
1,

View file

@ -1,25 +1,23 @@
import { lookupModule } from '@revenge-mod/modules/finders'
import {
byDependencies,
byName,
byProps,
preferExports,
withDependencies,
withName,
withProps,
} from '@revenge-mod/modules/finders/filters'
import { ReactJSXRuntimeModuleId, ReactModuleId } from '@revenge-mod/react'
import { proxify } from '@revenge-mod/utils/proxy'
const { loose, relative } = byDependencies
const { loose, relative } = withDependencies
const [, _createClassModuleId] = lookupModule(byName('_createClass'))
const [, _classCallCheckModuleId] = lookupModule(byName('_classCallCheck'))
const [, _createClassModuleId] = lookupModule(withName('_createClass'))
const [, _classCallCheckModuleId] = lookupModule(withName('_classCallCheck'))
export let ReactNavigationNative: typeof import('@react-navigation/native') =
proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<typeof ReactNavigationNative>('useLinkTo'),
byDependencies(
withProps<typeof ReactNavigationNative>('useLinkTo').and(
withDependencies(
loose([
[],
loose([
@ -44,9 +42,8 @@ export let ReactNavigationStack: typeof import('@react-navigation/stack') =
proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<typeof ReactNavigationStack>('StackView'),
byDependencies(
withProps<typeof ReactNavigationStack>('StackView').and(
withDependencies(
loose([
relative.withDependencies(
loose([

View file

@ -1,8 +1,7 @@
import { lookupModule } from '@revenge-mod/modules/finders'
import {
byDependencies,
byProps,
preferExports,
withDependencies,
withProps,
} from '@revenge-mod/modules/finders/filters'
import {
ReactJSXRuntimeModuleId,
@ -14,9 +13,10 @@ import { proxify } from '@revenge-mod/utils/proxy'
export let FlashList: typeof import('@shopify/flash-list') = proxify(
() => {
const [module] = lookupModule(
preferExports(
byProps<typeof FlashList>('FlashList'),
byDependencies([
withProps<typeof FlashList>('FlashList').and(
// Dependencies. One in brackets are dynamic or late initialized.
// [React, RN, JSXRuntime, (FlashListExports), (Reanimated), (RNBottomSheet), ImportTracker, (BottomSheetFlashList)]
withDependencies([
ReactModuleId,
ReactNativeModuleId,
ReactJSXRuntimeModuleId,
@ -26,8 +26,6 @@ export let FlashList: typeof import('@shopify/flash-list') = proxify(
2,
null,
]),
// Dependencies. One in brackets are dynamic or late initialized.
// [React, RN, JSXRuntime, (FlashListExports), (Reanimated), (RNBottomSheet), ImportTracker, (BottomSheetFlashList)]
),
{
uninitialized: true,

View file

@ -4,7 +4,7 @@
"type": "module",
"exports": {
"./finders/filters": {
"default": "./src/finders/filters.ts"
"default": "./src/finders/filters/index.ts"
},
"./finders": {
"default": "./src/finders/index.ts"
@ -25,4 +25,4 @@
"types": "./src/types.ts"
}
}
}
}

View file

@ -65,7 +65,7 @@ export const FilterResultFlagToHumanReadable: Record<FilterResultFlag, string> =
[FilterResultFlags.Found]: '\u001b[96mexportsless\u001b[0m',
}
// The reason this returns a flag is because flags are never falsy, while exports may be falsy when using ID-only filters (eg. `byDependencies`).
// The reason this returns a flag is because flags are never falsy, while exports may be falsy when using ID-only filters (eg. `withDependencies`).
// Currently, we only have options that are relevant for checking exports
// If we add more options later on, do NOT forget about

View file

@ -1,739 +0,0 @@
import { getCurrentStack } from '@revenge-mod/utils/error'
import { mInitialized } from '../metro/patches'
import { getModuleDependencies, 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
: F extends FilterBase<infer R>
? R
: never
export type IsFilterWithExports<F> = F extends Filter<any, infer WE>
? WE
: F extends FilterBase<any, infer WE>
? WE
: never
interface FilterBase<_Inferable = any, WithExports extends boolean = boolean> {
(
...args: If<
WithExports,
[id: Metro.ModuleID, exports: Metro.ModuleExports],
[id: Metro.ModuleID, exports?: never]
>
): boolean
key: string
}
export type Filter<
Inferable = any,
WithExports extends boolean = boolean,
> = FilterHelpers & FilterBase<Inferable, WithExports>
interface FilterHelpers {
/**
* Manually the key for this filter.
*
* **Don't use this unless you know what you're doing.** Only API exports should be using
*
* @param key The key to set for this filter.
*/
keyAs<T extends FilterBase>(this: T, key: string): T
/**
* Combines this filter with another filter, returning a new filter that matches if **both** filters match.
*
* @param filter The filter to combine with.
*/
and<T extends FilterBase, F extends FilterBase>(
this: T,
filter: F,
): Filter<
FilterResult<T> & FilterResult<F>,
LogicalOr<IsFilterWithExports<T>, IsFilterWithExports<F>>
>
/**
* Combines this filter with another filter, returning a new filter that matches if **either** filter matches.
*
* @param filter
*/
or<T extends FilterBase, F extends FilterBase>(
this: T,
filter: F,
): Filter<
FilterResult<T> | FilterResult<F>,
IsFilterWithExports<T> | IsFilterWithExports<F>
>
/**
* Combines this filter with another filter that filters by exports, returning a new filter that matches if **both** filters match.
*
* @see {@link preferExports} for more information on how to use this.
*/
withExports<T extends FilterBase<any, false>>(
this: T,
withExportsFilter: FilterBase<any, true>,
strict?: boolean,
): Filter<
FilterResult<typeof withExportsFilter>,
IsFilterWithExports<T> | IsFilterWithExports<typeof withExportsFilter>
>
/**
* Combines this filter with another filter that filters by dependencies, returning a new filter that matches if **both** filters match.
*
* @see {@link byDependencies} for more information on how to use this.
*/
withDependencies<T extends FilterBase<any, true>>(
this: T,
deps: ComparableDependencyMap,
strict?: boolean,
): Filter<FilterResult<T>, false>
}
export type FilterGenerator<G extends (...args: any[]) => Filter> = G & {
keyFor(args: Parameters<G>): string
}
const Helpers: FilterHelpers = Object.setPrototypeOf(
{
keyAs(key) {
this.key = key
return this
},
and(filter) {
return every(this, filter)
},
or(filter) {
return some(this, filter)
},
withExports(withExportsFilter, strict) {
return preferExports(withExportsFilter, this, strict)
},
withDependencies(deps, strict) {
return preferExports(this, byDependencies(deps), strict)
},
} satisfies FilterHelpers,
Function.prototype,
)
/**
* Create a filter generator.
*
* @param filter The function that filters the modules.
* @param keyFor The function that generates the key for the filter.
* @returns A function that generates a filter with the specified arguments.
*
* @example
* ```ts
* const custom = createFilterGenerator<[arg1: number, arg2: string]>(
* ([arg1, arg2], id, exports) => {
* // filter logic
* return true
* },
* ([arg1, arg2]) => `custom(${arg1}, ${arg2})`
* )
* ```
*
* @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,
keyFor: (args: A) => string,
): FilterGenerator<(...args: A) => Filter<any, true>>
export function createFilterGenerator<A extends any[]>(
filter: (args: A, id: Metro.ModuleID) => boolean,
keyFor: (args: A) => string,
): FilterGenerator<(...args: A) => Filter<any, false>>
export function createFilterGenerator<A extends any[]>(
filter: (
args: A,
id: Metro.ModuleID,
exports?: Metro.ModuleExports,
) => boolean,
keyFor: (args: A) => string,
): FilterGenerator<(...args: A) => Filter> {
type GeneratorType = ReturnType<typeof createFilterGenerator<A>>
const generator: GeneratorType = (...args: A) => {
const filter_ = ((id: Metro.ModuleID, exports?: Metro.ModuleExports) =>
filter(args, id, exports)) as ReturnType<GeneratorType>
filter_.key = keyFor(args)
return Object.setPrototypeOf(filter_, Helpers)
}
generator.keyFor = keyFor
return generator
}
export type ByProps = FilterGenerator<
<T extends Record<string, any> = Record<string, any>>(
prop: keyof T,
...props: Array<keyof T>
) => Filter<T, true>
>
/**
* Filter modules by their exports having all of the specified properties.
*
* @param prop The property to check for.
* @param props More properties to check for (optional).
*
* @example
* ```ts
* const [React] = lookupModule(byProps<typeof import('react')>('createElement'))
* // const React: typeof import('react')
* ```
*/
export const byProps = createFilterGenerator<Parameters<ByProps>>(
(props, _, exports) => {
const type = typeof exports
if (type === 'object' || type === 'function') {
for (const prop of props) {
if (prop in exports) continue
return false
}
return true
}
return false
},
props => `revenge.props(${props.join(',')})`,
) as ByProps
export type WithoutProps = FilterGenerator<
<T extends Record<string, any>>(
prop: string,
...props: string[]
) => Filter<T, true>
>
/**
* Filter modules by their exports having none of the specified properties.
*
* @param prop The property to check for.
* @param props More properties to check for (optional).
*/
export const withoutProps = createFilterGenerator<Parameters<WithoutProps>>(
(props, _, exports) => {
const type = typeof exports
if (type === 'object' || type === 'function')
for (const prop of props) if (prop in exports) return false
return true
},
props => `revenge.withoutProps(${props.join(',')})`,
) as WithoutProps
export type BySingleProp = FilterGenerator<
<T extends Record<string, any>>(prop: keyof T) => Filter<T, true>
>
/**
* Filter modules by their exports having only the specified property.
*
* @param prop The property to check for.
*
* @example
* ```ts
* const [FormSwitchModule] = lookupModule(bySingleProp('FormSwitch'))
* // const FormSwitchModule: { FormSwitch: any }
* ```
*/
export const bySingleProp = createFilterGenerator<Parameters<BySingleProp>>(
([prop], _, exports) => {
if (typeof exports === 'object' && prop in exports)
return Object.keys(exports).length === 1
return false
},
([prop]) => `revenge.singleProp(${prop})`,
) as BySingleProp
export type ByName = FilterGenerator<
<T extends object = object>(name: string) => Filter<T, true>
>
/**
* Filter modules by their exports having the specified name.
*
* Usually used for function components or classes.
*
* @param name The name to check for.
*
* @example Auto-typing as object
* ```ts
* const [SomeComponent] = lookupModule(byName('SomeComponent'))
* // const SomeComponent: { name: 'SomeComponent' }
* ```
*
* @example Typing as function component
* ```ts
* type MyComponent = React.FC<{ foo: string }>
*
* const [MyComponent] = lookupModule(byName<MyComponent>('MyComponent'))
* // const MyComponent: MyComponent & { name: 'MyComponent' }
* ```
*
* @example Typing as class
* ```
* interface SomeClass {
* someMethod(): void
* }
*
* const [SomeClass] = lookupModule(byName<{ new(param: string): SomeClass }>('SomeClass'))
* // const SomeClass: { new(): SomeClass, name: 'SomeClass' }
*/
export const byName = createFilterGenerator<Parameters<ByName>>(
([name], _, exports) => exports.name === name,
([name]) => `revenge.name(${name})`,
) as ByName
export interface ComparableDependencyMap
extends Array<
Metro.ModuleID | number | null | undefined | ComparableDependencyMap
> {
l?: boolean
r?: number
}
type ByDependencies = FilterGenerator<
<T>(deps: ComparableDependencyMap) => Filter<T, false>
> & {
loose: typeof loose
relative: typeof relative
}
const __DEBUG_WARNED_BAD_BY_DEPENDENCIES_FILTERS__ =
new Set<ComparableDependencyMap>()
/**
* Filter modules by their dependency map.
*
* @param deps The dependency map to check for, can be a sparse array or have `null` to be any dependency ("dynamic"). **Order and size matters!**
*
* To do proper fingerprinting for modules:
* @see {@link byDependencies.loose} to loosen the checks.
* @see {@link byDependencies.relative} to compare dependencies relatively.
*
* @example
* ```ts
* const { loose, relative } = byDependencies
*
* // Logger's module ID is 5
* // It has 3 dependencies [4, ?, 2]
*
* const [Logger] = lookupModule(byDependencies([4, null, 2]))
* // or
* const [Logger] = lookupModule(byDependencies([4, , 2]))
*
* // Relative dependencies
* const [Logger] = lookupModule(byDependencies([relative(-1), null, 2]))
*
* // Nested dependencies
* // The last dependency (module ID 2) would need to have zero dependencies:
* const [Logger] = lookupModule(byDependencies([4, null, []]))
*
* // Loose dependencies
* // Module having these dependencies: [4, ...], [4, ..., ...], [4, ..., ..., ...], etc. would match:
* const [SomeOtherModule] = lookupModule(byDependencies(loose([4])))
* ```
*
* @example With filter helpers (preferred)
* ```ts
* const [Logger] = lookupModule(
* byProps('log')
* .withDependencies([4, null, 2]),
* )
* ```
*/
export const byDependencies = createFilterGenerator<Parameters<ByDependencies>>(
([deps], id) => depCompare(getModuleDependencies(id)!, deps, id, id),
deps => `revenge.deps(${depGenFilterKey(deps)})`,
) as ByDependencies
byDependencies.loose = loose
byDependencies.relative = relative
/**
* Make this set of comparable dependencies as loose.
*
* Making a dependency loose skips the exact length check, but the order of the set dependencies still matters.
* If you mark an index as dynamic, the same index must also be present in the other map during comparison to pass.
*
* @param deps The dependency map to make loose. This permanently modifies the array.
* @returns The modified dependency map.
*/
function loose(deps: ComparableDependencyMap) {
deps.l = true
return deps
}
const RelativeSignBit = 1 << 30
const RelativeBit = 1 << 29
const RelativeRootBit = 1 << 28
const RelativeBitMask = ~(RelativeSignBit | RelativeBit | RelativeRootBit)
/**
* Marks this dependency to compare relatively to the module ID being compared.
*
* @param id The dependency ID to mark as relative.
* @param root Marks this dependency to compare relatively to the root (returning) module ID being compared. Useful for nested comparisons where you want to compare by the root module ID instead of the parent's module ID of the nested dependency.
*/
function relative(id: Metro.ModuleID, root?: boolean) {
id = (id < 0 ? -id | RelativeSignBit : id) | RelativeBit
if (root) id |= RelativeRootBit
return id
}
/**
* Marks this dependency to compare relatively to the module ID being compared, with an additional dependencies check.
*
* @param deps The dependency map to add the relative dependency to. This permanently modifies the array.
* @param id The dependency ID to mark as relative.
* @param root Whether to use {@link relative.toRoot} instead of {@link relative}. Defaults to `false`.
* @returns The modified dependency map.
*
* @see {@link byDependencies}
* @see {@link relative}
*
* @example
* ```ts
* const { relative } = byDependencies
*
* // This filter will match modules having one dependency that is its module ID + 1
* // And module ID + 1 would have exactly two dependencies: [Any, 2]
* byDependencies(
* relative.withDependencies(
* [null, 2],
* 1, // Always the next module to the one being compared
* true, // The module ID being compared matches the returning (root) module ID
* )
* )
* ```
*/
relative.withDependencies = (
deps: ComparableDependencyMap,
id: Metro.ModuleID,
root?: boolean,
) => {
deps.r = relative(id, root)
return deps
}
/**
* Warns the developer about a bad `byDependencies` filter using `undefined` in its comparisons.
*
* - `undefined` should only be used as a fallback to when a module ID can really not be found.
* - Use `null` instead to indicate a dynamic dependency.
*/
function DEBUG_warnBadByDependenciesFilter(
deps: ComparableDependencyMap,
index: number,
) {
// already warned
if (__DEBUG_WARNED_BAD_BY_DEPENDENCIES_FILTERS__.has(deps)) return
nativeLoggingHook(
`\u001b[33mBad ${byDependencies.name} filter, undefined ID at index ${index} (if intentional, set to null): [${depGenFilterKey(deps)}]\n${getCurrentStack()}\u001b[0m`,
2,
)
}
function depCompare(
a: Metro.ModuleID[],
b: ComparableDependencyMap,
root: Metro.ModuleID,
parent: Metro.ModuleID,
): boolean {
const lenA = a.length
const lenB = b.length
if (b.l ? lenA < lenB : lenA !== lenB) return false
for (let i = 0; i < lenB; i++) {
const compare = b[i]
if (__DEV__ && compare === undefined)
DEBUG_warnBadByDependenciesFilter(b, i)
// Skip dynamic
if (compare == null) continue
const id = a[i]
// Check if it's an array (typeof is faster than Array.isArray)
if (typeof compare === 'object') {
// relative.withDependencies?
if (compare.r && !depShallowCompare(compare.r, id, root, parent))
return false
if (depCompare(getModuleDependencies(id)!, compare, root, id))
continue
} else if (depShallowCompare(compare, id, root, parent)) continue
return false
}
return true
}
function depShallowCompare(
compare: number,
id: Metro.ModuleID,
root: Metro.ModuleID,
parent: Metro.ModuleID,
) {
// relative?
if (compare & RelativeBit)
compare =
(compare & RelativeRootBit ? root : parent) +
depGetRelMagnitude(compare)
return compare === id
}
function depGetRelMagnitude(dep: number) {
const rootRelative = dep & RelativeSignBit
dep = dep & RelativeBitMask
if (rootRelative) dep = -dep
return dep
}
function depGenFilterKey(deps: ComparableDependencyMap): string {
let key = ''
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
if (dep == null) key += ','
else if (typeof dep === 'object') {
if (dep.l) key += '#'
// relative.withDependencies?
if (dep.r) key += depGenRelativeKeyPart(dep.r)
key += `[${depGenFilterKey(dep)}],`
} else {
if (dep & RelativeBit) key += depGenRelativeKeyPart(dep)
else key += `${dep},`
}
}
return key.substring(0, key.length - 1)
}
function depGenRelativeKeyPart(dep: number) {
const magnitude = depGetRelMagnitude(dep)
const prefix = dep & RelativeRootBit ? '~' : '^'
return `${prefix}${magnitude},`
}
export type Every = FilterGenerator<{
<F1 extends FilterBase, F2 extends FilterBase>(
f1: F1,
f2: F2,
): Filter<
FilterResult<F1> & FilterResult<F2>,
LogicalOr<IsFilterWithExports<F1>, IsFilterWithExports<F2>>
>
<F1 extends FilterBase, F2 extends FilterBase, F3 extends FilterBase>(
f1: F1,
f2: F2,
f3: F3,
): Filter<
FilterResult<F1> & FilterResult<F2> & FilterResult<F3>,
LogicalOr<
LogicalOr<IsFilterWithExports<F1>, IsFilterWithExports<F2>>,
IsFilterWithExports<F3>
>
>
(...filters: FilterBase[]): Filter
}>
/**
* Combines multiple filters into one, returning true if **every** filter matches.
*
* @param filters The filters to combine.
*
* @example With filter helpers (preferred)
* ```ts
* const [SomeModule] = lookupModule(
* byProps('x', 'name')
* .and(byName('SomeName'))
* .and(byDependencies([1, 485, null, 2])),
* )
* ```
*
* @example
* ```ts
* const [SomeModule] = lookupModule(every(
* byProps('x', 'name'),
* byName('SomeName'),
* byDependencies([1, 485, null, 2]),
* ))
* ```
*/
export const every = createFilterGenerator<[...filters: Filter[]]>(
(filters, id, exports) => {
for (const filter of filters) {
if (filter(id, exports)) continue
return false
}
return true
},
filters => `revenge.every(${filtersToKey(filters)})`,
) as Every
export type Some = FilterGenerator<{
<F1 extends FilterBase, F2 extends FilterBase>(
f1: F1,
f2: F2,
): Filter<
FilterResult<F1> | FilterResult<F2>,
IsFilterWithExports<F1> | IsFilterWithExports<F2>
>
<F1 extends FilterBase, F2 extends FilterBase, F3 extends FilterBase>(
f1: F1,
f2: F2,
f3: F3,
): Filter<
FilterResult<F1> | FilterResult<F2> | FilterResult<F3>,
| IsFilterWithExports<F1>
| IsFilterWithExports<F2>
| IsFilterWithExports<F3>
>
(...filters: FilterBase[]): Filter
}>
/**
* Combines multiple filters into one, returning true if **some** filters match.
*
* @param filters The filters to combine.
*
* @example With filter helpers (preferred)
* ```ts
* const [SomeModule] = lookupModule(
* byProps('x', 'name')
* .or(byName('SomeName'))
* .or(byDependencies([1, 485, null, 2])),
* )
* ```
*
* @example
* ```ts
* const [SomeModule] = lookupModule(some(
* byProps('x', 'name'),
* byName('SomeName'),
* byDependencies([1, 485, null, 2]),
* ))
* ```
*/
export const some = createFilterGenerator<[...filters: FilterBase[]]>(
(filters, id, exports) => {
for (const filter of filters) if (filter(id, exports)) return true
return false
},
filters => `revenge.some(${filtersToKey(filters)})`,
) as Some
function filtersToKey(filters: FilterBase[]): string {
let s = ''
for (const filter of filters) s += `${filter.key},`
return s.substring(0, s.length - 1)
}
export type ModuleStateAware = FilterGenerator<
<IF extends FilterBase>(
initializedFilter: IF,
uninitializedFilter: FilterBase<any, false>,
strict?: boolean,
) => Filter<FilterResult<IF>, false>
>
/**
* Filter modules depending on their initialized state. **Initialized modules with bad exports are skipped.**
*
* @param initializedFilter The filter to use for initialized modules.
* @param uninitializedFilter The filter to use for uninitialized modules.
* @param strict Whether to also filter with `uninitializedFilter` after `initializedFilter` passes, confirming the module is definitely the correct module. Defaults to `false`.
*
* @example
* ```ts
* // will filter byProps('x') for initialized modules
* // and byDependencies([1, 485, null, 2]) for uninitialized modules
* const [SomeModule] = lookupModule(moduleStateAware(
* byProps('x'),
* byDependencies([1, 485, null, 2]),
* ))
* ```
*/
export const moduleStateAware = createFilterGenerator<
Parameters<ModuleStateAware>
>(
([initializedFilter, uninitializedFilter, strict], id, exports) => {
if (isModuleInitialized(id)) {
if (mInitialized.has(id) && initializedFilter(id, exports))
return strict ? uninitializedFilter(id) : true
return false
}
return uninitializedFilter(id)
},
([f1, f2]) => `revenge.moduleStateAware(${f1.key},${f2.key})`,
) as ModuleStateAware
export type PreferExports = FilterGenerator<
<WEF extends FilterBase>(
withExportsFilter: WEF,
exportslessFilter: FilterBase<any, false>,
strict?: boolean,
) => Filter<FilterResult<WEF>, false>
>
/**
* Filter modules depending on if their exports are available and filterable.
*
* @see {@link isModuleExportsBad} for more information on what is considered bad module exports.
*
* @see {@link moduleStateAware} for an alternative that filters based on the module's initialized state.
*
* @param withExportsFilter The filter to use for modules with proper exports.
* @param exportslessFilter The filter to use for modules without proper exports (uninitialized or bad).
* @param strict Whether to also filter with `exportslessFilter` after `withExportsFilter` passes, confirming the module is definitely the correct module. Defaults to `false`.
*
* @example With filter helpers (preferred)
* ```ts
* const [SomeModule] = lookupModule(
* byDependencies([1, 485, null, 2])
* .withExports(byProps('x')),
* )
* ```
*
* @example
* ```ts
* // will filter byProps('x') for modules with proper exports
* // and byDependencies([1, 485, null, 2]) for without proper exports (uninitialized or bad)
* const [SomeModule] = lookupModule(preferExports(
* byProps('x'),
* byDependencies([1, 485, null, 2]),
* ))
* ```
*/
export const preferExports = createFilterGenerator<Parameters<PreferExports>>(
([withExportsFilter, exportslessFilter, strict], id, exports) => {
if (mInitialized.has(id)) {
if (withExportsFilter(id, exports))
return strict ? exportslessFilter(id) : true
return false
}
return exportslessFilter(id)
},
([f1, f2]) => `revenge.preferExports(${f1.key},${f2.key})`,
) as PreferExports

View file

@ -0,0 +1,189 @@
import { FilterFlag } from './constants'
import { createFilterGenerator } from './utils'
import type { LogicalAnd } from '@revenge-mod/utils/types'
import type { Metro } from '#modules/src/types'
import type {
Filter,
FilterBase,
FilterGenerator,
FilterRequiresExports,
FilterResult,
} from '.'
const compositeHandler = <G extends (a: FilterBase, b: FilterBase) => any>(
sameHandler: G,
dynamicHandler: G,
) =>
((a, b) =>
a.flags === b.flags
? sameHandler(a, b)
: a.flags === FilterFlag.RequiresExports
? dynamicHandler(a, b)
: dynamicHandler(b, a)) as G
const compositeArrayHandler = <
G extends (args: [a: FilterBase, b: FilterBase]) => any,
>(
sameHandler: G,
dynamicHandler: G,
) =>
(([a, b]) =>
a.flags === b.flags
? sameHandler([a, b])
: a.flags === FilterFlag.RequiresExports
? dynamicHandler([a, b])
: dynamicHandler([b, a])) as G
export type And = FilterGenerator<
<F1 extends FilterBase, F2 extends FilterBase>(
f1: F1,
f2: F2,
) => Filter<
FilterResult<F1> & FilterResult<F2>,
LogicalAnd<FilterRequiresExports<F1>, FilterRequiresExports<F2>>
>
>
const andKeyGenerator = ([a, b]: Parameters<And>) =>
`revenge.and(${a.key},${b.key})`
const sameFlagsAnd = createFilterGenerator(
([a, b], id, exports) => a(id, exports) && b(id, exports),
andKeyGenerator,
([a]) => a.flags,
) as And
const andFallbackCache = new WeakMap<FilterBase, Set<Metro.ModuleID>>()
const dynamicFlagsAnd = createFilterGenerator(
([filter, fallbackFilter], id, exports) => {
if (exports) {
if (filter(id, exports)) {
// Avoid running the fallback filter if we already know the first one passed
const cache = andFallbackCache.get(fallbackFilter)
return (
// biome-ignore lint/complexity/useOptionalChain: Hot path should be optimized
(cache && cache.has(id)) || fallbackFilter(id, exports)
)
}
return false
}
const result = fallbackFilter(id)
if (result) {
// Cache fallback hits to avoid calling the fallback filter again
// Fallback filters are usually more expensive
let set = andFallbackCache.get(fallbackFilter)
if (!set) andFallbackCache.set(fallbackFilter, (set = new Set()))
set.add(id)
}
return result
},
andKeyGenerator,
FilterFlag.Dynamic,
) as And
/**
* Combines two filters into one, returning true if **every** filter matches.
*
* If each filter has different flags,
*
* @param filters The filters to combine.
*
* @example With filter helpers (preferred)
* ```ts
* const [SomeModule] = lookupModule(
* withProps('x', 'name')
* .and(withName('SomeName'))
* .and(withDependencies([1, 485, null, 2])),
* )
* ```
*
* @example
* ```ts
* const [SomeModule] = lookupModule(
* and(
* and(withProps('x', 'name'), withName('SomeName')),
* withDependencies([1, 485, null, 2]),
* ),
* )
* ```
*/
export const and = Object.assign(
compositeHandler(sameFlagsAnd, dynamicFlagsAnd),
{
keyFor: compositeArrayHandler(
sameFlagsAnd.keyFor,
dynamicFlagsAnd.keyFor,
),
flagsFor: compositeArrayHandler(
sameFlagsAnd.flagsFor,
dynamicFlagsAnd.flagsFor,
),
},
) satisfies And
export type Or = FilterGenerator<
<F1 extends FilterBase, F2 extends FilterBase>(
f1: F1,
f2: F2,
) => Filter<
FilterResult<F1> | FilterResult<F2>,
LogicalAnd<FilterRequiresExports<F1>, FilterRequiresExports<F2>>
>
>
const orKeyGenerator = ([a, b]: Parameters<Or>) =>
`revenge.or(${a.key},${b.key})`
const sameFlagOr = createFilterGenerator(
([a, b], id, exports) => a(id, exports) || b(id, exports),
orKeyGenerator,
([a]) => a.flags,
) as Or
const dynamicFlagOr = createFilterGenerator(
([filter, fallbackFilter], id, exports) => {
// TODO(PalmDevs): Potential optimization: Add fallback cache here too?
if (exports) return filter(id, exports) || fallbackFilter(id, exports)
return fallbackFilter(id)
},
orKeyGenerator,
FilterFlag.Dynamic,
) as Or
/**
* Combines two filters into one, returning true if **some** filters match.
*
* @param filters The filters to combine.
*
* @example With filter helpers (preferred)
* ```ts
* const [SomeModule] = lookupModule(
* withProps('x', 'name')
* .or(withName('SomeName'))
* .or(withDependencies([1, 485, null, 2])),
* )
* ```
*
* @example
* ```ts
* const [SomeModule] = lookupModule(
* or(
* or(withProps('x', 'name'), withName('SomeName')),
* withDependencies([1, 485, null, 2]),
* ),
* )
* ```
*/
export const or = Object.assign(
compositeHandler<Or>(sameFlagOr, dynamicFlagOr),
{
keyFor: compositeArrayHandler(sameFlagOr.keyFor, dynamicFlagOr.keyFor),
flagsFor: compositeArrayHandler(
sameFlagOr.flagsFor,
dynamicFlagOr.flagsFor,
),
},
) satisfies Or

View file

@ -0,0 +1,17 @@
export const FilterFlag = {
/**
* This filter works with and without module exports.
* Allowing for both initialized and uninitialized modules to be matched.
*/
Dynamic: 0,
/**
* This filter requires module exports to work.
* Only initialized modules will be matched.
*/
RequiresExports: 1,
} as const
/**
* @see {@link FilterFlag}
*/
export type FilterFlag = number

View file

@ -0,0 +1,247 @@
import { getCurrentStack } from '@revenge-mod/utils/error'
import { getModuleDependencies } from '../../metro/utils'
import { FilterFlag } from './constants'
import { createFilterGenerator } from './utils'
import type { Metro } from '../../types'
import type { Filter, FilterGenerator } from './utils'
export interface ComparableDependencyMap
extends Array<
Metro.ModuleID | number | null | undefined | ComparableDependencyMap
> {
l?: boolean
r?: number
}
const __DEBUG_WARNED_BAD_BY_DEPENDENCIES_FILTERS__ =
new Set<ComparableDependencyMap>()
/**
* Filter modules by their dependency map.
*
* @param deps The dependency map to check for, can be a sparse array or have `null` to be any dependency ("dynamic"). **Order and size matters!**
*
* To do proper fingerprinting for modules:
* @see {@link withDependencies.loose} to loosen the checks.
* @see {@link withDependencies.relative} to compare dependencies relatively.
*
* @example
* ```ts
* const { loose, relative } = withDependencies
*
* // Logger's module ID is 5
* // It has 3 dependencies [4, ?, 2]
*
* const [Logger] = lookupModule(withDependencies([4, null, 2]))
* // or
* const [Logger] = lookupModule(withDependencies([4, , 2]))
*
* // Relative dependencies
* const [Logger] = lookupModule(withDependencies([relative(-1), null, 2]))
*
* // Nested dependencies
* // The last dependency (module ID 2) would need to have zero dependencies:
* const [Logger] = lookupModule(withDependencies([4, null, []]))
*
* // Loose dependencies
* // Module having these dependencies: [4, ...], [4, ..., ...], [4, ..., ..., ...], etc. would match:
* const [SomeOtherModule] = lookupModule(withDependencies(loose([4])))
* ```
*
* @example With filter helpers (preferred)
* ```ts
* const [Logger] = lookupModule(
* withProps('log')
* .withDependencies([4, null, 2]),
* )
* ```
*/
export const withDependencies = createFilterGenerator<
Parameters<WithDependencies>
>(
([deps], id) => depCompare(getModuleDependencies(id)!, deps, id, id),
deps => `revenge.deps(${depGenFilterKey(deps)})`,
FilterFlag.Dynamic,
) as WithDependencies
withDependencies.loose = loose
withDependencies.relative = relative
type WithDependencies = FilterGenerator<
<T>(deps: ComparableDependencyMap) => Filter<T, false>
> & {
loose: typeof loose
relative: typeof relative
}
/**
* Make this set of comparable dependencies as loose.
*
* Making a dependency loose skips the exact length check, but the order of the set dependencies still matters.
* If you mark an index as dynamic, the same index must also be present in the other map during comparison to pass.
*
* @param deps The dependency map to make loose. This permanently modifies the array.
* @returns The modified dependency map.
*/
function loose(deps: ComparableDependencyMap) {
deps.l = true
return deps
}
const RelativeSignBit = 1 << 30
const RelativeBit = 1 << 29
const RelativeRootBit = 1 << 28
const RelativeBitMask = ~(RelativeSignBit | RelativeBit | RelativeRootBit)
/**
* Marks this dependency to compare relatively to the module ID being compared.
*
* @param id The dependency ID to mark as relative.
* @param root Marks this dependency to compare relatively to the root (returning) module ID being compared. Useful for nested comparisons where you want to compare by the root module ID instead of the parent's module ID of the nested dependency.
*/
function relative(id: Metro.ModuleID, root?: boolean) {
id = (id < 0 ? -id | RelativeSignBit : id) | RelativeBit
if (root) id |= RelativeRootBit
return id
}
/**
* Marks this dependency to compare relatively to the module ID being compared, with an additional dependencies check.
*
* @param deps The dependency map to add the relative dependency to. This permanently modifies the array.
* @param id The dependency ID to mark as relative.
* @param root Whether to use {@link relative.toRoot} instead of {@link relative}. Defaults to `false`.
* @returns The modified dependency map.
*
* @see {@link withDependencies}
* @see {@link relative}
*
* @example
* ```ts
* const { relative } = withDependencies
*
* // This filter will match modules having one dependency that is its module ID + 1
* // And module ID + 1 would have exactly two dependencies: [Any, 2]
* withDependencies(
* relative.withDependencies(
* [null, 2],
* 1, // Always the next module to the one being compared
* true, // The module ID being compared matches the returning (root) module ID
* )
* )
* ```
*/
relative.withDependencies = (
deps: ComparableDependencyMap,
id: Metro.ModuleID,
root?: boolean,
) => {
deps.r = relative(id, root)
return deps
}
/**
* Warns the developer about a bad `withDependencies` filter using `undefined` in its comparisons.
*
* - `undefined` should only be used as a fallback to when a module ID can really not be found.
* - Use `null` instead to indicate a dynamic dependency.
*/
function DEBUG_warnBadWithDependenciesFilter(
deps: ComparableDependencyMap,
index: number,
) {
// already warned
if (__DEBUG_WARNED_BAD_BY_DEPENDENCIES_FILTERS__.has(deps)) return
nativeLoggingHook(
`\u001b[33mBad ${withDependencies.name} filter, undefined ID at index ${index} (if intentional, set to null): [${depGenFilterKey(deps)}]\n${getCurrentStack()}\u001b[0m`,
2,
)
}
function depCompare(
a: Metro.ModuleID[],
b: ComparableDependencyMap,
root: Metro.ModuleID,
parent: Metro.ModuleID,
): boolean {
const lenA = a.length
const lenB = b.length
if (b.l ? lenA < lenB : lenA !== lenB) return false
for (let i = 0; i < lenB; i++) {
const compare = b[i]
if (__DEV__ && compare === undefined)
DEBUG_warnBadWithDependenciesFilter(b, i)
// Skip dynamic
if (compare == null) continue
const id = a[i]
// Check if it's an array (typeof is faster than Array.isArray)
if (typeof compare === 'object') {
// relative.withDependencies?
if (compare.r && !depShallowCompare(compare.r, id, root, parent))
return false
if (depCompare(getModuleDependencies(id)!, compare, root, id))
continue
} else if (depShallowCompare(compare, id, root, parent)) continue
return false
}
return true
}
function depShallowCompare(
compare: number,
id: Metro.ModuleID,
root: Metro.ModuleID,
parent: Metro.ModuleID,
) {
// relative?
if (compare & RelativeBit)
compare =
(compare & RelativeRootBit ? root : parent) +
depGetRelMagnitude(compare)
return compare === id
}
function depGetRelMagnitude(dep: number) {
const rootRelative = dep & RelativeSignBit
dep = dep & RelativeBitMask
if (rootRelative) dep = -dep
return dep
}
function depGenFilterKey(deps: ComparableDependencyMap): string {
let key = ''
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
if (dep == null) key += ','
else if (typeof dep === 'object') {
if (dep.l) key += '#'
// relative.withDependencies?
if (dep.r) key += depGenRelativeKeyPart(dep.r)
key += `[${depGenFilterKey(dep)}],`
} else {
if (dep & RelativeBit) key += depGenRelativeKeyPart(dep)
else key += `${dep},`
}
}
return key.substring(0, key.length - 1)
}
function depGenRelativeKeyPart(dep: number) {
const magnitude = depGetRelMagnitude(dep)
const prefix = dep & RelativeRootBit ? '~' : '^'
return `${prefix}${magnitude},`
}

View file

@ -0,0 +1,137 @@
import { FilterFlag } from './constants'
import { createFilterGenerator } from './utils'
import type { Filter, FilterGenerator } from './utils'
export * from './constants'
export * from './utils'
/**
* Filter modules by their exports having all of the specified properties.
*
* @param prop The property to check for.
* @param props More properties to check for (optional).
*
* @example
* ```ts
* const [React] = lookupModule(withProps<typeof import('react')>('createElement'))
* // const React: typeof import('react')
* ```
*/
export const withProps = createFilterGenerator<Parameters<WithProps>>(
(props, _, exports) => {
const type = typeof exports
if (type === 'object' || type === 'function') {
for (const prop of props) {
if (prop in exports) continue
return false
}
return true
}
return false
},
props => `revenge.props(${props.join(',')})`,
FilterFlag.RequiresExports,
) as WithProps
export type WithProps = FilterGenerator<
<T extends Record<string, any> = Record<string, any>>(
prop: keyof T,
...props: Array<keyof T>
) => Filter<T, true>
>
/**
* Filter modules by their exports having none of the specified properties.
*
* @param prop The property to check for.
* @param props More properties to check for (optional).
*/
export const withoutProps = createFilterGenerator<Parameters<WithoutProps>>(
(props, _, exports) => {
const type = typeof exports
if (type === 'object' || type === 'function')
for (const prop of props) if (prop in exports) return false
return true
},
props => `revenge.withoutProps(${props.join(',')})`,
FilterFlag.RequiresExports,
) as WithoutProps
export type WithoutProps = FilterGenerator<
<T extends Record<string, any>>(
prop: string,
...props: string[]
) => Filter<T, true>
>
/**
* Filter modules by their exports having only the specified property.
*
* @param prop The property to check for.
*
* @example
* ```ts
* const [FormSwitchModule] = lookupModule(withSingleProp('FormSwitch'))
* // const FormSwitchModule: { FormSwitch: any }
* ```
*/
export const withSingleProp = createFilterGenerator<Parameters<WithSingleProp>>(
([prop], _, exports) => {
if (typeof exports === 'object' && prop in exports)
return Object.keys(exports).length === 1
return false
},
([prop]) => `revenge.singleProp(${prop})`,
FilterFlag.RequiresExports,
) as WithSingleProp
export type WithSingleProp = FilterGenerator<
<T extends Record<string, any>>(prop: keyof T) => Filter<T, true>
>
/**
* Filter modules by their exports having the specified name.
*
* Usually used for function components or classes.
*
* @param name The name to check for.
*
* @example Auto-typing as object
* ```ts
* const [SomeComponent] = lookupModule(withName('SomeComponent'))
* // const SomeComponent: { name: 'SomeComponent' }
* ```
*
* @example Typing as function component
* ```ts
* type MyComponent = React.FC<{ foo: string }>
*
* const [MyComponent] = lookupModule(withName<MyComponent>('MyComponent'))
* // const MyComponent: MyComponent & { name: 'MyComponent' }
* ```
*
* @example Typing as class
* ```
* interface SomeClass {
* someMethod(): void
* }
*
* const [SomeClass] = lookupModule(withName<{ new(param: string): SomeClass }>('SomeClass'))
* // const SomeClass: { new(): SomeClass, name: 'SomeClass' }
*/
export const withName = createFilterGenerator<Parameters<WithName>>(
([name], _, exports) => exports.name === name,
([name]) => `revenge.name(${name})`,
FilterFlag.RequiresExports,
) as WithName
export type WithName = FilterGenerator<
<T extends object = object>(name: string) => Filter<T, true>
>
export * from './composite'
export * from './dynamic'

View file

@ -0,0 +1,166 @@
import { and, or } from './composite'
import type { Metro } from '@revenge-mod/modules/types'
import type { If, LogicalAnd } from '@revenge-mod/utils/types'
import type { FilterFlag } from './constants'
export type FilterResult<F> = F extends Filter<infer R, boolean>
? R
: F extends FilterBase<infer R>
? R
: never
export type FilterRequiresExports<F> = F extends Filter<any, infer RE>
? RE
: F extends FilterBase<any, infer RE>
? RE
: never
export interface FilterBase<
_Result = any,
RequiresExports extends boolean = boolean,
> {
(
...args: If<
RequiresExports,
[id: Metro.ModuleID, exports: Metro.ModuleExports],
[id: Metro.ModuleID, exports?: never]
>
): boolean
key: string
flags: If<
RequiresExports,
(typeof FilterFlag)['RequiresExports'],
FilterFlag
>
}
export type Filter<
Result = any,
RequiresExports extends boolean = boolean,
> = FilterHelpers & FilterBase<Result, RequiresExports>
export interface FilterHelpers {
/**
* Manually the key for this filter.
*
* **Don't use this unless you know what you're doing.** Only API exports should be using
*
* @param key The key to set for this filter.
*/
keyAs<T extends FilterBase>(this: T, key: string): T
/**
* Combines this filter with another filter, returning a new filter that matches if **both** filters match.
*
* @param filter The filter to combine with.
*/
and<T extends FilterBase, F extends FilterBase>(
this: T,
filter: F,
): Filter<
FilterResult<T> & FilterResult<F>,
LogicalAnd<FilterRequiresExports<T>, FilterRequiresExports<F>>
>
/**
* Combines this filter with another filter, returning a new filter that matches if **either** filter matches.
*
* Note that exportsless filters must come first to avoid gotchas with uninitialized modules.
*
* @param filter The filter to combine with.
*/
or<T extends FilterBase, F extends FilterBase>(
this: T,
filter: F,
): Filter<
FilterResult<T> | FilterResult<F>,
LogicalAnd<FilterRequiresExports<T>, FilterRequiresExports<F>>
>
}
export type FilterGenerator<G extends (...args: any[]) => Filter> = G & {
keyFor(args: Parameters<G>): string
flagsFor(args: Parameters<G>): FilterFlag
}
const Helpers: FilterHelpers = Object.setPrototypeOf(
{
keyAs(key) {
this.key = key
return this
},
and(filter) {
return and(this, filter)
},
or(filter) {
return or(this, filter)
},
} satisfies FilterHelpers,
Function.prototype,
)
/**
* Create a filter generator.
*
* @param filter The function that filters the modules.
* @param keyFor The function that generates the key for the filter.
* @param flagFor The function that generates the flags for the filter, or a static flag.
* @returns A function that generates a filter with the specified arguments.
*
* @example
* ```ts
* const custom = createFilterGenerator<[arg1: number, arg2: string]>(
* ([arg1, arg2], id, exports) => {
* // filter logic
* return true
* },
* ([arg1, arg2]) => `custom(${arg1}, ${arg2})`
* )
* ```
*
* @see {@link withProps} for an example on custom-typed filters.
*/
export function createFilterGenerator<A extends any[]>(
filter: (
args: A,
id: Metro.ModuleID,
exports: Metro.ModuleExports,
) => boolean,
keyFor: (args: A) => string,
flagFor: ((args: A) => FilterFlag) | FilterFlag,
): FilterGenerator<(...args: A) => Filter<any, true>>
export function createFilterGenerator<A extends any[]>(
filter: (args: A, id: Metro.ModuleID) => boolean,
keyFor: (args: A) => string,
flagFor: ((args: A) => FilterFlag) | FilterFlag,
): FilterGenerator<(...args: A) => Filter<any, false>>
export function createFilterGenerator<A extends any[]>(
filter: (
args: A,
id: Metro.ModuleID,
exports?: Metro.ModuleExports,
) => boolean,
keyFor: (args: A) => string,
flagFor: ((args: A) => FilterFlag) | FilterFlag,
): FilterGenerator<(...args: A) => Filter> {
type GeneratorType = ReturnType<typeof createFilterGenerator<A>>
const isFlagsStatic = typeof flagFor === 'number'
const generator: GeneratorType = (...args: A) => {
const filter_ = ((id: Metro.ModuleID, exports?: Metro.ModuleExports) =>
filter(args, id, exports)) as ReturnType<GeneratorType>
filter_.key = keyFor(args)
filter_.flags = isFlagsStatic ? flagFor : flagFor(args)
return Object.setPrototypeOf(filter_, Helpers)
}
generator.flagsFor = isFlagsStatic
? () => flagFor
: (args: A) => flagFor(args)
generator.keyFor = keyFor
return generator
}

View file

@ -1,10 +1,10 @@
import { asap, noop } from '@revenge-mod/utils/callback'
import {
lookupModule,
lookupModuleByImportedPath,
lookupModules,
lookupModuleWithImportedPath,
} from './lookup'
import { waitForModuleByImportedPath, waitForModules } from './wait'
import { waitForModules, waitForModuleWithImportedPath } from './wait'
import type { If } from '@revenge-mod/utils/types'
import type { MaybeDefaultExportMatched, Metro } from '../types'
import type { RunFilterReturnExportsOptions } from './_internal'
@ -12,7 +12,7 @@ import type { Filter, FilterResult } from './filters'
import type { LookupModulesOptions } from './lookup'
import type { WaitForModulesOptions } from './wait'
export type GetModuleOptions<
export type GetModulesOptions<
ReturnNamespace extends boolean = boolean,
Uninitialized extends boolean = boolean,
All extends boolean = boolean,
@ -26,16 +26,16 @@ export type GetModuleOptions<
max?: number
}
export type GetModuleResult<
export type GetModulesResult<
F extends Filter,
O extends GetModuleOptions,
O extends GetModulesOptions,
> = O extends RunFilterReturnExportsOptions<true>
? MaybeDefaultExportMatched<FilterResult<F>>
: FilterResult<F>
export type GetModuleCallback<T> = (exports: T, id: Metro.ModuleID) => any
export type GetModulesCallback<T> = (exports: T, id: Metro.ModuleID) => any
export type GetModuleUnsubscribeFunction = () => void
export type GetModulesUnsubscribeFunction = () => void
/**
* Get modules matching the filter.
@ -48,40 +48,40 @@ export type GetModuleUnsubscribeFunction = () => void
*
* @example
* ```ts
* getModule(byProps<typeof import('react')>('createElement'), React => {
* getModules(withProps<typeof import('react')>('createElement'), React => {
* // Immediately called because React is always initialized when plugins are loaded
* })
*
* getModule(byProps<typeof import('@shopify/flash-list')>('FlashList'), FlashList => {
* getModules(withProps<typeof import('@shopify/flash-list')>('FlashList'), FlashList => {
* // Called when the module is initialized
* })
*
* // Get multiple modules matching the filter
* getModule(byProps<ReactNative.AssetsRegistry>('registerAsset'), AssetsRegistry => {
* getModules(withProps<ReactNative.AssetsRegistry>('registerAsset'), AssetsRegistry => {
* // Called 2 times, once for each module that matches the filter
* }, { max: 2 })
* ```
*/
export function getModule<F extends Filter>(
export function getModules<F extends Filter>(
filter: F,
callback: GetModuleCallback<FilterResult<F>>,
): GetModuleUnsubscribeFunction
callback: GetModulesCallback<FilterResult<F>>,
): GetModulesUnsubscribeFunction
export function getModule<
export function getModules<
F extends Filter,
const O extends F extends Filter<any, infer WE>
? If<WE, GetModuleOptions<boolean, boolean, false>, GetModuleOptions>
const O extends F extends Filter<any, infer RE>
? If<RE, GetModulesOptions<boolean, boolean, false>, GetModulesOptions>
: never,
>(
filter: F,
callback: GetModuleCallback<FilterResult<F>>,
callback: GetModulesCallback<FilterResult<F>>,
options: O,
): GetModuleUnsubscribeFunction
): GetModulesUnsubscribeFunction
export function getModule(
export function getModules(
filter: Filter,
callback: GetModuleCallback<any>,
options?: GetModuleOptions,
callback: GetModulesCallback<any>,
options?: GetModulesOptions,
) {
let max = options?.max ?? 1
@ -123,22 +123,22 @@ export function getModule(
*
* @example
* ```ts
* getModuleByImportedPath('modules/main_tabs_v2/native/settings/SettingsConstants.tsx', SettingsConstants => {
* getModuleWithImportedPath('modules/main_tabs_v2/native/settings/SettingsConstants.tsx', SettingsConstants => {
* console.log('Settings page opened') // Logs once the module is initialized
* })
* ```
*/
export function getModuleByImportedPath<T>(
export function getModuleWithImportedPath<T>(
path: string,
callback: GetModuleCallback<T>,
): GetModuleUnsubscribeFunction {
const [exports, id] = lookupModuleByImportedPath(path)
callback: GetModulesCallback<T>,
): GetModulesUnsubscribeFunction {
const [exports, id] = lookupModuleWithImportedPath(path)
if (id !== undefined) {
callback(exports, id)
return noop
}
const unsub = waitForModuleByImportedPath(path, (exports, id) => {
const unsub = waitForModuleWithImportedPath(path, (exports, id) => {
unsub()
callback(exports, id)
})

View file

@ -128,8 +128,7 @@ type LookupNotFoundResult = typeof NotFoundResult
/**
* Lookup modules.
*
* You can lookup uninitialized modules by passing `options.uninitialized` when filtering via exportsless filters (eg. `byDependencies`).
* Use the `moduleStateAware` helper to filter dynamically based on whether the module is initialized or not.
* You can lookup uninitialized modules by passing `options.uninitialized` when filtering via exportsless filters (eg. `withDependencies`).
*
* @param filter The filter to use.
* @param options The options to use for the lookup.
@ -137,9 +136,9 @@ type LookupNotFoundResult = typeof NotFoundResult
*
* @example
* ```ts
* const lookup = lookupModules(byProps('x'))
* const lookup = lookupModules(withProps('x'))
* // Log all module exports that has exports.x
* for (const exports of lookup) console.log(exports)
* for (const [exports, id] of lookup) console.log(id, exports)
* ```
*/
export function lookupModules<F extends Filter>(
@ -148,9 +147,9 @@ export function lookupModules<F extends Filter>(
export function lookupModules<
F extends Filter,
const O extends F extends Filter<any, infer WE>
const O extends F extends Filter<any, infer RE>
? If<
WE,
RE,
LookupModulesOptions<boolean, false, false>,
LookupModulesOptions
>
@ -286,7 +285,7 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
*
* @example
* ```ts
* const React = lookupModule(byProps<typeof import('react')>('createElement'))
* const [React, ReactModuleId] = lookupModule(withProps<typeof import('react')>('createElement'))
* ```
*/
export function lookupModule<F extends Filter>(
@ -295,9 +294,9 @@ export function lookupModule<F extends Filter>(
export function lookupModule<
F extends Filter,
const O extends F extends Filter<any, infer WE>
const O extends F extends Filter<any, infer RE>
? If<
WE,
RE,
LookupModulesOptions<boolean, false, false>,
LookupModulesOptions
>
@ -415,10 +414,10 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
*
* @example
* ```ts
* const [{ default: Logger }] = lookupModuleByImportedPath<{ default: typeof DiscordModules.Logger }>('modules/debug/Logger.tsx')
* const [{ default: Logger }] = lookupModuleWithImportedPath<{ default: typeof DiscordModules.Logger }>('modules/debug/Logger.tsx')
* ```
*/
export function lookupModuleByImportedPath<T = any>(
export function lookupModuleWithImportedPath<T = any>(
path: string,
): [exports: T, id: Metro.ModuleID] | LookupNotFoundResult {
const id = mImportedPaths.get(path)

View file

@ -66,7 +66,7 @@ export type WaitForModulesResult<
* @example
* ```ts
* const unsub = waitForModules(
* byName<typeof import('@shopify/flash-list')>('FlashList'),
* withName<typeof import('@shopify/flash-list')>('FlashList'),
* // (exports: typeof import('@shopify/flash-list'), id: Metro.ModuleID) => any
* (exports, id) => {
* unsub()
@ -192,7 +192,7 @@ export function waitForModules(
*
* @example
* ```ts
* waitForModuleByImportedPath(
* waitForModuleWithImportedPath(
* 'utils/PlatformUtils.tsx',
* (exports, id) => {
* // Do something with the module...
@ -200,7 +200,7 @@ export function waitForModules(
* )
* ```
*/
export function waitForModuleByImportedPath<T = any>(
export function waitForModuleWithImportedPath<T = any>(
path: string,
callback: WaitForModulesCallback<T>,
options?: BaseWaitForModulesOptions,

View file

@ -1,5 +1,5 @@
import { waitForModules } from '@revenge-mod/modules/finders'
import { byProps } from '@revenge-mod/modules/finders/filters'
import { withProps } from '@revenge-mod/modules/finders/filters'
import type { Metro } from '@revenge-mod/modules/types'
export let ReactModuleId: Metro.ModuleID
@ -13,7 +13,7 @@ export let ReactJSXRuntime: typeof import('react/jsx-runtime')
let RtCount = 0
const unsubRt = waitForModules(
byProps<typeof React>('useState'),
withProps<typeof React>('useState'),
(exports, id) => {
if (RtCount++ === 2) return unsubRt()
@ -23,7 +23,7 @@ const unsubRt = waitForModules(
)
const unsubRN = waitForModules(
byProps<typeof ReactNative>('AppRegistry'),
withProps<typeof ReactNative>('AppRegistry'),
(exports, id) => {
unsubRN()
@ -40,7 +40,7 @@ const unsubRN = waitForModules(
let RJsxRCount = 0
const unsubRJSXR = waitForModules(
byProps<typeof ReactJSXRuntime>('jsxs'),
withProps<typeof ReactJSXRuntime>('jsxs'),
(exports, id) => {
if (RJsxRCount++ === 2) return unsubRJSXR()

View file

@ -25,7 +25,10 @@ export const asap = (cb: (...args: any[]) => any) => {
;(HermesInternal.useEngineQueue()
? // @ts-expect-error
HermesInternal.enqueueJob
: (globalThis.setImmediate ?? globalThis.setTimeout ?? now))(cb, 0)
: (globalThis.queueMicrotask ??
globalThis.setImmediate ??
globalThis.setTimeout ??
now))(cb, 0)
}
export const noop = () => {}

View file

@ -2,14 +2,15 @@ import { getAssetByName } from '@revenge-mod/assets'
import { TokensModuleId } from '@revenge-mod/discord/common'
import { lookupModule } from '@revenge-mod/modules/finders'
import {
byDependencies,
createFilterGenerator,
withDependencies,
} from '@revenge-mod/modules/finders/filters'
import {
ReactJSXRuntimeModuleId,
ReactModuleId,
ReactNativeModuleId,
} from '@revenge-mod/react'
import { FilterFlag } from '#modules/src/finders/filters/constants'
import type {
Filter,
FilterGenerator,
@ -38,7 +39,7 @@ const MultiIconComponentFilterBase = [
null,
]
export type ByGeneratedIconComponent = FilterGenerator<
export type WithGeneratedIconComponent = FilterGenerator<
<N extends string>(
name: N,
...assets: string[]
@ -54,7 +55,7 @@ export type ByGeneratedIconComponent = FilterGenerator<
* @example
* ```ts
* const [CopyIconModule] = lookupModule(
* byGeneratedIconComponent('CopyIcon'),
* withGeneratedIconComponent('CopyIcon'),
* {
* uninitialized: true,
* }
@ -67,7 +68,7 @@ export type ByGeneratedIconComponent = FilterGenerator<
* @example
* ```ts
* const [CircleXIconModule] = lookupModule(
* byGeneratedIconComponent(
* withGeneratedIconComponent(
* 'CircleXIcon',
* 'CircleXIcon-secondary',
* 'CircleXIcon-primary',
@ -78,8 +79,8 @@ export type ByGeneratedIconComponent = FilterGenerator<
* )
* ```
*/
export const byGeneratedIconComponent = createFilterGenerator<
Parameters<ByGeneratedIconComponent>
export const withGeneratedIconComponent = createFilterGenerator<
Parameters<WithGeneratedIconComponent>
>(
(names, id, exports) => {
if (typeof exports === 'object') {
@ -111,13 +112,14 @@ export const byGeneratedIconComponent = createFilterGenerator<
IconComponentFilter[4] = mid
}
return byDependencies(filter)(id, exports)
return withDependencies(filter)(id, exports)
}
return false
},
names => `revenge.byGeneratedIconComponent(${names.join(',')})`,
) as ByGeneratedIconComponent
names => `revenge.utils.discord.generatedIconComponent(${names.join(',')})`,
FilterFlag.Dynamic,
) as WithGeneratedIconComponent
/**
* Looks up a generated icon component by its name and asset names.
@ -139,7 +141,7 @@ export function lookupGeneratedIconComponent<N extends string>(
if (__DEV__ && badFind) return
}
const [module] = lookupModule(byGeneratedIconComponent(...names), {
const [module] = lookupModule(withGeneratedIconComponent(...names), {
uninitialized: true,
})

View file

@ -6,6 +6,11 @@ export type LogicalOr<T1, T2> = T1 extends true
: T2 extends true
? true
: false
export type LogicalAnd<T1, T2> = T1 extends true
? T2 extends true
? true
: false
: false
export type DeepPartial<T> = {
[K in keyof T]?: T[K] extends AnyObject ? DeepPartial<T[K]> : T[K]
}

View file

@ -1,5 +1,5 @@
import { waitForModules } from '@revenge-mod/modules/finders'
import { byProps } from '@revenge-mod/modules/finders/filters'
import { withProps } from '@revenge-mod/modules/finders/filters'
import { getModuleDependencies } from '@revenge-mod/modules/metro/utils'
import { instead } from '@revenge-mod/patcher'
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
@ -27,7 +27,7 @@ registerPlugin(
// utils/SentryUtils.native.tsx
const unsubSU = waitForModules(
byProps<{
withProps<{
profiledRootComponent<T>(x: T): T
addBreadcrumb(): void
setTags(): void
@ -51,7 +51,7 @@ registerPlugin(
// modules/errors/native/SentryInitUtils.tsx
const unsubSIU = waitForModules(
byProps('initSentry'),
withProps('initSentry'),
SentryInitUtils => {
unsubSIU()
@ -62,9 +62,9 @@ registerPlugin(
// Discord uses ReactNavigationInstrumentation to track navigation
// Discord also uses Profiler to track performance, but we blocked that by patching profiledRootComponent
// Note that to actually find Sentry, we just need to add 'init' to byProps
// Note that to actually find Sentry, we just need to add 'init' to withProps
const unsubSentryInst = waitForModules(
byProps('ReactNavigationInstrumentation'),
withProps('ReactNavigationInstrumentation'),
exports => {
unsubSentryInst()
@ -96,7 +96,7 @@ registerPlugin(
init({ cleanup }) {
// utils/AnalyticsUtils.tsx
const unsubAU = waitForModules(
byProps<{
withProps<{
trackNetworkAction: () => void
default: {
track: () => void
@ -133,7 +133,7 @@ registerPlugin(
}) {
// modules/app_analytics/useTrackImpression.tsx
const unsubTI = waitForModules(
byProps<{
withProps<{
default: () => void
trackImpression: () => void
}>('trackImpression'),
@ -148,7 +148,7 @@ registerPlugin(
// actions/AnalyticsTrackActionCreators.tsx
const unsubATAC = waitForModules(
byProps('track'),
withProps('track'),
(AnalyticsTrackActionCreators, id) => {
if (getModuleDependencies(id)![0] === DispatcherModuleId) {
unsubATAC()

View file

@ -1,5 +1,5 @@
import { waitForModules } from '@revenge-mod/modules/finders'
import { byName } from '@revenge-mod/modules/finders/filters'
import { withName } from '@revenge-mod/modules/finders/filters'
import { instead } from '@revenge-mod/patcher'
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
@ -17,7 +17,7 @@ registerPlugin(
{
start({ cleanup }) {
const unsubEB = waitForModules(
byName<typeof DiscordErrorBoundary>('ErrorBoundary'),
withName<typeof DiscordErrorBoundary>('ErrorBoundary'),
exports => {
unsubEB()

View file

@ -1,10 +1,10 @@
import { sRefresher, sSections } from '@revenge-mod/discord/_/modules/settings'
import { onSettingsModulesLoaded } from '@revenge-mod/discord/modules/settings'
import {
waitForModuleByImportedPath,
waitForModules,
waitForModuleWithImportedPath,
} from '@revenge-mod/modules/finders'
import { byName } from '@revenge-mod/modules/finders/filters'
import { withName } from '@revenge-mod/modules/finders/filters'
import { instead } from '@revenge-mod/patcher'
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
@ -30,7 +30,7 @@ const pluginSettings = registerPlugin(
require('./register')
})
waitForModuleByImportedPath(
waitForModuleWithImportedPath(
'modules/main_tabs_v2/native/settings/SettingsNavigator.tsx',
exports => {
patchSettingsNavigator(exports)
@ -38,7 +38,7 @@ const pluginSettings = registerPlugin(
)
const unsubSOS = waitForModules(
byName('SettingsOverviewScreen'),
withName('SettingsOverviewScreen'),
exports => {
unsubSOS()
patchSettingsOverviewScreen(exports)

View file

@ -1,7 +1,7 @@
import { ToastActionCreators } from '@revenge-mod/discord/actions'
import { getStore, Stores } from '@revenge-mod/discord/flux'
import { getModule } from '@revenge-mod/modules/finders'
import { byProps } from '@revenge-mod/modules/finders/filters'
import { getModules } from '@revenge-mod/modules/finders'
import { withProps } from '@revenge-mod/modules/finders/filters'
import { instead } from '@revenge-mod/patcher'
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
@ -50,7 +50,7 @@ registerPlugin(
}
cleanup(
getModule(byProps('isStaffEnv'), UserStoreUtils => {
getModules(withProps('isStaffEnv'), UserStoreUtils => {
logger.log('Patching UserStoreUtils...')
cleanup(

View file

@ -1,7 +1,7 @@
import { AlertActionCreators } from '@revenge-mod/discord/actions'
import { Design } from '@revenge-mod/discord/design'
import { getModule } from '@revenge-mod/modules/finders'
import { byProps } from '@revenge-mod/modules/finders/filters'
import { getModules } from '@revenge-mod/modules/finders'
import { withProps } from '@revenge-mod/modules/finders/filters'
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
import { afterJSX, beforeJSX } from '@revenge-mod/react/jsx-runtime'
@ -57,13 +57,11 @@ registerPlugin(
},
{
start({ cleanup }) {
const unsub = getModule(
byProps<{ ProfileBadgeRows: FC<ProfileBadgeRowsProps> }>(
const unsub = getModules(
withProps<{ ProfileBadgeRows: FC<ProfileBadgeRowsProps> }>(
'ProfileBadgeRows',
),
({ ProfileBadgeRows }) => {
unsub()
cleanup(
beforeJSX(ProfileBadgeRows, args => {
const [, props] = args

1
types/globals.d.ts vendored
View file

@ -110,6 +110,7 @@ declare global {
var __turboModuleProxy: ((name: string) => unknown) | undefined
function nativeLoggingHook(str: string, level: number): void
function alert(message: unknown): void
function queueMicrotask(cb: () => void): void
var nativePerformanceNow: typeof performance.now
var performance: {