diff --git a/lib/modules/package.json b/lib/modules/package.json index ff6d220..93dd950 100644 --- a/lib/modules/package.json +++ b/lib/modules/package.json @@ -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" diff --git a/lib/modules/src/finders/_internal.ts b/lib/modules/src/finders/_internal.ts index 6dcb81b..784142e 100644 --- a/lib/modules/src/finders/_internal.ts +++ b/lib/modules/src/finders/_internal.ts @@ -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 } diff --git a/lib/modules/src/finders/filters.ts b/lib/modules/src/finders/filters.ts index 56425b4..898b6ab 100644 --- a/lib/modules/src/finders/filters.ts +++ b/lib/modules/src/finders/filters.ts @@ -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( ): (...args: A) => Filter export function createFilterGenerator( - 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 = = Record>( * ``` */ export const byProps = createFilterGenerator>( - (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>( ([name]) => `revenge.name(${name})`, ) as ByName +export type ComparableDependencyMap = Array & { + 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>( * const Logger = await findModule(byDependencies([4, , 2])) * ``` */ -export const byDependencies = createFilterGenerator<[Array]>( - ([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: F1, + f2: F2, + ): Filter & FilterResult, LogicalOr, IsFilterWithExports>> + ( + f1: F1, + f2: F2, + f3: F3, + ): Filter< + FilterResult & FilterResult & FilterResult, + LogicalOr, IsFilterWithExports>, IsFilterWithExports> + > + (...filters: Filter[]): Filter +} + /** * Combines multiple filters into one, returning true if **every** filter matches. * @@ -158,29 +222,25 @@ export const byDependencies = createFilterGenerator<[Array( - f1: F1, - f2: F2, -): Filter & FilterResult, LogicalOr, IsFilterWithExports>> +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: F1, - f2: F2, - f3: F3, -): Filter< - FilterResult & FilterResult & FilterResult, - LogicalOr, IsFilterWithExports>, IsFilterWithExports> -> - -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: F1, + f2: F2, + ): Filter | FilterResult, IsFilterWithExports | IsFilterWithExports> + ( + f1: F1, + f2: F2, + f3: F3, + ): Filter< + FilterResult | FilterResult | FilterResult, + IsFilterWithExports | IsFilterWithExports | IsFilterWithExports + > + (...filters: Filter[]): Filter } /** @@ -197,31 +257,22 @@ export function every(...filters: Filter[]): Filter { * )) * ``` */ -export function some( - f1: F1, - f2: F2, -): Filter | FilterResult, IsFilterWithExports | IsFilterWithExports> +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: F1, - f2: F2, - f3: F3, -): Filter< - FilterResult | FilterResult | FilterResult, - IsFilterWithExports | IsFilterWithExports | IsFilterWithExports -> - -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 = ( + initializedFilter: IF, + uninitializedFilter: Filter, +) => Filter, false> + /** * Filter modules depending on their initialized state. * @@ -238,16 +289,10 @@ export function some(...filters: Filter[]): Filter { * )) * ``` */ -export function moduleStateAware( - initializedFilter: IF, - uninitializedFilter: Filter, -): Filter, false> { - const f = (id: Metro.ModuleID, exports?: Metro.ModuleExports) => { +export const moduleStateAware = createFilterGenerator>( + ([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 diff --git a/lib/modules/src/finders/find.ts b/lib/modules/src/finders/find.ts index 76b84a7..ca17e80 100644 --- a/lib/modules/src/finders/find.ts +++ b/lib/modules/src/finders/find.ts @@ -48,7 +48,7 @@ export function findModule(filter return new Promise ? MaybeDefaultExportMatched : Result>( (ok, err) => { const exports = lookupModule(filter, options!) - if (exports !== undefined) return ok(exports) + if (exports != null) return ok(exports) const unsub = waitForModules( filter, @@ -105,7 +105,7 @@ export function findModuleId( export function findModuleId(filter: Filter, options?: FindModuleIdOptions) { return new Promise((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(path: string, options?: BaseFindModuleOptions): Promise { 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(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( options?: BaseFindModuleSyncOptions, ): T | undefined { const exports = lookupModuleByImportedPath(path) - if (exports !== undefined) return exports + if (exports != null) return exports let value: unknown | undefined const unsub = waitForModuleByImportedPath(path, (_, exports) => { diff --git a/lib/modules/src/finders/lookup.ts b/lib/modules/src/finders/lookup.ts index 3e7dadf..da8af3d 100644 --- a/lib/modules/src/finders/lookup.ts +++ b/lib/modules/src/finders/lookup.ts @@ -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( options?: O, ): Generator { 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( 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( 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(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) } diff --git a/lib/modules/src/finders/wait.ts b/lib/modules/src/finders/wait.ts index 914c4e9..86f50d1 100644 --- a/lib/modules/src/finders/wait.ts +++ b/lib/modules/src/finders/wait.ts @@ -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(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)) } }) diff --git a/lib/modules/src/metro/_internal.ts b/lib/modules/src/metro/_internal.ts index f3a50f2..2a478d7 100644 --- a/lib/modules/src/metro/_internal.ts +++ b/lib/modules/src/metro/_internal.ts @@ -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() @@ -12,16 +12,16 @@ export const _mUninited = new Set() /** Initialized IDs */ export const _mInited = new Set() -export const _mMetadatas = new Map() export const _mPaths = new Map() +export const _mMd = new Map() 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 ? V : never - _mMetadatas.set(id, metadata) + const metadata = [deps!] as typeof _mMd extends Map ? 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 - ) -} diff --git a/lib/modules/src/metro/index.ts b/lib/modules/src/metro/index.ts index 578dc86..1e66f0a 100644 --- a/lib/modules/src/metro/index.ts +++ b/lib/modules/src/metro/index.ts @@ -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 diff --git a/lib/modules/src/metro/patches.ts b/lib/modules/src/metro/patches.ts index c0c2d65..485587e 100644 --- a/lib/modules/src/metro/patches.ts +++ b/lib/modules/src/metro/patches.ts @@ -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()