fix(lib/modules): don't make assumptions about what consumers are filtering

This commit is contained in:
PalmDevs 2026-08-02 16:48:22 +07:00
parent 215cd487b9
commit 9a10e9d509
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
6 changed files with 38 additions and 15 deletions

View file

@ -132,7 +132,7 @@ export type WithStoreName = FilterGenerator<
*/
export const withStoreName = createFilterGenerator(
([name], _, exports) =>
exports.getName?.length === 0 && exports.getName() === name,
exports?.getName?.length === 0 && exports.getName() === name,
([name]) => `revenge.discord.storeName(${name})`,
FilterFlag.RequiresExports,
FilterScopes.Uninitialized | FilterScopes.Initialized,

View file

@ -2,7 +2,6 @@ import { getCurrentStack } from '@revenge-mod/utils/error'
import { cacheFilterResultForId } from '../caches'
import { mInitialized } from '../metro/patches'
import { metroRequire } from '../metro/runtime'
import { isModuleExportBad } from '../metro/utils'
import { FilterFlag } from './filters'
import type { If } from '@revenge-mod/utils/types'
import type { Metro } from '../types'
@ -66,6 +65,8 @@ export const FilterResultFlagToHumanReadable: Record<FilterResultFlag, string> =
[FilterResultFlags.Found]: '\u001b[96mexportsless\u001b[0m',
}
const noDefaultExportsCache = new Set<Metro.ModuleID>()
// 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
@ -92,7 +93,13 @@ export function runFilter(
if (exports === undefined) {
if (filter.flags & FilterFlag.RequiresExports) return
if (filter(id)) {
const filter_ = filter as Filter<{
Result: any
RequiresExports: false
Scopes: any[]
}>
if (filter_(id)) {
if (options?.initialize ?? true) {
const module = metroRequire(id)
// Check if the required module is not blacklisted
@ -111,7 +118,13 @@ export function runFilter(
return
}
if (filter(id, exports))
const filter_ = filter as Filter<{
Result: any
RequiresExports: true
Scopes: any[]
}>
if (filter_(id, exports))
return cacheFilterResultForId(
filter.key,
id,
@ -119,9 +132,14 @@ export function runFilter(
)
if (options?.skipDefault) return
if (noDefaultExportsCache.has(id)) return
if (!('default' in exports)) {
noDefaultExportsCache.add(id)
return
}
const { default: defaultExport } = exports
if (!isModuleExportBad(defaultExport) && filter(id, defaultExport))
if (filter_(id, defaultExport))
return cacheFilterResultForId(filter.key, id, FilterResultFlags.Default)
}

View file

@ -1,3 +1,4 @@
import { isModuleExportBad } from '@revenge-mod/modules/metro/utils'
import { FilterFlag, FilterScopes } from './constants'
import { createFilterGenerator } from './utils'
import type { Filter, FilterGenerator } from './utils'
@ -26,6 +27,8 @@ type FilterRequiringExports<T> = Filter<{
export const withProps = createFilterGenerator<Parameters<WithProps>>(
(props, _, exports) => {
const type = typeof exports
if (isModuleExportBad(exports)) return false
if (type === 'object' || type === 'function') {
for (const prop of props) {
if (prop in exports) continue
@ -58,7 +61,7 @@ export type WithProps = FilterGenerator<
export const withoutProps = createFilterGenerator<Parameters<WithoutProps>>(
(props, _, exports) => {
const type = typeof exports
if (type === 'object' || type === 'function')
if ((type === 'object' && exports !== null) || type === 'function')
for (const prop of props) if (prop in exports) return false
return true
@ -88,8 +91,8 @@ export type WithoutProps = FilterGenerator<
*/
export const withSingleProp = createFilterGenerator<Parameters<WithSingleProp>>(
([prop], _, exports) => {
if (typeof exports === 'object' && prop in exports)
return Object.keys(exports).length === 1
if (typeof exports === 'object' && exports !== null && prop in exports)
return Reflect.ownKeys(exports).length === 1
return false
},
@ -133,7 +136,7 @@ export type WithSingleProp = FilterGenerator<
* // const SomeClass: { new(): SomeClass, name: 'SomeClass' }
*/
export const withName = createFilterGenerator<Parameters<WithName>>(
([name], _, exports) => exports.name === name,
([name], _, exports) => exports?.name === name,
([name]) => `revenge.name(${name})`,
FilterFlag.RequiresExports,
FilterScopes.Initialized,

View file

@ -157,6 +157,7 @@ const Helpers: FilterHelpers = Object.setPrototypeOf(
* ```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
* // filter logic
* return true
* },

View file

@ -1,4 +1,3 @@
import { isProxy } from '@revenge-mod/utils/proxy'
import { mDeps, mList } from './patches'
import { Initialized } from './runtime'
import type { Metro } from '../types'
@ -38,8 +37,11 @@ export function getInitializedModuleExports(
return mList.get(id)?.module?.exports
}
const CATCH_ALL_TEST = Symbol()
/**
* Returns whether a particular module export is bad. This is used for filter functions to check whether an export is filterable.
* Returns whether a particular module export is bad.
* This is used for filter functions to check whether an export is filterable.
* @param exp The export to check.
*/
export function isModuleExportBad(
@ -48,9 +50,7 @@ export function isModuleExportBad(
return (
// Nullish?
exp == null ||
// Is it a proxy? (discord-intl has proxy exports)
isProxy(exp) ||
// Does it have some non-existent key? (Turbo modules)
(typeof exp === 'object' && '\u0001' in exp)
(typeof exp === 'object' && CATCH_ALL_TEST in exp)
)
}

View file

@ -6,6 +6,7 @@ import {
createFilterGenerator,
withDependencies,
} from '@revenge-mod/modules/finders/filters'
import { isModuleExportBad } from '@revenge-mod/modules/metro/utils'
import {
ReactJSXRuntimeModuleId,
ReactModuleId,
@ -86,7 +87,7 @@ export const withGeneratedIconComponent = createFilterGenerator<
Parameters<WithGeneratedIconComponent>
>(
(names, id, exports) => {
if (typeof exports === 'object') {
if (typeof exports === 'object' && isModuleExportBad(exports)) {
if (typeof exports[names[0]] === 'function')
return Object.keys(exports).length === 1
} else {