From 5f5fc6f2b26c6a12f1e1508f77afa33bb814845a Mon Sep 17 00:00:00 2001 From: Palm Date: Sat, 30 Aug 2025 09:19:57 +0700 Subject: [PATCH] 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(...)) ``` --- bun.lock | 6 +- lib/assets/src/preinit.ts | 22 +- lib/discord/src/actions.ts | 38 +- lib/discord/src/common/flux.ts | 12 +- lib/discord/src/common/index.ts | 19 +- lib/discord/src/common/utils.ts | 18 +- lib/discord/src/design.ts | 53 +- lib/discord/src/flux/stores.ts | 41 +- lib/discord/src/modules/main_tabs_v2.ts | 12 +- lib/discord/src/modules/settings/renderer.ts | 12 +- lib/discord/src/preinit.ts | 12 +- lib/discord/src/start.ts | 4 +- lib/externals/src/browserify.ts | 12 +- lib/externals/src/react-native-clipboard.ts | 12 +- lib/externals/src/react-navigation.ts | 23 +- lib/externals/src/shopify.ts | 14 +- lib/modules/package.json | 4 +- lib/modules/src/finders/_internal.ts | 2 +- lib/modules/src/finders/filters.ts | 739 ------------------- lib/modules/src/finders/filters/composite.ts | 189 +++++ lib/modules/src/finders/filters/constants.ts | 17 + lib/modules/src/finders/filters/dynamic.ts | 247 +++++++ lib/modules/src/finders/filters/index.ts | 137 ++++ lib/modules/src/finders/filters/utils.ts | 166 +++++ lib/modules/src/finders/get.ts | 54 +- lib/modules/src/finders/lookup.ts | 21 +- lib/modules/src/finders/wait.ts | 6 +- lib/react/src/index.ts | 8 +- lib/utils/src/callback.ts | 5 +- lib/utils/src/discord.ts | 22 +- lib/utils/src/types.ts | 5 + src/plugins/preinit/no-track/index.ts | 16 +- src/plugins/start/error-boundary/index.tsx | 4 +- src/plugins/start/settings/index.ts | 8 +- src/plugins/start/staff-settings/index.ts | 6 +- src/plugins/start/user-badges/index.tsx | 10 +- types/globals.d.ts | 1 + 37 files changed, 985 insertions(+), 992 deletions(-) delete mode 100644 lib/modules/src/finders/filters.ts create mode 100644 lib/modules/src/finders/filters/composite.ts create mode 100644 lib/modules/src/finders/filters/constants.ts create mode 100644 lib/modules/src/finders/filters/dynamic.ts create mode 100644 lib/modules/src/finders/filters/index.ts create mode 100644 lib/modules/src/finders/filters/utils.ts diff --git a/bun.lock b/bun.lock index 63dcc0b..1f3eed3 100644 --- a/bun.lock +++ b/bun.lock @@ -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", diff --git a/lib/assets/src/preinit.ts b/lib/assets/src/preinit.ts index 08bc64d..21b4ec0 100644 --- a/lib/assets/src/preinit.ts +++ b/lib/assets/src/preinit.ts @@ -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('registerAsset'), + withProps('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 diff --git a/lib/discord/src/actions.ts b/lib/discord/src/actions.ts index 1951bea..fa3ad5b 100644 --- a/lib/discord/src/actions.ts +++ b/lib/discord/src/actions.ts @@ -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( - 'hideActionSheet', - 'openLazy', - ), - byDependencies([ + withProps( + 'hideActionSheet', + 'openLazy', + ).and( + withDependencies([ null, ReactModuleId, ReactJSXRuntimeModuleId, @@ -54,11 +51,10 @@ export let AlertActionCreators: DiscordModules.Actions.AlertActionCreators = proxify( () => { const [module] = lookupModule( - preferExports( - byProps( - 'openAlert', - ), - byDependencies([ + withProps( + '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('open'), - withoutProps('init'), - ), - byDependencies([DispatcherModuleId, 2]), - ), + withProps('open') + .and(withoutProps('init')) + .and(withDependencies([DispatcherModuleId, 2])), { uninitialized: true, }, diff --git a/lib/discord/src/common/flux.ts b/lib/discord/src/common/flux.ts index 7810555..75876c6 100644 --- a/lib/discord/src/common/flux.ts +++ b/lib/discord/src/common/flux.ts @@ -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('_interceptors'), - byDependencies([relative(1), null, null, null, null, 2]), + withProps('_interceptors').and( + withDependencies([relative(1), null, null, null, null, 2]), ), { uninitialized: true, diff --git a/lib/discord/src/common/index.ts b/lib/discord/src/common/index.ts index 73c5ab5..af78183 100644 --- a/lib/discord/src/common/index.ts +++ b/lib/discord/src/common/index.ts @@ -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('Logger'), + withName('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('ME'), - + withProps('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), diff --git a/lib/discord/src/common/utils.ts b/lib/discord/src/common/utils.ts index 9f68a36..bdc389f 100644 --- a/lib/discord/src/common/utils.ts +++ b/lib/discord/src/common/utils.ts @@ -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( - 'TypedEventEmitter', - ), - byDependencies([ + withName( + 'TypedEventEmitter', + ).and( + withDependencies([ _classCallCheckModuleId, _createClassModuleId, [], diff --git a/lib/discord/src/design.ts b/lib/discord/src/design.ts index 01758ea..06b818d 100644 --- a/lib/discord/src/design.ts +++ b/lib/discord/src/design.ts @@ -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('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('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, diff --git a/lib/discord/src/flux/stores.ts b/lib/discord/src/flux/stores.ts index 3d0c817..9b89d07 100644 --- a/lib/discord/src/flux/stores.ts +++ b/lib/discord/src/flux/stores.ts @@ -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( return noop } - return waitForModules(byStoreName(name), callback, { cached: true }) + return waitForModules(withStoreName(name), callback, { cached: true }) } /// STORE FILTERING @@ -70,15 +71,15 @@ export function getStore( ] */ -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< () => Filter, 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< (name: string) => Filter, 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, }) diff --git a/lib/discord/src/modules/main_tabs_v2.ts b/lib/discord/src/modules/main_tabs_v2.ts index 2d8d940..3b23e13 100644 --- a/lib/discord/src/modules/main_tabs_v2.ts +++ b/lib/discord/src/modules/main_tabs_v2.ts @@ -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('getRootNavigationRef'), - byDependencies([ + withProps('getRootNavigationRef').and( + withDependencies([ loose([[], loose([relative(1), relative(2)])]), 2, ]), diff --git a/lib/discord/src/modules/settings/renderer.ts b/lib/discord/src/modules/settings/renderer.ts index a4cc20e..b1bf0da 100644 --- a/lib/discord/src/modules/settings/renderer.ts +++ b/lib/discord/src/modules/settings/renderer.ts @@ -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('SettingsList'), - byDependencies( + withProps('SettingsList').and( + withDependencies( loose([ ReactModuleId, ReactNativeModuleId, diff --git a/lib/discord/src/preinit.ts b/lib/discord/src/preinit.ts index 8f8209c..9b68e71 100644 --- a/lib/discord/src/preinit.ts +++ b/lib/discord/src/preinit.ts @@ -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('markAndLog'), - byDependencies([relative(-1), null, null, 2]), + withProps('markAndLog').and( + withDependencies([relative(-1), null, null, 2]), ), { uninitialized: true, diff --git a/lib/discord/src/start.ts b/lib/discord/src/start.ts index c7673d1..e4ae334 100644 --- a/lib/discord/src/start.ts +++ b/lib/discord/src/start.ts @@ -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 }>( + withProps<{ SETTING_RENDERER_CONFIG: Record }>( 'SETTING_RENDERER_CONFIG', ), SettingRendererConfig => { diff --git a/lib/externals/src/browserify.ts b/lib/externals/src/browserify.ts index 48178f8..d02a823 100644 --- a/lib/externals/src/browserify.ts +++ b/lib/externals/src/browserify.ts @@ -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('inspect'), - byDependencies([ + withProps('inspect').and( + withDependencies([ loose([relative(2, true), relative(4, true)]), [], [], diff --git a/lib/externals/src/react-native-clipboard.ts b/lib/externals/src/react-native-clipboard.ts index 607d7c1..4bb0bb4 100644 --- a/lib/externals/src/react-native-clipboard.ts +++ b/lib/externals/src/react-native-clipboard.ts @@ -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('useClipboard'), - byDependencies([ + withProps('useClipboard').and( + withDependencies([ relative.withDependencies( [ReactModuleId, relative(2, true)], 1, diff --git a/lib/externals/src/react-navigation.ts b/lib/externals/src/react-navigation.ts index 4bf21b2..6e17cab 100644 --- a/lib/externals/src/react-navigation.ts +++ b/lib/externals/src/react-navigation.ts @@ -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('useLinkTo'), - byDependencies( + withProps('useLinkTo').and( + withDependencies( loose([ [], loose([ @@ -44,9 +42,8 @@ export let ReactNavigationStack: typeof import('@react-navigation/stack') = proxify( () => { const [module] = lookupModule( - preferExports( - byProps('StackView'), - byDependencies( + withProps('StackView').and( + withDependencies( loose([ relative.withDependencies( loose([ diff --git a/lib/externals/src/shopify.ts b/lib/externals/src/shopify.ts index 6c4630b..3246054 100644 --- a/lib/externals/src/shopify.ts +++ b/lib/externals/src/shopify.ts @@ -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('FlashList'), - byDependencies([ + withProps('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, diff --git a/lib/modules/package.json b/lib/modules/package.json index ada707b..e3e143f 100644 --- a/lib/modules/package.json +++ b/lib/modules/package.json @@ -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" } } -} +} \ No newline at end of file diff --git a/lib/modules/src/finders/_internal.ts b/lib/modules/src/finders/_internal.ts index 46963d4..21a0d47 100644 --- a/lib/modules/src/finders/_internal.ts +++ b/lib/modules/src/finders/_internal.ts @@ -65,7 +65,7 @@ export const FilterResultFlagToHumanReadable: Record = [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 diff --git a/lib/modules/src/finders/filters.ts b/lib/modules/src/finders/filters.ts deleted file mode 100644 index c1dc0b4..0000000 --- a/lib/modules/src/finders/filters.ts +++ /dev/null @@ -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 extends Filter - ? R - : F extends FilterBase - ? R - : never - -export type IsFilterWithExports = F extends Filter - ? WE - : F extends FilterBase - ? 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 - -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(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( - this: T, - filter: F, - ): Filter< - FilterResult & FilterResult, - LogicalOr, IsFilterWithExports> - > - /** - * Combines this filter with another filter, returning a new filter that matches if **either** filter matches. - * - * @param filter - */ - or( - this: T, - filter: F, - ): Filter< - FilterResult | FilterResult, - IsFilterWithExports | IsFilterWithExports - > - /** - * 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>( - this: T, - withExportsFilter: FilterBase, - strict?: boolean, - ): Filter< - FilterResult, - IsFilterWithExports | IsFilterWithExports - > - /** - * 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>( - this: T, - deps: ComparableDependencyMap, - strict?: boolean, - ): Filter, false> -} - -export type FilterGenerator Filter> = G & { - keyFor(args: Parameters): 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( - filter: ( - args: A, - id: Metro.ModuleID, - exports: Metro.ModuleExports, - ) => boolean, - keyFor: (args: A) => string, -): FilterGenerator<(...args: A) => Filter> - -export function createFilterGenerator( - filter: (args: A, id: Metro.ModuleID) => boolean, - keyFor: (args: A) => string, -): FilterGenerator<(...args: A) => Filter> - -export function createFilterGenerator( - filter: ( - args: A, - id: Metro.ModuleID, - exports?: Metro.ModuleExports, - ) => boolean, - keyFor: (args: A) => string, -): FilterGenerator<(...args: A) => Filter> { - type GeneratorType = ReturnType> - - const generator: GeneratorType = (...args: A) => { - const filter_ = ((id: Metro.ModuleID, exports?: Metro.ModuleExports) => - filter(args, id, exports)) as ReturnType - - filter_.key = keyFor(args) - return Object.setPrototypeOf(filter_, Helpers) - } - - generator.keyFor = keyFor - return generator -} - -export type ByProps = FilterGenerator< - = Record>( - prop: keyof T, - ...props: Array - ) => Filter -> - -/** - * 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('createElement')) - * // const React: typeof import('react') - * ``` - */ -export const byProps = createFilterGenerator>( - (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< - >( - prop: string, - ...props: string[] - ) => Filter -> - -/** - * 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>( - (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< - >(prop: keyof T) => Filter -> - -/** - * 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>( - ([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< - (name: string) => Filter -> - -/** - * 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')) - * // 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>( - ([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< - (deps: ComparableDependencyMap) => Filter -> & { - loose: typeof loose - relative: typeof relative -} - -const __DEBUG_WARNED_BAD_BY_DEPENDENCIES_FILTERS__ = - new Set() - -/** - * 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>( - ([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: F1, - f2: F2, - ): Filter< - FilterResult & FilterResult, - LogicalOr, IsFilterWithExports> - > - ( - f1: F1, - f2: F2, - f3: F3, - ): Filter< - FilterResult & FilterResult & FilterResult, - LogicalOr< - LogicalOr, IsFilterWithExports>, - IsFilterWithExports - > - > - (...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: F1, - f2: F2, - ): Filter< - FilterResult | FilterResult, - IsFilterWithExports | IsFilterWithExports - > - ( - f1: F1, - f2: F2, - f3: F3, - ): Filter< - FilterResult | FilterResult | FilterResult, - | IsFilterWithExports - | IsFilterWithExports - | IsFilterWithExports - > - (...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< - ( - initializedFilter: IF, - uninitializedFilter: FilterBase, - strict?: boolean, - ) => Filter, 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 ->( - ([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< - ( - withExportsFilter: WEF, - exportslessFilter: FilterBase, - strict?: boolean, - ) => Filter, 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>( - ([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 diff --git a/lib/modules/src/finders/filters/composite.ts b/lib/modules/src/finders/filters/composite.ts new file mode 100644 index 0000000..e403054 --- /dev/null +++ b/lib/modules/src/finders/filters/composite.ts @@ -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 = 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: F1, + f2: F2, + ) => Filter< + FilterResult & FilterResult, + LogicalAnd, FilterRequiresExports> + > +> + +const andKeyGenerator = ([a, b]: Parameters) => + `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>() + +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: F1, + f2: F2, + ) => Filter< + FilterResult | FilterResult, + LogicalAnd, FilterRequiresExports> + > +> + +const orKeyGenerator = ([a, b]: Parameters) => + `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(sameFlagOr, dynamicFlagOr), + { + keyFor: compositeArrayHandler(sameFlagOr.keyFor, dynamicFlagOr.keyFor), + flagsFor: compositeArrayHandler( + sameFlagOr.flagsFor, + dynamicFlagOr.flagsFor, + ), + }, +) satisfies Or diff --git a/lib/modules/src/finders/filters/constants.ts b/lib/modules/src/finders/filters/constants.ts new file mode 100644 index 0000000..e1ad7fd --- /dev/null +++ b/lib/modules/src/finders/filters/constants.ts @@ -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 diff --git a/lib/modules/src/finders/filters/dynamic.ts b/lib/modules/src/finders/filters/dynamic.ts new file mode 100644 index 0000000..faac17a --- /dev/null +++ b/lib/modules/src/finders/filters/dynamic.ts @@ -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() + +/** + * 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 +>( + ([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< + (deps: ComparableDependencyMap) => Filter +> & { + 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},` +} diff --git a/lib/modules/src/finders/filters/index.ts b/lib/modules/src/finders/filters/index.ts new file mode 100644 index 0000000..bf1a542 --- /dev/null +++ b/lib/modules/src/finders/filters/index.ts @@ -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('createElement')) + * // const React: typeof import('react') + * ``` + */ +export const withProps = createFilterGenerator>( + (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< + = Record>( + prop: keyof T, + ...props: Array + ) => Filter +> + +/** + * 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>( + (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< + >( + prop: string, + ...props: string[] + ) => Filter +> + +/** + * 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>( + ([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< + >(prop: keyof T) => Filter +> + +/** + * 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')) + * // 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>( + ([name], _, exports) => exports.name === name, + ([name]) => `revenge.name(${name})`, + FilterFlag.RequiresExports, +) as WithName + +export type WithName = FilterGenerator< + (name: string) => Filter +> + +export * from './composite' +export * from './dynamic' diff --git a/lib/modules/src/finders/filters/utils.ts b/lib/modules/src/finders/filters/utils.ts new file mode 100644 index 0000000..7e72b53 --- /dev/null +++ b/lib/modules/src/finders/filters/utils.ts @@ -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 extends Filter + ? R + : F extends FilterBase + ? R + : never + +export type FilterRequiresExports = F extends Filter + ? RE + : F extends FilterBase + ? 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 + +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(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( + this: T, + filter: F, + ): Filter< + FilterResult & FilterResult, + LogicalAnd, FilterRequiresExports> + > + /** + * 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( + this: T, + filter: F, + ): Filter< + FilterResult | FilterResult, + LogicalAnd, FilterRequiresExports> + > +} + +export type FilterGenerator Filter> = G & { + keyFor(args: Parameters): string + flagsFor(args: Parameters): 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( + filter: ( + args: A, + id: Metro.ModuleID, + exports: Metro.ModuleExports, + ) => boolean, + keyFor: (args: A) => string, + flagFor: ((args: A) => FilterFlag) | FilterFlag, +): FilterGenerator<(...args: A) => Filter> + +export function createFilterGenerator( + filter: (args: A, id: Metro.ModuleID) => boolean, + keyFor: (args: A) => string, + flagFor: ((args: A) => FilterFlag) | FilterFlag, +): FilterGenerator<(...args: A) => Filter> + +export function createFilterGenerator( + 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> + + const isFlagsStatic = typeof flagFor === 'number' + + const generator: GeneratorType = (...args: A) => { + const filter_ = ((id: Metro.ModuleID, exports?: Metro.ModuleExports) => + filter(args, id, exports)) as ReturnType + + 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 +} diff --git a/lib/modules/src/finders/get.ts b/lib/modules/src/finders/get.ts index 45fade7..e620479 100644 --- a/lib/modules/src/finders/get.ts +++ b/lib/modules/src/finders/get.ts @@ -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 ? MaybeDefaultExportMatched> : FilterResult -export type GetModuleCallback = (exports: T, id: Metro.ModuleID) => any +export type GetModulesCallback = (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('createElement'), React => { + * getModules(withProps('createElement'), React => { * // Immediately called because React is always initialized when plugins are loaded * }) * - * getModule(byProps('FlashList'), FlashList => { + * getModules(withProps('FlashList'), FlashList => { * // Called when the module is initialized * }) * * // Get multiple modules matching the filter - * getModule(byProps('registerAsset'), AssetsRegistry => { + * getModules(withProps('registerAsset'), AssetsRegistry => { * // Called 2 times, once for each module that matches the filter * }, { max: 2 }) * ``` */ -export function getModule( +export function getModules( filter: F, - callback: GetModuleCallback>, -): GetModuleUnsubscribeFunction + callback: GetModulesCallback>, +): GetModulesUnsubscribeFunction -export function getModule< +export function getModules< F extends Filter, - const O extends F extends Filter - ? If, GetModuleOptions> + const O extends F extends Filter + ? If, GetModulesOptions> : never, >( filter: F, - callback: GetModuleCallback>, + callback: GetModulesCallback>, options: O, -): GetModuleUnsubscribeFunction +): GetModulesUnsubscribeFunction -export function getModule( +export function getModules( filter: Filter, - callback: GetModuleCallback, - options?: GetModuleOptions, + callback: GetModulesCallback, + 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( +export function getModuleWithImportedPath( path: string, - callback: GetModuleCallback, -): GetModuleUnsubscribeFunction { - const [exports, id] = lookupModuleByImportedPath(path) + callback: GetModulesCallback, +): 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) }) diff --git a/lib/modules/src/finders/lookup.ts b/lib/modules/src/finders/lookup.ts index 2d32626..a82e27a 100644 --- a/lib/modules/src/finders/lookup.ts +++ b/lib/modules/src/finders/lookup.ts @@ -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( @@ -148,9 +147,9 @@ export function lookupModules( export function lookupModules< F extends Filter, - const O extends F extends Filter + const O extends F extends Filter ? If< - WE, + RE, LookupModulesOptions, LookupModulesOptions > @@ -286,7 +285,7 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) { * * @example * ```ts - * const React = lookupModule(byProps('createElement')) + * const [React, ReactModuleId] = lookupModule(withProps('createElement')) * ``` */ export function lookupModule( @@ -295,9 +294,9 @@ export function lookupModule( export function lookupModule< F extends Filter, - const O extends F extends Filter + const O extends F extends Filter ? If< - WE, + RE, LookupModulesOptions, 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( +export function lookupModuleWithImportedPath( path: string, ): [exports: T, id: Metro.ModuleID] | LookupNotFoundResult { const id = mImportedPaths.get(path) diff --git a/lib/modules/src/finders/wait.ts b/lib/modules/src/finders/wait.ts index ed97cff..ca072a1 100644 --- a/lib/modules/src/finders/wait.ts +++ b/lib/modules/src/finders/wait.ts @@ -66,7 +66,7 @@ export type WaitForModulesResult< * @example * ```ts * const unsub = waitForModules( - * byName('FlashList'), + * withName('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( +export function waitForModuleWithImportedPath( path: string, callback: WaitForModulesCallback, options?: BaseWaitForModulesOptions, diff --git a/lib/react/src/index.ts b/lib/react/src/index.ts index 8ab438a..57b4849 100644 --- a/lib/react/src/index.ts +++ b/lib/react/src/index.ts @@ -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('useState'), + withProps('useState'), (exports, id) => { if (RtCount++ === 2) return unsubRt() @@ -23,7 +23,7 @@ const unsubRt = waitForModules( ) const unsubRN = waitForModules( - byProps('AppRegistry'), + withProps('AppRegistry'), (exports, id) => { unsubRN() @@ -40,7 +40,7 @@ const unsubRN = waitForModules( let RJsxRCount = 0 const unsubRJSXR = waitForModules( - byProps('jsxs'), + withProps('jsxs'), (exports, id) => { if (RJsxRCount++ === 2) return unsubRJSXR() diff --git a/lib/utils/src/callback.ts b/lib/utils/src/callback.ts index e553dd4..cf50fe7 100644 --- a/lib/utils/src/callback.ts +++ b/lib/utils/src/callback.ts @@ -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 = () => {} diff --git a/lib/utils/src/discord.ts b/lib/utils/src/discord.ts index 7a79ded..0e4a3d0 100644 --- a/lib/utils/src/discord.ts +++ b/lib/utils/src/discord.ts @@ -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< ( 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 +export const withGeneratedIconComponent = createFilterGenerator< + Parameters >( (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( if (__DEV__ && badFind) return } - const [module] = lookupModule(byGeneratedIconComponent(...names), { + const [module] = lookupModule(withGeneratedIconComponent(...names), { uninitialized: true, }) diff --git a/lib/utils/src/types.ts b/lib/utils/src/types.ts index d550a55..6f1cc71 100644 --- a/lib/utils/src/types.ts +++ b/lib/utils/src/types.ts @@ -6,6 +6,11 @@ export type LogicalOr = T1 extends true : T2 extends true ? true : false +export type LogicalAnd = T1 extends true + ? T2 extends true + ? true + : false + : false export type DeepPartial = { [K in keyof T]?: T[K] extends AnyObject ? DeepPartial : T[K] } diff --git a/src/plugins/preinit/no-track/index.ts b/src/plugins/preinit/no-track/index.ts index 82a8508..2bbf0bb 100644 --- a/src/plugins/preinit/no-track/index.ts +++ b/src/plugins/preinit/no-track/index.ts @@ -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(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() diff --git a/src/plugins/start/error-boundary/index.tsx b/src/plugins/start/error-boundary/index.tsx index 0faec21..053e214 100644 --- a/src/plugins/start/error-boundary/index.tsx +++ b/src/plugins/start/error-boundary/index.tsx @@ -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('ErrorBoundary'), + withName('ErrorBoundary'), exports => { unsubEB() diff --git a/src/plugins/start/settings/index.ts b/src/plugins/start/settings/index.ts index ae09e2b..3c8219d 100644 --- a/src/plugins/start/settings/index.ts +++ b/src/plugins/start/settings/index.ts @@ -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) diff --git a/src/plugins/start/staff-settings/index.ts b/src/plugins/start/staff-settings/index.ts index 45ede53..02398fc 100644 --- a/src/plugins/start/staff-settings/index.ts +++ b/src/plugins/start/staff-settings/index.ts @@ -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( diff --git a/src/plugins/start/user-badges/index.tsx b/src/plugins/start/user-badges/index.tsx index 3c3553d..b7122a9 100644 --- a/src/plugins/start/user-badges/index.tsx +++ b/src/plugins/start/user-badges/index.tsx @@ -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 }>( + const unsub = getModules( + withProps<{ ProfileBadgeRows: FC }>( 'ProfileBadgeRows', ), ({ ProfileBadgeRows }) => { - unsub() - cleanup( beforeJSX(ProfileBadgeRows, args => { const [, props] = args diff --git a/types/globals.d.ts b/types/globals.d.ts index ce6360d..9aac29c 100644 --- a/types/globals.d.ts +++ b/types/globals.d.ts @@ -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: {