Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

getByPrototype #472

Closed
wants to merge 13 commits into from
10 changes: 9 additions & 1 deletion src/renderer/modules/webpack/filters.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getExportsForProps } from "./get-modules";
import { getExportsForProps, getFunctionForPrototypes } from "./get-modules";
import { sourceStrings } from "./patch-load";
import type { RawModule } from "../../../types";

Expand All @@ -10,6 +10,14 @@ export const byProps = <P extends PropertyKey = PropertyKey>(...props: P[]) => {
return (m: RawModule) => typeof getExportsForProps(m.exports, props) !== "undefined";
};

/**
* Get a module that has all the given prototypes on one of its functions
* @param prototypes List of prototype names
*/
export const byPrototypes = <P extends PropertyKey = PropertyKey>(...prototypes: P[]) => {
return (m: RawModule) => typeof getFunctionForPrototypes(m.exports, prototypes) !== "undefined";
};

/**
* Get a module whose source code matches the given string or RegExp
* @param match String or RegExp to match in the module's source code
Expand Down
54 changes: 43 additions & 11 deletions src/renderer/modules/webpack/get-modules.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Filter, GetModuleOptions, RawModule } from "src/types";
import type { AnyFunction, Filter, GetModuleOptions, RawModule, WithPrototype } from "src/types";
import { wpRequire } from "./patch-load";
import { logError } from "./util";

Expand All @@ -22,27 +22,40 @@ export function getExports<T>(m: RawModule): T | undefined {
}

/**
* Iterates over an object and its top-level children that could have properties
* Iterates over an object and its top-level and second-level (if specified) children that could have properties
* @param m Object (module exports) to iterate over
* @param secondLevel Boolean on whether to iterate over second level children in object
*/
function* iterateModuleExports(m: unknown): IterableIterator<Record<PropertyKey, unknown>> {
function* iterateModuleExports(
m: unknown,
secondLevel?: boolean,
): IterableIterator<Record<PropertyKey, unknown> | AnyFunction> {
// if m is null or not an object/function, then it will obviously not have the props
// if if has no props, then it definitely has no children either
if (m && (typeof m === "object" || typeof m === "function")) {
yield m as Record<PropertyKey, unknown>;
for (const key in m) {
try {
// if it has no props, then it definitely has no children either
try {
if (m && (typeof m === "object" || typeof m === "function")) {
yield m as Record<PropertyKey, unknown>;
for (const key in m) {
// This could throw an error ("illegal invocation") if val === DOMTokenList.prototype
// and key === "length"
// There could be other cases too, hence this try-catch instead of a specific exclusion
const val = (m as Record<PropertyKey, unknown>)[key];
if (val && (typeof val === "object" || typeof val === "function")) {
yield val as Record<PropertyKey, unknown>;
yield val as Record<PropertyKey, unknown> | AnyFunction;
if (secondLevel && typeof val === "object") {
for (const subKey in val) {
const subVal = (val as Record<PropertyKey, unknown>)[subKey];
if (subVal && (typeof val === "object" || typeof val === "function")) {
yield subVal as Record<PropertyKey, unknown> | AnyFunction;
continue;
}
}
}
}
} catch {
// ignore this export
}
}
} catch {
// ignore this export
}
}

Expand All @@ -65,6 +78,25 @@ export function getExportsForProps<T, P extends PropertyKey = keyof T>(
}
}

/**
yofukashino marked this conversation as resolved.
Show resolved Hide resolved
* Find an object in a module that has all the given properties. You will usually not need this function.
* @param m Module to search
* @param props Array of prototype names
* @returns Function that contains all the given prototypes (and any others), or undefined if not found
*/
export function getFunctionForPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(m: unknown, prototypes: P[]): T | undefined {
// Loop over the module and its exports at the top level
// Return the first thing that has all the indicated props
for (const exported of iterateModuleExports(m, true)) {
if (prototypes.every((p) => (exported as AnyFunction)?.prototype?.[p])) {
return exported as T;
}
}
}

// This doesn't have anywhere else to go

export function getById<T>(id: number, raw?: false): T | undefined;
Expand Down
135 changes: 133 additions & 2 deletions src/renderer/modules/webpack/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { GetModuleOptions, RawModule, WaitForOptions } from "src/types";
import { getExportsForProps, getModule } from "./get-modules";
import type {
AnyFunction,
GetModuleOptions,
RawModule,
WaitForOptions,
WithPrototype,
} from "src/types";
import { getExportsForProps, getFunctionForPrototypes, getModule } from "./get-modules";
import * as filters from "./filters";
import Flux, { Store } from "../common/flux";
import { waitForModule } from "./lazy";
Expand Down Expand Up @@ -165,6 +171,131 @@ export async function waitForProps<T, P extends PropertyKey = keyof T>(
return getExportsForProps<T, P>(result as T, props)!;
}

// Get by prototypes

export function getByPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(prototypes: P[], options?: { all?: false; raw?: false }): T | undefined;
export function getByPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(prototypes: P[], options: { all: true; raw?: false }): T[];
export function getByPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(prototypes: P[], options: { all?: false; raw: true }): RawModule<T> | undefined;
export function getByPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(prototypes: P[], options: { all: true; raw: true }): Array<RawModule<T>>;
export function getByPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(prototypes: P[], options?: { all: true; raw?: boolean }): T[] | Array<RawModule<T>>;
export function getByPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(prototypes: P[], options: { all?: false; raw?: boolean }): T | RawModule<T> | undefined;
export function getByPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(
prototypes: P[],
options: { all?: boolean; raw: true },
): RawModule<T> | Array<RawModule<T>> | undefined;
export function getByPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(prototypes: P, options: { all?: boolean; raw?: false }): T | T[] | undefined;
export function getByPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(
prototypes: P[],
options?: { all?: boolean; raw?: boolean },
): T | T[] | RawModule<T> | Array<RawModule<T>> | undefined;
export function getByPrototypes<
T extends AnyFunction,
P extends PropertyKey[] = Array<keyof WithPrototype<T>>,
>(...prototypes: P): T | undefined;

/**
* Equivalent to `getModule(filters.byPrototypes(...props), options)`
*
* @see {@link filters.byPrototypes}
* @see {@link getModule}
*/
export function getByPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(
...args: [P[], GetModuleOptions] | P[]
): T | T[] | RawModule<T> | Array<RawModule<T>> | undefined {
const prototypes = (typeof args[0] === "string" ? args : args[0]) as P[];
const raw = typeof args[0] === "string" ? false : (args[1] as GetModuleOptions)?.raw;

const result =
typeof args.at(-1) === "object"
? getModule<T>(filters.byPrototypes(...prototypes), args.at(-1) as GetModuleOptions)
: getModule<T>(filters.byPrototypes(...prototypes));

if (raw || typeof result === "undefined") {
return result as RawModule<T> | undefined;
}

if (result instanceof Array) {
// @ts-expect-error TypeScript isn't going to infer types based on the raw variable, so this is fine
return result.map((m) => getFunctionForPrototypes(m, prototypes));
}

return getFunctionForPrototypes<T, P>(result, prototypes);
}

// Wait for prototypes

export function waitForPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(prototypes: P[], options: WaitForOptions & { raw?: false }): Promise<T>;
export function waitForPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(prototypes: P[], options: WaitForOptions & { raw: true }): Promise<RawModule<T>>;
export function waitForPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(prototypes: P[], options?: WaitForOptions): Promise<T | RawModule<T>>;
export function waitForPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(...prototypes: P[]): Promise<T>;

/**
* Like {@link getByPrototypes} but waits for the module to be loaded.
*
* @see {@link getByPrototypes}
* @see {@link waitForModule}
*/
export async function waitForPrototypes<
T extends AnyFunction,
P extends PropertyKey = keyof WithPrototype<T>,
>(...args: [P[], WaitForOptions] | P[]): Promise<T | RawModule<T>> {
const prototypes = (typeof args[0] === "string" ? args : args[0]) as P[];
const raw = typeof args[0] === "string" ? false : (args[1] as WaitForOptions)?.raw;

const result = await (typeof args.at(-1) === "object"
? waitForModule<T>(filters.byPrototypes(...prototypes), args.at(-1) as WaitForOptions)
: waitForModule<T>(filters.byPrototypes(...prototypes)));

if (raw) {
return result as RawModule<T>;
}

// We know this will always exist since filters.byPrototypes will always return a module that has the function with prototypes
return getFunctionForPrototypes<T, P>(result as T, prototypes)!;
}

// Get by value

export function getByValue<T>(
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/modules/webpack/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export { waitForModule } from "./lazy";

export { getFunctionBySource, getFunctionKeyBySource } from "./inner-search";

export { getById, getExportsForProps, getModule } from "./get-modules";
export { getById, getExportsForProps, getFunctionForPrototypes, getModule } from "./get-modules";

/**
* Filter functions to use with {@link getModule}
Expand Down
2 changes: 2 additions & 0 deletions src/types/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export type ModuleExports =
export type ModuleExportsWithProps<P extends string> = Record<P, unknown> &
Record<PropertyKey, unknown>;

export type WithPrototype<T> = T extends { prototype: infer P } ? P : never;

export interface RawModule<T = unknown> {
id: number;
loaded: boolean;
Expand Down