From 9258695ec735d271e8da29d582662b13ab5cc733 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Wed, 12 Aug 2026 01:12:14 +0700 Subject: [PATCH 01/11] feat(lib/modules/finders)!: initialized boolean for filter predicates + remove `FilterFlag` and weird assumptions + fix partial matches causing modules skip in lookup --- lib/discord/src/flux/stores.ts | 34 ++--- lib/modules/src/finders/_internal.ts | 40 +---- lib/modules/src/finders/filters/composite.ts | 148 +++++++++---------- lib/modules/src/finders/filters/constants.ts | 31 +--- lib/modules/src/finders/filters/dynamic.ts | 4 +- lib/modules/src/finders/filters/index.ts | 7 +- lib/modules/src/finders/filters/utils.ts | 72 +++------ lib/modules/src/finders/lookup.ts | 23 ++- lib/utils/src/discord.ts | 7 +- 9 files changed, 145 insertions(+), 221 deletions(-) diff --git a/lib/discord/src/flux/stores.ts b/lib/discord/src/flux/stores.ts index 00eb471..3150fbe 100644 --- a/lib/discord/src/flux/stores.ts +++ b/lib/discord/src/flux/stores.ts @@ -17,10 +17,7 @@ import { Uncached, } from '../../../modules/src/caches' import { FilterResultFlags } from '../../../modules/src/finders/_internal' -import { - FilterFlag, - FilterScopes, -} from '../../../modules/src/finders/filters/constants' +import { FilterScopes } from '../../../modules/src/finders/filters/constants' import type { Filter, FilterGenerator, @@ -91,7 +88,6 @@ const withLeadingFluxStoreDeps = withDependencies( export type WithStore = FilterGenerator< () => Filter<{ Result: DiscordModules.Flux.Store - RequiresExports: boolean Scopes: [ typeof FilterScopes.Uninitialized, typeof FilterScopes.Initialized, @@ -103,27 +99,21 @@ export type WithStore = FilterGenerator< * A dynamic filter that matches all Flux stores. */ export const withStore = createFilterGenerator( - (_, id, exports) => { - if (exports) return Boolean(exports._dispatchToken) - else { - if (!withLeadingFluxStoreDeps(id)) return false - const deps = getModuleDependencies(id)! - return deps[deps.length - 1] === ImportTrackerModuleId - } + (_, id, exports, initialized) => { + if (initialized) return Boolean(exports?._dispatchToken) + + if (!withLeadingFluxStoreDeps(id)) return false + const deps = getModuleDependencies(id)! + return deps[deps.length - 1] === ImportTrackerModuleId }, () => 'revenge.discord.store', - FilterFlag.Dynamic, FilterScopes.Uninitialized | FilterScopes.Initialized, ) as WithStore export type WithStoreName = FilterGenerator< (name: string) => Filter<{ Result: DiscordModules.Flux.Store - RequiresExports: true - Scopes: [ - typeof FilterScopes.Uninitialized, - typeof FilterScopes.Initialized, - ] + Scopes: [typeof FilterScopes.Initialized] }> > @@ -132,10 +122,12 @@ export type WithStoreName = FilterGenerator< */ export const withStoreName = createFilterGenerator( ([name], _, exports) => - exports?.getName?.length === 0 && exports.getName() === name, + typeof exports?.getName === 'function' && + exports.getName.length === 0 && + exports.getName() === name, ([name]) => `revenge.discord.storeName(${name})`, - FilterFlag.RequiresExports, - FilterScopes.Uninitialized | FilterScopes.Initialized, + // Uninitialized to make sure cached results resolve immediately + FilterScopes.Initialized | FilterScopes.Uninitialized, ) as WithStoreName /// STORE CACHING diff --git a/lib/modules/src/finders/_internal.ts b/lib/modules/src/finders/_internal.ts index 6266d69..88187d4 100644 --- a/lib/modules/src/finders/_internal.ts +++ b/lib/modules/src/finders/_internal.ts @@ -2,7 +2,7 @@ import { getCurrentStack } from '@revenge-mod/utils/error' import { cacheFilterResultForId } from '../caches' import { mInitialized } from '../metro/patches' import { metroRequire } from '../metro/runtime' -import { FilterFlag } from './filters' +import { isModuleInitialized } from '../metro/utils' import type { If } from '@revenge-mod/utils/types' import type { Metro } from '../types' import type { Filter } from './filters' @@ -72,34 +72,14 @@ const noDefaultExportsCache = new Set() // Currently, we only have options that are relevant for checking exports // If we add more options later on, do NOT forget about // adding them here, and passing them in the lookup* functions -export function runFilter( - filter: Filter<{ Result: any; RequiresExports: false; Scopes: any[] }>, - id: Metro.ModuleID, -): FilterResultFlag | undefined - -export function runFilter( - filter: Filter, - id: Metro.ModuleID, - exports: Metro.ModuleExports, - options?: RunFilterOptions, -): FilterResultFlag | undefined - export function runFilter( filter: Filter, id: Metro.ModuleID, exports?: Metro.ModuleExports, options?: RunFilterOptions, ): FilterResultFlag | undefined { - if (exports === undefined) { - if (filter.flags & FilterFlag.RequiresExports) return - - const filter_ = filter as Filter<{ - Result: any - RequiresExports: false - Scopes: any[] - }> - - if (filter_(id)) { + if (!isModuleInitialized(id)) { + if (filter(id, undefined, false)) { if (options?.initialize ?? true) { const module = metroRequire(id) // Check if the required module is not blacklisted @@ -118,13 +98,7 @@ export function runFilter( return } - const filter_ = filter as Filter<{ - Result: any - RequiresExports: true - Scopes: any[] - }> - - if (filter_(id, exports)) + if (filter(id, exports, true)) return cacheFilterResultForId( filter.key, id, @@ -133,13 +107,15 @@ export function runFilter( if (options?.skipDefault) return if (noDefaultExportsCache.has(id)) return - if (!('default' in exports)) { + + const type = typeof exports + if (type !== 'object' || exports === null || !('default' in exports)) { noDefaultExportsCache.add(id) return } const { default: defaultExport } = exports - if (filter_(id, defaultExport)) + if (filter(id, defaultExport, true)) return cacheFilterResultForId(filter.key, id, FilterResultFlags.Default) } diff --git a/lib/modules/src/finders/filters/composite.ts b/lib/modules/src/finders/filters/composite.ts index 6bfe13c..571da96 100644 --- a/lib/modules/src/finders/filters/composite.ts +++ b/lib/modules/src/finders/filters/composite.ts @@ -1,4 +1,4 @@ -import { FilterFlag } from './constants' +import { FilterScopes } from '.' import { createFilterGenerator } from './utils' import type { Metro } from '@revenge-mod/modules/types' import type { @@ -6,33 +6,48 @@ import type { FilterBase, FilterGenerator, FilterInfoOf, + FilterScopeValue, MergeFilterInfo, UnionFilterInfo, } from '.' +/** {@link FilterScopes.All} sets no predicate assumptions. Only {@link FilterScopes.Uninitialized} implies exportless. */ +const canFilterUninitialized = (scopes: FilterScopeValue) => + Boolean(scopes & FilterScopes.Uninitialized) + +/** + * If both or neither can filter uninitialized, positional order is preserved. + * + * If only one can, the one that can will be used as a prefilter for uninitialized modules, + * and the other will only run on initialized modules. + */ const compositeHandler = any>( - sameHandler: G, - dynamicHandler: G, + directHandler: G, + prefilteredHandler: G, ) => - ((a, b) => - a.flags === b.flags - ? sameHandler(a, b) - : a.flags === FilterFlag.RequiresExports - ? dynamicHandler(a, b) - : dynamicHandler(b, a)) as G + ((a, b) => { + const ea = canFilterUninitialized(a.scopes) + return ea === canFilterUninitialized(b.scopes) + ? directHandler(a, b) + : ea + ? prefilteredHandler(b, a) + : prefilteredHandler(a, b) + }) as G const compositeArrayHandler = < G extends (args: [a: FilterBase, b: FilterBase]) => any, >( - sameHandler: G, - dynamicHandler: G, + directHandler: G, + prefilteredHandler: G, ) => - (([a, b]) => - a.flags === b.flags - ? sameHandler([a, b]) - : a.flags === FilterFlag.RequiresExports - ? dynamicHandler([a, b]) - : dynamicHandler([b, a])) as G + (([a, b]) => { + const ea = canFilterUninitialized(a.scopes) + return ea === canFilterUninitialized(b.scopes) + ? directHandler([a, b]) + : ea + ? prefilteredHandler([b, a]) + : prefilteredHandler([a, b]) + }) as G export type And = FilterGenerator< ( @@ -46,49 +61,49 @@ const andKeyGenerator = ([a, b]: Parameters) => const andScopesGenerator = ([a, b]: Parameters) => a.scopes | b.scopes -const sameFlagsAnd = createFilterGenerator( - ([a, b], id, exports) => a(id, exports) && b(id, exports), +const directAnd = createFilterGenerator( + ([a, b], id, exports, initialized) => + a(id, exports, initialized) && b(id, exports, initialized), andKeyGenerator, - ([a]) => a.flags, andScopesGenerator, ) as And -const andFallbackCache = new WeakMap>() +const andPrefilterCache = 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) +const prefilteredAnd = createFilterGenerator( + ([filter, prefilter], id, exports, initialized) => { + if (initialized) { + if (filter(id, exports, true)) { + // Avoid running the prefilter again if we already know it passed + const cache = andPrefilterCache.get(prefilter) return ( // biome-ignore lint/complexity/useOptionalChain: Hot path should be optimized - (cache && cache.has(id)) || fallbackFilter(id, exports) + (cache && cache.has(id)) || prefilter(id, exports, true) ) } return false } - const result = fallbackFilter(id) + const result = prefilter(id, undefined, false) 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())) + // Cache prefilter hits to avoid calling the prefilter again + // Prefilters are usually more expensive + let set = andPrefilterCache.get(prefilter) + if (!set) andPrefilterCache.set(prefilter, (set = new Set())) set.add(id) } return result }, andKeyGenerator, - FilterFlag.Dynamic, andScopesGenerator, ) as And /** * Combines two filters into one, returning true if **every** filter matches. * - * If each filter has different flags, + * If only one of the filters can run on uninitialized modules ({@link FilterScopes.Uninitialized}), + * it is used as the prefilter for uninitialized modules, and the other only runs once a candidate is initialized. * * @param filters The filters to combine. * @@ -111,23 +126,13 @@ const dynamicFlagsAnd = createFilterGenerator( * ) * ``` */ -export const and = Object.assign( - compositeHandler(sameFlagsAnd, dynamicFlagsAnd), - { - keyFor: compositeArrayHandler( - sameFlagsAnd.keyFor, - dynamicFlagsAnd.keyFor, - ), - flagsFor: compositeArrayHandler( - sameFlagsAnd.flagsFor, - dynamicFlagsAnd.flagsFor, - ), - defaultScopesFor: compositeArrayHandler( - sameFlagsAnd.defaultScopesFor, - dynamicFlagsAnd.defaultScopesFor, - ), - }, -) satisfies And +export const and = Object.assign(compositeHandler(directAnd, prefilteredAnd), { + keyFor: compositeArrayHandler(directAnd.keyFor, prefilteredAnd.keyFor), + defaultScopesFor: compositeArrayHandler( + directAnd.defaultScopesFor, + prefilteredAnd.defaultScopesFor, + ), +}) satisfies And export type Or = FilterGenerator< ( @@ -141,21 +146,21 @@ const orKeyGenerator = ([a, b]: Parameters) => const orScopesGenerator = ([a, b]: Parameters) => a.scopes | b.scopes -const sameFlagOr = createFilterGenerator( - ([a, b], id, exports) => a(id, exports) || b(id, exports), +const directOr = createFilterGenerator( + ([a, b], id, exports, initialized) => + a(id, exports, initialized) || b(id, exports, initialized), orKeyGenerator, - ([a]) => a.flags, orScopesGenerator, ) 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) +const prefilteredOr = createFilterGenerator( + ([filter, prefilter], id, exports, initialized) => { + // TODO(PalmDevs): Potential optimization: Add prefilter cache here too? + if (initialized) + return filter(id, exports, true) || prefilter(id, exports, true) + return prefilter(id, undefined, false) }, orKeyGenerator, - FilterFlag.Dynamic, orScopesGenerator, ) as Or @@ -183,17 +188,10 @@ const dynamicFlagOr = createFilterGenerator( * ) * ``` */ -export const or = Object.assign( - compositeHandler(sameFlagOr, dynamicFlagOr), - { - keyFor: compositeArrayHandler(sameFlagOr.keyFor, dynamicFlagOr.keyFor), - flagsFor: compositeArrayHandler( - sameFlagOr.flagsFor, - dynamicFlagOr.flagsFor, - ), - defaultScopesFor: compositeArrayHandler( - sameFlagOr.defaultScopesFor, - dynamicFlagOr.defaultScopesFor, - ), - }, -) satisfies Or +export const or = Object.assign(compositeHandler(directOr, prefilteredOr), { + keyFor: compositeArrayHandler(directOr.keyFor, prefilteredOr.keyFor), + defaultScopesFor: compositeArrayHandler( + directOr.defaultScopesFor, + prefilteredOr.defaultScopesFor, + ), +}) satisfies Or diff --git a/lib/modules/src/finders/filters/constants.ts b/lib/modules/src/finders/filters/constants.ts index 5871853..571dc71 100644 --- a/lib/modules/src/finders/filters/constants.ts +++ b/lib/modules/src/finders/filters/constants.ts @@ -1,21 +1,3 @@ -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 - /** * Scopes to limit filters to certain module states. */ @@ -23,10 +5,16 @@ export const FilterScopes = { /** * Include all modules (both initialized and uninitialized, including blacklisted). * This overrides {@link FilterScopes.Uninitialized} and {@link FilterScopes.Initialized}. + * + * **Filter generators generally don't need this scope.** + * + * When combining multiple filters with composite filters, the {@link FilterScopes.All} scope doesn't set assumptions for the filter predicate. + * It only decides which modules to run the predicate against. + * **Filter generators must include {@link FilterScopes.Uninitialized} and/or {@link FilterScopes.Initialized} as well.** */ All: 1, /** - * Include uninitialized modules in the search. + * Include uninitialized modules in the search. Implies the predicate can run without exports. */ Uninitialized: 2, /** @@ -47,10 +35,6 @@ 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. */ @@ -59,6 +43,5 @@ export interface FilterInfo { export interface DefaultFilterInfo extends FilterInfo { Result: any - RequiresExports: boolean Scopes: FilterScope[] } diff --git a/lib/modules/src/finders/filters/dynamic.ts b/lib/modules/src/finders/filters/dynamic.ts index dc8de9e..f6faef8 100644 --- a/lib/modules/src/finders/filters/dynamic.ts +++ b/lib/modules/src/finders/filters/dynamic.ts @@ -4,7 +4,7 @@ import { getModuleDependencies, } from '../../metro/utils' import { runFilter } from '../_internal' -import { FilterFlag, FilterScopes } from './constants' +import { FilterScopes } from './constants' import { createFilterGenerator } from './utils' import type { Metro } from '../../types' import type { Filter, FilterGenerator } from './utils' @@ -76,7 +76,6 @@ export interface ComparableDependencyMap const withDependencies_ = createFilterGenerator>( ([deps], id) => depCompare(getModuleDependencies(id)!, deps, id, id), deps => `revenge.deps(${depGenFilterKey(deps)})`, - FilterFlag.Dynamic, FilterScopes.Uninitialized | FilterScopes.Initialized, ) as WithDependencies @@ -98,7 +97,6 @@ withDependencies.relative = relative type WithDependencies = FilterGenerator< (deps: ComparableDependencyMap) => Filter<{ Result: T - RequiresExports: false Scopes: [ typeof FilterScopes.Uninitialized, typeof FilterScopes.Initialized, diff --git a/lib/modules/src/finders/filters/index.ts b/lib/modules/src/finders/filters/index.ts index a8439eb..3db77aa 100644 --- a/lib/modules/src/finders/filters/index.ts +++ b/lib/modules/src/finders/filters/index.ts @@ -1,5 +1,5 @@ import { isModuleExportBad } from '@revenge-mod/modules/metro/utils' -import { FilterFlag, FilterScopes } from './constants' +import { FilterScopes } from './constants' import { createFilterGenerator } from './utils' import type { Filter, FilterGenerator } from './utils' @@ -8,7 +8,6 @@ export * from './utils' type FilterRequiringExports = Filter<{ Result: T - RequiresExports: true Scopes: [typeof FilterScopes.Initialized] }> @@ -41,7 +40,6 @@ export const withProps = createFilterGenerator>( return false }, props => `revenge.props(${props.join(',')})`, - FilterFlag.RequiresExports, FilterScopes.Initialized, ) as WithProps @@ -67,7 +65,6 @@ export const withoutProps = createFilterGenerator>( return true }, props => `revenge.withoutProps(${props.join(',')})`, - FilterFlag.RequiresExports, FilterScopes.Initialized, ) as WithoutProps @@ -97,7 +94,6 @@ export const withSingleProp = createFilterGenerator>( return false }, ([prop]) => `revenge.singleProp(${prop})`, - FilterFlag.RequiresExports, FilterScopes.Initialized, ) as WithSingleProp @@ -138,7 +134,6 @@ export type WithSingleProp = FilterGenerator< export const withName = createFilterGenerator>( ([name], _, exports) => exports?.name === name, ([name]) => `revenge.name(${name})`, - FilterFlag.RequiresExports, FilterScopes.Initialized, ) as WithName diff --git a/lib/modules/src/finders/filters/utils.ts b/lib/modules/src/finders/filters/utils.ts index 0f3d101..e853899 100644 --- a/lib/modules/src/finders/filters/utils.ts +++ b/lib/modules/src/finders/filters/utils.ts @@ -1,40 +1,32 @@ +import { isModuleInitialized } from '@revenge-mod/modules/metro/utils' import { getErrorStack } from '@revenge-mod/utils/error' 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 { DefaultFilterInfo, - FilterFlag, FilterInfo, FilterScope, - FilterScopes as FilterScopesObject, FilterScopeValue, } from './constants' export type FilterResult = F extends Filter ? I['Result'] : never -export type FilterRequiresExports = - F extends Filter ? I['RequiresExports'] : never - export type FilterInfoOf = F extends Filter ? I : FilterInfo +declare const FilterInfoBrand: unique symbol + export interface FilterBase { ( - ...args: If< - Info['RequiresExports'], - [id: Metro.ModuleID, exports: Metro.ModuleExports], - [id: Metro.ModuleID, exports?: never] - > + id: Metro.ModuleID, + exports?: Metro.ModuleExports, + initialized?: boolean, ): boolean key: string - flags: If< - Info['RequiresExports'], - (typeof FilterFlag)['RequiresExports'], - FilterFlag - > scopes: FilterScopeValue + /** @internal */ + readonly [FilterInfoBrand]?: Info } export type Filter = @@ -42,13 +34,11 @@ export type Filter = export type MergeFilterInfo = { Result: I1['Result'] & I2['Result'] - RequiresExports: LogicalAnd Scopes: [...I1['Scopes'], ...I2['Scopes']] } export type UnionFilterInfo = { Result: I1['Result'] | I2['Result'] - RequiresExports: LogicalAnd Scopes: [...I1['Scopes'], ...I2['Scopes']] } @@ -91,25 +81,12 @@ export interface FilterHelpers { */ scope, const S extends FilterScope[]>( this: T, - ...scopes: If< - Info['RequiresExports'], - [typeof FilterScopesObject.Initialized], - S - > - ): Filter< - Info & { - Scopes: If< - Info['RequiresExports'], - [typeof FilterScopesObject.Initialized], - S - > - } - > + ...scopes: S + ): Filter } export type FilterGenerator Filter> = G & { keyFor(args: Parameters): string - flagsFor(args: Parameters): FilterFlag defaultScopesFor(args: Parameters): FilterScopeValue } @@ -129,7 +106,8 @@ const Helpers: FilterHelpers = Object.setPrototypeOf( const newFilter = (( id: Metro.ModuleID, exports?: Metro.ModuleExports, - ) => this(id, exports)) as typeof this + initialized?: boolean, + ) => this(id, exports, initialized)) as typeof this Object.assign(newFilter, this) @@ -139,7 +117,9 @@ const Helpers: FilterHelpers = Object.setPrototypeOf( const filter = this.new() filter.scopes = 0 for (const scope of scopes) filter.scopes |= scope - return filter + return filter as Filter< + FilterInfoOf & { Scopes: typeof scopes } + > }, } satisfies FilterHelpers, Function.prototype, @@ -150,15 +130,14 @@ 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 * ```ts * const custom = createFilterGenerator<[arg1: number, arg2: string]>( - * ([arg1, arg2], id, exports) => { - * // WARNING: exports can be Proxy or nullish, so be careful when using it + * ([arg1, arg2], id, exports, initialized) => { + * // WARNING: exports can be a Proxy, nullish, or a primitive, so be careful when using it * // filter logic * return true * }, @@ -173,16 +152,15 @@ export function createFilterGenerator( args: A, id: Metro.ModuleID, exports: Metro.ModuleExports, + initialized: boolean, ) => boolean, keyFor: (args: A) => string, - flagFor: ((args: A) => FilterFlag) | FilterFlag, defaultScopesFor?: ((args: A) => FilterScopeValue) | FilterScopeValue, ): FilterGenerator<(...args: A) => Filter> export function createFilterGenerator( filter: (args: A, id: Metro.ModuleID) => boolean, keyFor: (args: A) => string, - flagFor: ((args: A) => FilterFlag) | FilterFlag, defaultScopesFor?: ((args: A) => FilterScopeValue) | FilterScopeValue, ): FilterGenerator<(...args: A) => Filter> @@ -190,16 +168,14 @@ export function createFilterGenerator( filter: ( args: A, id: Metro.ModuleID, - exports?: Metro.ModuleExports, + exports: Metro.ModuleExports, + initialized: boolean, ) => boolean, keyFor: (args: A) => string, - flagFor: ((args: A) => FilterFlag) | FilterFlag, defaultScopesFor?: ((args: A) => FilterScopeValue) | FilterScopeValue, ): FilterGenerator<(...args: A) => Filter> { type GeneratorType = ReturnType> - const isFlagsStatic = typeof flagFor === 'number' - const defaultScopes = defaultScopesFor ?? FilterScopes.Initialized const isDefaultScopesStatic = typeof defaultScopes === 'number' @@ -207,9 +183,10 @@ export function createFilterGenerator( const filter_ = (( id: Metro.ModuleID, exports?: Metro.ModuleExports, + initialized: boolean = Boolean(isModuleInitialized(id)), ) => { try { - return filter(args, id, exports) + return filter(args, id, exports, initialized) } catch (e) { DEBUG_warnFilterThrown(id, keyFor(args), e) return false @@ -217,17 +194,12 @@ export function createFilterGenerator( }) as ReturnType filter_.key = keyFor(args) - filter_.flags = isFlagsStatic ? flagFor : flagFor(args) filter_.scopes = isDefaultScopesStatic ? defaultScopes : defaultScopes(args) return Object.setPrototypeOf(filter_, Helpers) } - generator.flagsFor = isFlagsStatic - ? () => flagFor - : (args: A) => flagFor(args) - generator.defaultScopesFor = isDefaultScopesStatic ? () => defaultScopes : defaultScopes diff --git a/lib/modules/src/finders/lookup.ts b/lib/modules/src/finders/lookup.ts index c517e3a..9623246 100644 --- a/lib/modules/src/finders/lookup.ts +++ b/lib/modules/src/finders/lookup.ts @@ -183,11 +183,19 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) { } if (includeUninit) - for (const id of mUninitialized) { + // Must snapshot because partial matches can cause modules that haven't been looped over to be initialized + // which would remove them from the Set + for (const id of Array.from(mUninitialized)) { // biome-ignore lint/complexity/useOptionalChain: Hot path should be optimized if (cached && cached.has(id)) continue - const flag = runFilter(filter, id, undefined, options) + const flag = runFilter( + filter, + id, + // Could be initialized by a previous partial match + getInitializedModuleExports(id), + options, + ) if (flag) { notFound = false @@ -308,8 +316,15 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) { } if (includeUninit) - for (const id of mUninitialized) { - const flag = runFilter(filter, id, undefined, options) + // See equivalent code in `lookupModules` for why this is snapshotted + for (const id of [...mUninitialized]) { + const flag = runFilter( + filter, + id, + // See equivalent code in `lookupModules` for why this isn't undefined + getInitializedModuleExports(id), + options, + ) if (flag) { if (__BUILD_FLAG_DEBUG_MODULE_LOOKUPS__) DEBUG_logLookupMatched(filter.key, flag, id) diff --git a/lib/utils/src/discord.ts b/lib/utils/src/discord.ts index b058856..b50ddae 100644 --- a/lib/utils/src/discord.ts +++ b/lib/utils/src/discord.ts @@ -12,10 +12,7 @@ import { ReactModuleId, ReactNativeModuleId, } from '@revenge-mod/react' -import { - FilterFlag, - FilterScopes, -} from '../../modules/src/finders/filters/constants' +import { FilterScopes } from '../../modules/src/finders/filters/constants' import type { Filter, FilterGenerator, @@ -50,7 +47,6 @@ export type WithGeneratedIconComponent = FilterGenerator< ...assets: string[] ) => Filter<{ Result: { [K in N]: FC } - RequiresExports: boolean Scopes: [ typeof FilterScopes.Uninitialized, typeof FilterScopes.Initialized, @@ -126,7 +122,6 @@ export const withGeneratedIconComponent = createFilterGenerator< return false }, names => `revenge.utils.discord.generatedIconComponent(${names.join(',')})`, - FilterFlag.Dynamic, FilterScopes.Uninitialized | FilterScopes.Initialized, ) as WithGeneratedIconComponent From 07291f6c208d81a7a36d18936dcd643afa050675 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Wed, 12 Aug 2026 01:33:36 +0700 Subject: [PATCH 02/11] fix(lib/modules/metro/runtime): use module ID from closure mInitializingId will never match the actual ID outside of the factory call. --- lib/modules/src/metro/runtime.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/modules/src/metro/runtime.ts b/lib/modules/src/metro/runtime.ts index 5e7cbf1..81f0e13 100644 --- a/lib/modules/src/metro/runtime.ts +++ b/lib/modules/src/metro/runtime.ts @@ -8,7 +8,7 @@ import { callNativeMethodSync } from '@revenge-mod/modules/native' import { getErrorStack } from '@revenge-mod/utils/error' import { FullVersion } from '~constants' -import { loadModuleFromSegment, mInitializingId, mList } from './patches' +import { loadModuleFromSegment, mList } from './patches' import { executeInitializeSubscriptions, executeRequireSubscriptions, @@ -62,7 +62,7 @@ export const metroRequire = (moduleId => { mod.flags = (flags & NotInitializedOrInitializingMask) | HasError mod.error = e - const msg = `Module ${mInitializingId} failed to initialize:\n\n${getErrorStack(e)}` + const msg = `Module ${moduleId} failed to initialize:\n\n${getErrorStack(e)}` if (__DEV__) { callNativeMethodSync('revenge.alertError', [msg, FullVersion]) @@ -71,7 +71,7 @@ export const metroRequire = (moduleId => { } // Some modifications can cause modules to fail by initializing them in the wrong order, we can't just blacklist them - // cacheBlacklistedModule(mInitializingId) + // cacheBlacklistedModule(moduleId) // So... it wasn't a great idea to throw, Discord has pushed a broken build that has some failing modules // Vanilla Metro would swallow the error and just return an empty object as the exports..., insanity From 095d40677962dac20840c08b1b463d86e166eb3a Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Wed, 12 Aug 2026 01:35:49 +0700 Subject: [PATCH 03/11] feat(plugins/settings.plugins): increase auto update intervals --- src/plugins/start/settings.plugins/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/start/settings.plugins/index.ts b/src/plugins/start/settings.plugins/index.ts index c69d730..3e6b96a 100644 --- a/src/plugins/start/settings.plugins/index.ts +++ b/src/plugins/start/settings.plugins/index.ts @@ -22,7 +22,7 @@ export interface Storage { defaultRepoRestored?: boolean } -const OUTDATED_THRESHOLD = 24 * 60 * 60 * 1000 // 1 day +const OUTDATED_THRESHOLD = 60 * 60 * 1000 // 1 hour const AUTO_UPDATE_CHECK_DELAY = 10_000 const CircleXIconComponent = lookupGeneratedIconComponent( From d3fb84b76d6737b37e7da361b6576b01274f153a Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Wed, 12 Aug 2026 15:12:08 +0700 Subject: [PATCH 04/11] feat(lib/modules/finders): new `withDependencies` builders - `skip` and `last`: Skips the first N/to the end of dependencies before doing comparisons. - `atLeast` and `atMost`: Length check before doing comparisons. - `includes`: Simply checks if the depedencies exist. This is performance intensive though. --- lib/modules/src/finders/filters/dynamic.ts | 314 ++++++++++++++++++--- 1 file changed, 274 insertions(+), 40 deletions(-) diff --git a/lib/modules/src/finders/filters/dynamic.ts b/lib/modules/src/finders/filters/dynamic.ts index f6faef8..956a3b8 100644 --- a/lib/modules/src/finders/filters/dynamic.ts +++ b/lib/modules/src/finders/filters/dynamic.ts @@ -18,8 +18,18 @@ export interface ComparableDependencyMap | ComparableDependencyMap | Filter > { + // loose l?: boolean + // relative r?: number + // skip + s?: number + // atLeast + n?: number + // atMost + x?: number + // includes + i?: boolean } /** @@ -30,6 +40,9 @@ export interface ComparableDependencyMap * To do proper fingerprinting for modules: * @see {@link withDependencies.loose} to loosen the checks. * @see {@link withDependencies.relative} to compare dependencies relatively. + * @see {@link withDependencies.skip} and {@link withDependencies.last} to move where comparison starts. + * @see {@link withDependencies.atLeast} and {@link withDependencies.atMost} to bound the dependency count. + * @see {@link withDependencies.includes} to compare without caring about order. * * @example * ```ts @@ -75,24 +88,26 @@ export interface ComparableDependencyMap */ const withDependencies_ = createFilterGenerator>( ([deps], id) => depCompare(getModuleDependencies(id)!, deps, id, id), + // The dep map is keyed as a nested set, so its modifiers are already part of the key deps => `revenge.deps(${depGenFilterKey(deps)})`, FilterScopes.Uninitialized | FilterScopes.Initialized, ) as WithDependencies export const withDependencies = __DEV__ - ? (((deps: ComparableDependencyMap) => { - // Warn about using undefined in deps, which is likely a mistake - for (let i = 0; i < deps.length; i++) { - if (deps[i] === undefined) - DEBUG_warnBadWithDependenciesFilter(deps, i) - } + ? (Object.assign((deps: ComparableDependencyMap) => { + DEBUG_validateWithDependenciesFilter(deps) return withDependencies_(deps) - }) as WithDependencies) + }, withDependencies_) as WithDependencies) : withDependencies_ withDependencies.loose = loose withDependencies.relative = relative +withDependencies.skip = skip +withDependencies.last = last +withDependencies.atLeast = atLeast +withDependencies.atMost = atMost +withDependencies.includes = includes type WithDependencies = FilterGenerator< (deps: ComparableDependencyMap) => Filter<{ @@ -105,6 +120,11 @@ type WithDependencies = FilterGenerator< > & { loose: typeof loose relative: typeof relative + skip: typeof skip + last: typeof last + atLeast: typeof atLeast + atMost: typeof atMost + includes: typeof includes } /** @@ -121,6 +141,96 @@ function loose(deps: ComparableDependencyMap) { return deps } +/** + * Skip a number of dependencies before comparing positionally. + * + * Passing `Infinity` anchors the set to the end, matching the **last** `deps.length` dependencies. + * Anything before them is unconstrained. + * + * @param amount The amount of dependencies to skip from the start, or `Infinity` to anchor to the end. + * @param deps The dependency map to skip in. This permanently modifies the array. + * @returns The modified dependency map. + * + * @see {@link withDependencies.last} for the `Infinity` shorthand. + */ +function skip(amount: number, deps: ComparableDependencyMap = []) { + deps.s = amount + return deps +} + +/** + * Match the **last** `deps.length` dependencies, leaving anything before them unconstrained. + * + * Shorthand for {@link withDependencies.skip} with `Infinity`. + * Prefer this over leading comparisons when a module's trailing dependencies are the stable part of its fingerprint. + * + * @param deps The dependency map to anchor to the end. This permanently modifies the array. + * @returns The modified dependency map. + * + * @example + * ```ts + * const { last, relative } = withDependencies + * + * // Matches modules whose last three dependencies are [Any, module ID + 1, 2] + * withDependencies(last([null, relative(1), 2])) + * ``` + */ +function last(deps: ComparableDependencyMap = []) { + deps.s = Infinity + return deps +} + +/** + * Require the module to have at least `count` dependencies. + * + * This implies {@link withDependencies.loose}, as an exact length check would never pass alongside a bound. + * + * @param count The minimum amount of dependencies. + * @param deps The dependency map to bound. This permanently modifies the array. + * @returns The modified dependency map. + */ +function atLeast(count: number, deps: ComparableDependencyMap = []) { + deps.n = count + deps.l = true + return deps +} + +/** + * Require the module to have at most `count` dependencies. + * + * This implies {@link withDependencies.loose}, as an exact length check would never pass alongside a bound. + * + * @param count The maximum amount of dependencies. + * @param deps The dependency map to bound. This permanently modifies the array. + * @returns The modified dependency map. + */ +function atMost(count: number, deps: ComparableDependencyMap = []) { + deps.x = count + deps.l = true + return deps +} + +/** + * Compare the set without caring about order or position, only that every dependency exists somewhere. + * + * Entries are matched independently, so two identical entries can both match the same dependency. + * Dynamic (`null`) entries are meaningless here and are ignored. + * + * **This is much more expensive than positional comparison**, as every entry is compared against every dependency. + * Bound it with {@link withDependencies.atLeast} or {@link withDependencies.atMost} where possible, as those are checked first. + * + * @param deps The dependency map to compare unordered. This permanently modifies the array. + * @returns The modified dependency map. + */ +function includes(deps: ComparableDependencyMap) { + if (__DEV__) + for (let i = 0; i < deps.length; i++) + if (deps[i] == null) DEBUG_warnBadIncludesDependency(deps, i) + + deps.i = true + return deps +} + const RelativeSignBit = 1 << 30 const RelativeBit = 1 << 29 const RelativeRootBit = 1 << 28 @@ -190,6 +300,70 @@ function DEBUG_warnBadWithDependenciesFilter( ) } +/** + * Warns the developer about a dynamic dependency in a `withDependencies.includes` set, which matches anything and is therefore a no-op. + */ +function DEBUG_warnBadIncludesDependency( + deps: ComparableDependencyMap, + index: number, +) { + nativeLoggingHook( + `\u001b[33mBad withDependencies.includes set, dynamic ID at index ${index} matches anything: [${depGenFilterKey(deps)}]\n${getCurrentStack()}\u001b[0m`, + 2, + ) +} + +/** + * Warns the developer about dependency count bounds that contradict, or are already implied by, the set being compared. + */ +function DEBUG_warnBadDependencyBounds( + deps: ComparableDependencyMap, + reason: string, +) { + nativeLoggingHook( + `\u001b[33mBad withDependencies filter, ${reason}: [${depGenFilterKey(deps)}]\n${getCurrentStack()}\u001b[0m`, + 2, + ) +} + +/** + * Validates a dependency map and its nested sets, warning about mistakes that would otherwise silently never match. + */ +function DEBUG_validateWithDependenciesFilter(deps: ComparableDependencyMap) { + const { n: min, x: max, s: skip } = deps + + // The lowest dependency count the set itself can match. + // Unordered sets have no window, so they only ever constrain the count through bounds. + const floor = deps.i + ? 0 + : (skip === undefined || skip === Infinity ? 0 : skip) + deps.length + + if (max !== undefined && max < floor) + DEBUG_warnBadDependencyBounds( + deps, + `atMost(${max}) can never match, the set requires at least ${floor} dependencies`, + ) + else if (min !== undefined && max !== undefined && min > max) + DEBUG_warnBadDependencyBounds( + deps, + `atLeast(${min}) can never match, atMost(${max}) is lower`, + ) + else if (min !== undefined && min <= floor) + DEBUG_warnBadDependencyBounds( + deps, + `atLeast(${min}) does nothing, the set already requires at least ${floor} dependencies`, + ) + + for (let i = 0; i < deps.length; i++) { + const dep = deps[i] + + // Warn about using undefined in deps, which is likely a mistake + if (dep === undefined) DEBUG_warnBadWithDependenciesFilter(deps, i) + else if (typeof dep === 'object' && dep !== null) + DEBUG_validateWithDependenciesFilter(dep) + } +} + function depCompare( a: Metro.ModuleID[], b: ComparableDependencyMap, @@ -198,7 +372,29 @@ function depCompare( ): boolean { const lenA = a.length const lenB = b.length - if (b.l ? lenA < lenB : lenA !== lenB) return false + + const min = b.n + if (min !== undefined && lenA < min) return false + + const max = b.x + if (max !== undefined && lenA > max) return false + + if (b.i) return depIncludesCompare(a, b, root, parent) + + // Index in a where positional comparison starts + let start = 0 + const skip = b.s + + if (skip === undefined) { + if (b.l ? lenA < lenB : lenA !== lenB) return false + } else if (skip === Infinity) { + // Anchor to the end, so everything before the last lenB dependencies is unconstrained + start = lenA - lenB + if (start < 0) return false + } else { + start = skip + if (b.l ? lenA < start + lenB : lenA !== start + lenB) return false + } for (let i = 0; i < lenB; i++) { const compare = b[i] @@ -209,43 +405,71 @@ function depCompare( // Skip dynamic if (compare == null) continue - const id = a[i] + const id = a[start + i] // Very rare case where a module dependency has an unused `null` dependency // TODO: Probably a Metro/Discord bug? if (id === null) return false - switch (typeof compare) { - case 'function': { - const filter = compare - if (!runFilter(filter, id, getInitializedModuleExports(id))) - return false - - continue - } - case 'object': { - const nested = compare as ComparableDependencyMap - - // relative.withDependencies? - if (nested.r && !depShallowCompare(nested.r, id, root, parent)) - return false - - if (depCompare(getModuleDependencies(id)!, nested, root, id)) - continue - - return false - } - default: { - if (depShallowCompare(compare as number, id, root, parent)) - continue - return false - } - } + if (!depMatches(compare, id, root, parent)) return false } return true } +function depIncludesCompare( + a: Metro.ModuleID[], + b: ComparableDependencyMap, + root: Metro.ModuleID, + parent: Metro.ModuleID, +): boolean { + const lenA = a.length + + for (let i = 0; i < b.length; i++) { + const compare = b[i] + + // Dynamic dependencies match anything, so they carry no meaning when unordered + if (compare == null) continue + + let found = false + + for (let j = 0; j < lenA; j++) { + const id = a[j] + if (id !== null && depMatches(compare, id, root, parent)) { + found = true + break + } + } + + if (!found) return false + } + + return true +} + +function depMatches( + compare: NonNullable, + id: Metro.ModuleID, + root: Metro.ModuleID, + parent: Metro.ModuleID, +): boolean { + switch (typeof compare) { + case 'function': + return !!runFilter(compare, id, getInitializedModuleExports(id)) + case 'object': { + const nested = compare + + // relative.withDependencies? + if (nested.r && !depShallowCompare(nested.r, id, root, parent)) + return false + + return depCompare(getModuleDependencies(id)!, nested, root, id) + } + default: + return depShallowCompare(compare, id, root, parent) + } +} + function depShallowCompare( compare: number, id: Metro.ModuleID, @@ -289,11 +513,7 @@ function depGenFilterKey(deps: ComparableDependencyMap): string { case 'object': { // It's a nested dependency array const nested = dep as ComparableDependencyMap - if (nested.l) key += '#' - // relative.withDependencies? - if (nested.r) key += `${depGenRelativeKeyPart(nested.r)}:` - - key += `[${depGenFilterKey(nested)}],` + key += `${depGenModifierKey(nested)}[${depGenFilterKey(nested)}],` break } default: { @@ -309,6 +529,20 @@ function depGenFilterKey(deps: ComparableDependencyMap): string { return key.substring(0, key.length - 1) } +function depGenModifierKey(deps: ComparableDependencyMap): string { + let key = '' + + if (deps.i) key += '?' + if (deps.l) key += '#' + if (deps.n !== undefined) key += `>${deps.n}` + if (deps.x !== undefined) key += `<${deps.x}` + if (deps.s !== undefined) key += `+${deps.s === Infinity ? '*' : deps.s}` + // relative.withDependencies? + if (deps.r) key += `${depGenRelativeKeyPart(deps.r)}:` + + return key +} + function depGenRelativeKeyPart(dep: number) { const magnitude = depGetRelMagnitude(dep) const prefix = dep & RelativeRootBit ? '~' : '^' From ecb2cf810bd38d25add828d3f627ff6a4d74ba81 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Wed, 12 Aug 2026 15:47:36 +0700 Subject: [PATCH 05/11] fix(lib): migrate to new `withDependencies` APIs --- lib/discord/src/design.ts | 31 +++++----- lib/discord/src/flux/stores.ts | 26 +++++---- lib/utils/src/discord.ts | 102 +++++++++++++-------------------- 3 files changed, 71 insertions(+), 88 deletions(-) diff --git a/lib/discord/src/design.ts b/lib/discord/src/design.ts index d17f2e6..3c3a9d0 100644 --- a/lib/discord/src/design.ts +++ b/lib/discord/src/design.ts @@ -1,6 +1,5 @@ import { lookupModule, lookupModules } from '@revenge-mod/modules/finders' import { withDependencies } from '@revenge-mod/modules/finders/filters' -import { getModuleDependencies } from '@revenge-mod/modules/metro/utils' import { ReactJSXRuntimeModuleId, ReactModuleId, @@ -10,7 +9,7 @@ import { proxify } from '@revenge-mod/utils/proxy' import { ImportTrackerModuleId } from './common/import-tracker' import type { DiscordModules } from './types' -const { loose } = withDependencies +const { atLeast, last, loose, relative } = withDependencies /** * The lowest amount of dependencies `design/native.tsx` is expected to have. @@ -29,13 +28,12 @@ export let Design: Design = proxify( // [3909, 4608, 5182, 13172, 2, ...] (141 dependencies) const [, id] = lookupModule( withDependencies( - loose([ + atLeast(DesignMinimumDependencies, [ [ImportTrackerModuleId], [ImportTrackerModuleId], [ReactNativeModuleId, ImportTrackerModuleId], [ImportTrackerModuleId], ImportTrackerModuleId, - ...Array(DesignMinimumDependencies - 5).fill(null), ]), ).keyAs('revenge.discord.design.Design'), { @@ -55,9 +53,6 @@ export let Design: Design = proxify( // design/components/Forms/native/FormSwitch.native.tsx export let FormSwitch: DiscordModules.Components.FormSwitch = proxify(() => { - // TODO: Possibly come up with a better dependency fingerprinting API - // to not have to deal with this bullshit - for (const [, id] of lookupModules( withDependencies( loose([ @@ -66,17 +61,25 @@ export let FormSwitch: DiscordModules.Components.FormSwitch = proxify(() => { ReactNativeModuleId, ReactJSXRuntimeModuleId, ]), - ).keyAs('revenge.discord.design.FormSwitch'), + ) + .and( + withDependencies( + last([ + relative(1), + relative(2), + null, + null, + ImportTrackerModuleId, + ]), + ), + ) + .keyAs('revenge.discord.design.FormSwitch'), { initialize: false, }, )) { - const deps = getModuleDependencies(id)! - if (deps.at(-1) !== ImportTrackerModuleId) continue - if (deps.at(-4) === id + 2 && deps.at(-5) === id + 1) { - const FormSwitch_ = __r(id)!.FormSwitch - if (FormSwitch_) return (FormSwitch = FormSwitch_) - } + const FormSwitch_ = __r(id)!.FormSwitch + if (FormSwitch_) return (FormSwitch = FormSwitch_) } })! diff --git a/lib/discord/src/flux/stores.ts b/lib/discord/src/flux/stores.ts index 3150fbe..4737a3e 100644 --- a/lib/discord/src/flux/stores.ts +++ b/lib/discord/src/flux/stores.ts @@ -9,7 +9,6 @@ import { withDependencies, withName, } from '@revenge-mod/modules/finders/filters' -import { getModuleDependencies } from '@revenge-mod/modules/metro/utils' import { asap, noop } from '@revenge-mod/utils/callback' import { cache, @@ -75,14 +74,19 @@ export function getStore( ] */ -const withLeadingFluxStoreDeps = withDependencies( - withDependencies.loose([ - withName('_classCallCheck'), - withName('_createClass'), - withName('_possibleConstructorReturn'), - withName('bound getPrototypeOf'), - withName('_inherits'), - ]), +const { last, loose } = withDependencies + +// The import tracker is checked first, as it is far cheaper than resolving the leading dependencies' exports +const withFluxStoreDeps = withDependencies(last([ImportTrackerModuleId])).and( + withDependencies( + loose([ + withName('_classCallCheck'), + withName('_createClass'), + withName('_possibleConstructorReturn'), + withName('bound getPrototypeOf'), + withName('_inherits'), + ]), + ), ) export type WithStore = FilterGenerator< @@ -102,9 +106,7 @@ export const withStore = createFilterGenerator( (_, id, exports, initialized) => { if (initialized) return Boolean(exports?._dispatchToken) - if (!withLeadingFluxStoreDeps(id)) return false - const deps = getModuleDependencies(id)! - return deps[deps.length - 1] === ImportTrackerModuleId + return withFluxStoreDeps(id, undefined, false) }, () => 'revenge.discord.store', FilterScopes.Uninitialized | FilterScopes.Initialized, diff --git a/lib/utils/src/discord.ts b/lib/utils/src/discord.ts index b50ddae..e4f0454 100644 --- a/lib/utils/src/discord.ts +++ b/lib/utils/src/discord.ts @@ -7,39 +7,50 @@ import { withDependencies, } from '@revenge-mod/modules/finders/filters' import { isModuleExportBad } from '@revenge-mod/modules/metro/utils' -import { - ReactJSXRuntimeModuleId, - ReactModuleId, - ReactNativeModuleId, -} from '@revenge-mod/react' import { FilterScopes } from '../../modules/src/finders/filters/constants' import type { Filter, FilterGenerator, } from '@revenge-mod/modules/finders/filters' +import type { Metro } from '@revenge-mod/modules/types' import type { FC } from 'react' -// [React, ReactJsxRuntime, (Tokens), (BaseIconImage), (Asset), ImportTracker] -// Asset = relativeDep(1) -const IconComponentFilter = [ - ReactModuleId, - ReactJSXRuntimeModuleId, - TokensModuleId, - null, - null, - ImportTrackerModuleId, -] +const { last } = withDependencies -// Generated IconComponents that have multiple assets, there is no specific length, so the filter has to be generated on-the-fly. -// [React, ReactNative, ReactJsxRuntime, (Tokens), (BaseIconImage), (...Assets), ImportTracker] -// ...Assets = relativeDep(1..n) -const MultiIconComponentFilterBase = [ - ReactModuleId, - ReactNativeModuleId, - ReactJSXRuntimeModuleId, - TokensModuleId, - null, -] +const depsFilters: Record = {} + +/** + * Builds a dependency filter for an icon component. + * + * `[React, (ReactNative), ReactJsxRuntime, Tokens, (BaseIconImage), (...Assets), ImportTracker]` + * + * `ReactNative` is only present on components with multiple assets, and it sits right after `React`, + * so everything from `ReactJsxRuntime` onwards is at a fixed distance from the end on both shapes. + * + * @param names The component name, then the asset names if the component has multiple assets. + * @returns The filter, or `undefined` if any of the assets aren't registered yet. + */ +function depsFilterFor(names: string[]) { + const key = names.join(',') + const cached = depsFilters[key] + if (cached) return cached + + // Components with multiple assets are named separately from their assets + const assets = names.length > 1 ? names.slice(1) : names + const mids: Metro.ModuleID[] = [] + + for (const name of assets) { + const mid = getAssetByName(name)?.moduleId + // Module ID can never be zero, so if it's falsy, it means the asset was not found. + if (!mid) return + + mids.push(mid) + } + + return (depsFilters[key] = withDependencies( + last([TokensModuleId, null, ...mids, ImportTrackerModuleId]), + )) +} export type WithGeneratedIconComponent = FilterGenerator< ( @@ -82,44 +93,11 @@ export type WithGeneratedIconComponent = FilterGenerator< export const withGeneratedIconComponent = createFilterGenerator< Parameters >( - (names, id, exports) => { - if (typeof exports === 'object' && isModuleExportBad(exports)) { - if (typeof exports[names[0]] === 'function') - return Object.keys(exports).length === 1 - } else { - let filter = IconComponentFilter + (names, id, exports, initialized) => { + // Icon components always export their component, so unusable exports can never match + if (initialized && isModuleExportBad(exports)) return false - if (names.length > 1) { - const mids = [] - - for (let i = 1; i < names.length; i++) { - const name = names[i] - const mid = getAssetByName(name)?.moduleId - // Module ID can never be zero, so if it's falsy, it means the asset was not found. - if (!mid) return false - - mids.push(mid) - } - - filter = [ - ...MultiIconComponentFilterBase, - ...mids, - ImportTrackerModuleId, - ] - } else { - const [name] = names - - const mid = getAssetByName(name)?.moduleId - // Module ID can never be zero, so if it's falsy, it means the asset was not found. - if (!mid) return false - - IconComponentFilter[4] = mid - } - - return withDependencies(filter)(id, exports) - } - - return false + return depsFilterFor(names)?.(id, exports, initialized) ?? false }, names => `revenge.utils.discord.generatedIconComponent(${names.join(',')})`, FilterScopes.Uninitialized | FilterScopes.Initialized, From c16dcaa6540edd3a8cb47ef9d16e6c30d2290570 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Wed, 12 Aug 2026 15:52:18 +0700 Subject: [PATCH 06/11] feat(lib/discord/types): add `expressive` Button type --- lib/discord/src/types/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/discord/src/types/index.ts b/lib/discord/src/types/index.ts index bdea852..f87f343 100644 --- a/lib/discord/src/types/index.ts +++ b/lib/discord/src/types/index.ts @@ -263,6 +263,7 @@ export namespace DiscordModules { | 'tertiary' | 'destructive' | 'active' + | 'expressive' | 'primary-overlay' | 'secondary-overlay' loading?: boolean From fc41d45f2aa8e88edf1429613d7ecaec568a890d Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Wed, 12 Aug 2026 15:58:37 +0700 Subject: [PATCH 07/11] feat(plugins/settings): add donate UI --- scripts/build.ts | 3 +- src/assets/RevengeDonate.webp | Bin 0 -> 13974 bytes .../settings/screens/RevengeSettingScreen.tsx | 164 ++++++++++++++---- types/build.d.ts | 1 + 4 files changed, 135 insertions(+), 33 deletions(-) create mode 100644 src/assets/RevengeDonate.webp diff --git a/scripts/build.ts b/scripts/build.ts index 22f583a..69ac97c 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -79,7 +79,8 @@ export default async function build(dev = Dev, log = true) { .trim(), ), __DEV__: String(dev), - // __BUILD_DEFAULT_PLUGIN_REPOSITORY_URL__: ..., + __BUILD_DEFAULT_PLUGIN_REPOSITORY_URL__: 'undefined', + __BUILD_DONATE_URL__: 'undefined', // See types/build.d.ts for what these flags do __BUILD_FLAG_DEBUG_MODULE_LOOKUPS__: String(dev), diff --git a/src/assets/RevengeDonate.webp b/src/assets/RevengeDonate.webp new file mode 100644 index 0000000000000000000000000000000000000000..bbbec70fcd377d214047b780d77a06d57d1b28d1 GIT binary patch literal 13974 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 + + + + + + + + + + + ) +} + +const cardStyles = StyleSheet.create({ + background: { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + }, + illustration: { + width: 96, + height: 96, + }, +}) + +function DonateCard() { + return ( + + + + + + + + Support Revenge + + + You can support the development of Revenge by + donating or contributing! + + + + {__BUILD_DONATE_URL__ && ( +