feat(lib/modules/finders)!: add ability to use finders with blacklisted modules, add isModuleInitialized Metro util, added and changed filters

This commit is contained in:
PalmDevs 2025-04-16 14:23:48 +07:00
parent 424ecfbed0
commit af23c2352e
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
8 changed files with 189 additions and 69 deletions

View file

@ -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
}

View file

@ -76,7 +76,7 @@ export type ByProps = <T extends Record<string, any> = Record<string, any>>(
* ```
*/
export const byProps = createFilterGenerator<Parameters<ByProps>>(
(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 = <IF extends Filter>(
) => Filter<FilterResult<IF>, 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 = <IF extends Filter>(
*/
export const moduleStateAware = createFilterGenerator<Parameters<ModuleStateAware>>(
([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 = <WEF extends Filter>(
withExportsFilter: WEF,
exportslessFilter: Filter<any, false>,
) => Filter<FilterResult<WEF>, 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<Parameters<PreferExports>>(
([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

View file

@ -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<ReturnNamespace, IncludeUninitialized> &
BaseFindModuleOptions
export type FindModuleIdOptions<IncludeUninitialized extends boolean = boolean> =
LookupModuleIdsOptions<IncludeUninitialized> & BaseFindModuleOptions
export type FindModuleIdOptions<
IncludeUninitialized extends boolean = boolean,
IncludeAll extends If<IncludeUninitialized, false, boolean> = If<IncludeUninitialized, false, boolean>,
> = LookupModuleIdsOptions<IncludeUninitialized, IncludeAll> & 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.

View file

@ -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<IncludeUninitialized extends boolean = boolean> = RunFilterOptions & {
/**
@ -41,8 +42,23 @@ export type LookupModulesResult<
O extends LookupModulesOptions,
> = O extends RunFilterReturnExportsOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>
export type LookupModuleIdsOptions<IncludeUninitialized extends boolean = boolean> =
BaseLookupModulesOptions<IncludeUninitialized>
export type LookupModuleIdsOptions<
IncludeUninitialized extends boolean = boolean,
IncludeAll extends If<IncludeUninitialized, false, boolean> = If<IncludeUninitialized, false, boolean>,
> = RunFilterOptions &
(
| (BaseLookupModulesOptions<IncludeUninitialized> & {
includeAll?: false
})
| ({
/**
* Whether to include all modules, including ones with bad exports.
*
* @default false
*/
includeAll: IncludeAll
} & BaseLookupModulesOptions<false>)
)
/**
* Lookup module IDs.
@ -50,6 +66,9 @@ export type LookupModuleIdsOptions<IncludeUninitialized extends boolean = boolea
* You can lookup uninitialized module IDs by passing `options.includeUninitialized` when filtering via without-exports filters (eg. `byDependencies`).
* Use the `moduleStateAware` helper to filter dynamically based on whether the module is initialized or not.
*
* You can also lookup all modules (even ones with bad exports) by passing `options.includeAll`.
* Use the `preferExports` helper to filter dynamically based on whether the module has good exports or not.
*
* @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.
@ -72,10 +91,14 @@ export function* lookupModuleIds<O extends LookupModuleIdsOptions>(
filter: O extends LookupModuleIdsOptions<true> ? Filter<any, false> : Filter,
options?: O,
): Generator<Metro.ModuleID, undefined> {
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<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) 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<F extends Filter, O extends LookupModulesOptions>(
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<T = any>(path: string): T | undefined {
const id = _mPaths.get(path)
if (id === undefined) return
return getInitializedModuleExports(id)
const id = lookupModuleIdByImportedPath(path)
return getInitializedModuleExports(id!)
}

View file

@ -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<ReturnNamespace extends boolean = boolean> =
RunFilterReturnExportsOptions<ReturnNamespace>
export type BaseWaitForModulesOptions<IncludeAll extends boolean = boolean> = {
/**
* 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<ReturnNamespace> & BaseWaitForModulesOptions<IncludeBadExports>
export type WaitForModulesResult<
F extends Filter,
@ -38,28 +54,32 @@ export function waitForModules<F extends Filter>(
callback: (id: Metro.ModuleID, exports: FilterResult<F>) => any,
): () => void
export function waitForModules<F extends Filter, O extends WaitForModulesOptions>(
filter: F,
callback: (id: Metro.ModuleID, exports: WaitForModulesResult<F, O>) => any,
options: O,
): () => void
export function waitForModules<
F extends O extends WaitForModulesOptions<boolean, true> ? Filter<any, false> : Filter,
O extends WaitForModulesOptions,
>(filter: F, callback: (id: Metro.ModuleID, exports: WaitForModulesResult<F, O>) => 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<T = any>(path: string, callback: (id: Metro.ModuleID, exports: T) => any) {
export function waitForModuleByImportedPath<T = any>(
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))
}
})

View file

@ -11,18 +11,19 @@ export const _mUninited = new Set<Metro.ModuleID>()
export const _mInited = new Set<Metro.ModuleID>()
export const _mPaths = new Map<string, Metro.ModuleID>()
export const _mMd = new Map<Metro.ModuleID, [deps: Metro.DependencyMap, module?: Metro.Module]>()
export const _mMd = new Map<Metro.ModuleID, [deps: Metro.DependencyMap, initialized: boolean, 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 _mMd extends Map<any, infer V> ? V : never
const metadata = [deps!, false] as typeof _mMd extends Map<any, infer V> ? 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

View file

@ -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.

View file

@ -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.
*