diff --git a/lib/modules/src/finders/filters.ts b/lib/modules/src/finders/filters.ts index 826cdeb..c1dc0b4 100644 --- a/lib/modules/src/finders/filters.ts +++ b/lib/modules/src/finders/filters.ts @@ -4,16 +4,19 @@ import { getModuleDependencies, isModuleInitialized } from '../metro/utils' import type { If, LogicalOr } from '@revenge-mod/utils/types' import type { Metro } from '../types' -export type FilterResult = F extends Filter ? R : never +export type FilterResult = F extends Filter + ? R + : F extends FilterBase + ? R + : never export type IsFilterWithExports = F extends Filter ? WE - : never + : F extends FilterBase + ? WE + : never -export interface Filter< - _Inferable = any, - WithExports extends boolean = boolean, -> { +interface FilterBase<_Inferable = any, WithExports extends boolean = boolean> { ( ...args: If< WithExports, @@ -23,11 +26,95 @@ export interface Filter< ): boolean key: string } +export type Filter< + Inferable = any, + WithExports extends boolean = boolean, +> = FilterHelpers & FilterBase + +interface FilterHelpers { + /** + * Manually the key for this filter. + * + * **Don't use this unless you know what you're doing.** Only API exports should be using + * + * @param key The key to set for this filter. + */ + keyAs(this: T, key: string): T + /** + * Combines this filter with another filter, returning a new filter that matches if **both** filters match. + * + * @param filter The filter to combine with. + */ + and( + this: T, + filter: F, + ): Filter< + FilterResult & FilterResult, + LogicalOr, IsFilterWithExports> + > + /** + * Combines this filter with another filter, returning a new filter that matches if **either** filter matches. + * + * @param filter + */ + or( + this: T, + filter: F, + ): Filter< + FilterResult | FilterResult, + IsFilterWithExports | IsFilterWithExports + > + /** + * Combines this filter with another filter that filters by exports, returning a new filter that matches if **both** filters match. + * + * @see {@link preferExports} for more information on how to use this. + */ + withExports>( + this: T, + withExportsFilter: FilterBase, + strict?: boolean, + ): Filter< + FilterResult, + IsFilterWithExports | IsFilterWithExports + > + /** + * Combines this filter with another filter that filters by dependencies, returning a new filter that matches if **both** filters match. + * + * @see {@link byDependencies} for more information on how to use this. + */ + withDependencies>( + this: T, + deps: ComparableDependencyMap, + strict?: boolean, + ): Filter, false> +} export type FilterGenerator Filter> = G & { - keyFor: (args: Parameters) => string + keyFor(args: Parameters): string } +const Helpers: FilterHelpers = Object.setPrototypeOf( + { + keyAs(key) { + this.key = key + return this + }, + and(filter) { + return every(this, filter) + }, + or(filter) { + return some(this, filter) + }, + withExports(withExportsFilter, strict) { + return preferExports(withExportsFilter, this, strict) + }, + withDependencies(deps, strict) { + return preferExports(this, byDependencies(deps), strict) + }, + } satisfies FilterHelpers, + Function.prototype, +) + /** * Create a filter generator. * @@ -63,15 +150,23 @@ export function createFilterGenerator( ): FilterGenerator<(...args: A) => Filter> export function createFilterGenerator( - f: (args: A, id: Metro.ModuleID, exports?: Metro.ModuleExports) => boolean, + filter: ( + args: A, + id: Metro.ModuleID, + exports?: Metro.ModuleExports, + ) => boolean, keyFor: (args: A) => string, ): FilterGenerator<(...args: A) => Filter> { - const generator = (...args: A) => { - const filter = (id: Metro.ModuleID, exports?: Metro.ModuleExports) => - f(args, id, exports) - filter.key = keyFor(args) - return filter + type GeneratorType = ReturnType> + + const generator: GeneratorType = (...args: A) => { + const filter_ = ((id: Metro.ModuleID, exports?: Metro.ModuleExports) => + filter(args, id, exports)) as ReturnType + + filter_.key = keyFor(args) + return Object.setPrototypeOf(filter_, Helpers) } + generator.keyFor = keyFor return generator } @@ -249,6 +344,14 @@ const __DEBUG_WARNED_BAD_BY_DEPENDENCIES_FILTERS__ = * // Module having these dependencies: [4, ...], [4, ..., ...], [4, ..., ..., ...], etc. would match: * const [SomeOtherModule] = lookupModule(byDependencies(loose([4]))) * ``` + * + * @example With filter helpers (preferred) + * ```ts + * const [Logger] = lookupModule( + * byProps('log') + * .withDependencies([4, null, 2]), + * ) + * ``` */ export const byDependencies = createFilterGenerator>( ([deps], id) => depCompare(getModuleDependencies(id)!, deps, id, id), @@ -299,7 +402,6 @@ function relative(id: Metro.ModuleID, root?: boolean) { * * @see {@link byDependencies} * @see {@link relative} - * @see {@link relative.toRoot} * * @example * ```ts @@ -432,14 +534,14 @@ function depGenRelativeKeyPart(dep: number) { } export type Every = FilterGenerator<{ - ( + ( f1: F1, f2: F2, ): Filter< FilterResult & FilterResult, LogicalOr, IsFilterWithExports> > - ( + ( f1: F1, f2: F2, f3: F3, @@ -450,7 +552,7 @@ export type Every = FilterGenerator<{ IsFilterWithExports > > - (...filters: Filter[]): Filter + (...filters: FilterBase[]): Filter }> /** @@ -458,6 +560,15 @@ export type Every = FilterGenerator<{ * * @param filters The filters to combine. * + * @example With filter helpers (preferred) + * ```ts + * const [SomeModule] = lookupModule( + * byProps('x', 'name') + * .and(byName('SomeName')) + * .and(byDependencies([1, 485, null, 2])), + * ) + * ``` + * * @example * ```ts * const [SomeModule] = lookupModule(every( @@ -480,14 +591,14 @@ export const every = createFilterGenerator<[...filters: Filter[]]>( ) as Every export type Some = FilterGenerator<{ - ( + ( f1: F1, f2: F2, ): Filter< FilterResult | FilterResult, IsFilterWithExports | IsFilterWithExports > - ( + ( f1: F1, f2: F2, f3: F3, @@ -497,7 +608,7 @@ export type Some = FilterGenerator<{ | IsFilterWithExports | IsFilterWithExports > - (...filters: Filter[]): Filter + (...filters: FilterBase[]): Filter }> /** @@ -505,6 +616,15 @@ export type Some = FilterGenerator<{ * * @param filters The filters to combine. * + * @example With filter helpers (preferred) + * ```ts + * const [SomeModule] = lookupModule( + * byProps('x', 'name') + * .or(byName('SomeName')) + * .or(byDependencies([1, 485, null, 2])), + * ) + * ``` + * * @example * ```ts * const [SomeModule] = lookupModule(some( @@ -514,7 +634,7 @@ export type Some = FilterGenerator<{ * )) * ``` */ -export const some = createFilterGenerator<[...filters: Filter[]]>( +export const some = createFilterGenerator<[...filters: FilterBase[]]>( (filters, id, exports) => { for (const filter of filters) if (filter(id, exports)) return true return false @@ -522,16 +642,16 @@ export const some = createFilterGenerator<[...filters: Filter[]]>( filters => `revenge.some(${filtersToKey(filters)})`, ) as Some -function filtersToKey(filters: Filter[]): string { +function filtersToKey(filters: FilterBase[]): string { let s = '' for (const filter of filters) s += `${filter.key},` return s.substring(0, s.length - 1) } export type ModuleStateAware = FilterGenerator< - ( + ( initializedFilter: IF, - uninitializedFilter: Filter, + uninitializedFilter: FilterBase, strict?: boolean, ) => Filter, false> > @@ -569,9 +689,9 @@ export const moduleStateAware = createFilterGenerator< ) as ModuleStateAware export type PreferExports = FilterGenerator< - ( + ( withExportsFilter: WEF, - exportslessFilter: Filter, + exportslessFilter: FilterBase, strict?: boolean, ) => Filter, false> > @@ -587,6 +707,14 @@ export type PreferExports = FilterGenerator< * @param exportslessFilter The filter to use for modules without proper exports (uninitialized or bad). * @param strict Whether to also filter with `exportslessFilter` after `withExportsFilter` passes, confirming the module is definitely the correct module. Defaults to `false`. * + * @example With filter helpers (preferred) + * ```ts + * const [SomeModule] = lookupModule( + * byDependencies([1, 485, null, 2]) + * .withExports(byProps('x')), + * ) + * ``` + * * @example * ```ts * // will filter byProps('x') for modules with proper exports