From af23c2352e6f443913053ebb5d723c277fd687ed Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Wed, 16 Apr 2025 14:23:48 +0700 Subject: [PATCH] feat(lib/modules/finders)!: add ability to use finders with blacklisted modules, add `isModuleInitialized` Metro util, added and changed filters --- lib/modules/src/finders/_internal.ts | 13 +--- lib/modules/src/finders/filters.ts | 43 ++++++++++- lib/modules/src/finders/find.ts | 18 ++++- lib/modules/src/finders/lookup.ts | 78 +++++++++++++++----- lib/modules/src/finders/wait.ts | 61 ++++++++++----- lib/modules/src/metro/_internal.ts | 7 +- lib/modules/src/metro/subscriptions/index.ts | 12 +-- lib/modules/src/metro/utils.ts | 26 ++++--- 8 files changed, 189 insertions(+), 69 deletions(-) diff --git a/lib/modules/src/finders/_internal.ts b/lib/modules/src/finders/_internal.ts index 784142e..f5d54e1 100644 --- a/lib/modules/src/finders/_internal.ts +++ b/lib/modules/src/finders/_internal.ts @@ -89,16 +89,11 @@ export function runFilter( return false } -export function runFilterReturnExports( - filter: Filter, - id: Metro.ModuleID, +export function exportsFromFilterResultFlag( + flag: FilterResultFlag, exports: Metro.ModuleExports, options?: RunFilterReturnExportsOptions, ) { - const flag = runFilter(filter, id, exports, options) - - if (flag) { - if (flag === FilterResultFlags.ESModuleDefault && !options?.esmReturnNamespace) return exports.default - return exports - } + if (flag === FilterResultFlags.ESModuleDefault && !options?.esmReturnNamespace) return exports.default + return exports } diff --git a/lib/modules/src/finders/filters.ts b/lib/modules/src/finders/filters.ts index 898b6ab..4e65a99 100644 --- a/lib/modules/src/finders/filters.ts +++ b/lib/modules/src/finders/filters.ts @@ -76,7 +76,7 @@ export type ByProps = = Record>( * ``` */ export const byProps = createFilterGenerator>( - (props, _, exports) => typeof exports === 'object' && props.every(prop => prop in exports), + (props, _, exports) => props.every(prop => prop in exports), props => `revenge.props(${props.join(',')})`, ) as ByProps @@ -274,7 +274,7 @@ export type ModuleStateAware = ( ) => Filter, false> /** - * Filter modules depending on their initialized state. + * Filter modules depending on their initialized state. **Initialized modules with bad exports are skipped.** * * @param initializedFilter The filter to use for initialized modules. * @param uninitializedFilter The filter to use for uninitialized modules. @@ -291,8 +291,45 @@ export type ModuleStateAware = ( */ export const moduleStateAware = createFilterGenerator>( ([initializedFilter, uninitializedFilter], id, exports) => { - if (_mInited.has(id)) return initializedFilter(id, exports) + if (isModuleInitialized(id)) { + if (initializedModuleHasBadExports(id)) return initializedFilter(id, exports) + return false + } + return uninitializedFilter(id) }, ([f1, f2]) => `revenge.moduleStateAware(${f1.key},${f2.key})`, ) as ModuleStateAware + +export type PreferExports = ( + withExportsFilter: WEF, + exportslessFilter: Filter, +) => Filter, false> + +/** + * Filter modules depending on if their exports are available and filterable. + * + * @see {@link isModuleExportsBad} for more information on what is considered bad module exports. + * + * @see {@link moduleStateAware} for an alternative that filters based on the module's initialized state. + * + * @param withExportsFilter The filter to use for modules with proper exports. + * @param exportslessFilter The filter to use for modules without proper exports (uninitialized or bad). + * + * @example + * ```ts + * // will filter byProps('x') for modules with proper exports + * // and byDependencies([1, 485, , 2]) for without proper exports (uninitialized or bad) + * const SomeModule = await findModule(preferExports( + * byProps('x'), + * byDependencies([1, 485, , 2]), + * )) + * ``` + */ +export const preferExports = createFilterGenerator>( + ([withExportsFilter, exportslessFilter], id, exports) => { + if (_mInited.has(id)) return withExportsFilter(id, exports) + return exportslessFilter(id) + }, + ([f1, f2]) => `revenge.preferExports(${f1.key},${f2.key})`, +) as PreferExports diff --git a/lib/modules/src/finders/find.ts b/lib/modules/src/finders/find.ts index 6a766b4..b8c0219 100644 --- a/lib/modules/src/finders/find.ts +++ b/lib/modules/src/finders/find.ts @@ -6,6 +6,7 @@ import type { LookupModuleIdsOptions, MaybeDefaultExportMatched } from '.' import type { Metro } from '../../types/metro' import type { Filter, FilterResult } from './filters' import type { RunFilterReturnExportsOptions } from './_internal' +import type { If } from '@revenge-mod/utils/types' export type BaseFindModuleOptions = { /** @@ -22,8 +23,10 @@ export type FindModuleOptions< LookupModulesOptions & BaseFindModuleOptions -export type FindModuleIdOptions = - LookupModuleIdsOptions & BaseFindModuleOptions +export type FindModuleIdOptions< + IncludeUninitialized extends boolean = boolean, + IncludeAll extends If = If, +> = LookupModuleIdsOptions & BaseFindModuleOptions export type FindModuleResult< F extends Filter, @@ -33,6 +36,9 @@ export type FindModuleResult< /** * Find a module. * + * @see {@link lookupModule} + * @see {@link waitForModules} + * * @param filter The filter to use to find the module. * @param options The options to use for the find. * @returns A promise that resolves to the module's exports or rejects if the find is aborted before a module is found. @@ -87,6 +93,9 @@ export function findModule(filter: Filter, options?: FindModuleOptions) { /** * Find a module ID. * + * @see {@link lookupModuleId} + * @see {@link waitForModules} + * * @param filter The filter to use to find the module. * @param options The options to use for the find. * @returns A promise that resolves to the module ID or rejects if the find is aborted before a module ID is found. @@ -191,6 +200,11 @@ export type FindModuleSyncOptions< /** * Find a module synchronously. * + * @see {@link lookupModule} + * @see {@link waitForModules} + * + * @see {@link proxify} for how proxification works. + * * @param filter The filter to use to find the module. * @param options The options to use for the find. * @returns The module exports object if the module is already initialized, or proxified object of the module exports once the module is initialized, or undefined if otherwise. diff --git a/lib/modules/src/finders/lookup.ts b/lib/modules/src/finders/lookup.ts index a9c2ed0..39ffae0 100644 --- a/lib/modules/src/finders/lookup.ts +++ b/lib/modules/src/finders/lookup.ts @@ -1,9 +1,9 @@ -import { _mInited, _mPaths, _mUninited } from '../metro/_internal' +import { _mInited, _mMd, _mPaths, _mUninited } from '../metro/_internal' import { type RunFilterOptions, type RunFilterReturnExportsOptions, runFilter, - runFilterReturnExports, + exportsFromFilterResultFlag, } from './_internal' import { getInitializedModuleExports } from '../metro' @@ -11,6 +11,7 @@ import { getInitializedModuleExports } from '../metro' import type { MaybeDefaultExportMatched } from '.' import type { Metro } from '../../types/metro' import type { Filter, FilterResult } from './filters' +import type { If } from '@revenge-mod/utils/types' export type BaseLookupModulesOptions = RunFilterOptions & { /** @@ -41,8 +42,23 @@ export type LookupModulesResult< O extends LookupModulesOptions, > = O extends RunFilterReturnExportsOptions ? MaybeDefaultExportMatched> : FilterResult -export type LookupModuleIdsOptions = - BaseLookupModulesOptions +export type LookupModuleIdsOptions< + IncludeUninitialized extends boolean = boolean, + IncludeAll extends If = If, +> = RunFilterOptions & + ( + | (BaseLookupModulesOptions & { + includeAll?: false + }) + | ({ + /** + * Whether to include all modules, including ones with bad exports. + * + * @default false + */ + includeAll: IncludeAll + } & BaseLookupModulesOptions) + ) /** * Lookup module IDs. @@ -50,6 +66,9 @@ export type LookupModuleIdsOptions( filter: O extends LookupModuleIdsOptions ? Filter : Filter, options?: O, ): Generator { - if (options?.includeInitialized ?? true) - for (const id of _mInited) if (runFilter(filter, id, getInitializedModuleExports(id), options)) yield id + if (options?.includeAll) { + for (const id of _mMd.keys()) if (runFilter(filter, id, getInitializedModuleExports(id), options)) yield id + } else { + if (options?.includeInitialized ?? true) + 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 + if (options?.includeUninitialized) for (const id of _mUninited) if (runFilter(filter, id)) yield id + } } /** @@ -105,8 +128,9 @@ export function lookupModules< export function* lookupModules(filter: Filter, options?: LookupModulesOptions) { if (options?.includeInitialized ?? true) for (const id of _mInited) { - const result = runFilterReturnExports(filter, id, getInitializedModuleExports(id), options) - if (result != null) yield result + const exports = getInitializedModuleExports(id) + const flag = runFilter(filter, id, exports, options) + if (flag) yield exportsFromFilterResultFlag(flag, exports, options) } if (options?.includeUninitialized) for (const id of _mUninited) if (runFilter(filter, id)) yield __r(id) @@ -139,10 +163,14 @@ export function lookupModuleId( filter: O extends BaseLookupModulesOptions ? Filter : Filter, options?: O, ): Metro.ModuleID | undefined { - if (options?.includeInitialized ?? true) - for (const id of _mInited) if (runFilter(filter, id, getInitializedModuleExports(id), options)) return id + if (options?.includeAll) { + for (const id of _mMd.keys()) if (runFilter(filter, id, getInitializedModuleExports(id), options)) return id + } else { + if (options?.includeInitialized ?? true) + 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 + if (options?.includeUninitialized) for (const id of _mUninited) if (runFilter(filter, id)) return id + } } /** @@ -169,13 +197,29 @@ export function lookupModule( export function lookupModule(filter: Filter, options?: LookupModulesOptions) { if (options?.includeInitialized ?? true) for (const id of _mInited) { - const result = runFilterReturnExports(filter, id, getInitializedModuleExports(id), options) - if (result != null) return result + const exports = getInitializedModuleExports(id) + const flag = runFilter(filter, id, exports, options) + if (flag) return exportsFromFilterResultFlag(flag, exports, options) } if (options?.includeUninitialized) for (const id of _mUninited) if (runFilter(filter, id)) return __r(id) } +/** + * Lookup a module ID by its imported path. **The module may have bad exports.** + * + * @param path The path to lookup the module ID by. + * @returns The module ID if the module is initialized, or `undefined` if the module is not found or not initialized. + * + * @example + * ```ts + * const LoggerId = lookupModuleIdByImportedPath('modules/debug/Logger.tsx') + * ``` + */ +export function lookupModuleIdByImportedPath(path: string): Metro.ModuleID | undefined { + return _mPaths.get(path) +} + /** * Lookup an initialized module by its imported path. * @@ -190,8 +234,6 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) { * ``` */ export function lookupModuleByImportedPath(path: string): T | undefined { - const id = _mPaths.get(path) - if (id === undefined) return - - return getInitializedModuleExports(id) + const id = lookupModuleIdByImportedPath(path) + return getInitializedModuleExports(id!) } diff --git a/lib/modules/src/finders/wait.ts b/lib/modules/src/finders/wait.ts index 86f50d1..b946c82 100644 --- a/lib/modules/src/finders/wait.ts +++ b/lib/modules/src/finders/wait.ts @@ -1,12 +1,28 @@ -import { getInitializedModuleExports, isBlacklisted, onAnyModuleInitialized, onModuleFinishedImporting } from '../metro' -import { runFilterReturnExports, type RunFilterReturnExportsOptions } from './_internal' +import { exportsFromFilterResultFlag, runFilter, type RunFilterReturnExportsOptions } from './_internal' +import { + getInitializedModuleExports, + initializedModuleHasBadExports, + onAnyModuleInitialized, + onModuleFinishedImporting, +} from '../metro' import type { MaybeDefaultExportMatched } from '.' import type { Metro } from '../../types/metro' import type { Filter, FilterResult } from './filters' -export type WaitForModulesOptions = - RunFilterReturnExportsOptions +export type BaseWaitForModulesOptions = { + /** + * Whether to include all modules, including ones with bad exports. + * + * @default false + */ + includeAll?: IncludeAll +} + +export type WaitForModulesOptions< + ReturnNamespace extends boolean = boolean, + IncludeBadExports extends boolean = boolean, +> = RunFilterReturnExportsOptions & BaseWaitForModulesOptions export type WaitForModulesResult< F extends Filter, @@ -38,28 +54,32 @@ export function waitForModules( callback: (id: Metro.ModuleID, exports: FilterResult) => any, ): () => void -export function waitForModules( - filter: F, - callback: (id: Metro.ModuleID, exports: WaitForModulesResult) => any, - options: O, -): () => void +export function waitForModules< + F extends O extends WaitForModulesOptions ? Filter : Filter, + O extends WaitForModulesOptions, +>(filter: F, callback: (id: Metro.ModuleID, exports: WaitForModulesResult) => any, options: O): () => void export function waitForModules( filter: Filter, callback: (id: Metro.ModuleID, exports: Metro.ModuleExports) => any, options?: WaitForModulesOptions, ) { - const unsub = onAnyModuleInitialized((id, exports) => { - if (isBlacklisted(id)) return - const result = runFilterReturnExports(filter, id, exports, options) - if (result != null) callback(id, exports) - }) - - return unsub + return options?.includeAll + ? onAnyModuleInitialized((id, exports) => { + const flag = runFilter(filter, id, exports, options) + if (flag) callback(id, exportsFromFilterResultFlag(flag, exports, options)) + }) + : onAnyModuleInitialized((id, exports) => { + if (initializedModuleHasBadExports(id)) return + const flag = runFilter(filter, id, exports, options) + if (flag) callback(id, exportsFromFilterResultFlag(flag, exports, options)) + }) } /** - * Wait for a module to initialize by its imported path. Once callback is called, the subscription will be removed automatically. + * Wait for a module to initialize by its imported path. **Callback won't be called if the module is already initialized!** + * + * Once callback is called, the subscription will be removed automatically, because modules have unique imported paths. * * Think of it as if you are doing `import * as exports from path`, and you are also waiting for the app to initialize the module by itself. * @@ -77,10 +97,15 @@ export function waitForModules( * ) * ``` */ -export function waitForModuleByImportedPath(path: string, callback: (id: Metro.ModuleID, exports: T) => any) { +export function waitForModuleByImportedPath( + path: string, + callback: (id: Metro.ModuleID, exports: T) => any, + options?: BaseWaitForModulesOptions, +) { const unsub = onModuleFinishedImporting((id, cmpPath) => { if (path === cmpPath) { unsub() + if (!options?.includeAll && initializedModuleHasBadExports(id)) return callback(id, getInitializedModuleExports(id)) } }) diff --git a/lib/modules/src/metro/_internal.ts b/lib/modules/src/metro/_internal.ts index 13d25f0..c02615d 100644 --- a/lib/modules/src/metro/_internal.ts +++ b/lib/modules/src/metro/_internal.ts @@ -11,18 +11,19 @@ export const _mUninited = new Set() export const _mInited = new Set() export const _mPaths = new Map() -export const _mMd = 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 _mMd extends Map ? V : never + const metadata = [deps!, false] as typeof _mMd extends Map ? V : never _mMd.set(id, metadata) _mUninited.add(id) metroDefine( ((global, req, importDefault, importAll, module, exports, deps) => { - metadata[1] = module + metadata[1] = true + metadata[2] = module const prevIId = _mInitingId _mInitingId = id diff --git a/lib/modules/src/metro/subscriptions/index.ts b/lib/modules/src/metro/subscriptions/index.ts index 38f05c9..80300be 100644 --- a/lib/modules/src/metro/subscriptions/index.ts +++ b/lib/modules/src/metro/subscriptions/index.ts @@ -1,6 +1,6 @@ import { _initAll, _importedPath, _initSpecific, _reqSpecific, _reqAll } from './_internal' -import type { isInitializedModuleBlacklisted } from '../utils' +import type { initializedModuleHasBadExports } from '../utils' import type { Metro } from '../../../types/metro' export type ModuleRequiredCallback = (id: Metro.ModuleID) => void @@ -10,7 +10,7 @@ export type ModuleWithImportedPathInitializedCallback = (id: Metro.ModuleID, pat /** * Registers a callback to be called when any module is initialized. * - * @see {@link isInitializedModuleBlacklisted} to avoid bad module exports. + * @see {@link initializedModuleHasBadExports} to avoid bad module exports. * * @param callback The callback to be called. * @returns A function that unregisters the callback. @@ -23,7 +23,7 @@ export function onAnyModuleInitialized(callback: ModuleInitializedCallback) { /** * Registers a callback to be called when a specific module is initialized. * - * @see {@link isInitializedModuleBlacklisted} to avoid bad module exports. + * @see {@link initializedModuleHasBadExports} to avoid bad module exports. * * @param id The ID of the module. * @param callback The callback to be called. @@ -39,7 +39,7 @@ export function onModuleInitialized(id: Metro.ModuleID, callback: ModuleInitiali /** * Registers a callback to be called when a module with a specific import path is initialized. * - * @see {@link isInitializedModuleBlacklisted} to avoid bad module exports. + * @see {@link initializedModuleHasBadExports} to avoid bad module exports. * * @param callback The callback to be called. * @returns A function that unregisters the callback. @@ -52,7 +52,7 @@ export function onModuleFinishedImporting(callback: ModuleWithImportedPathInitia /** * Registers a callback to be called when any module is required the first time. * - * @see {@link isInitializedModuleBlacklisted} to avoid bad module exports. + * @see {@link initializedModuleHasBadExports} to avoid bad module exports. * * @param callback The callback to be called. * @returns A function that unregisters the callback. @@ -65,7 +65,7 @@ export function onAnyModuleFirstRequired(callback: ModuleRequiredCallback) { /** * Registers a callback to be called when a specific module is required the first time. * - * @see {@link isInitializedModuleBlacklisted} to avoid bad module exports. + * @see {@link initializedModuleHasBadExports} to avoid bad module exports. * * @param id The ID of the module. * @param callback The callback to be called. diff --git a/lib/modules/src/metro/utils.ts b/lib/modules/src/metro/utils.ts index eb719e0..0bfa587 100644 --- a/lib/modules/src/metro/utils.ts +++ b/lib/modules/src/metro/utils.ts @@ -4,24 +4,24 @@ import { _mInited, _mMd, _mUninited } from './_internal' import type { Metro } from '../../types/metro' /** - * Returns whether an uninitialized module is blacklisted. + * Returns whether an uninitialized module has bad exports. * - * @see {@link isModuleExportsBad} for more information on what is considered a bad module export. + * @see {@link isModuleExportsBad} for more information on what is considered bad module exports. * * @param id The module ID. */ -export function isUninitializedModuleBlacklisted(id: Metro.ModuleID): boolean { +export function uninitializedModuleHasBadExports(id: Metro.ModuleID): boolean { return !_mUninited.has(id) } /** - * Returns whether an initialized module is blacklisted. + * Returns whether an initialized module has bad exports. * - * @see {@link isModuleExportsBad} for more information on what is considered a bad module export. + * @see {@link isModuleExportsBad} for more information on what is considered bad module exports. * * @param id The module ID. */ -export function isInitializedModuleBlacklisted(id: Metro.ModuleID): boolean { +export function initializedModuleHasBadExports(id: Metro.ModuleID): boolean { return !_mInited.has(id) } @@ -33,12 +33,20 @@ export function getModuleDependencies(id: Metro.ModuleID): Metro.DependencyMap | return _mMd.get(id)?.[0] } +/** + * Returns whether a module is initialized. + * @param id The module ID. + */ +export function isModuleInitialized(id: Metro.ModuleID): boolean { + return _mMd.get(id)?.[1]! +} + /** * Returns the exports of an initialized module. * @param id The module ID. */ export function getInitializedModuleExports(id: Metro.ModuleID): Metro.ModuleExports | undefined { - return _mMd.get(id)?.[1]?.exports + return _mMd.get(id)?.[2]?.exports } /** @@ -58,9 +66,7 @@ export function isModuleExportBad(exp: Metro.ModuleExports[PropertyKey]): boolea } /** - * Returns whether the module has bad exports. If it does, it will be blacklisted to avoid filtering issues. - * - * `/finders` will always avoid blacklisted modules. To look for blacklisted modules, use `/metro/subscriptions`. + * Returns whether the module has bad exports. If it does, it will be "blacklisted" to avoid filtering issues. * * **Which module exports are considered bad?** Anything not an object or function, or an empty object. *