feat(lib/modules)!: refactor finders and metro APIs

This commit is contained in:
PalmDevs 2025-04-05 20:52:09 +07:00
parent 206cd6ee78
commit f15c3d38db
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
12 changed files with 387 additions and 330 deletions

View file

@ -1,71 +1,91 @@
import { _isExportsBad } from '../metro/_internal'
import { _isExportBad } from '~/metro/_internal'
import type { If } from '@revenge-mod/utils/types'
import type { Metro } from '#/metro'
import type { Filter } from './filters'
import type { LookupModulesOptions } from './lookup'
import type { Metro } from '../../types'
const FindResultFlags = {
// ? Our design does not allow for this to be used, but it is here for future reference.
// /**
// * The find was unsuccessful.
// */
// NotFound: 0,
export type RunFilterOptions = {
/**
* A module was found and the find was successful.
* However, the module type could not be determined. It is most likely a CommonJS module.
* For ES modules, whether to skip checking the default export.
*
* @default false
*/
esmSkipDefault?: boolean
}
export type RunFilterReturnExportsOptions<ReturnNamespace extends boolean = boolean> = RunFilterOptions &
If<
ReturnNamespace,
{
/**
* For ES modules, whether to return the whole module with all exports instead of just the default export **if the default export matches**.
*
* CommonJS modules will always return all exports.
*
* @default false
*/
esmReturnNamespace: true
},
{
esmReturnNamespace?: false
}
>
const FilterResultFlags = {
/**
* A module was found.
*/
Found: 1,
/**
* The module was found and the find was successful.
* The module is an ES module, and the filter matched the module namespace.
* A module was found, and it is an ES module, and the filter matched the module namespace.
*/
ESModuleNamespace: 2,
/**
* The module was found and the find was successful.
* The module is an ES module, and the filter matched the default export.
* A module was found, and it is an ES module, and the filter matched the default export.
*/
ESModuleDefault: 3,
}
export type FindResultFlag = (typeof FindResultFlags)[keyof typeof FindResultFlags]
export type FilterResultFlag = (typeof FilterResultFlags)[keyof typeof FilterResultFlags]
export function runFilterReturnExports(
filter: Filter<any>,
exports: Metro.ModuleExports,
// 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,
id: Metro.ModuleID,
options?: LookupModulesOptions,
) {
const flag = runFilter(filter, exports, id, options)
if (flag) {
if (flag === FindResultFlags.ESModuleDefault && !options?.esmReturnNamespace) return exports.default
return exports
}
}
export function runFilter(
filter: Filter<any>,
exports: Metro.ModuleExports,
id: Metro.ModuleID,
options?: LookupModulesOptions,
): FindResultFlag | false {
if (exports.__esModule) {
exports: F extends Filter<any, true> ? Metro.ModuleExports : undefined,
options?: RunFilterOptions,
): FilterResultFlag | false {
if (exports?.__esModule) {
const { default: defaultExport } = exports
if (!options?.esmSkipDefault && !_isExportsBad(defaultExport) && filter(defaultExport, id)) {
// TODO(modules/caches/flags): could cache here that default export is bad
if (!options?.esmSkipDefault && !_isExportBad(defaultExport) && filter(id, defaultExport)) {
// TODO(modules/finders/caches::esm::default)
return FindResultFlags.ESModuleDefault
return FilterResultFlags.ESModuleDefault
}
if (filter(exports, id)) {
if (filter(id, exports)) {
// TODO(modules/finders/caches::esm::namespace)
return FindResultFlags.ESModuleNamespace
return FilterResultFlags.ESModuleNamespace
}
} else if (filter(exports, id)) {
} else if (filter(id, exports)) {
// TODO(modules/finders/caches)
return FindResultFlags.Found
return FilterResultFlags.Found
}
return false
}
export function runFilterReturnExports(
filter: Filter,
id: Metro.ModuleID,
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
}
}

View file

@ -1,29 +1,59 @@
import { _mMetadatas } from '../metro/_internal'
import type { Metro } from '../../types/metro'
import { _mMetadatas } from '~/metro/_internal'
export type FilterResult<F> = F extends Filter<infer R> ? R : never
import type { If, LogicalOr } from '@revenge-mod/utils/types'
import type { Metro } from '#/metro'
export interface Filter<_Inferable> {
(exports: Metro.ModuleExports, id: Metro.ModuleID): boolean
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 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
key: string
}
export function createFilterGenerator<A extends any[]>(
filter: (args: A, exps: Metro.ModuleExports, id: Metro.ModuleID) => boolean,
/**
* Create a filter generator.
*
* @param filter The function that filters the modules.
* @param keyFor The function that generates the key for the filter.
* @returns A function that generates a filter with the specified arguments.
*
* @example
* ```ts
* // function createFilterGenerator<Arguments, WithExports>(filter, keyFor): ...
* // * Arguments: The arguments for the filter function.
* // * WithExports: Whether the filter function also filters exports.
*
* const custom = createFilterGenerator<[arg1: number, arg2: string], false>(
* ([arg1, arg2], id, exports) => {
* // filter logic
* return true
* },
* ([arg1, arg2]) => `revenge.custom(${arg1}, ${arg2})`
* )
* ```
*
* @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,
keyFor: (args: A) => string,
) {
return (...args: A) => {
const f = (exps: Metro.ModuleExports, id: Metro.ModuleID) => filter(args, exps, id)
const f = (id: Metro.ModuleID, exps: If<WithExports, Metro.ModuleExports, never>) => filter(args, id, exps)
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<{}>
return f as Filter<{}, WithExports>
}
}
export type ByProps = <T extends Record<string, any> = Record<string, any>>(
prop: keyof T,
...props: Array<keyof T>
) => Filter<T>
) => Filter<T, true>
/**
* Filter modules by their exports having all of the specified properties.
@ -33,18 +63,18 @@ export type ByProps = <T extends Record<string, any> = Record<string, any>>(
*
* @example
* ```ts
* const React = await find(byProps<typeof import('react')>('createElement'))
* const React = await findModule(byProps<typeof import('react')>('createElement'))
* // const React: typeof import('react')
* ```
*/
export const byProps = createFilterGenerator<Parameters<ByProps>>(
(props, exports) => props.every(prop => exports[prop]),
export const byProps = createFilterGenerator<Parameters<ByProps>, IsFilterGeneratorWithExports<ByProps>>(
(props, _, exports) => props.every(prop => exports[prop]),
props => `revenge.props(${props.join(',')})`,
) as ByProps
export type ByName = <T extends string | object>(
name: T extends string ? T : T extends { name: string } ? T['name'] : string,
) => Filter<T extends string ? { name: T } : T & { name: typeof name }>
) => Filter<T extends string ? { name: T } : T & { name: typeof name }, true>
/**
* Filter modules by their exports having the specified name.
@ -55,7 +85,7 @@ export type ByName = <T extends string | object>(
*
* @example Auto-typing as object
* ```ts
* const SomeComponent = await find(byName('SomeComponent'))
* const SomeComponent = await findModule(byName('SomeComponent'))
* // const SomeComponent: { name: 'SomeComponent' }
* ```
*
@ -63,7 +93,7 @@ export type ByName = <T extends string | object>(
* ```ts
* type MyComponent = React.FC<{ foo: string }>
*
* const MyComponent = await find(byName<MyComponent>('MyComponent'))
* const MyComponent = await findModule(byName<MyComponent>('MyComponent'))
* // const MyComponent: MyComponent & { name: 'MyComponent' }
* ```
*
@ -73,11 +103,11 @@ export type ByName = <T extends string | object>(
* someMethod(): void
* }
*
* const SomeClass = await find(byName<{ new(param: string): SomeClass }>('SomeClass'))
* const SomeClass = await findModule(byName<{ new(param: string): SomeClass }>('SomeClass'))
* // const SomeClass: { new(): SomeClass, name: 'SomeClass' }
*/
export const byName = createFilterGenerator<Parameters<ByName>>(
([name], exports) => exports.name === name,
export const byName = createFilterGenerator<Parameters<ByName>, IsFilterGeneratorWithExports<ByName>>(
([name], _, exports) => exports.name === name,
([name]) => `revenge.name(${name})`,
) as ByName
@ -88,19 +118,20 @@ export const byName = createFilterGenerator<Parameters<ByName>>(
*
* @example
* ```ts
* const Logger = await find(byDependencies([4, undefined, 2]))
* const Logger = await findModule(byDependencies([4, undefined, 2]))
* // or
* const Logger = await find(byDependencies([4, , 2]))
* const Logger = await findModule(byDependencies([4, , 2]))
* ```
*/
export const byDependencies = createFilterGenerator<[Array<Metro.ModuleID | undefined>]>(
([deps], _, id) => {
export const byDependencies = createFilterGenerator<[Array<Metro.ModuleID | undefined>], false>(
([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
},
@ -108,57 +139,79 @@ export const byDependencies = createFilterGenerator<[Array<Metro.ModuleID | unde
)
/**
* Combines multiple filters into one.
* Combines multiple filters into one, returning true if **every** filter matches.
*
* @param filters The filters to combine.
*
* @example
* ```ts
* const SomeModule = await find(combine(
* const SomeModule = await findModule(every(
* byProps('x', 'name'),
* byName('SomeName'),
* byDependencies([1, 485, , 2]),
* ))
* ```
*/
export function combine<F1 extends Filter<any>, F2 extends Filter<any>>(
export function every<F1 extends Filter, F2 extends Filter>(
f1: F1,
f2: F2,
): Filter<FilterResult<F1> & FilterResult<F2>>
): Filter<FilterResult<F1> & FilterResult<F2>, LogicalOr<IsFilterWithExports<F1>, IsFilterWithExports<F2>>>
export function combine<F1 extends Filter<any>, F2 extends Filter<any>, F3 extends Filter<any>>(
export function every<F1 extends Filter, F2 extends Filter, F3 extends Filter>(
f1: F1,
f2: F2,
f3: F3,
): Filter<FilterResult<F1> & FilterResult<F2> & FilterResult<F3>>
): Filter<
FilterResult<F1> & FilterResult<F2> & FilterResult<F3>,
LogicalOr<LogicalOr<IsFilterWithExports<F1>, IsFilterWithExports<F2>>, IsFilterWithExports<F3>>
>
export function combine<F1 extends Filter<any>, F2 extends Filter<any>, F3 extends Filter<any>, F4 extends Filter<any>>(
f1: F1,
f2: F2,
f3: F3,
f4: F4,
): Filter<FilterResult<F1> & FilterResult<F2> & FilterResult<F3> & FilterResult<F4>>
export function combine<
F1 extends Filter<any>,
F2 extends Filter<any>,
F3 extends Filter<any>,
F4 extends Filter<any>,
F5 extends Filter<any>,
>(
f1: F1,
f2: F2,
f3: F3,
f4: F4,
f5: F5,
): Filter<FilterResult<F1> & FilterResult<F2> & FilterResult<F3> & FilterResult<F4> & FilterResult<F5>>
export function combine(...filters: Filter<any>[]): Filter<any> {
const cf: Filter<any> = (exports, id) => {
for (const filter of filters) if (!filter(exports, id)) return false
export function every(...filters: Filter[]): Filter {
const cf: Filter = (id, exports) => {
for (const filter of filters) if (!filter(id, exports)) return false
return true
}
cf.key = `revenge.combine(${filters.map(filter => filter.key).join(',')})`
cf.key = `revenge.every(${filters.map(filter => filter.key).join(',')})`
return cf
}
/**
* Combines multiple filters into one, returning true if **some** filters match.
*
* @param filters The filters to combine.
*
* @example
* ```ts
* const SomeModule = await findModule(some(
* byProps('x', 'name'),
* byName('SomeName'),
* byDependencies([1, 485, , 2]),
* ))
* ```
*/
export function some<F1 extends Filter, F2 extends Filter>(
f1: F1,
f2: F2,
): Filter<FilterResult<F1> | FilterResult<F2>, IsFilterWithExports<F1> | IsFilterWithExports<F2>>
export function some<F1 extends Filter, F2 extends Filter, F3 extends Filter>(
f1: F1,
f2: F2,
f3: F3,
): Filter<
FilterResult<F1> | FilterResult<F2> | FilterResult<F3>,
IsFilterWithExports<F1> | IsFilterWithExports<F2> | IsFilterWithExports<F3>
>
export function some(...filters: Filter[]): Filter {
const cf: Filter = (id, exports) => {
for (const filter of filters) if (filter(id, exports)) return true
return false
}
cf.key = `revenge.some(${filters.map(filter => filter.key).join(',')})`
return cf
}

View file

@ -1,23 +1,23 @@
import { lookupModuleByImportedPath, lookupModuleIds, lookupModules, type LookupModulesOptions } from './lookup'
import { waitForModuleByImportedPath, waitForModules, type WaitForModulesOptions } from './wait'
import { type LookupModulesOptions, lookupModuleByImportedPath, lookupModuleIds, lookupModules } from './lookup'
import { type WaitForModulesOptions, waitForModuleByImportedPath, waitForModules } from './wait'
import type { MaybeDefaultExportMatched } from '.'
import type { LookupModuleIdsOptions, MaybeDefaultExportMatched } from '.'
import type { Metro } from '#/metro'
import type { Filter, FilterResult } from './filters'
import type { Metro } from '../../types/metro'
import { proxify, type ProxifyOptions } from '@revenge-mod/utils/proxy'
export type FindModuleOptions = WaitForModulesOptions & {
// TODO(modules/finders::options.force): This can cause issues by initializing unexpected modules. Likely won't actually be implemented.
// Maybe we can add an option to force initialize when cached instead.
// /**
// * Force initialization of modules that haven't been initialized yet. **Not recommended for general use.**
// *
// * Forcing initialization of modules can lead to unexpected behavior and side effects and should be avoided if possible.
// *
// * @default false
// */
// force?: boolean
export type BaseFindModuleOptions = {
/**
* Abort signal for the find.
*/
abortSignal?: AbortSignal
// 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 FindModuleIdOptions = LookupModuleIdsOptions & FindModuleOptions
/**
* Find a module by its exports.
*
@ -33,44 +33,24 @@ export type FindModuleOptions = WaitForModulesOptions & {
* const ReactNative = await findModule(byProps<typeof import('react-native')>('AppRegistry'))
* ```
*/
export function findModule<F extends Filter<any>>(filter: F): Promise<FilterResult<F>>
export function findModule<F extends Filter<any>, O extends 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>>
export function findModule<F extends Filter<any>, O extends FindModuleOptions>(filter: F, options?: O) {
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)
// TODO(modules/finders::options.force): Initial implementation for `options.force`.
// if (options?.force) {
// if (signal) console.warn(
// "You shouldn't use `options.force` and `options.abortSignal` together! `options.abortSignal` will be ignored.",
// )
const unsub = waitForModules(filter, (_, exports) => ok(exports), options)
// for (const id of _uninitdIds) {
// // TODO(modules/finders): check blacklist cache
const onAbort = () => {
unsub()
err(new Error(`${findModule.name} aborted before resolved: ${filter.key}`))
}
// try {
// __r!(id)
// } catch {}
// const { exports } = _modules.get(id)![1]!
// if (_initdIds.has(id)) {
// const result = runFilter(filter, exports, options)
// if (result) return rs(result)
// }
// }
// // TODO(modules/finders/caches::notfound)
// return
// }
waitForModules(filter, ok, options)
const onAbort = () => err(new Error(`findModule aborted before resolved: ${filter.key}`))
const signal = options?.abortSignal
if (signal) {
if (signal.aborted) return onAbort()
@ -84,7 +64,7 @@ export function findModule<F extends Filter<any>, O extends FindModuleOptions>(f
*
* @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 module is not found.
* @returns A promise that resolves to the module ID or rejects if the find is aborted before a module ID is found.
*
* @example
* ```ts
@ -94,18 +74,22 @@ export function findModule<F extends Filter<any>, O extends FindModuleOptions>(f
* const React = __r(ReactId)
* const ReactNative = __r(ReactNativeId)
*/
export function findModuleId<F extends Filter<any>>(filter: F): Promise<Metro.ModuleID>
export function findModuleId<F extends Filter<any>, O extends FindModuleOptions>(
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<any>, options?: FindModuleOptions) {
export function findModuleId(filter: Filter, options?: FindModuleIdOptions) {
return new Promise<Metro.ModuleID>((ok, err) => {
for (const id of lookupModuleIds(filter, options)) return ok(id)
waitForModules(filter, (_, id) => ok(id), options)
const unsub = waitForModules(filter, (_, id) => ok(id), options)
const onAbort = () => {
unsub()
err(new Error(`${findModuleId.name} aborted before resolved: ${filter.key}`))
}
const onAbort = () => err(new Error(`findModuleId aborted before resolved: ${filter.key}`))
const signal = options?.abortSignal
if (signal) {
if (signal.aborted) return onAbort()
@ -114,32 +98,34 @@ export function findModuleId(filter: Filter<any>, options?: FindModuleOptions) {
})
}
export function findModuleByImportedPath(path: string, options?: FindModuleOptions) {
return new Promise<any>((ok, err) => {
const m1 = lookupModuleByImportedPath(path)
if (m1) return ok(m1)
waitForModuleByImportedPath(path, ok, options)
const onAbort = () => err(new Error(`findModuleByImportedPath aborted before resolved: ${path}`))
const signal = options?.abortSignal
if (signal) {
if (signal.aborted) return onAbort()
signal.addEventListener('abort', onAbort, { once: true })
}
})
}
export type FindModuleSyncOptions = WaitForModulesOptions & {
/**
* The hint for the proxified object.
*
* @default 'function'
* Find a module by its imported path.
* @param path The path to find the module by.
* @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.
*/
hint?: 'object' | 'function'
export function findModuleByImportedPath(path: string, options?: BaseFindModuleOptions) {
return new Promise<any>((ok, err) => {
const m = lookupModuleByImportedPath(path)
if (m) return ok(m)
const unsub = waitForModuleByImportedPath(path, (_, exports) => ok(exports))
const onAbort = () => {
unsub()
err(new Error(`${findModuleByImportedPath.name} aborted before resolved: ${path}`))
}
// TODO(modules/finders::sync): make a lazyValue function to create a proxified object instead of dupe
const signal = options?.abortSignal
if (signal) {
if (signal.aborted) return onAbort()
signal.addEventListener('abort', onAbort, { once: true })
}
})
}
export type BaseFindModuleSyncOptions = BaseFindModuleOptions & ProxifyOptions
export type FindModuleSyncOptions = FindModuleOptions & BaseFindModuleSyncOptions
/**
* Find a module by its exports synchronously.
@ -150,7 +136,7 @@ export type FindModuleSyncOptions = WaitForModulesOptions & {
*
* @example A little bit after index module is required:
* ```ts
* const React = findModuleSync(byProps<typeof import('react')>('createElement'))
* 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
@ -167,45 +153,31 @@ export type FindModuleSyncOptions = WaitForModulesOptions & {
* })
* ```
*/
export function findModuleSync<F extends Filter<any>>(filter: F): FilterResult<F>
export function findModuleSync<F extends Filter<any>, O extends FindModuleSyncOptions>(
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<any>, O extends FindModuleSyncOptions>(filter: F, options?: O) {
export function findModuleSync<F extends Filter, O extends FindModuleSyncOptions>(filter: F, options?: O) {
for (const exports of lookupModules(filter, options!)) return exports
type Result = FilterResult<F>
let value: (O extends LookupModulesOptions<true> ? MaybeDefaultExportMatched<Result> : Result) | undefined
let value: unknown | undefined
const unsub = waitForModules(
filter,
exports => {
(_, exports) => {
unsub()
value = exports
},
options,
)
const handler: ProxyHandler<any> = {
// biome-ignore lint/complexity/noBannedTypes: Function is the right type here
apply: (_, thisArg, argArray) => Reflect.apply(value as Function, thisArg, argArray),
// biome-ignore lint/complexity/noBannedTypes: Function is the right type here
construct: (_, argArray, newTarget) => Reflect.construct(value as Function, argArray, newTarget),
defineProperty: (_, property, attributes) => Reflect.defineProperty(value!, property, attributes),
deleteProperty: (_, p) => Reflect.deleteProperty(value!, p),
get: (_, p, receiver) => Reflect.get(value!, p, receiver),
getOwnPropertyDescriptor: (_, p) => Reflect.getOwnPropertyDescriptor(value!, p),
getPrototypeOf: _ => Reflect.getPrototypeOf(value!),
has: (_, p) => Reflect.has(value!, p),
isExtensible: _ => Reflect.isExtensible(value!),
ownKeys: _ => Reflect.ownKeys(value!),
preventExtensions: _ => Reflect.preventExtensions(value!),
set: (_, p, newValue, receiver) => Reflect.set(value!, p, newValue, receiver),
setPrototypeOf: (_, v) => Reflect.setPrototypeOf(value!, v),
const signal = options?.abortSignal
if (signal) {
if (signal.aborted) unsub()
else signal.addEventListener('abort', unsub, { once: true })
}
// biome-ignore lint/complexity/useArrowFunction: We need a function with a constructor
return new Proxy((options?.hint === 'object' ? {} : function () {}) as any, handler) as FilterResult<F>
return proxify(() => value, options)
}
/**
@ -215,38 +187,24 @@ export function findModuleSync<F extends Filter<any>, O extends FindModuleSyncOp
* @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.
*/
export function findModuleByImportedPathSync<T = any>(path: string, options?: FindModuleSyncOptions): T | undefined {
const m1 = lookupModuleByImportedPath<T>(path)
if (m1) return m1
export function findModuleByImportedPathSync<T = any>(
path: string,
options?: BaseFindModuleSyncOptions,
): T | undefined {
const m = lookupModuleByImportedPath<T>(path)
if (m) return m
let value: T | undefined
const unsub = waitForModuleByImportedPath(
path,
(exports: T) => {
let value: unknown | undefined
const unsub = waitForModuleByImportedPath(path, (_, exports) => {
unsub()
value = exports
},
options,
)
})
const handler: ProxyHandler<any> = {
// biome-ignore lint/complexity/noBannedTypes: Function is the right type here
apply: (_, thisArg, argArray) => Reflect.apply(value as Function, thisArg, argArray),
// biome-ignore lint/complexity/noBannedTypes: Function is the right type here
construct: (_, argArray, newTarget) => Reflect.construct(value as Function, argArray, newTarget),
defineProperty: (_, property, attributes) => Reflect.defineProperty(value!, property, attributes),
deleteProperty: (_, p) => Reflect.deleteProperty(value!, p),
get: (_, p, receiver) => Reflect.get(value!, p, receiver),
getOwnPropertyDescriptor: (_, p) => Reflect.getOwnPropertyDescriptor(value!, p),
getPrototypeOf: _ => Reflect.getPrototypeOf(value!),
has: (_, p) => Reflect.has(value!, p),
isExtensible: _ => Reflect.isExtensible(value!),
ownKeys: _ => Reflect.ownKeys(value!),
preventExtensions: _ => Reflect.preventExtensions(value!),
set: (_, p, newValue, receiver) => Reflect.set(value!, p, newValue, receiver),
setPrototypeOf: (_, v) => Reflect.setPrototypeOf(value!, v),
const signal = options?.abortSignal
if (signal) {
if (signal.aborted) unsub()
else signal.addEventListener('abort', unsub, { once: true })
}
// biome-ignore lint/complexity/useArrowFunction: We need a function with a constructor
return new Proxy((options?.hint === 'object' ? {} : function () {}) as any, handler) as T
return proxify(() => value, options)
}

View file

@ -1,5 +1,3 @@
import { _bl, _blacklist, _mInited, _isExportsBad, _mMetadatas, _mUninited, _mPaths } from '../metro/_internal'
export * from './lookup'
export * from './wait'
export * from './find'

View file

@ -1,63 +1,96 @@
import { runFilter, runFilterReturnExports } from './_internal'
import { _mInited, _mMetadatas, _mPaths } from '../metro/_internal'
import { _mInited, _mMetadatas, _mPaths, _mUninited } from '~/metro/_internal'
import {
runFilter,
runFilterReturnExports,
type RunFilterOptions,
type RunFilterReturnExportsOptions,
} from './_internal'
import type { MaybeDefaultExportMatched } from '.'
import type { Metro } from '#/metro'
import type { Filter, FilterResult } from './filters'
export type LookupModulesOptions<ReturnNamespace extends boolean = boolean> = {
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 & {
/**
* For ES modules, whether to skip checking the default export.
* Whether to include initialized modules in the lookup.
*
* @default true
*/
includeInitialized?: boolean
/**
* Whether to include uninitialized modules in the lookup.
*
* Options that require modules to be initialized (eg. `esmReturnNamespace`, `esmSkipDefault`) will be ignored during uninitialized module ID lookup.
*
* @default false
*/
esmSkipDefault?: boolean
} & (ReturnNamespace extends true
? {
/**
* For ES modules, whether to return the whole module with all exports instead of just the default export if the default export matches.
*
* CommonJS modules will always return all exports.
*
* @default false
*/
esmReturnNamespace: true
includeUninitialized?: IncludeUninitialized
}
: {
esmReturnNamespace?: false
})
/**
* Lookup module IDs by its exports.
* This is the way to get **initialized** module IDs that match a filter.
* Lookup module IDs by its exports. You may lookup both uninitialized module IDs when filtering via ID-only filters (eg. `byDependencies`).
*
* @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 lookup = lookupModuleIds(byProps('createElement'))
* // Log all module IDs that export React
* for (const id of lookup) console.log(id)
* ```
*
* @example Lookup uninitialized modules
* ```ts
* const lookup = lookupModuleIds(byDependencies([...]), { includeUninitialized: true })
* // Log all module IDs that has those dependencies
* for (const id of lookup) console.log(id)
* ```
*/
export function* lookupModuleIds(filter: Filter<any>, options?: LookupModulesOptions) {
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) {
const { exports } = _mMetadatas.get(id)![1]!
if (runFilter(filter, exports, id, options)) yield id
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
}
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.
* This is the way to get **initialized** module exports that match a filter.
*
* `lookupModules` only filters initialized modules. You will need to use `waitForModules` to filter initializing modules.
*
* @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<any>>(filter: F): Generator<FilterResult<F>>
export function lookupModules<F extends Filter<any>, O extends LookupModulesOptions>(
filter: F,
options: O,
): Generator<O extends LookupModulesOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>>
export function* lookupModules<F extends Filter<any>, O extends LookupModulesOptions>(filter: F, options?: O) {
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, exports, id, options)
const result = runFilterReturnExports(filter, id, exports, options)
if (result) yield result
}
}
@ -65,6 +98,8 @@ export function* lookupModules<F extends Filter<any>, O extends LookupModulesOpt
/**
* Lookup module exports by its imported path.
*
* `lookupModuleByImportedPath` only finds initialized modules. You will need to use `waitForModuleByImportedPath` to filter initializing modules.
*
* @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,20 +1,13 @@
import { onAnyModuleInitialized, onModuleFinishedImporting } from '~/metro'
import { _bl, _mMetadatas } from '~/metro/_internal'
import { runFilterReturnExports } from './_internal'
import { onAnyModuleInitialized, onModuleFinishedImporting } from '../metro'
import { _bl, _mMetadatas } from '../metro/_internal'
import type { MaybeDefaultExportMatched } from '.'
import type { Metro } from '#/metro'
import type { Filter, FilterResult } from './filters'
import type { LookupModulesOptions } from './lookup'
import type { Metro } from '../../types'
export type WaitForModulesOptions = LookupModulesOptions & {
/**
* Abort signal for the wait.
*
* **The find will only be aborted when it is pending/requires waiting for the module to be initialized.**
*/
abortSignal?: AbortSignal
}
export type WaitForModulesOptions = LookupModulesOptions
/**
* Wait for modules to initialize by its exports.
@ -32,46 +25,35 @@ export type WaitForModulesOptions = LookupModulesOptions & {
* waitForModule(
* byName<typeof import('@shopify/flash-list')>('FlashList'),
* // (exports: typeof import('@shopify/flash-list'), id: Metro.ModuleID) => any
* (exports, id) => {...}
* (id, exports) => {...}
* )
* ```
*/
export function waitForModules<F extends Filter<any>>(
export function waitForModules<F extends Filter>(
filter: F,
callback: (exports: FilterResult<F>, id: Metro.ModuleID) => any,
callback: (id: Metro.ModuleID, exports: FilterResult<F>) => any,
): () => void
export function waitForModules<F extends Filter<any>, O extends WaitForModulesOptions>(
export function waitForModules<F extends Filter, O extends WaitForModulesOptions | undefined>(
filter: F,
callback: (
exports: O extends LookupModulesOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>,
id: Metro.ModuleID,
exports: O extends LookupModulesOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>,
) => any,
options?: O,
options: O,
): () => void
export function waitForModules<F extends Filter<any>, O extends WaitForModulesOptions>(
filter: F,
callback: (
exports: O extends LookupModulesOptions<true> ? MaybeDefaultExportMatched<FilterResult<F>> : FilterResult<F>,
id: Metro.ModuleID,
) => any,
options?: O,
export function waitForModules(
filter: Filter,
callback: (id: Metro.ModuleID, exports: Metro.ModuleExports) => any,
options?: WaitForModulesOptions,
) {
const unsub = onAnyModuleInitialized(({ exports }, id) => {
const unsub = onAnyModuleInitialized((id, exports) => {
if (_bl.has(id)) return
const result = runFilterReturnExports(filter, exports, id, options)
if (result) callback(result, id)
const result = runFilterReturnExports(filter, id, exports, options)
if (result) callback(id, exports)
})
const signal = options?.abortSignal
if (signal) {
if (signal.aborted) {
unsub()
return unsub
}
signal.addEventListener('abort', unsub, { once: true })
}
return unsub
}
@ -81,27 +63,14 @@ export function waitForModules<F extends Filter<any>, O extends WaitForModulesOp
* @param callback The callback to call once the module is initialized.
* @returns A function to unsubscribe.
*/
export function waitForModuleByImportedPath<T = any>(
path: string,
callback: (exports: T, id: Metro.ModuleID) => any,
options?: WaitForModulesOptions,
) {
export function waitForModuleByImportedPath<T = any>(path: string, callback: (id: Metro.ModuleID, exports: T) => any) {
const unsub = onModuleFinishedImporting((id, cmpPath) => {
if (path === cmpPath) {
callback(_mMetadatas.get(id)![1]!.exports, id)
const { exports } = _mMetadatas.get(id)![1]!
callback(id, exports)
unsub()
}
})
const signal = options?.abortSignal
if (signal) {
if (signal.aborted) {
unsub()
return unsub
}
signal.addEventListener('abort', unsub, { once: true })
}
return unsub
}

View file

@ -1,7 +1,7 @@
import { isProxy } from '@revenge-mod/utils/proxy'
import { _executeSubscription } from './subscriptions/_internal'
import type { Metro } from '../../types/metro'
import type { Metro } from '#/metro'
export const _bl = new Set<Metro.ModuleID>()
@ -44,7 +44,7 @@ export function patchMetroDefine(metroDefine: Metro.DefineFn) {
} finally {
_mInitingId = prevIId
_mUninited.delete(id)
_executeSubscription(id, m)
_executeSubscription(id, m.exports)
}
}) satisfies Metro.FactoryFn,
id,
@ -73,6 +73,22 @@ export function _blacklist(id: Metro.ModuleID) {
// TODO(modules): caching
}
/**
* Returns whether a particular export of a module is bad.
* @param export The exports of the module
* @returns Whether the module has bad exports
* @internal
*/
export function _isExportBad(exp: Metro.ModuleExports) {
return (
// Nullish?
exp === undefined ||
exp === null ||
// Is it a proxy?
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
@ -81,11 +97,9 @@ export function _blacklist(id: Metro.ModuleID) {
*/
export function _isExportsBad(exports: Metro.ModuleExports) {
return (
exports === undefined ||
exports === null ||
// Empty object (module 0 for example, exports an empty object)
(exports.__proto__ === Object.prototype && !Reflect.ownKeys(exports).length) ||
isProxy(exports.default) ||
// Exports will always be an object, we are checking if the object is empty
!Reflect.ownKeys(exports).length ||
// Circular reference
exports === globalThis
)
}

View file

@ -1,10 +1,11 @@
import { _mMetadatas, _mInitingId, _mPaths, _mUninited, _bl } from './_internal'
import '@revenge-mod/assets/patches'
import { _bl, _mInitingId, _mMetadatas, _mPaths, _mUninited } from './_internal'
import { onAnyModuleInitialized } from './subscriptions'
import '@revenge-mod/assets/patches'
import { _executeImportedPathSubscription } from './subscriptions/_internal'
const unsubForImportTracker = onAnyModuleInitialized(({ exports }, id) => {
const unsubForImportTracker = onAnyModuleInitialized((id, exports) => {
if (_bl.has(id)) return
const orig = exports.fileFinishedImporting

View file

@ -1,13 +1,13 @@
import type { ModuleInitializedCallback, ModuleWithImportedPathInitializedCallback } from '.'
import type { Metro } from '../../../types/metro'
import type { Metro } from '#/metro'
export const _all = new Set<ModuleInitializedCallback>()
export const _specific = new Map<Metro.ModuleID, Set<ModuleInitializedCallback>>()
export const _importedPath = new Set<ModuleWithImportedPathInitializedCallback>()
export function _executeSubscription(id: Metro.ModuleID, module: Metro.ModuleExports) {
for (const cb of _all) cb(module, id)
if (_specific.has(id)) for (const cb of _specific.get(id)!) cb(module, id)
export function _executeSubscription(id: Metro.ModuleID, exports: Metro.ModuleExports) {
for (const cb of _all) cb(id, exports)
if (_specific.has(id)) for (const cb of _specific.get(id)!) cb(id, exports)
}
export function _executeImportedPathSubscription(id: Metro.ModuleID, path: string) {

View file

@ -1,8 +1,8 @@
import type { Metro } from '#/metro'
import { _all, _importedPath, _specific } from './_internal'
import type { Metro } from '../../../types/metro'
export type ModuleInitializedCallback = (module: Metro.Module, id: Metro.ModuleID) => void
export type ModuleWithImportedPathInitializedCallback = (id: Metro.ModuleID, importedPath: string) => void
export type ModuleInitializedCallback = (id: Metro.ModuleID, exports: Metro.ModuleExports) => void
export type ModuleWithImportedPathInitializedCallback = (id: Metro.ModuleID, path: string) => void
/**
* Registers a callback to be called when any module is initialized. **Some module exports may be bad.**

View file

@ -1,4 +1,4 @@
import type { DiscordNativeModules } from '../types/discord.native'
import type { DiscordNativeModules } from '#/discord.native'
/**
* Naming conventions:

View file

@ -0,0 +1,9 @@
{
"extends": "@revenge-mod/types/internal/tsconfig.json",
"compilerOptions": {
"paths": {
"~/*": ["./src/*"],
"#/*": ["./types/*"]
}
}
}