From e5178632ba73ea98357da403d648d8ed9425b1d3 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Wed, 9 Apr 2025 03:42:06 +0700 Subject: [PATCH] feat(lib/modules/finders)!: refactor, fix `find` bugs, add more `lookup` shorthands, add `moduleStateAware` filter --- lib/modules/src/finders/_internal.ts | 19 +++- lib/modules/src/finders/filters.ts | 72 ++++++++++----- lib/modules/src/finders/find.ts | 117 ++++++++++++++++-------- lib/modules/src/finders/lookup.ts | 130 ++++++++++++++++++++------- lib/modules/src/finders/wait.ts | 26 +++--- 5 files changed, 261 insertions(+), 103 deletions(-) diff --git a/lib/modules/src/finders/_internal.ts b/lib/modules/src/finders/_internal.ts index c74f67f..6dcb81b 100644 --- a/lib/modules/src/finders/_internal.ts +++ b/lib/modules/src/finders/_internal.ts @@ -49,10 +49,23 @@ const FilterResultFlags = { export type FilterResultFlag = (typeof FilterResultFlags)[keyof typeof FilterResultFlags] // The reason this returns a flag is because flags are never falsy, while exports may be falsy when using ID-only filters (eg. `byDependencies`). -export function runFilter( - filter: F, + +// 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, id: Metro.ModuleID): FilterResultFlag | false + +export function runFilter( + filter: Filter, id: Metro.ModuleID, - exports: F extends Filter ? Metro.ModuleExports : undefined, + exports: Metro.ModuleExports, + options?: RunFilterOptions, +): FilterResultFlag | false + +export function runFilter( + filter: Filter, + id: Metro.ModuleID, + exports?: Metro.ModuleExports, options?: RunFilterOptions, ): FilterResultFlag | false { if (exports?.__esModule) { diff --git a/lib/modules/src/finders/filters.ts b/lib/modules/src/finders/filters.ts index 6cc9750..444f930 100644 --- a/lib/modules/src/finders/filters.ts +++ b/lib/modules/src/finders/filters.ts @@ -1,16 +1,17 @@ -import { _mMetadatas } from '../metro/_internal' +import { _mInited, _mMetadatas } from '../metro/_internal' import type { If, LogicalOr } from '@revenge-mod/utils/types' import type { Metro } from '../../types/metro' export type FilterResult = F extends Filter ? R : never -export type IsFilterWithExports = F extends Filter ? W : never +export type IsFilterWithExports = F extends Filter ? WE : never -export type IsFilterGeneratorWithExports = F extends (...args: any[]) => infer F ? IsFilterWithExports : never - -export interface Filter<_Inferable = any, WithExports extends boolean = boolean> { - (id: Metro.ModuleID, exports: If): boolean +export type Filter<_Inferable = any, WithExports extends boolean = boolean> = If< + WithExports, + (id: Metro.ModuleID, exports: Metro.ModuleExports) => boolean, + (id: Metro.ModuleID, exports?: undefined) => boolean +> & { key: string } @@ -38,15 +39,24 @@ export interface Filter<_Inferable = any, WithExports extends boolean = boolean> * * @see {@link byProps} for an example on custom-typed filters. */ -export function createFilterGenerator( - filter: (args: A, id: Metro.ModuleID, exps: If) => boolean, +export function createFilterGenerator( + filter: (args: A, id: Metro.ModuleID, exports: Metro.ModuleExports) => boolean, keyFor: (args: A) => string, -) { +): (...args: A) => Filter + +export function createFilterGenerator( + filter: (args: A, id: Metro.ModuleID) => boolean, + keyFor: (args: A) => string, +): (...args: A) => Filter + +export function createFilterGenerator( + filter: (args: A, id: Metro.ModuleID, exports?: Metro.ModuleExports) => boolean, + keyFor: (args: A) => string, +): (...args: any[]) => Filter { return (...args: A) => { - const f = (id: Metro.ModuleID, exps: If) => filter(args, id, exps) + const f = (id: Metro.ModuleID, exports?: Metro.ModuleExports) => filter(args, id, exports) f.key = keyFor(args) - // biome-ignore lint/complexity/noBannedTypes: So it doesn't turn into `any` when you combine it with other typed filters - return f as Filter<{}, WithExports> + return f } } @@ -67,7 +77,7 @@ export type ByProps = = Record>( * // const React: typeof import('react') * ``` */ -export const byProps = createFilterGenerator, IsFilterGeneratorWithExports>( +export const byProps = createFilterGenerator>( (props, _, exports) => props.every(prop => exports[prop]), props => `revenge.props(${props.join(',')})`, ) as ByProps @@ -106,7 +116,7 @@ export type ByName = ( * const SomeClass = await findModule(byName<{ new(param: string): SomeClass }>('SomeClass')) * // const SomeClass: { new(): SomeClass, name: 'SomeClass' } */ -export const byName = createFilterGenerator, IsFilterGeneratorWithExports>( +export const byName = createFilterGenerator>( ([name], _, exports) => exports.name === name, ([name]) => `revenge.name(${name})`, ) as ByName @@ -123,7 +133,7 @@ export const byName = createFilterGenerator, IsFilterGenerato * const Logger = await findModule(byDependencies([4, , 2])) * ``` */ -export const byDependencies = createFilterGenerator<[Array], false>( +export const byDependencies = createFilterGenerator<[Array]>( ([deps], id) => { const actDeps = _mMetadatas.get(id)![0] if (actDeps.length === deps.length) @@ -167,14 +177,14 @@ export function every( > export function every(...filters: Filter[]): Filter { - const cf: Filter = (id, exports) => { + const f = (id: Metro.ModuleID, exports?: Metro.ModuleExports) => { for (const filter of filters) if (!filter(id, exports)) return false return true } - cf.key = `revenge.every(${filters.map(filter => filter.key).join(',')})` + f.key = `revenge.every(${filters.map(filter => filter.key).join(',')})` - return cf + return f } /** @@ -206,12 +216,32 @@ export function some( > export function some(...filters: Filter[]): Filter { - const cf: Filter = (id, exports) => { + const f = (id: Metro.ModuleID, exports?: Metro.ModuleExports) => { for (const filter of filters) if (filter(id, exports)) return true return false } - cf.key = `revenge.some(${filters.map(filter => filter.key).join(',')})` + f.key = `revenge.some(${filters.map(filter => filter.key).join(',')})` - return cf + return f +} + +/** + * Filter modules depending on their initialized state. + * + * @param initializedFilter The filter to use for initialized modules. + * @param uninitializedFilter The filter to use for uninitialized modules. + */ +export function moduleStateAware( + initializedFilter: IF, + uninitializedFilter: Filter, +): Filter, false> { + const f = (id: Metro.ModuleID, exports?: Metro.ModuleExports) => { + if (_mInited.has(id)) return initializedFilter(id, exports) + return uninitializedFilter(id) + } + + f.key = `revenge.moduleStateAware(${initializedFilter.key},${uninitializedFilter.key})` + + return f } diff --git a/lib/modules/src/finders/find.ts b/lib/modules/src/finders/find.ts index ae19d90..f50dad9 100644 --- a/lib/modules/src/finders/find.ts +++ b/lib/modules/src/finders/find.ts @@ -1,10 +1,11 @@ -import { type LookupModulesOptions, lookupModuleByImportedPath, lookupModuleIds, lookupModules } from './lookup' +import { type ProxifyOptions, proxify } from '@revenge-mod/utils/proxy' +import { type LookupModulesOptions, lookupModule, lookupModuleByImportedPath, lookupModuleId } from './lookup' import { type WaitForModulesOptions, waitForModuleByImportedPath, waitForModules } from './wait' -import { type ProxifyOptions, proxify } from '@revenge-mod/utils/proxy' import type { LookupModuleIdsOptions, MaybeDefaultExportMatched } from '.' import type { Metro } from '../../types/metro' import type { Filter, FilterResult } from './filters' +import type { RunFilterReturnExportsOptions } from './_internal' export type BaseFindModuleOptions = { /** @@ -14,7 +15,7 @@ export type BaseFindModuleOptions = { // TODO(modules/finders/find::options): Add an option to force initialize when module ID is known (find is cached). } -export type FindModuleOptions = WaitForModulesOptions & BaseFindModuleOptions +export type FindModuleOptions = WaitForModulesOptions & LookupModulesOptions & BaseFindModuleOptions export type FindModuleIdOptions = LookupModuleIdsOptions & FindModuleOptions @@ -34,29 +35,45 @@ export type FindModuleIdOptions = LookupModuleIdsOptions & FindModuleOptions * ``` */ export function findModule(filter: F): Promise> + export function findModule( filter: F, options: O, -): Promise ? MaybeDefaultExportMatched> : FilterResult> +): Promise ? MaybeDefaultExportMatched> : FilterResult> + export function findModule(filter: F, options?: O) { type Result = FilterResult - return new Promise ? MaybeDefaultExportMatched : Result>((ok, err) => { - for (const exports of lookupModules(filter, options!)) return ok(exports) + return new Promise ? MaybeDefaultExportMatched : Result>( + (ok, err) => { + const exports = lookupModule(filter, options!) + if (exports !== undefined) return ok(exports) - const unsub = waitForModules(filter, (_, exports) => ok(exports), options) + const unsub = waitForModules( + filter, + (_, exports) => { + unsub() + ok(exports) + }, + options!, + ) - const onAbort = () => { - unsub() - err(new Error(`${findModule.name} aborted before resolved: ${filter.key}`)) - } + const onAbort = () => { + unsub() + err(new Error(`${findModule.name} aborted before resolved: ${filter.key}`)) + } - const signal = options?.abortSignal - if (signal) { - if (signal.aborted) return onAbort() - signal.addEventListener('abort', onAbort, { once: true }) - } - }) + const signal = options?.abortSignal + if (signal) { + if (signal.aborted) { + onAbort() + return + } + + signal.addEventListener('abort', onAbort, { once: true }) + } + }, + ) } /** @@ -75,15 +92,28 @@ export function findModule(filter * const ReactNative = __r(ReactNativeId) */ export function findModuleId(filter: F): Promise + export function findModuleId( filter: F, options: O, ): Promise ? MaybeDefaultExportMatched> : FilterResult> + export function findModuleId(filter: Filter, options?: FindModuleIdOptions) { return new Promise((ok, err) => { - for (const id of lookupModuleIds(filter, options)) return ok(id) + const id = lookupModuleId(filter, options) + if (id !== undefined) { + ok(id) + return + } - const unsub = waitForModules(filter, (_, id) => ok(id), options) + const unsub = waitForModules( + filter, + id => { + unsub() + ok(id) + }, + options!, + ) const onAbort = () => { unsub() @@ -92,7 +122,11 @@ export function findModuleId(filter: Filter, options?: FindModuleIdOptions) { const signal = options?.abortSignal if (signal) { - if (signal.aborted) return onAbort() + if (signal.aborted) { + onAbort() + return + } + signal.addEventListener('abort', onAbort, { once: true }) } }) @@ -104,12 +138,18 @@ export function findModuleId(filter: Filter, options?: FindModuleIdOptions) { * @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 the module is found. */ -export function findModuleByImportedPath(path: string, options?: BaseFindModuleOptions) { - return new Promise((ok, err) => { - const m = lookupModuleByImportedPath(path) - if (m) return ok(m) +export function findModuleByImportedPath(path: string, options?: BaseFindModuleOptions): Promise { + return new Promise((ok, err) => { + const exports = lookupModuleByImportedPath(path) + if (exports !== undefined) { + ok(exports) + return + } - const unsub = waitForModuleByImportedPath(path, (_, exports) => ok(exports)) + const unsub = waitForModuleByImportedPath(path, (_, exports) => { + unsub() + ok(exports) + }) const onAbort = () => { unsub() @@ -118,7 +158,11 @@ export function findModuleByImportedPath(path: string, options?: BaseFindModuleO const signal = options?.abortSignal if (signal) { - if (signal.aborted) return onAbort() + if (signal.aborted) { + onAbort() + return + } + signal.addEventListener('abort', onAbort, { once: true }) } }) @@ -134,32 +178,35 @@ export type FindModuleSyncOptions = FindModuleOptions & BaseFindModuleSyncOption * @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. * - * @example A little bit after index module is required: + * @example * ```ts * const React = findModuleSync(byProps('createElement'), { hint: 'object' }) * const App = findModuleSync(byName('App')) * - * // React is defined here because React is loaded before index module is required + * // No proxification happens because React is loaded before index module is required * console.log(React) * // { createElement: ..., ... } * * console.log(App) - * // undefined + * // Logs: [Function], but is actually undefined, calling it will throw an error * * onAppInitialized(() => { * // When the app is initialized, the App component must also be initialized * console.log(App) - * // function App() { ... } + * // Logs: [Function], but is actually function App() { ... } * }) * ``` */ export function findModuleSync(filter: F): FilterResult + export function findModuleSync( filter: F, options: O, ): O extends LookupModulesOptions ? MaybeDefaultExportMatched> : FilterResult + export function findModuleSync(filter: F, options?: O) { - for (const exports of lookupModules(filter, options!)) return exports + const exports = lookupModule(filter, options!) + if (exports !== undefined) return exports let value: unknown | undefined const unsub = waitForModules( @@ -168,7 +215,7 @@ export function findModuleSync( path: string, options?: BaseFindModuleSyncOptions, ): T | undefined { - const m = lookupModuleByImportedPath(path) - if (m) return m + const exports = lookupModuleByImportedPath(path) + if (exports !== undefined) return exports let value: unknown | undefined const unsub = waitForModuleByImportedPath(path, (_, exports) => { @@ -206,5 +253,5 @@ export function findModuleByImportedPathSync( else signal.addEventListener('abort', unsub, { once: true }) } - return proxify(() => value, options) + return proxify(() => value as T | undefined, options) } diff --git a/lib/modules/src/finders/lookup.ts b/lib/modules/src/finders/lookup.ts index 025b060..cfc3017 100644 --- a/lib/modules/src/finders/lookup.ts +++ b/lib/modules/src/finders/lookup.ts @@ -10,16 +10,7 @@ import type { MaybeDefaultExportMatched } from '.' import type { Metro } from '../../types/metro' import type { Filter, FilterResult } from './filters' -export type BaseLookupModulesOptions = RunFilterOptions - -export type LookupModulesOptions = - RunFilterReturnExportsOptions - -export type LookupModulesResult = O extends LookupModulesOptions - ? MaybeDefaultExportMatched> - : FilterResult - -export type LookupModuleIdsOptions = BaseLookupModulesOptions & { +export type BaseLookupModulesOptions = RunFilterOptions & { /** * Whether to include initialized modules in the lookup. * @@ -31,13 +22,29 @@ export type LookupModuleIdsOptions = RunFilterReturnExportsOptions & BaseLookupModulesOptions + +export type LookupModulesResult< + O extends LookupModulesOptions, + F extends Filter, +> = O extends RunFilterReturnExportsOptions ? MaybeDefaultExportMatched> : FilterResult + +export type LookupModuleIdsOptions = + BaseLookupModulesOptions + /** - * Lookup module IDs by its exports. You may lookup both uninitialized module IDs when filtering via ID-only filters (eg. `byDependencies`). + * Lookup module IDs by their exports. + * You may lookup both uninitialized module IDs by passing `options.includeUninitialized` when filtering via ID-only filters (eg. `byDependencies`). * * @param filter The filter to use. * @param options The options to use for the lookup. @@ -67,38 +74,99 @@ export function* lookupModuleIds( if (runFilter(filter, id, exports, options)) yield id } - if (options?.includeUninitialized) - for (const id of _mUninited) if (runFilter>(filter, id, undefined, options)) yield id + if (options?.includeUninitialized) for (const id of _mUninited) if (runFilter(filter, id)) yield id } -export function lookupModules(filter: F): Generator, undefined> - -export function lookupModules( - filter: F, - options: O, -): Generator, undefined> - /** - * Lookup modules by its exports. - * - * `lookupModules` only filters initialized modules. You will need to use `waitForModules` to filter initializing modules. + * Lookup initialized modules by their exports. * * @param filter The filter to use. * @param options The options to use for the lookup. * @returns A generator that yields the module exports that match the filter. */ -export function* lookupModules(filter: F, options?: O) { - for (const id of _mInited) { - const { exports } = _mMetadatas.get(id)![1]! - const result = runFilterReturnExports(filter, id, exports, options) - if (result) yield result - } +export function lookupModules(filter: F): Generator, undefined> + +export function lookupModules< + F extends O extends BaseLookupModulesOptions ? Filter : Filter, + O extends LookupModulesOptions, +>(filter: F, options: O): Generator, undefined> + +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 + } + + if (options?.includeUninitialized) for (const id of _mUninited) if (runFilter(filter, id)) yield __r(id) } /** - * Lookup module exports by its imported path. + * Lookup a module ID by its exports. Skipping creating a `Generator`. * - * `lookupModuleByImportedPath` only finds initialized modules. You will need to use `waitForModuleByImportedPath` to filter initializing modules. + * @see {@link lookupModuleIds} for more documentation. + * + * @param filter The filter to use. + * @param options The options to use for the lookup. + * @returns A generator that yields the module exports that match the filter. + * + * @example + * ```ts + * const id = lookupModuleIds(byProps('createElement')) + * // Log the first module ID that exports React + * console.log(id) + * ``` + * + * @example Lookup uninitialized modules + * ```ts + * const id = lookupModuleIds(byDependencies([...]), { includeUninitialized: true }) + * // Log the first module ID that has those dependencies + * console.log(id) + * ``` + */ +export function lookupModuleId( + filter: O extends BaseLookupModulesOptions ? Filter : Filter, + 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 + } + + if (options?.includeUninitialized) for (const id of _mUninited) if (runFilter(filter, id)) return id +} + +/** + * Lookup an initialized module by its exports. Skipping creating a `Generator`. + * + * @see {@link lookupModules} for more documentation. + * + * @param filter The filter to use. + * @param options The options to use for the lookup. + * @returns The first module exports that match the filter. + */ +export function lookupModule(filter: F): FilterResult | undefined + +export function lookupModule( + filter: F, + options: O, +): LookupModulesResult | undefined + +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 + } + + if (options?.includeUninitialized) for (const id of _mUninited) if (runFilter(filter, id)) return __r(id) +} + +/** + * Lookup an initialized module by its imported path. * * @param path The path to lookup the module by. * @returns The module exports if the module is initialized, or undefined if the module is not found or not initialized. diff --git a/lib/modules/src/finders/wait.ts b/lib/modules/src/finders/wait.ts index 9507191..b6e7eab 100644 --- a/lib/modules/src/finders/wait.ts +++ b/lib/modules/src/finders/wait.ts @@ -1,19 +1,21 @@ import { onAnyModuleInitialized, onModuleFinishedImporting } from '../metro' import { _bl, _mMetadatas } from '../metro/_internal' -import { runFilterReturnExports } from './_internal' +import { runFilterReturnExports, type RunFilterReturnExportsOptions } from './_internal' import type { MaybeDefaultExportMatched } from '.' import type { Metro } from '../../types/metro' import type { Filter, FilterResult } from './filters' -import type { LookupModulesOptions } from './lookup' -export type WaitForModulesOptions = LookupModulesOptions +export type WaitForModulesOptions = + RunFilterReturnExportsOptions + +export type WaitForModulesResult< + F extends Filter, + O extends WaitForModulesOptions, +> = O extends RunFilterReturnExportsOptions ? MaybeDefaultExportMatched> : FilterResult /** - * Wait for modules to initialize by its exports. - * This is the way to find **uninitialized** module exports that match a filter. - * - * **Callback won't be called if the module is already initialized! Use `lookupModule` for already initialized modules.** + * Wait for modules to initialize by their exports. **Callback won't be called if the module is already initialized!** * * @param filter The filter to use. * @param callback The callback to call when matching modules are initialized. @@ -22,7 +24,7 @@ export type WaitForModulesOptions = LookupModulesOptions * * @example * ```ts - * waitForModule( + * waitForModules( * byName('FlashList'), * // (exports: typeof import('@shopify/flash-list'), id: Metro.ModuleID) => any * (id, exports) => {...} @@ -34,12 +36,9 @@ export function waitForModules( callback: (id: Metro.ModuleID, exports: FilterResult) => any, ): () => void -export function waitForModules( +export function waitForModules( filter: F, - callback: ( - id: Metro.ModuleID, - exports: O extends LookupModulesOptions ? MaybeDefaultExportMatched> : FilterResult, - ) => any, + callback: (id: Metro.ModuleID, exports: WaitForModulesResult) => any, options: O, ): () => void @@ -59,6 +58,7 @@ export function waitForModules( /** * Wait for a module to initialize by its imported path. + * * @param path The path to wait for. * @param callback The callback to call once the module is initialized. * @returns A function to unsubscribe.