feat(lib/modules): export metro, fix finders result checks, refactor filters to use creator function, add loose dependency checks

This commit is contained in:
PalmDevs 2025-04-10 02:03:56 +07:00
parent e39ad598d9
commit f9c048b623
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
9 changed files with 221 additions and 148 deletions

View file

@ -10,7 +10,7 @@
"default": "./src/finders/filters.ts"
},
"./metro": {
"default": "./src/metro.ts"
"default": "./src/metro/index.ts"
},
"./metro/subscriptions": {
"default": "./src/metro/subscriptions/index.ts"

View file

@ -1,4 +1,4 @@
import { _isExportBad } from '../metro/_internal'
import { isModuleExportBad } from '../metro'
import type { If } from '@revenge-mod/utils/types'
import type { Metro } from '../../types/metro'
@ -72,7 +72,7 @@ export function runFilter(
const { default: defaultExport } = exports
// TODO(modules/caches/flags): could cache here that default export is bad
if (!options?.esmSkipDefault && !_isExportBad(defaultExport) && filter(id, defaultExport)) {
if (!options?.esmSkipDefault && !isModuleExportBad(defaultExport) && filter(id, defaultExport)) {
// TODO(modules/finders/caches::esm::default)
return FilterResultFlags.ESModuleDefault
}

View file

@ -1,4 +1,6 @@
import { _mInited, _mMetadatas } from '../metro/_internal'
import { _mInited } from '../metro/_internal'
import { getModuleDependencies } from '../metro'
import type { If, LogicalOr } from '@revenge-mod/utils/types'
import type { Metro } from '../../types/metro'
@ -46,13 +48,13 @@ export function createFilterGenerator<A extends any[]>(
): (...args: A) => Filter<object, false>
export function createFilterGenerator<A extends any[]>(
filter: (args: A, id: Metro.ModuleID, exports?: Metro.ModuleExports) => boolean,
f: (args: A, id: Metro.ModuleID, exports?: Metro.ModuleExports) => boolean,
keyFor: (args: A) => string,
): (...args: any[]) => Filter {
return (...args: A) => {
const f = (id: Metro.ModuleID, exports?: Metro.ModuleExports) => filter(args, id, exports)
f.key = keyFor(args)
return f
const filter = (id: Metro.ModuleID, exports?: Metro.ModuleExports) => f(args, id, exports)
filter.key = keyFor(args)
return filter
}
}
@ -74,7 +76,7 @@ export type ByProps = <T extends Record<string, any> = Record<string, any>>(
* ```
*/
export const byProps = createFilterGenerator<Parameters<ByProps>>(
(props, _, exports) => props.every(prop => prop in exports),
(props, _, exports) => typeof exports === 'object' && props.every(prop => prop in exports),
props => `revenge.props(${props.join(',')})`,
) as ByProps
@ -117,10 +119,15 @@ export const byName = createFilterGenerator<Parameters<ByName>>(
([name]) => `revenge.name(${name})`,
) as ByName
export type ComparableDependencyMap = Array<Metro.ModuleID | undefined | ComparableDependencyMap> & {
loose?: boolean
}
/**
* Filter modules by their dependency map.
*
* @param deps The dependency map to check for, can be a sparse array or have `undefined` for "any dependency". **Order and size matters!**
* @param deps The dependency map to check for, can be a sparse array or have `undefined` to be any dependency ("dynamic"). **Order and size matters!**
* @see {@link looseDeps} to make comparisons less strict.
*
* @example
* ```ts
@ -129,21 +136,78 @@ export const byName = createFilterGenerator<Parameters<ByName>>(
* const Logger = await findModule(byDependencies([4, , 2]))
* ```
*/
export const byDependencies = createFilterGenerator<[Array<Metro.ModuleID | undefined>]>(
([deps], id) => {
const actDeps = _mMetadatas.get(id)![0]
if (actDeps.length === deps.length)
for (let i = 0; i < deps.length; i++) {
const cmp = deps[i]
if (Number.isInteger(cmp) && actDeps[i] !== cmp) return false
}
else return false
return true
},
deps => `revenge.deps(${deps.join(',')})`,
export const byDependencies = createFilterGenerator<[deps: ComparableDependencyMap]>(
([deps], id) => compareDeps(id, deps, deps.loose),
deps => `revenge.deps(${depsToKey(deps)})`,
)
/**
* Make this set of comparable dependencies as loose, to be used with {@link byDependencies}.
*
* Making a dependency loose skips the exact length check, but the order of the set dependencies still matters.
* If you mark an index as dynamic, the same index must also be present in the other map during comparison to pass.
*
* @param deps The dependency map to make loose. This permanently modifies the array.
* @returns The modified dependency map.
*/
export function looseDeps(deps: ComparableDependencyMap) {
deps.loose = true
return deps
}
function compareDeps(of: Metro.ModuleID, compare: ComparableDependencyMap, loose = false): boolean {
const stack: Array<[of: Metro.ModuleID[], compare: ComparableDependencyMap, loose: boolean]> = []
stack.push([getModuleDependencies(of)!, compare, loose])
while (stack.length) {
const [of, compare, loose] = stack.pop()!
if (loose ? of.length < compare.length : of.length !== compare.length) return false
for (let i = 0; i < compare.length; i++) {
const orig = of[i]
const cmp = compare[i]
if (cmp === undefined) continue
if (Array.isArray(cmp)) {
const dependencies = getModuleDependencies(orig)
if (!dependencies) return false
stack.push([dependencies, cmp, cmp.loose ?? false])
} else if (orig !== cmp) return false
}
}
return true
}
function depsToKey(deps: ComparableDependencyMap): string {
let key = ''
for (const dep of deps)
if (dep === undefined) key += ','
else if (Array.isArray(dep)) {
if (dep.loose) key += '#'
key += `[${depsToKey(dep)}],`
} else key += `${dep},`
return key.substring(0, key.length - 1)
}
export type Every = {
<F1 extends Filter, F2 extends Filter>(
f1: F1,
f2: F2,
): Filter<FilterResult<F1> & FilterResult<F2>, LogicalOr<IsFilterWithExports<F1>, IsFilterWithExports<F2>>>
<F1 extends Filter, F2 extends Filter, F3 extends Filter>(
f1: F1,
f2: F2,
f3: F3,
): Filter<
FilterResult<F1> & FilterResult<F2> & FilterResult<F3>,
LogicalOr<LogicalOr<IsFilterWithExports<F1>, IsFilterWithExports<F2>>, IsFilterWithExports<F3>>
>
(...filters: Filter[]): Filter
}
/**
* Combines multiple filters into one, returning true if **every** filter matches.
*
@ -158,29 +222,25 @@ export const byDependencies = createFilterGenerator<[Array<Metro.ModuleID | unde
* ))
* ```
*/
export function every<F1 extends Filter, F2 extends Filter>(
f1: F1,
f2: F2,
): Filter<FilterResult<F1> & FilterResult<F2>, LogicalOr<IsFilterWithExports<F1>, IsFilterWithExports<F2>>>
export const every = createFilterGenerator<[...filters: Filter[]]>(
(filters, id, exports) => filters.every(filter => filter(id, exports)),
filters => `revenge.every(${filtersToKey(filters)})`,
) as Every
export function every<F1 extends Filter, F2 extends Filter, F3 extends Filter>(
f1: F1,
f2: F2,
f3: F3,
): Filter<
FilterResult<F1> & FilterResult<F2> & FilterResult<F3>,
LogicalOr<LogicalOr<IsFilterWithExports<F1>, IsFilterWithExports<F2>>, IsFilterWithExports<F3>>
>
export function every(...filters: Filter[]): Filter {
const f = (id: Metro.ModuleID, exports?: Metro.ModuleExports) => {
for (const filter of filters) if (!filter(id, exports)) return false
return true
}
f.key = `revenge.every(${filters.map(filter => filter.key).join(',')})`
return f
export type Some = {
<F1 extends Filter, F2 extends Filter>(
f1: F1,
f2: F2,
): Filter<FilterResult<F1> | FilterResult<F2>, IsFilterWithExports<F1> | IsFilterWithExports<F2>>
<F1 extends Filter, F2 extends Filter, F3 extends Filter>(
f1: F1,
f2: F2,
f3: F3,
): Filter<
FilterResult<F1> | FilterResult<F2> | FilterResult<F3>,
IsFilterWithExports<F1> | IsFilterWithExports<F2> | IsFilterWithExports<F3>
>
(...filters: Filter[]): Filter
}
/**
@ -197,31 +257,22 @@ export function every(...filters: Filter[]): Filter {
* ))
* ```
*/
export function some<F1 extends Filter, F2 extends Filter>(
f1: F1,
f2: F2,
): Filter<FilterResult<F1> | FilterResult<F2>, IsFilterWithExports<F1> | IsFilterWithExports<F2>>
export const some = createFilterGenerator<[...filters: Filter[]]>(
(filters, id, exports) => filters.some(filter => filter(id, exports)),
filters => `revenge.some(${filtersToKey(filters)})`,
) as Some
export function some<F1 extends Filter, F2 extends Filter, F3 extends Filter>(
f1: F1,
f2: F2,
f3: F3,
): Filter<
FilterResult<F1> | FilterResult<F2> | FilterResult<F3>,
IsFilterWithExports<F1> | IsFilterWithExports<F2> | IsFilterWithExports<F3>
>
export function some(...filters: Filter[]): Filter {
const f = (id: Metro.ModuleID, exports?: Metro.ModuleExports) => {
for (const filter of filters) if (filter(id, exports)) return true
return false
}
f.key = `revenge.some(${filters.map(filter => filter.key).join(',')})`
return f
function filtersToKey(filters: Filter[]): string {
let s = ''
for (const filter of filters) s += `${filter.key},`
return s.substring(0, s.length - 1)
}
export type ModuleStateAware = <IF extends Filter>(
initializedFilter: IF,
uninitializedFilter: Filter<any, false>,
) => Filter<FilterResult<IF>, false>
/**
* Filter modules depending on their initialized state.
*
@ -238,16 +289,10 @@ export function some(...filters: Filter[]): Filter {
* ))
* ```
*/
export function moduleStateAware<IF extends Filter>(
initializedFilter: IF,
uninitializedFilter: Filter<any, false>,
): Filter<FilterResult<IF>, false> {
const f = (id: Metro.ModuleID, exports?: Metro.ModuleExports) => {
export const moduleStateAware = createFilterGenerator<Parameters<ModuleStateAware>>(
([initializedFilter, uninitializedFilter], id, exports) => {
if (_mInited.has(id)) return initializedFilter(id, exports)
return uninitializedFilter(id)
}
f.key = `revenge.moduleStateAware(${initializedFilter.key},${uninitializedFilter.key})`
return f
}
},
([f1, f2]) => `revenge.moduleStateAware(${f1.key},${f2.key})`,
) as ModuleStateAware

View file

@ -48,7 +48,7 @@ export function findModule<F extends Filter, O extends FindModuleOptions>(filter
return new Promise<O extends RunFilterReturnExportsOptions<true> ? MaybeDefaultExportMatched<Result> : Result>(
(ok, err) => {
const exports = lookupModule<F, O>(filter, options!)
if (exports !== undefined) return ok(exports)
if (exports != null) return ok(exports)
const unsub = waitForModules<F, O>(
filter,
@ -105,7 +105,7 @@ export function findModuleId<F extends Filter, O extends FindModuleIdOptions>(
export function findModuleId(filter: Filter, options?: FindModuleIdOptions) {
return new Promise<Metro.ModuleID>((ok, err) => {
const id = lookupModuleId(filter, options)
if (id !== undefined) {
if (id != null) {
ok(id)
return
}
@ -152,7 +152,7 @@ export function findModuleId(filter: Filter, options?: FindModuleIdOptions) {
export function findModuleByImportedPath<T>(path: string, options?: BaseFindModuleOptions): Promise<T> {
return new Promise((ok, err) => {
const exports = lookupModuleByImportedPath(path)
if (exports !== undefined) {
if (exports != null) {
ok(exports)
return
}
@ -221,7 +221,7 @@ export function findModuleSync<F extends Filter, O extends FindModuleSyncOptions
export function findModuleSync<F extends Filter, O extends FindModuleSyncOptions>(filter: F, options?: O) {
const exports = lookupModule(filter, options!)
if (exports !== undefined) return exports
if (exports != null) return exports
let value: unknown | undefined
const unsub = waitForModules(
@ -261,7 +261,7 @@ export function findModuleByImportedPathSync<T = any>(
options?: BaseFindModuleSyncOptions,
): T | undefined {
const exports = lookupModuleByImportedPath<T>(path)
if (exports !== undefined) return exports
if (exports != null) return exports
let value: unknown | undefined
const unsub = waitForModuleByImportedPath(path, (_, exports) => {

View file

@ -1,4 +1,4 @@
import { _mInited, _mMetadatas, _mPaths, _mUninited } from '../metro/_internal'
import { _mInited, _mPaths, _mUninited } from '../metro/_internal'
import {
type RunFilterOptions,
type RunFilterReturnExportsOptions,
@ -6,6 +6,8 @@ import {
runFilterReturnExports,
} from './_internal'
import { getInitializedModuleExports } from '../metro'
import type { MaybeDefaultExportMatched } from '.'
import type { Metro } from '../../types/metro'
import type { Filter, FilterResult } from './filters'
@ -71,10 +73,7 @@ export function* lookupModuleIds<O extends LookupModuleIdsOptions>(
options?: O,
): Generator<Metro.ModuleID, undefined> {
if (options?.includeInitialized ?? true)
for (const id of _mInited) {
const { exports } = _mMetadatas.get(id)![1]!
if (runFilter(filter, id, exports, options)) yield id
}
for (const id of _mInited) if (runFilter(filter, id, getInitializedModuleExports(id), options)) yield id
if (options?.includeUninitialized) for (const id of _mUninited) if (runFilter(filter, id)) yield id
}
@ -106,9 +105,8 @@ export function lookupModules<
export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
if (options?.includeInitialized ?? true)
for (const id of _mInited) {
const { exports } = _mMetadatas.get(id)![1]!
const result = runFilterReturnExports(filter, id, exports, options)
if (result) yield result
const result = runFilterReturnExports(filter, id, getInitializedModuleExports(id), options)
if (result != null) yield result
}
if (options?.includeUninitialized) for (const id of _mUninited) if (runFilter(filter, id)) yield __r(id)
@ -142,10 +140,7 @@ export function lookupModuleId<O extends LookupModuleIdsOptions>(
options?: O,
): Metro.ModuleID | undefined {
if (options?.includeInitialized ?? true)
for (const id of _mInited) {
const { exports } = _mMetadatas.get(id)![1]!
if (runFilter(filter, id, exports, options)) return id
}
for (const id of _mInited) if (runFilter(filter, id, getInitializedModuleExports(id), options)) return id
if (options?.includeUninitialized) for (const id of _mUninited) if (runFilter(filter, id)) return id
}
@ -174,9 +169,8 @@ export function lookupModule<F extends Filter, O extends LookupModulesOptions>(
export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
if (options?.includeInitialized ?? true)
for (const id of _mInited) {
const { exports } = _mMetadatas.get(id)![1]!
const result = runFilterReturnExports(filter, id, exports, options)
if (result) return result
const result = runFilterReturnExports(filter, id, getInitializedModuleExports(id), options)
if (result != null) return result
}
if (options?.includeUninitialized) for (const id of _mUninited) if (runFilter(filter, id)) return __r(id)
@ -199,5 +193,5 @@ export function lookupModuleByImportedPath<T = any>(path: string): T | undefined
const id = _mPaths.get(path)
if (id === undefined) return
if (_mMetadatas.has(id)) return _mMetadatas.get(id)![1]!.exports
return getInitializedModuleExports(id)
}

View file

@ -1,5 +1,4 @@
import { onAnyModuleInitialized, onModuleFinishedImporting } from '../metro'
import { _bl, _mMetadatas } from '../metro/_internal'
import { getInitializedModuleExports, isBlacklisted, onAnyModuleInitialized, onModuleFinishedImporting } from '../metro'
import { runFilterReturnExports, type RunFilterReturnExportsOptions } from './_internal'
import type { MaybeDefaultExportMatched } from '.'
@ -51,9 +50,9 @@ export function waitForModules(
options?: WaitForModulesOptions,
) {
const unsub = onAnyModuleInitialized((id, exports) => {
if (_bl.has(id)) return
if (isBlacklisted(id)) return
const result = runFilterReturnExports(filter, id, exports, options)
if (result) callback(id, exports)
if (result != null) callback(id, exports)
})
return unsub
@ -81,9 +80,8 @@ export function waitForModules(
export function waitForModuleByImportedPath<T = any>(path: string, callback: (id: Metro.ModuleID, exports: T) => any) {
const unsub = onModuleFinishedImporting((id, cmpPath) => {
if (path === cmpPath) {
const { exports } = _mMetadatas.get(id)![1]!
callback(id, exports)
unsub()
callback(id, getInitializedModuleExports(id))
}
})

View file

@ -1,7 +1,7 @@
import { isProxy } from '@revenge-mod/utils/proxy'
import { _executeSubscription } from './subscriptions/_internal'
import type { Metro } from '../../types/metro'
import { isBlacklisted, isModuleExportBad } from '.'
export const _bl = new Set<Metro.ModuleID>()
@ -12,16 +12,16 @@ export const _mUninited = new Set<Metro.ModuleID>()
/** Initialized IDs */
export const _mInited = new Set<Metro.ModuleID>()
export const _mMetadatas = new Map<Metro.ModuleID, [deps: Metro.DependencyMap, module?: Metro.Module]>()
export const _mPaths = new Map<string, Metro.ModuleID>()
export const _mMd = new Map<Metro.ModuleID, [deps: Metro.DependencyMap, module?: Metro.Module]>()
export function patchMetroDefine(metroDefine: Metro.DefineFn) {
return ((origFactory, id, deps) => {
// deps won't be undefined last time I checked
const metadata = [deps!] as typeof _mMetadatas extends Map<any, infer V> ? V : never
_mMetadatas.set(id, metadata)
const metadata = [deps!] as typeof _mMd extends Map<any, infer V> ? V : never
_mMd.set(id, metadata)
if (_bl.has(id)) {
if (isBlacklisted(id)) {
metroDefine(origFactory, id, deps)
return
}
@ -38,7 +38,7 @@ export function patchMetroDefine(metroDefine: Metro.DefineFn) {
try {
origFactory(g, r, ipD, ipA, m, e, d)
if (_isExportsBad(m.exports)) _blacklist(id)
if (isModuleExportBad(m.exports)) _blacklist(id)
// If exports isn't bad, we can put it in the list of initialized modules
else _mInited.add(id)
} finally {
@ -58,9 +58,9 @@ export function patchMetroDefine(metroDefine: Metro.DefineFn) {
*
* Blacklisting modules prevents Revenge from doing the following things:
*
* - Iterating over the module during `find()`s
* - Iterating over the module during `find()`s.
*
* @param id The module ID to blacklist
* @param id The module ID to blacklist.
* @internal
*/
export function _blacklist(id: Metro.ModuleID) {
@ -72,34 +72,3 @@ export function _blacklist(id: Metro.ModuleID) {
// TODO(modules): caching
}
/**
* 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 _isExportBad(exp: Metro.ModuleExports[PropertyKey]) {
return (
// Nullish?
exp == null ||
// Is it a proxy? (discord-intl has proxy exports)
isProxy(exp)
)
}
/**
* Returns whether the module has bad exports. If it does, it should be blacklisted and never hooked into.
* @param exports The exports of the module
* @returns Whether the module has bad exports
* @internal
*/
export function _isExportsBad(exports: Metro.ModuleExports) {
return (
// Nullish?
exports == null ||
// Checking if the object is empty
(exports.__proto__ === Object.prototype && !Reflect.ownKeys(exports).length) ||
// Can't run isProxy() on this because this isn't your typical proxy:
// https://github.com/discord/react-native/blob/master/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp
exports === nativeModuleProxy
)
}

View file

@ -1,5 +1,72 @@
import { isProxy } from '@revenge-mod/utils/proxy'
import { _bl, _mMd, _mInited } from './_internal'
import type { Metro } from '../../types/metro'
import './patches'
export * from './subscriptions'
/**
* Returns whether a module is blacklisted.
* @param id The module ID to check.
*/
export function isBlacklisted(id: Metro.ModuleID) {
return _bl.has(id)
}
/**
* Returns whether a module is initialized.
* @param id The module ID to check.
*/
export function isModuleInitialized(id: Metro.ModuleID) {
return _mInited.has(id)
}
/**
* Returns the dependencies of a module.
* @param id The module ID.
*/
export function getModuleDependencies(id: Metro.ModuleID) {
return _mMd.get(id)?.[0]
}
/**
* Returns the exports of an initialized module.
* @param id The module ID.
*/
export function getInitializedModuleExports(id: Metro.ModuleID) {
return _mMd.get(id)?.[1]?.exports
}
/**
* 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(exp: Metro.ModuleExports[PropertyKey]) {
return (
// Nullish?
exp == null ||
// Is it a proxy? (discord-intl has proxy exports)
isProxy(exp)
)
}
/**
* Returns whether the module has bad exports. If it does, it should be blacklisted and never hooked into.
* @param exports The exports of the module.
*/
export function isModuleExportsBad(exports: Metro.ModuleExports) {
return (
// Nullish?
exports == null ||
// Checking if the object is empty
(exports.__proto__ === Object.prototype && !Reflect.ownKeys(exports).length) ||
// Can't run isProxy() on this because this isn't your typical proxy:
// https://github.com/discord/react-native/blob/master/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp
exports === nativeModuleProxy
)
}
// TODO(modules): caching

View file

@ -1,13 +1,13 @@
import '@revenge-mod/assets/patches'
import { _bl, _mInitingId, _mMetadatas, _mPaths, _mUninited } from './_internal'
import { onAnyModuleInitialized } from './subscriptions'
import { _mInitingId, _mPaths } from './_internal'
import { _executeImportedPathSubscription } from './subscriptions/_internal'
const unsubForImportTracker = onAnyModuleInitialized((id, exports) => {
if (_bl.has(id)) return
import { waitForModules } from '../finders'
import { byProps } from '../finders/filters'
const unsubForImportTracker = waitForModules(byProps('fileFinishedImporting'), (_, exports) => {
const orig = exports.fileFinishedImporting
if (orig) {
unsubForImportTracker()