feat(lib/modules)!: filter scopes + fixes and improvements

# Features

- Filters now control what types of modules they see.
- Filters can be reinstantiated via `.new()`
- Added the `Not` type in @revenge-mod/utils/types

# Fixes

- Pitfall with `lookupModules` and `withDependencies` without `uninitialize: true` fixed.
- Filter type-metadata is now stored in a single object instead of multiple parameters.
- Incorrect caching behavior for `lookupModules` where cache always reports not found if you finish the iterator.
- Also attempt to report not found when using `lookupModule`.
This commit is contained in:
PalmDevs 2025-10-28 01:01:17 +07:00
parent dde81af9f6
commit b6d1cff9f5
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
24 changed files with 287 additions and 298 deletions

View file

@ -102,7 +102,6 @@ export let AssetsRegistryModuleId: Metro.ModuleID | undefined
export let AssetsRegistry: ReactNative.AssetsRegistry = proxify(() => {
for (const [, id] of lookupModules(withDependencies([[]]), {
initialize: false,
uninitialized: true,
})) {
const deps = getModuleDependencies(id)!
if (deps.length !== 1) continue

View file

@ -34,9 +34,6 @@ export let ActionSheetActionCreators: DiscordModules.Actions.ActionSheetActionCr
2,
]),
),
{
uninitialized: true,
},
)
if (module) return (ActionSheetActionCreators = module)
@ -67,9 +64,6 @@ export let AlertActionCreators: DiscordModules.Actions.AlertActionCreators =
]),
),
),
{
uninitialized: true,
},
)
if (module) return (AlertActionCreators = module)
@ -89,9 +83,6 @@ export let ToastActionCreators: DiscordModules.Actions.ToastActionCreators =
withProps<DiscordModules.Actions.ToastActionCreators>('open')
.and(withoutProps('init'))
.and(withDependencies([DispatcherModuleId, 2])),
{
uninitialized: true,
},
)
for (const [module] of generator)

View file

@ -14,7 +14,4 @@ export const [Dispatcher, DispatcherModuleId] = lookupModule(
withProps<DiscordModules.Flux.Dispatcher>('_interceptors').and(
withDependencies([relative(1), null, null, null, null, 2]),
),
{
uninitialized: true,
},
) as [DiscordModules.Flux.Dispatcher, Metro.ModuleID]

View file

@ -26,9 +26,6 @@ export let TypedEventEmitter: typeof DiscordModules.Utils.TypedEventEmitter =
2,
]),
),
{
uninitialized: true,
},
)
if (module) return (TypedEventEmitter = module)

View file

@ -52,10 +52,6 @@ export let Design: Design = proxify(
),
)
.keyAs('revenge.discord.design.Design'),
{
uninitialized: true,
},
)
if (module) return (Design = module)
@ -93,9 +89,6 @@ export let FormSwitch: DiscordModules.Components.FormSwitch = proxify(() => {
2,
]),
),
{
uninitialized: true,
},
)
if (module) return (FormSwitch = module.FormSwitch)

View file

@ -15,7 +15,10 @@ import {
Uncached,
} from '../../../modules/src/caches'
import { FilterResultFlags } from '../../../modules/src/finders/_internal'
import { FilterFlag } from '../../../modules/src/finders/filters/constants'
import {
FilterFlag,
FilterScopes,
} from '../../../modules/src/finders/filters/constants'
import type {
Filter,
FilterGenerator,
@ -36,9 +39,7 @@ const _stores: Record<string, DiscordModules.Flux.Store> = {}
export const Stores = new Proxy(_stores, {
ownKeys: target => Reflect.ownKeys(target),
get: (target, prop: string) =>
target[prop] ??
// @ts-expect-error: This always uses cache
lookupModule(withStoreName(prop), { uninitialized: true })[0],
target[prop] ?? lookupModule(withStoreName(prop))[0],
})
/**
@ -94,7 +95,14 @@ const FluxStoreLeadingDeps = [
]
export type WithStore = FilterGenerator<
<T>() => Filter<DiscordModules.Flux.Store<T>, boolean>
<T>() => Filter<{
Result: DiscordModules.Flux.Store<T>
RequiresExports: boolean
Scopes: [
typeof FilterScopes.Uninitialized,
typeof FilterScopes.Initialized,
]
}>
>
/**
@ -115,10 +123,18 @@ export const withStore = createFilterGenerator(
},
() => 'revenge.discord.store',
FilterFlag.Dynamic,
FilterScopes.Uninitialized | FilterScopes.Initialized,
) as WithStore
export type WithStoreName = FilterGenerator<
<T>(name: string) => Filter<DiscordModules.Flux.Store<T>, true>
<T>(name: string) => Filter<{
Result: DiscordModules.Flux.Store<T>
RequiresExports: true
Scopes: [
typeof FilterScopes.Uninitialized,
typeof FilterScopes.Initialized,
]
}>
>
/**
@ -129,6 +145,7 @@ export const withStoreName = createFilterGenerator(
exports.getName?.length === 0 && exports.getName() === name,
([name]) => `revenge.discord.storeName(${name})`,
FilterFlag.RequiresExports,
FilterScopes.Uninitialized | FilterScopes.Initialized,
) as WithStoreName
/// STORE CACHING
@ -146,9 +163,7 @@ waitForModules(withStore(), (store, id) => {
if (cache === Uncached)
asap(() => {
const lookup = lookupModules(withStore(), {
uninitialized: true,
})
const lookup = lookupModules(withStore())
while (lookup.next().done);
})

View file

@ -32,9 +32,6 @@ export let RootNavigationRef: RootNavigationRef = proxify(
]),
)
.keyAs('revenge.discord.modules.mainTabsV2.RootNavigationRef'),
{
uninitialized: true,
},
)
if (module) return (RootNavigationRef = module)

View file

@ -32,9 +32,6 @@ export let SettingListRenderer: SettingListRenderer = proxify(
]),
),
),
{
uninitialized: true,
},
)
if (module) return (SettingListRenderer = module)

View file

@ -13,9 +13,6 @@ export const [AppStartPerformance] = lookupModule(
withProps<DiscordModules.AppStartPerformance>('markAndLog').and(
withDependencies([relative(-1), null, null, 2]),
),
{
uninitialized: true,
},
) as [DiscordModules.AppStartPerformance, Metro.ModuleID]
AppStartPerformance.mark('👊', 'Pre-init')

View file

@ -17,9 +17,6 @@ export let nodeUtil: typeof import('node:util') = proxify(
[],
]),
),
{
uninitialized: true,
},
)
if (module) return (nodeUtil = module)

View file

@ -35,9 +35,6 @@ let ClipboardModule: typeof import('@react-native-clipboard/clipboard') =
),
]),
),
{
uninitialized: true,
},
)
if (module) {

View file

@ -26,9 +26,6 @@ export let ReactNavigationNative: typeof import('@react-navigation/native') =
]),
),
),
{
uninitialized: true,
},
)
if (module) return (ReactNavigationNative = module)
@ -52,9 +49,6 @@ export let ReactNavigationStack: typeof import('@react-navigation/stack') =
.keyAs(
'revenge.externals.ReactNavigation.ReactNavigationStack',
),
{
uninitialized: true,
},
)
if (module) return (ReactNavigationStack = module)

View file

@ -27,9 +27,6 @@ export let FlashList: typeof import('@shopify/flash-list') = proxify(
null,
]),
),
{
uninitialized: true,
},
)
if (module) return (FlashList = module)

View file

@ -16,7 +16,7 @@ export interface RunFilterOptions {
*/
skipDefault?: boolean
/**
* Whether to allow initializing modules to check their exports.
* Whether to allow initializing modules to confirm their exports.
*
* @default true
*/
@ -72,7 +72,7 @@ export const FilterResultFlagToHumanReadable: Record<FilterResultFlag, string> =
// If we add more options later on, do NOT forget about
// adding them here, and passing them in the lookup* functions
export function runFilter(
filter: Filter<any, false>,
filter: Filter<{ Result: any; RequiresExports: false; Scopes: any[] }>,
id: Metro.ModuleID,
): FilterResultFlag | undefined

View file

@ -1,13 +1,13 @@
import { FilterFlag } from './constants'
import { createFilterGenerator } from './utils'
import type { Metro } from '@revenge-mod/modules/types'
import type { LogicalAnd } from '@revenge-mod/utils/types'
import type {
Filter,
FilterBase,
FilterGenerator,
FilterRequiresExports,
FilterResult,
FilterInfoOf,
MergeFilterInfo,
UnionFilterInfo,
} from '.'
const compositeHandler = <G extends (a: FilterBase, b: FilterBase) => any>(
@ -38,19 +38,19 @@ export type And = FilterGenerator<
<F1 extends FilterBase, F2 extends FilterBase>(
f1: F1,
f2: F2,
) => Filter<
FilterResult<F1> & FilterResult<F2>,
LogicalAnd<FilterRequiresExports<F1>, FilterRequiresExports<F2>>
>
) => Filter<MergeFilterInfo<FilterInfoOf<F1>, FilterInfoOf<F2>>>
>
const andKeyGenerator = ([a, b]: Parameters<And>) =>
`revenge.and(${a.key},${b.key})`
const andScopesGenerator = ([a, b]: Parameters<And>) => a.scopes | b.scopes
const sameFlagsAnd = createFilterGenerator(
([a, b], id, exports) => a(id, exports) && b(id, exports),
andKeyGenerator,
([a]) => a.flags,
andScopesGenerator,
) as And
const andFallbackCache = new WeakMap<FilterBase, Set<Metro.ModuleID>>()
@ -82,6 +82,7 @@ const dynamicFlagsAnd = createFilterGenerator(
},
andKeyGenerator,
FilterFlag.Dynamic,
andScopesGenerator,
) as And
/**
@ -121,6 +122,10 @@ export const and = Object.assign(
sameFlagsAnd.flagsFor,
dynamicFlagsAnd.flagsFor,
),
defaultScopesFor: compositeArrayHandler(
sameFlagsAnd.defaultScopesFor,
dynamicFlagsAnd.defaultScopesFor,
),
},
) satisfies And
@ -128,19 +133,19 @@ export type Or = FilterGenerator<
<F1 extends FilterBase, F2 extends FilterBase>(
f1: F1,
f2: F2,
) => Filter<
FilterResult<F1> | FilterResult<F2>,
LogicalAnd<FilterRequiresExports<F1>, FilterRequiresExports<F2>>
>
) => Filter<UnionFilterInfo<FilterInfoOf<F1>, FilterInfoOf<F2>>>
>
const orKeyGenerator = ([a, b]: Parameters<Or>) =>
`revenge.or(${a.key},${b.key})`
const orScopesGenerator = ([a, b]: Parameters<Or>) => a.scopes | b.scopes
const sameFlagOr = createFilterGenerator(
([a, b], id, exports) => a(id, exports) || b(id, exports),
orKeyGenerator,
([a]) => a.flags,
orScopesGenerator,
) as Or
const dynamicFlagOr = createFilterGenerator(
@ -151,6 +156,7 @@ const dynamicFlagOr = createFilterGenerator(
},
orKeyGenerator,
FilterFlag.Dynamic,
orScopesGenerator,
) as Or
/**
@ -185,5 +191,9 @@ export const or = Object.assign(
sameFlagOr.flagsFor,
dynamicFlagOr.flagsFor,
),
defaultScopesFor: compositeArrayHandler(
sameFlagOr.defaultScopesFor,
dynamicFlagOr.defaultScopesFor,
),
},
) satisfies Or

View file

@ -15,3 +15,50 @@ export const FilterFlag = {
* @see {@link FilterFlag}
*/
export type FilterFlag = number
/**
* Scopes to limit filters to certain module states.
*/
export const FilterScopes = {
/**
* Include all modules (both initialized and uninitialized, including blacklisted).
* This overrides {@link FilterScopes.Uninitialized} and {@link FilterScopes.Initialized}.
*/
All: 1,
/**
* Include uninitialized modules in the search.
*/
Uninitialized: 2,
/**
* Include initialized modules from the search.
*/
Initialized: 4,
} as const
export type FilterScope = (typeof FilterScopes)[keyof typeof FilterScopes]
/**
* @see {@link FilterScopes}
*/
export type FilterScopeValue = number
export interface FilterInfo {
/**
* The result type of the filter.
*/
Result: any
/**
* Whether the filter requires exports to work.
*/
RequiresExports: boolean
/**
* Scopes the filter matches modules in.
*/
Scopes: FilterScope[]
}
export interface DefaultFilterInfo extends FilterInfo {
Result: any
RequiresExports: boolean
Scopes: FilterScope[]
}

View file

@ -1,6 +1,6 @@
import { getCurrentStack } from '@revenge-mod/utils/error'
import { getModuleDependencies } from '../../metro/utils'
import { FilterFlag } from './constants'
import { FilterFlag, FilterScopes } from './constants'
import { createFilterGenerator } from './utils'
import type { Metro } from '../../types'
import type { Filter, FilterGenerator } from './utils'
@ -62,13 +62,21 @@ export const withDependencies = createFilterGenerator<
([deps], id) => depCompare(getModuleDependencies(id)!, deps, id, id),
deps => `revenge.deps(${depGenFilterKey(deps)})`,
FilterFlag.Dynamic,
FilterScopes.Uninitialized | FilterScopes.Initialized,
) as WithDependencies
withDependencies.loose = loose
withDependencies.relative = relative
type WithDependencies = FilterGenerator<
<T>(deps: ComparableDependencyMap) => Filter<T, false>
<T>(deps: ComparableDependencyMap) => Filter<{
Result: T
RequiresExports: false
Scopes: [
typeof FilterScopes.Uninitialized,
typeof FilterScopes.Initialized,
]
}>
> & {
loose: typeof loose
relative: typeof relative

View file

@ -1,10 +1,16 @@
import { FilterFlag } from './constants'
import { FilterFlag, FilterScopes } from './constants'
import { createFilterGenerator } from './utils'
import type { Filter, FilterGenerator } from './utils'
export * from './constants'
export * from './utils'
type FilterRequiringExports<T> = Filter<{
Result: T
RequiresExports: true
Scopes: [typeof FilterScopes.Initialized]
}>
/**
* Filter modules by their exports having all of the specified properties.
*
@ -33,13 +39,14 @@ export const withProps = createFilterGenerator<Parameters<WithProps>>(
},
props => `revenge.props(${props.join(',')})`,
FilterFlag.RequiresExports,
FilterScopes.Initialized,
) as WithProps
export type WithProps = FilterGenerator<
<T extends Record<string, any> = Record<string, any>>(
prop: keyof T,
...props: Array<keyof T>
) => Filter<T, true>
) => FilterRequiringExports<T>
>
/**
@ -58,13 +65,14 @@ export const withoutProps = createFilterGenerator<Parameters<WithoutProps>>(
},
props => `revenge.withoutProps(${props.join(',')})`,
FilterFlag.RequiresExports,
FilterScopes.Initialized,
) as WithoutProps
export type WithoutProps = FilterGenerator<
<T extends Record<string, any>>(
prop: string,
...props: string[]
) => Filter<T, true>
) => FilterRequiringExports<T>
>
/**
@ -87,10 +95,11 @@ export const withSingleProp = createFilterGenerator<Parameters<WithSingleProp>>(
},
([prop]) => `revenge.singleProp(${prop})`,
FilterFlag.RequiresExports,
FilterScopes.Initialized,
) as WithSingleProp
export type WithSingleProp = FilterGenerator<
<T extends Record<string, any>>(prop: keyof T) => Filter<T, true>
<T extends Record<string, any>>(prop: keyof T) => FilterRequiringExports<T>
>
/**
@ -127,10 +136,11 @@ export const withName = createFilterGenerator<Parameters<WithName>>(
([name], _, exports) => exports.name === name,
([name]) => `revenge.name(${name})`,
FilterFlag.RequiresExports,
FilterScopes.Initialized,
) as WithName
export type WithName = FilterGenerator<
<T extends object = object>(name: string) => Filter<T, true>
<T extends object = object>(name: string) => FilterRequiringExports<T>
>
export * from './composite'

View file

@ -1,84 +1,116 @@
import { withProps } from '.'
import { and, or } from './composite'
import { FilterScopes } from './constants'
import type { Metro } from '@revenge-mod/modules/types'
import type { If, LogicalAnd } from '@revenge-mod/utils/types'
import type { FilterFlag } from './constants'
import type {
DefaultFilterInfo,
FilterFlag,
FilterInfo,
FilterScope,
FilterScopes as FilterScopesObject,
FilterScopeValue,
} from './constants'
export type FilterResult<F> = F extends Filter<infer R, boolean>
? R
: F extends FilterBase<infer R>
? R
: never
export type FilterResult<F> = F extends Filter<infer I> ? I['Result'] : never
export type FilterRequiresExports<F> = F extends Filter<any, infer RE>
? RE
: F extends FilterBase<any, infer RE>
? RE
: never
export type FilterRequiresExports<F> = F extends Filter<infer I>
? I['RequiresExports']
: never
export interface FilterBase<
_Result = any,
RequiresExports extends boolean = boolean,
> {
export type FilterInfoOf<F> = F extends Filter<infer I> ? I : FilterInfo
export interface FilterBase<Info extends FilterInfo = DefaultFilterInfo> {
(
...args: If<
RequiresExports,
Info['RequiresExports'],
[id: Metro.ModuleID, exports: Metro.ModuleExports],
[id: Metro.ModuleID, exports?: never]
>
): boolean
key: string
flags: If<
RequiresExports,
Info['RequiresExports'],
(typeof FilterFlag)['RequiresExports'],
FilterFlag
>
scopes: FilterScopeValue
}
export type Filter<
Result = any,
RequiresExports extends boolean = boolean,
> = FilterHelpers & FilterBase<Result, RequiresExports>
export type Filter<Info extends FilterInfo = DefaultFilterInfo> =
FilterHelpers<Info> & FilterBase<Info>
export interface FilterHelpers {
export type MergeFilterInfo<I1 extends FilterInfo, I2 extends FilterInfo> = {
Result: I1['Result'] & I2['Result']
RequiresExports: LogicalAnd<I1['RequiresExports'], I2['RequiresExports']>
Scopes: [...I1['Scopes'], ...I2['Scopes']]
}
export type UnionFilterInfo<I1 extends FilterInfo, I2 extends FilterInfo> = {
Result: I1['Result'] | I2['Result']
RequiresExports: LogicalAnd<I1['RequiresExports'], I2['RequiresExports']>
Scopes: [...I1['Scopes'], ...I2['Scopes']]
}
export interface FilterHelpers<Info extends FilterInfo = DefaultFilterInfo> {
/**
* Manually the key for this filter.
*
* **Don't use this unless you know what you're doing.** Only API exports should be using
* **Don't use this unless you know what you're doing.** Only API exports should be using this.
*
* @param key The key to set for this filter.
*/
keyAs<T extends FilterBase>(this: T, key: string): T
keyAs<T extends FilterBase<any>>(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>(
and<T extends FilterBase<any>, F extends FilterBase<any>>(
this: T,
filter: F,
): Filter<
FilterResult<T> & FilterResult<F>,
LogicalAnd<FilterRequiresExports<T>, FilterRequiresExports<F>>
>
): Filter<MergeFilterInfo<Info, FilterInfoOf<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>(
or<T extends Filter<Info>, F extends FilterBase<any>>(
this: T,
filter: F,
): Filter<UnionFilterInfo<Info, FilterInfoOf<F>>>
/**
* Creates a new instance of this filter.
*/
// biome-ignore format: Preserve special property name syntax for 'new' keyword
'new'(this: Filter<Info>): Filter<Info>
/**
* Scopes this filter to match specific modules.
*
* @param scopes The scopes of modules to match.
*/
scope<T extends Filter<Info>, const S extends FilterScope[]>(
this: T,
...scopes: If<
Info['RequiresExports'],
[typeof FilterScopesObject.Initialized],
S
>
): Filter<
FilterResult<T> | FilterResult<F>,
LogicalAnd<FilterRequiresExports<T>, FilterRequiresExports<F>>
Info & {
Scopes: If<
Info['RequiresExports'],
[typeof FilterScopesObject.Initialized],
S
>
}
>
}
export type FilterGenerator<G extends (...args: any[]) => Filter> = G & {
keyFor(args: Parameters<G>): string
flagsFor(args: Parameters<G>): FilterFlag
defaultScopesFor(args: Parameters<G>): FilterScopeValue
}
const Helpers: FilterHelpers = Object.setPrototypeOf(
@ -93,6 +125,22 @@ const Helpers: FilterHelpers = Object.setPrototypeOf(
or(filter) {
return or(this, filter)
},
new() {
const newFilter = ((
id: Metro.ModuleID,
exports?: Metro.ModuleExports,
) => this(id, exports)) as typeof this
Object.assign(newFilter, this)
return Object.setPrototypeOf(newFilter, Helpers)
},
scope(...scopes) {
const filter = this.new()
filter.scopes = 0
for (const scope of scopes) filter.scopes |= scope
return filter
},
} satisfies FilterHelpers,
Function.prototype,
)
@ -103,6 +151,7 @@ const Helpers: FilterHelpers = Object.setPrototypeOf(
* @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.
* @param defaultScopesFor The function that generates the default scopes for the filter, or static scopes. Defaults to {@link FilterScopes.Initialized}.
* @returns A function that generates a filter with the specified arguments.
*
* @example
@ -126,13 +175,15 @@ export function createFilterGenerator<A extends any[]>(
) => boolean,
keyFor: (args: A) => string,
flagFor: ((args: A) => FilterFlag) | FilterFlag,
): FilterGenerator<(...args: A) => Filter<any, true>>
defaultScopesFor?: ((args: A) => FilterScopeValue) | FilterScopeValue,
): FilterGenerator<(...args: A) => Filter>
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>>
defaultScopesFor?: ((args: A) => FilterScopeValue) | FilterScopeValue,
): FilterGenerator<(...args: A) => Filter>
export function createFilterGenerator<A extends any[]>(
filter: (
@ -142,17 +193,24 @@ export function createFilterGenerator<A extends any[]>(
) => boolean,
keyFor: (args: A) => string,
flagFor: ((args: A) => FilterFlag) | FilterFlag,
defaultScopesFor?: ((args: A) => FilterScopeValue) | FilterScopeValue,
): FilterGenerator<(...args: A) => Filter> {
type GeneratorType = ReturnType<typeof createFilterGenerator<A>>
const isFlagsStatic = typeof flagFor === 'number'
const defaultScopes = defaultScopesFor ?? FilterScopes.Initialized
const isDefaultScopesStatic = typeof defaultScopes === '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)
filter_.scopes = isDefaultScopesStatic
? defaultScopes
: defaultScopes(args)
return Object.setPrototypeOf(filter_, Helpers)
}
@ -160,6 +218,10 @@ export function createFilterGenerator<A extends any[]>(
? () => flagFor
: (args: A) => flagFor(args)
generator.defaultScopesFor = isDefaultScopesStatic
? () => defaultScopes
: defaultScopes
generator.keyFor = keyFor
return generator

View file

@ -5,25 +5,21 @@ import {
lookupModuleWithImportedPath,
} from './lookup'
import { waitForModules, waitForModuleWithImportedPath } from './wait'
import type { If } from '@revenge-mod/utils/types'
import type { Metro } from '../types'
import type { Filter, FilterResult } from './filters'
import type { LookupModulesOptions } from './lookup'
import type { WaitForModulesOptions, WaitForModulesResult } from './wait'
export type GetModulesOptions<
ReturnNamespace extends boolean = boolean,
Uninitialized extends boolean = boolean,
All extends boolean = boolean,
> = WaitForModulesOptions<ReturnNamespace> &
LookupModulesOptions<ReturnNamespace, Uninitialized, All, true> & {
/**
* The maximum number of modules to get.
*
* @default 1
*/
max?: number
}
export type GetModulesOptions<ReturnNamespace extends boolean = boolean> =
WaitForModulesOptions<ReturnNamespace> &
LookupModulesOptions<ReturnNamespace, true> & {
/**
* The maximum number of modules to get.
*
* @default 1
*/
max?: number
}
export type GetModulesResult<
F extends Filter,
@ -64,12 +60,7 @@ export function getModules<F extends Filter>(
callback: GetModulesCallback<FilterResult<F>>,
): GetModulesUnsubscribeFunction
export function getModules<
F extends Filter,
const O extends F extends Filter<any, infer RE>
? If<RE, GetModulesOptions<boolean, boolean, false>, GetModulesOptions>
: never,
>(
export function getModules<F extends Filter, const O extends GetModulesOptions>(
filter: F,
callback: GetModulesCallback<GetModulesResult<F, O>>,
options: O,

View file

@ -17,7 +17,8 @@ import {
FilterResultFlagToHumanReadable,
runFilter,
} from './_internal'
import type { If, Nullish } from '@revenge-mod/utils/types'
import { FilterScopes } from './filters'
import type { If, Not } from '@revenge-mod/utils/types'
import type { MaybeDefaultExportMatched, Metro } from '../types'
import type {
FilterResultFlag,
@ -25,55 +26,8 @@ import type {
} from './_internal'
import type { Filter, FilterResult } from './filters'
type LookupModulesOptionsWithAll<A extends boolean> = If<
A,
{
/**
* Whether to include all modules in the lookup, including blacklisted ones.
*
* **This overrides {@link BaseLookupModulesOptions.initialized} and {@link BaseLookupModulesOptions.uninitialized}.**
*/
all: A
},
{
/**
* You can only use `all` with exportsless filters!
*/
all?: false
}
>
type LookupModulesOptionsWithInitializedUninitialized<U extends boolean> = {
/**
* Whether to include initialized modules in the lookup.
*
* @default true
*/
initialized?: boolean
} & If<
U,
{
/**
* Whether to include uninitialized modules in the lookup.
*
* Set {@link BaseLookupModulesOptions.initialize} `true` to initialize uninitialized modules.
*
* @default false
*/
uninitialized: U
},
{
/**
* You can only use `uninitialized` with exportsless filters!
*/
uninitialized?: false
}
>
export type LookupModulesOptions<
ReturnNamespace extends boolean = boolean,
Uninitialized extends boolean = boolean,
All extends boolean = boolean,
Initialize extends boolean = boolean,
> = RunFilterReturnExportsOptions<ReturnNamespace> & {
/**
@ -81,42 +35,33 @@ export type LookupModulesOptions<
*/
cached?: boolean
} & If<
Initialize,
Not<Initialize>,
{
/**
* Whether to initialize matching uninitialized modules.
*
* **This will initialize any modules that match the exportsless filter and may cause unintended side effects.**
*/
initialize?: Initialize
},
{
initialize: false
}
> &
If<
All,
LookupModulesOptionsWithAll<All> & {
[K in keyof LookupModulesOptionsWithInitializedUninitialized<Uninitialized>]?: never
},
LookupModulesOptionsWithInitializedUninitialized<Uninitialized> & {
[K in keyof LookupModulesOptionsWithAll<All>]?: never
}
{ initialize?: true }
>
export type LookupModulesResult<
F extends Filter,
O extends LookupModulesOptions,
> = [
exports: O extends LookupModulesOptions<boolean, boolean, boolean, false>
? LookupFilterResult<F, O> | Nullish
: LookupFilterResult<F, O>,
id: Metro.ModuleID,
]
> = [exports: LookupFilterResult<F, O>, id: Metro.ModuleID]
type LookupFilterResult<
F extends Filter,
O extends LookupModulesOptions,
> = O extends LookupModulesOptions<any, false>
? InitializedLookupFilterResult<F, O> | undefined
: InitializedLookupFilterResult<F, O>
type InitializedLookupFilterResult<
F extends Filter,
O extends LookupModulesOptions,
> = O extends RunFilterReturnExportsOptions<true>
? MaybeDefaultExportMatched<FilterResult<F>>
: FilterResult<F>
@ -128,8 +73,6 @@ type LookupNotFoundResult = typeof NotFoundResult
/**
* Lookup modules.
*
* 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.
* @returns A generator that yields the module exports that match the filter.
@ -147,23 +90,10 @@ export function lookupModules<F extends Filter>(
export function lookupModules<
F extends Filter,
const O extends F extends Filter<any, infer RE>
? If<
RE,
LookupModulesOptions<boolean, false, false>,
LookupModulesOptions
>
: never,
const O extends LookupModulesOptions,
>(filter: F, options: O): Generator<LookupModulesResult<F, O>, undefined>
export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
if (
__DEV__ &&
filter.key.includes('revenge.deps') &&
!(options?.uninitialized ?? false)
)
DEBUG_warnFilterWithoutUninitialized(filter.key)
let notFound = true
let cached: Set<Metro.ModuleID> | undefined
@ -199,8 +129,10 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
}
}
// Full lookup
if (options?.all) {
const scopes = filter.scopes
const includeAll = scopes & FilterScopes.All
if (includeAll) {
for (const id of mList.keys()) {
// biome-ignore lint/complexity/useOptionalChain: Hot path should be optimized
if (cached && cached.has(id)) continue
@ -228,12 +160,11 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
]
}
}
} else {
const includeInit = scopes & FilterScopes.Initialized
const includeUninit = scopes & FilterScopes.Uninitialized
if (notFound) cacheFilterNotFound(filter.key) // Full lookup, and still not found!
}
// Partial lookup
else {
if (options?.initialized ?? true)
if (includeInit)
for (const id of mInitialized) {
// biome-ignore lint/complexity/useOptionalChain: Hot path should be optimized
if (cached && cached.has(id)) continue
@ -253,7 +184,7 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
}
}
if (options?.uninitialized)
if (includeUninit)
for (const id of mUninitialized) {
// biome-ignore lint/complexity/useOptionalChain: Hot path should be optimized
if (cached && cached.has(id)) continue
@ -277,8 +208,10 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
}
}
if (__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__)
if (notFound) DEBUG_warnLookupNoMatch(filter.key)
// Cache if nothing was found in a full lookup
if (notFound && includeAll) cacheFilterNotFound(filter.key)
if (__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__ && notFound)
DEBUG_warnLookupNoMatch(filter.key)
}
/**
@ -301,23 +234,10 @@ export function lookupModule<F extends Filter>(
export function lookupModule<
F extends Filter,
const O extends F extends Filter<any, infer RE>
? If<
RE,
LookupModulesOptions<boolean, false, false>,
LookupModulesOptions
>
: never,
const O extends LookupModulesOptions,
>(filter: F, options: O): LookupModulesResult<F, O> | LookupNotFoundResult
export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
if (
__DEV__ &&
filter.key.includes('revenge.deps') &&
!(options?.uninitialized ?? false)
)
DEBUG_warnFilterWithoutUninitialized(filter.key)
if (options?.cached ?? true) {
const notInit = !(options?.initialize ?? true)
@ -345,8 +265,10 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
}
}
// Full lookup
if (options?.all) {
const scopes = filter.scopes
const includeAll = scopes & FilterScopes.All
if (includeAll) {
for (const id of mList.keys()) {
const flag = runFilter(
filter,
@ -369,17 +291,11 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
]
}
}
} else {
const includeInit = scopes & FilterScopes.Initialized
const includeUninit = scopes & FilterScopes.Uninitialized
if (__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__)
DEBUG_warnLookupNoMatch(filter.key)
cacheFilterNotFound(filter.key) // Full lookup, and still not found!
return NotFoundResult
}
// Partial lookup
else {
if (options?.initialized ?? true)
if (includeInit)
for (const id of mInitialized) {
const exports = getInitializedModuleExports(id)
const flag = runFilter(filter, id, exports, options)
@ -394,7 +310,7 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
}
}
if (options?.uninitialized)
if (includeUninit)
for (const id of mUninitialized) {
const flag = runFilter(filter, id, undefined, options)
if (flag) {
@ -413,6 +329,8 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
}
}
// Cache if nothing was found in a full lookup
if (includeAll) cacheFilterNotFound(filter.key)
if (__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__) DEBUG_warnLookupNoMatch(filter.key)
return NotFoundResult
@ -478,15 +396,3 @@ function DEBUG_warnLookupNoMatch(key: string) {
nativeLoggingHook(`\u001b[31mFailed lookup: ${key}\n${stack}\u001b[0m`, 2)
}
/**
* Warns the developer about a potentially bad filter for lookups that do not have uninitialized modules.
*/
function DEBUG_warnFilterWithoutUninitialized(key: string) {
const stack = getCurrentStack()
nativeLoggingHook(
`\u001b[33mPotentially bad filter without uninitialized modules lookup: ${key}\n${stack}\u001b[0m`,
2,
)
}

View file

@ -11,6 +11,7 @@ import {
FilterResultFlagToHumanReadable,
runFilter,
} from './_internal'
import { FilterScopes } from './filters'
import type { MaybeDefaultExportMatched, Metro } from '../types'
import type {
FilterResultFlag,
@ -18,24 +19,12 @@ import type {
} from './_internal'
import type { Filter, FilterResult } from './filters'
export interface BaseWaitForModulesOptions<All extends boolean = boolean> {
/**
* Whether to include all modules, including blacklisted ones.
*
* @default false
*/
all?: All
}
export type WaitForModulesUnsubscribeFunction = () => void
export type WaitForModulesCallback<T> = (exports: T, id: Metro.ModuleID) => any
export type WaitForModulesOptions<
ReturnNamespace extends boolean = boolean,
All extends boolean = boolean,
> = RunFilterReturnExportsOptions<ReturnNamespace> &
BaseWaitForModulesOptions<All> & {
export type WaitForModulesOptions<ReturnNamespace extends boolean = boolean> =
RunFilterReturnExportsOptions<ReturnNamespace> & {
/**
* Use cached results **only** (if possible).
* If there is no cache result, this works as if you did not pass this option at all.
@ -81,9 +70,7 @@ export function waitForModules<F extends Filter>(
): WaitForModulesUnsubscribeFunction
export function waitForModules<
F extends O extends WaitForModulesOptions<boolean, true>
? Filter<any, false>
: Filter,
F extends Filter,
O extends WaitForModulesOptions,
>(
filter: F,
@ -144,7 +131,7 @@ export function waitForModules(
)
return onAnyModuleInitialized(
options?.all
filter.scopes & FilterScopes.All
? (id, exports) => {
const flag = runFilter(filter, id, exports, options)
if (flag) {
@ -187,7 +174,6 @@ export function waitForModules(
*
* @param path The path to wait for.
* @param callback The callback to call once the module is initialized.
* @param options The options to use for the wait.
* @returns A function to unsubscribe.
*
* @example
@ -203,14 +189,13 @@ export function waitForModules(
export function waitForModuleWithImportedPath<T = any>(
path: string,
callback: WaitForModulesCallback<T>,
options?: BaseWaitForModulesOptions,
): WaitForModulesUnsubscribeFunction {
const unsub = onModuleFinishedImporting((id, cmpPath) => {
if (path === cmpPath) {
unsub()
// Module is not fully initialized yet, so we need to wait for it
onModuleInitialized(id, (id, exports) => {
if (!options?.all || mInitialized.has(id)) callback(exports, id)
callback(exports, id)
})
}
})

View file

@ -10,7 +10,10 @@ import {
ReactModuleId,
ReactNativeModuleId,
} from '@revenge-mod/react'
import { FilterFlag } from '../../modules/src/finders/filters/constants'
import {
FilterFlag,
FilterScopes,
} from '../../modules/src/finders/filters/constants'
import type {
Filter,
FilterGenerator,
@ -43,22 +46,24 @@ export type WithGeneratedIconComponent = FilterGenerator<
<N extends string>(
name: N,
...assets: string[]
) => Filter<{ [K in N]: FC<any> }>
) => Filter<{
Result: { [K in N]: FC<any> }
RequiresExports: boolean
Scopes: [
typeof FilterScopes.Uninitialized,
typeof FilterScopes.Initialized,
]
}>
>
/**
* Filter by icon component name and asset names.
*
* **Make sure to set `uninitialized: true` when using this filter in `lookupModule`!**
*
* @param names The component name, then the asset names if the component has multiple assets. *
* @example
* ```ts
* const [CopyIconModule] = lookupModule(
* withGeneratedIconComponent('CopyIcon'),
* {
* uninitialized: true,
* }
* )
* if (CopyIconModule) {
* const { CopyIcon } = CopyIconModule
@ -73,9 +78,6 @@ export type WithGeneratedIconComponent = FilterGenerator<
* 'CircleXIcon-secondary',
* 'CircleXIcon-primary',
* ),
* {
* uninitialized: true,
* }
* )
* ```
*/
@ -119,6 +121,7 @@ export const withGeneratedIconComponent = createFilterGenerator<
},
names => `revenge.utils.discord.generatedIconComponent(${names.join(',')})`,
FilterFlag.Dynamic,
FilterScopes.Uninitialized | FilterScopes.Initialized,
) as WithGeneratedIconComponent
/**
@ -141,9 +144,7 @@ export function lookupGeneratedIconComponent<N extends string>(
if (__DEV__ && badFind) return
}
const [module] = lookupModule(withGeneratedIconComponent(...names), {
uninitialized: true,
})
const [module] = lookupModule(withGeneratedIconComponent(...names))
return module?.[names[0]] as FC<any> | undefined
}

View file

@ -1,5 +1,6 @@
export type Nullish = null | undefined
export type If<T, Then, Else> = T extends true ? Then : Else
export type Not<T extends boolean> = T extends true ? false : true
export type AnyObject = Record<any, any>
export type LogicalOr<T1, T2> = T1 extends true
? true