diff --git a/lib/modules/src/finders/_internal.ts b/lib/modules/src/finders/_internal.ts index 947d2bb..eab9281 100644 --- a/lib/modules/src/finders/_internal.ts +++ b/lib/modules/src/finders/_internal.ts @@ -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 = 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, - 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( + 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, - exports: Metro.ModuleExports, - id: Metro.ModuleID, - options?: LookupModulesOptions, -): FindResultFlag | false { - if (exports.__esModule) { + exports: F extends Filter ? 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 + } +} diff --git a/lib/modules/src/finders/filters.ts b/lib/modules/src/finders/filters.ts index 4445c5f..ca8c683 100644 --- a/lib/modules/src/finders/filters.ts +++ b/lib/modules/src/finders/filters.ts @@ -1,29 +1,59 @@ -import { _mMetadatas } from '../metro/_internal' -import type { Metro } from '../../types/metro' +import { _mMetadatas } from '~/metro/_internal' -export type FilterResult = F extends Filter ? 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 extends Filter ? R : never + +export type IsFilterWithExports = F extends Filter ? W : 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 key: string } -export function createFilterGenerator( - 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(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( + filter: (args: A, id: Metro.ModuleID, exps: If) => 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) => 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 = = Record>( prop: keyof T, ...props: Array -) => Filter +) => Filter /** * Filter modules by their exports having all of the specified properties. @@ -33,18 +63,18 @@ export type ByProps = = Record>( * * @example * ```ts - * const React = await find(byProps('createElement')) + * const React = await findModule(byProps('createElement')) * // const React: typeof import('react') * ``` */ -export const byProps = createFilterGenerator>( - (props, exports) => props.every(prop => exports[prop]), +export const byProps = createFilterGenerator, IsFilterGeneratorWithExports>( + (props, _, exports) => props.every(prop => exports[prop]), props => `revenge.props(${props.join(',')})`, ) as ByProps export type ByName = ( name: T extends string ? T : T extends { name: string } ? T['name'] : string, -) => Filter +) => Filter /** * Filter modules by their exports having the specified name. @@ -55,7 +85,7 @@ export type ByName = ( * * @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 = ( * ```ts * type MyComponent = React.FC<{ foo: string }> * - * const MyComponent = await find(byName('MyComponent')) + * const MyComponent = await findModule(byName('MyComponent')) * // const MyComponent: MyComponent & { name: 'MyComponent' } * ``` * @@ -73,11 +103,11 @@ export type ByName = ( * 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>( - ([name], exports) => exports.name === name, +export const byName = createFilterGenerator, IsFilterGeneratorWithExports>( + ([name], _, exports) => exports.name === name, ([name]) => `revenge.name(${name})`, ) as ByName @@ -88,19 +118,20 @@ export const byName = createFilterGenerator>( * * @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]>( - ([deps], _, id) => { +export const byDependencies = createFilterGenerator<[Array], 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, F2 extends Filter>( +export function every( f1: F1, f2: F2, -): Filter & FilterResult> +): Filter & FilterResult, LogicalOr, IsFilterWithExports>> -export function combine, F2 extends Filter, F3 extends Filter>( +export function every( f1: F1, f2: F2, f3: F3, -): Filter & FilterResult & FilterResult> +): Filter< + FilterResult & FilterResult & FilterResult, + LogicalOr, IsFilterWithExports>, IsFilterWithExports> +> -export function combine, F2 extends Filter, F3 extends Filter, F4 extends Filter>( - f1: F1, - f2: F2, - f3: F3, - f4: F4, -): Filter & FilterResult & FilterResult & FilterResult> - -export function combine< - F1 extends Filter, - F2 extends Filter, - F3 extends Filter, - F4 extends Filter, - F5 extends Filter, ->( - f1: F1, - f2: F2, - f3: F3, - f4: F4, - f5: F5, -): Filter & FilterResult & FilterResult & FilterResult & FilterResult> - -export function combine(...filters: Filter[]): Filter { - const cf: Filter = (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: F1, + f2: F2, +): Filter | FilterResult, IsFilterWithExports | IsFilterWithExports> + +export function some( + f1: F1, + f2: F2, + f3: F3, +): Filter< + FilterResult | FilterResult | FilterResult, + IsFilterWithExports | IsFilterWithExports | IsFilterWithExports +> + +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 } diff --git a/lib/modules/src/finders/find.ts b/lib/modules/src/finders/find.ts index f130266..d7ec3c0 100644 --- a/lib/modules/src/finders/find.ts +++ b/lib/modules/src/finders/find.ts @@ -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('AppRegistry')) * ``` */ -export function findModule>(filter: F): Promise> -export function findModule, O extends FindModuleOptions>( +export function findModule(filter: F): Promise> +export function findModule( filter: F, options: O, ): Promise ? MaybeDefaultExportMatched> : FilterResult> -export function findModule, O extends FindModuleOptions>(filter: F, options?: O) { +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) - // 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, 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, O extends FindModuleOptions>(f * const React = __r(ReactId) * const ReactNative = __r(ReactNativeId) */ -export function findModuleId>(filter: F): Promise -export function findModuleId, O extends FindModuleOptions>( +export function findModuleId(filter: F): Promise +export function findModuleId( filter: F, options: O, ): Promise ? MaybeDefaultExportMatched> : FilterResult> -export function findModuleId(filter: Filter, options?: FindModuleOptions) { +export function findModuleId(filter: Filter, options?: FindModuleIdOptions) { return new Promise((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,14 +98,24 @@ export function findModuleId(filter: Filter, options?: FindModuleOptions) { }) } -export function findModuleByImportedPath(path: string, options?: FindModuleOptions) { +/** + * 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. + */ +export function findModuleByImportedPath(path: string, options?: BaseFindModuleOptions) { return new Promise((ok, err) => { - const m1 = lookupModuleByImportedPath(path) - if (m1) return ok(m1) + const m = lookupModuleByImportedPath(path) + if (m) return ok(m) - waitForModuleByImportedPath(path, ok, options) + const unsub = waitForModuleByImportedPath(path, (_, exports) => ok(exports)) + + const onAbort = () => { + unsub() + err(new Error(`${findModuleByImportedPath.name} aborted before resolved: ${path}`)) + } - const onAbort = () => err(new Error(`findModuleByImportedPath aborted before resolved: ${path}`)) const signal = options?.abortSignal if (signal) { if (signal.aborted) return onAbort() @@ -130,16 +124,8 @@ export function findModuleByImportedPath(path: string, options?: FindModuleOptio }) } -export type FindModuleSyncOptions = WaitForModulesOptions & { - /** - * The hint for the proxified object. - * - * @default 'function' - */ - hint?: 'object' | 'function' -} - -// TODO(modules/finders::sync): make a lazyValue function to create a proxified object instead of dupe +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('createElement')) + * 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 @@ -167,45 +153,31 @@ export type FindModuleSyncOptions = WaitForModulesOptions & { * }) * ``` */ -export function findModuleSync>(filter: F): FilterResult -export function findModuleSync, O extends FindModuleSyncOptions>( +export function findModuleSync(filter: F): FilterResult +export function findModuleSync( filter: F, options: O, ): O extends LookupModulesOptions ? MaybeDefaultExportMatched> : FilterResult -export function findModuleSync, O extends FindModuleSyncOptions>(filter: F, options?: O) { +export function findModuleSync(filter: F, options?: O) { for (const exports of lookupModules(filter, options!)) return exports - type Result = FilterResult - let value: (O extends LookupModulesOptions ? MaybeDefaultExportMatched : Result) | undefined + let value: unknown | undefined const unsub = waitForModules( filter, - exports => { + (_, exports) => { unsub() value = exports }, options, ) - const handler: ProxyHandler = { - // 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 + return proxify(() => value, options) } /** @@ -215,38 +187,24 @@ export function findModuleSync, 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(path: string, options?: FindModuleSyncOptions): T | undefined { - const m1 = lookupModuleByImportedPath(path) - if (m1) return m1 +export function findModuleByImportedPathSync( + path: string, + options?: BaseFindModuleSyncOptions, +): T | undefined { + const m = lookupModuleByImportedPath(path) + if (m) return m - let value: T | undefined - const unsub = waitForModuleByImportedPath( - path, - (exports: T) => { - unsub() - value = exports - }, - options, - ) + let value: unknown | undefined + const unsub = waitForModuleByImportedPath(path, (_, exports) => { + unsub() + value = exports + }) - const handler: ProxyHandler = { - // 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) } diff --git a/lib/modules/src/finders/index.ts b/lib/modules/src/finders/index.ts index 5d83546..96ab140 100644 --- a/lib/modules/src/finders/index.ts +++ b/lib/modules/src/finders/index.ts @@ -1,5 +1,3 @@ -import { _bl, _blacklist, _mInited, _isExportsBad, _mMetadatas, _mUninited, _mPaths } from '../metro/_internal' - export * from './lookup' export * from './wait' export * from './find' diff --git a/lib/modules/src/finders/lookup.ts b/lib/modules/src/finders/lookup.ts index ba9cc24..67771f6 100644 --- a/lib/modules/src/finders/lookup.ts +++ b/lib/modules/src/finders/lookup.ts @@ -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 = { +export type BaseLookupModulesOptions = RunFilterOptions + +export type LookupModulesOptions = + RunFilterReturnExportsOptions + +export type LookupModulesResult = O extends LookupModulesOptions + ? MaybeDefaultExportMatched> + : FilterResult + +export type LookupModuleIdsOptions = 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 - } - : { - esmReturnNamespace?: false - }) - -/** - * Lookup module IDs by its exports. - * This is the way to get **initialized** module IDs that match a filter. - * - * @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* lookupModuleIds(filter: Filter, options?: LookupModulesOptions) { - for (const id of _mInited) { - const { exports } = _mMetadatas.get(id)![1]! - if (runFilter(filter, exports, id, options)) yield id - } + includeUninitialized?: IncludeUninitialized } +/** + * 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: O extends LookupModuleIdsOptions ? Filter : Filter, + options?: O, +): Generator { + if (options?.includeInitialized ?? true) + for (const id of _mInited) { + const { exports } = _mMetadatas.get(id)![1]! + if (runFilter(filter, id, exports, options)) yield id + } + + if (options?.includeUninitialized) + for (const id of _mUninited) if (runFilter>(filter, id, undefined, options)) yield id +} + +export function lookupModules(filter: F): Generator, undefined> + +export function lookupModules( + filter: F, + options: O, +): Generator, 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>(filter: F): Generator> -export function lookupModules, O extends LookupModulesOptions>( - filter: F, - options: O, -): Generator ? MaybeDefaultExportMatched> : FilterResult> -export function* lookupModules, O extends LookupModulesOptions>(filter: F, options?: O) { +export function* lookupModules(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, 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. */ diff --git a/lib/modules/src/finders/wait.ts b/lib/modules/src/finders/wait.ts index 60453bc..e6b43f3 100644 --- a/lib/modules/src/finders/wait.ts +++ b/lib/modules/src/finders/wait.ts @@ -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('FlashList'), * // (exports: typeof import('@shopify/flash-list'), id: Metro.ModuleID) => any - * (exports, id) => {...} + * (id, exports) => {...} * ) * ``` */ -export function waitForModules>( +export function waitForModules( filter: F, - callback: (exports: FilterResult, id: Metro.ModuleID) => any, + callback: (id: Metro.ModuleID, exports: FilterResult) => any, ): () => void -export function waitForModules, O extends WaitForModulesOptions>( + +export function waitForModules( filter: F, callback: ( - exports: O extends LookupModulesOptions ? MaybeDefaultExportMatched> : FilterResult, id: Metro.ModuleID, + exports: O extends LookupModulesOptions ? MaybeDefaultExportMatched> : FilterResult, ) => any, - options?: O, + options: O, ): () => void -export function waitForModules, O extends WaitForModulesOptions>( - filter: F, - callback: ( - exports: O extends LookupModulesOptions ? MaybeDefaultExportMatched> : FilterResult, - 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, O extends WaitForModulesOp * @param callback The callback to call once the module is initialized. * @returns A function to unsubscribe. */ -export function waitForModuleByImportedPath( - path: string, - callback: (exports: T, id: Metro.ModuleID) => any, - options?: WaitForModulesOptions, -) { +export function waitForModuleByImportedPath(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 } diff --git a/lib/modules/src/metro/_internal.ts b/lib/modules/src/metro/_internal.ts index 3583017..4c3f54d 100644 --- a/lib/modules/src/metro/_internal.ts +++ b/lib/modules/src/metro/_internal.ts @@ -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() @@ -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 ) } diff --git a/lib/modules/src/metro/patches.ts b/lib/modules/src/metro/patches.ts index 3cf4d66..c0c2d65 100644 --- a/lib/modules/src/metro/patches.ts +++ b/lib/modules/src/metro/patches.ts @@ -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 diff --git a/lib/modules/src/metro/subscriptions/_internal.ts b/lib/modules/src/metro/subscriptions/_internal.ts index 4f880c3..f1ad4e9 100644 --- a/lib/modules/src/metro/subscriptions/_internal.ts +++ b/lib/modules/src/metro/subscriptions/_internal.ts @@ -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() export const _specific = new Map>() export const _importedPath = new Set() -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) { diff --git a/lib/modules/src/metro/subscriptions/index.ts b/lib/modules/src/metro/subscriptions/index.ts index 17ba3a5..5300180 100644 --- a/lib/modules/src/metro/subscriptions/index.ts +++ b/lib/modules/src/metro/subscriptions/index.ts @@ -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.** diff --git a/lib/modules/src/native.ts b/lib/modules/src/native.ts index 9292107..c53aadc 100644 --- a/lib/modules/src/native.ts +++ b/lib/modules/src/native.ts @@ -1,4 +1,4 @@ -import type { DiscordNativeModules } from '../types/discord.native' +import type { DiscordNativeModules } from '#/discord.native' /** * Naming conventions: diff --git a/lib/modules/tsconfig.json b/lib/modules/tsconfig.json new file mode 100644 index 0000000..faa6bcb --- /dev/null +++ b/lib/modules/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@revenge-mod/types/internal/tsconfig.json", + "compilerOptions": { + "paths": { + "~/*": ["./src/*"], + "#/*": ["./types/*"] + } + } +}