feat(lib/modules/finders)!: refactor, fix find bugs, add more lookup shorthands, add moduleStateAware filter

This commit is contained in:
PalmDevs 2025-04-09 03:42:06 +07:00
parent e992183879
commit e5178632ba
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
5 changed files with 261 additions and 103 deletions

View file

@ -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<F extends Filter>(
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<any, false>, id: Metro.ModuleID): FilterResultFlag | false
export function runFilter(
filter: Filter,
id: Metro.ModuleID,
exports: F extends Filter<any, true> ? 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) {

View file

@ -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> = F extends Filter<infer R, boolean> ? R : never
export type IsFilterWithExports<F> = F extends Filter<any, infer W> ? W : never
export type IsFilterWithExports<F> = F extends Filter<any, infer WE> ? WE : never
export type IsFilterGeneratorWithExports<F> = F extends (...args: any[]) => infer F ? IsFilterWithExports<F> : never
export interface Filter<_Inferable = any, WithExports extends boolean = boolean> {
(id: Metro.ModuleID, exports: If<WithExports, Metro.ModuleExports, never>): 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<A extends any[], WithExports extends boolean>(
filter: (args: A, id: Metro.ModuleID, exps: If<WithExports, Metro.ModuleExports, never>) => boolean,
export function createFilterGenerator<A extends any[]>(
filter: (args: A, id: Metro.ModuleID, exports: Metro.ModuleExports) => boolean,
keyFor: (args: A) => string,
) {
): (...args: A) => Filter<object, true>
export function createFilterGenerator<A extends any[]>(
filter: (args: A, id: Metro.ModuleID) => boolean,
keyFor: (args: A) => string,
): (...args: A) => Filter<object, false>
export function createFilterGenerator<A extends any[]>(
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<WithExports, Metro.ModuleExports, never>) => 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 = <T extends Record<string, any> = Record<string, any>>(
* // const React: typeof import('react')
* ```
*/
export const byProps = createFilterGenerator<Parameters<ByProps>, IsFilterGeneratorWithExports<ByProps>>(
export const byProps = createFilterGenerator<Parameters<ByProps>>(
(props, _, exports) => props.every(prop => exports[prop]),
props => `revenge.props(${props.join(',')})`,
) as ByProps
@ -106,7 +116,7 @@ export type ByName = <T extends string | object>(
* const SomeClass = await findModule(byName<{ new(param: string): SomeClass }>('SomeClass'))
* // const SomeClass: { new(): SomeClass, name: 'SomeClass' }
*/
export const byName = createFilterGenerator<Parameters<ByName>, IsFilterGeneratorWithExports<ByName>>(
export const byName = createFilterGenerator<Parameters<ByName>>(
([name], _, exports) => exports.name === name,
([name]) => `revenge.name(${name})`,
) as ByName
@ -123,7 +133,7 @@ export const byName = createFilterGenerator<Parameters<ByName>, IsFilterGenerato
* const Logger = await findModule(byDependencies([4, , 2]))
* ```
*/
export const byDependencies = createFilterGenerator<[Array<Metro.ModuleID | undefined>], false>(
export const byDependencies = createFilterGenerator<[Array<Metro.ModuleID | undefined>]>(
([deps], id) => {
const actDeps = _mMetadatas.get(id)![0]
if (actDeps.length === deps.length)
@ -167,14 +177,14 @@ export function every<F1 extends Filter, F2 extends Filter, F3 extends Filter>(
>
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<F1 extends Filter, F2 extends Filter, F3 extends Filter>(
>
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<IF extends Filter>(
initializedFilter: IF,
uninitializedFilter: Filter<any, false>,
): Filter<FilterResult<IF>, 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
}

View file

@ -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<F extends Filter>(filter: F): Promise<FilterResult<F>>
export function findModule<F extends Filter, O extends FindModuleOptions>(
filter: F,
options: O,
): Promise<O extends LookupModulesOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>>
): Promise<O extends RunFilterReturnExportsOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>>
export function findModule<F extends Filter, O extends FindModuleOptions>(filter: F, options?: O) {
type Result = FilterResult<F>
return new Promise<O extends LookupModulesOptions<true> ? MaybeDefaultExportMatched<Result> : Result>((ok, err) => {
for (const exports of lookupModules(filter, options!)) return ok(exports)
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)
const unsub = waitForModules(filter, (_, exports) => ok(exports), options)
const unsub = waitForModules<F, O>(
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<F extends Filter, O extends FindModuleOptions>(filter
* const ReactNative = __r(ReactNativeId)
*/
export function findModuleId<F extends Filter>(filter: F): Promise<Metro.ModuleID>
export function findModuleId<F extends Filter, O extends FindModuleIdOptions>(
filter: F,
options: O,
): Promise<O extends LookupModulesOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>>
export function findModuleId(filter: Filter, options?: FindModuleIdOptions) {
return new Promise<Metro.ModuleID>((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<any>((ok, err) => {
const m = lookupModuleByImportedPath(path)
if (m) return ok(m)
export function findModuleByImportedPath<T>(path: string, options?: BaseFindModuleOptions): Promise<T> {
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<typeof import('react')>('createElement'), { hint: 'object' })
* const App = findModuleSync(byName<React.FC>('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<F extends Filter>(filter: F): FilterResult<F>
export function findModuleSync<F extends Filter, O extends FindModuleSyncOptions>(
filter: F,
options: O,
): O extends LookupModulesOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>
export function findModuleSync<F extends Filter, O extends FindModuleSyncOptions>(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<F extends Filter, O extends FindModuleSyncOptions
unsub()
value = exports
},
options,
options!,
)
const signal = options?.abortSignal
@ -191,8 +238,8 @@ export function findModuleByImportedPathSync<T = any>(
path: string,
options?: BaseFindModuleSyncOptions,
): T | undefined {
const m = lookupModuleByImportedPath<T>(path)
if (m) return m
const exports = lookupModuleByImportedPath<T>(path)
if (exports !== undefined) return exports
let value: unknown | undefined
const unsub = waitForModuleByImportedPath(path, (_, exports) => {
@ -206,5 +253,5 @@ export function findModuleByImportedPathSync<T = any>(
else signal.addEventListener('abort', unsub, { once: true })
}
return proxify(() => value, options)
return proxify(() => value as T | undefined, options)
}

View file

@ -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<ReturnNamespace extends boolean = boolean> =
RunFilterReturnExportsOptions<ReturnNamespace>
export type LookupModulesResult<O extends LookupModulesOptions, F extends Filter> = O extends LookupModulesOptions<true>
? MaybeDefaultExportMatched<FilterResult<F>>
: FilterResult<F>
export type LookupModuleIdsOptions<IncludeUninitialized extends boolean = boolean> = BaseLookupModulesOptions & {
export type BaseLookupModulesOptions<IncludeUninitialized extends boolean = boolean> = RunFilterOptions & {
/**
* Whether to include initialized modules in the lookup.
*
@ -31,13 +22,29 @@ export type LookupModuleIdsOptions<IncludeUninitialized extends boolean = boolea
*
* Options that require modules to be initialized (eg. `esmReturnNamespace`, `esmSkipDefault`) will be ignored during uninitialized module ID lookup.
*
* **This will initialize any modules that match the filter and may cause unintended side effects.**
*
* @default false
*/
includeUninitialized?: IncludeUninitialized
}
export type LookupModulesOptions<
ReturnNamespace extends boolean = boolean,
IncludeUninitialized extends boolean = boolean,
> = RunFilterReturnExportsOptions<ReturnNamespace> & BaseLookupModulesOptions<IncludeUninitialized>
export type LookupModulesResult<
O extends LookupModulesOptions,
F extends Filter,
> = O extends RunFilterReturnExportsOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>
export type LookupModuleIdsOptions<IncludeUninitialized extends boolean = boolean> =
BaseLookupModulesOptions<IncludeUninitialized>
/**
* 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<O extends LookupModuleIdsOptions>(
if (runFilter(filter, id, exports, options)) yield id
}
if (options?.includeUninitialized)
for (const id of _mUninited) if (runFilter<Filter<any, false>>(filter, id, undefined, options)) yield id
if (options?.includeUninitialized) for (const id of _mUninited) if (runFilter(filter, id)) yield id
}
export function lookupModules<F extends Filter>(filter: F): Generator<FilterResult<F>, undefined>
export function lookupModules<F extends Filter, O extends LookupModulesOptions>(
filter: F,
options: O,
): Generator<LookupModulesResult<O, F>, 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<F extends Filter, O extends LookupModulesOptions>(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<F extends Filter>(filter: F): Generator<FilterResult<F>, undefined>
export function lookupModules<
F extends O extends BaseLookupModulesOptions<true> ? Filter<any, false> : Filter,
O extends LookupModulesOptions,
>(filter: F, options: O): Generator<LookupModulesResult<O, F>, 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<O extends LookupModuleIdsOptions>(
filter: O extends BaseLookupModulesOptions<true> ? Filter<any, false> : 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<F extends Filter>(filter: F): FilterResult<F> | undefined
export function lookupModule<F extends Filter, O extends LookupModulesOptions>(
filter: F,
options: O,
): LookupModulesResult<O, F> | 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.

View file

@ -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<ReturnNamespace extends boolean = boolean> =
RunFilterReturnExportsOptions<ReturnNamespace>
export type WaitForModulesResult<
F extends Filter,
O extends WaitForModulesOptions,
> = O extends RunFilterReturnExportsOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>
/**
* 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<typeof import('@shopify/flash-list')>('FlashList'),
* // (exports: typeof import('@shopify/flash-list'), id: Metro.ModuleID) => any
* (id, exports) => {...}
@ -34,12 +36,9 @@ export function waitForModules<F extends Filter>(
callback: (id: Metro.ModuleID, exports: FilterResult<F>) => any,
): () => void
export function waitForModules<F extends Filter, O extends WaitForModulesOptions | undefined>(
export function waitForModules<F extends Filter, O extends WaitForModulesOptions>(
filter: F,
callback: (
id: Metro.ModuleID,
exports: O extends LookupModulesOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>,
) => any,
callback: (id: Metro.ModuleID, exports: WaitForModulesResult<F, O>) => 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.