haicord/src/webpack/api.tsx

1014 lines
38 KiB
TypeScript
Raw Normal View History

2024-05-02 23:18:12 -03:00
/*
* Vencord, a Discord client mod
2024-05-26 20:41:28 -03:00
* Copyright (c) 2024 Vendicated, Nuckyz and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
2024-05-02 23:18:12 -03:00
import { lazyString, makeLazy, proxyLazy } from "@utils/lazy";
import { LazyComponent, LazyComponentType, SYM_LAZY_COMPONENT_INNER } from "@utils/lazyReact";
2024-05-02 23:18:12 -03:00
import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches";
import { ProxyInner, proxyInner, SYM_PROXY_INNER_VALUE } from "@utils/proxyInner";
import { AnyObject } from "@utils/types";
2024-05-02 23:18:12 -03:00
import type { WebpackInstance } from "discord-types/other";
import { traceFunction } from "../debug/Tracer";
2024-05-11 06:05:20 -03:00
import { GenericStore } from "./common";
2024-05-02 23:18:12 -03:00
const logger = new Logger("Webpack");
2024-05-28 18:02:57 -03:00
export let _resolveDiscordLoaded: () => void;
2024-05-02 23:18:12 -03:00
/**
* Fired once a gateway connection to Discord has been established.
2024-05-28 18:02:57 -03:00
* This indicates that the core Webpack modules have been initialized, and we are logged in.
2024-05-02 23:18:12 -03:00
*/
2024-05-28 18:02:57 -03:00
export const onceDiscordLoaded = new Promise<void>(r => _resolveDiscordLoaded = r);
2024-05-02 23:18:12 -03:00
export let wreq: WebpackInstance;
export let cache: WebpackInstance["c"];
2024-06-22 02:44:44 -03:00
export function _initWebpack(webpackRequire: WebpackInstance) {
wreq = webpackRequire;
cache = webpackRequire.c;
}
type ModuleFactory = (module: any, exports: any, require: WebpackInstance) => void;
export type ModListenerInfo = {
id: PropertyKey;
factory: ModuleFactory;
};
export type ModCallbackInfo = {
id: PropertyKey;
exportKey: PropertyKey | null;
factory: ModuleFactory;
};
export type ModListenerFn = (module: any, info: ModListenerInfo) => void;
export type ModCallbackFn = ((module: any, info: ModCallbackInfo) => void) & {
$$vencordCallbackCalled?: () => boolean;
};
export const factoryListeners = new Set<(factory: ModuleFactory) => void>();
export const moduleListeners = new Set<ModListenerFn>();
export const waitForSubscriptions = new Map<FilterFn, ModCallbackFn>();
export const beforeInitListeners = new Set<(wreq: WebpackInstance) => void>();
let devToolsOpen = false;
if (IS_DEV && IS_DISCORD_DESKTOP) {
// At this point in time, DiscordNative has not been exposed yet, so setImmediate is needed
setTimeout(() => {
DiscordNative?.window.setDevtoolsCallbacks(() => devToolsOpen = true, () => devToolsOpen = false);
}, 0);
}
2024-06-21 02:23:04 -03:00
export type FilterFn = ((module: any) => boolean) & {
$$vencordProps?: string[];
2024-06-21 02:23:04 -03:00
$$vencordIsFactoryFilter?: boolean;
};
2024-05-02 23:18:12 -03:00
export const filters = {
byProps: (...props: string[]): FilterFn => {
const filter: FilterFn = props.length === 1
2024-05-02 23:18:12 -03:00
? m => m?.[props[0]] !== void 0
: m => props.every(p => m?.[p] !== void 0);
filter.$$vencordProps = ["byProps", ...props];
return filter;
},
byCode: (...code: string[]): FilterFn => {
const filter: FilterFn = m => {
2024-05-02 23:18:12 -03:00
if (typeof m !== "function") return false;
2024-06-21 02:23:04 -03:00
const s = String(m);
2024-05-02 23:18:12 -03:00
for (const c of code) {
if (!s.includes(c)) return false;
}
return true;
};
filter.$$vencordProps = ["byCode", ...code];
return filter;
},
byStoreName: (name: string): FilterFn => {
const filter: FilterFn = m => m?.constructor?.displayName === name;
2024-05-02 23:18:12 -03:00
filter.$$vencordProps = ["byStoreName", name];
return filter;
},
2024-06-21 02:23:04 -03:00
byComponentCode: (...code: string[]): FilterFn => {
const byCodeFilter = filters.byCode(...code);
const filter: FilterFn = m => {
let inner = m;
while (inner != null) {
if (byCodeFilter(inner)) return true;
else if (!inner.$$typeof) return false;
else if (inner.type) inner = inner.type; // memos
else if (inner.render) inner = inner.render; // forwardRefs
else return false;
}
2024-05-02 23:18:12 -03:00
return false;
};
filter.$$vencordProps = ["componentByCode", ...code];
return filter;
2024-06-21 02:23:04 -03:00
},
byFactoryCode: (...code: string[]): FilterFn => {
const byCodeFilter = filters.byCode(...code);
byCodeFilter.$$vencordProps = ["byFactoryCode", ...code];
byCodeFilter.$$vencordIsFactoryFilter = true;
return byCodeFilter;
2024-05-02 23:18:12 -03:00
}
};
export const webpackSearchHistory = [] as Array<["waitFor" | "find" | "findComponent" | "findExportedComponent" | "findComponentByCode" | "findByProps" | "findByPropsAndExtract" | "findByCode" | "findStore" | "findByFactoryCode" | "mapMangledModule" | "extractAndLoadChunks" | "webpackDependantLazy" | "webpackDependantLazyComponent", any[]]>;
2024-05-02 23:18:12 -03:00
function printFilter(filter: FilterFn) {
if (filter.$$vencordProps != null) {
const props = filter.$$vencordProps;
2024-06-21 02:23:04 -03:00
return `${props[0]}(${props.slice(1).map(arg => JSON.stringify(arg)).join(", ")})`;
2024-05-02 23:18:12 -03:00
}
return filter.toString();
}
/**
2024-06-21 02:23:04 -03:00
* Wait for the first export or module exports that matches the provided filter to be required,
* then call the callback with the export or module exports as the first argument.
2024-05-02 23:18:12 -03:00
*
2024-06-21 02:23:04 -03:00
* If the module containing the export(s) is already required, the callback will be called immediately.
*
2024-06-21 02:23:04 -03:00
* @param filter A function that takes an export or module exports and returns a boolean
* @param callback A function that takes the find result as its first argument
2024-05-02 23:18:12 -03:00
*/
export function waitFor(filter: FilterFn, callback: ModCallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) {
if (typeof filter !== "function")
throw new Error("Invalid filter. Expected a function got " + typeof filter);
if (typeof callback !== "function")
throw new Error("Invalid callback. Expected a function got " + typeof callback);
if (IS_REPORTER && !isIndirect) {
const originalCallback = callback;
let callbackCalled = false;
callback = function (this: unknown) {
callbackCalled = true;
Reflect.apply(originalCallback, this, arguments);
};
callback.$$vencordCallbackCalled = () => callbackCalled;
webpackSearchHistory.push(["waitFor", [callback, filter]]);
}
2024-05-02 23:18:12 -03:00
if (cache != null) {
2024-06-21 02:23:04 -03:00
const { result, id, exportKey, factory } = _cacheFind(filter);
if (result != null) return callback(result, { id: id!, exportKey: exportKey as PropertyKey | null, factory: factory! });
2024-05-02 23:18:12 -03:00
}
waitForSubscriptions.set(filter, callback);
}
/**
2024-06-21 02:23:04 -03:00
* Find the first export or module exports that matches the filter.
2024-05-02 23:18:12 -03:00
*
* The way this works internally is:
2024-06-21 02:23:04 -03:00
* Wait for the first export or module exports that matches the provided filter to be required,
* then call the parse function with the export or module exports as the first argument.
2024-05-02 23:18:12 -03:00
*
* If the module containing the export(s) is already required, the parse function will be called immediately.
2024-05-02 23:18:12 -03:00
*
* The parse function must return a value that will be used as the proxy inner value.
2024-05-02 23:18:12 -03:00
*
* If no parse function is specified, the default parse will assign the proxy inner value to the plain find result.
*
2024-06-21 02:23:04 -03:00
* @param filter A function that takes an export or module exports and returns a boolean
* @param parse A function that takes the find result as its first argument and returns something to use as the proxy inner value. Useful if you want to use a value from the find result, instead of all of it. Defaults to the find result itself
* @returns A proxy that has the parse function return value as its true value, or the plain parse function return value if it was called immediately.
2024-05-02 23:18:12 -03:00
*/
export function find<T = AnyObject>(filter: FilterFn, parse: (mod: any) => any = m => m, { isIndirect = false }: { isIndirect?: boolean; } = {}) {
2024-05-02 23:18:12 -03:00
if (typeof filter !== "function")
throw new Error("Invalid filter. Expected a function got " + typeof filter);
if (typeof parse !== "function")
throw new Error("Invalid find parse. Expected a function got " + typeof parse);
2024-05-02 23:18:12 -03:00
const [proxy, setInnerValue] = proxyInner<T>(`Webpack find matched no module. Filter: ${printFilter(filter)}`, "Webpack find with proxy called on a primitive value.");
waitFor(filter, m => setInnerValue(parse(m)), { isIndirect: true });
2024-05-02 23:18:12 -03:00
if (IS_REPORTER && !isIndirect) {
webpackSearchHistory.push(["find", [proxy, filter]]);
}
if (proxy[SYM_PROXY_INNER_VALUE] != null) return proxy[SYM_PROXY_INNER_VALUE] as ProxyInner<T>;
2024-05-02 23:18:12 -03:00
return proxy;
}
/**
2024-06-21 02:23:04 -03:00
* Find the first exported component that matches the filter.
2024-05-08 06:44:18 -03:00
*
2024-06-21 02:23:04 -03:00
* @param filter A function that takes an export or module exports and returns a boolean
2024-05-02 23:18:12 -03:00
* @param parse A function that takes the found component as its first argument and returns a component. Useful if you want to wrap the found component in something. Defaults to the original component
* @returns The component if found, or a noop component
*/
2024-06-12 17:45:54 -03:00
export function findComponent<T extends object = any>(filter: FilterFn, parse: (component: any) => LazyComponentType<T> = m => m, { isIndirect = false }: { isIndirect?: boolean; } = {}) {
2024-05-02 23:18:12 -03:00
if (typeof filter !== "function")
throw new Error("Invalid filter. Expected a function got " + typeof filter);
if (typeof parse !== "function")
throw new Error("Invalid component parse. Expected a function got " + typeof parse);
let InnerComponent = null as LazyComponentType<T> | null;
2024-05-06 23:22:36 -03:00
let findFailedLogged = false;
const WrapperComponent = (props: T) => {
if (InnerComponent === null && !findFailedLogged) {
findFailedLogged = true;
2024-05-02 23:18:12 -03:00
logger.error(`Webpack find matched no module. Filter: ${printFilter(filter)}`);
}
2024-05-06 23:22:36 -03:00
return InnerComponent && <InnerComponent {...props} />;
2024-05-02 23:18:12 -03:00
};
WrapperComponent[SYM_LAZY_COMPONENT_INNER] = () => InnerComponent;
2024-05-02 23:18:12 -03:00
waitFor(filter, (v: any) => {
const parsedComponent = parse(v);
InnerComponent = parsedComponent;
Object.assign(WrapperComponent, parsedComponent);
2024-05-02 23:18:12 -03:00
}, { isIndirect: true });
if (IS_REPORTER && !isIndirect) {
webpackSearchHistory.push(["findComponent", [WrapperComponent, filter]]);
}
2024-05-06 23:22:36 -03:00
if (InnerComponent !== null) return InnerComponent;
2024-05-02 23:18:12 -03:00
return WrapperComponent as LazyComponentType<T>;
2024-05-02 23:18:12 -03:00
}
/**
* Find the first component that is exported by the first prop name.
*
* @example findExportedComponent("FriendRow")
* @example findExportedComponent("FriendRow", "Friend", FriendRow => React.memo(FriendRow))
*
* @param props A list of prop names to search the exports for
* @param parse A function that takes the found component as its first argument and returns a component. Useful if you want to wrap the found component in something. Defaults to the original component
* @returns The component if found, or a noop component
*/
2024-06-12 17:45:54 -03:00
export function findExportedComponent<T extends object = any>(...props: string[] | [...string[], (component: any) => LazyComponentType<T>]) {
const parse = (typeof props.at(-1) === "function" ? props.pop() : m => m) as (component: any) => LazyComponentType<T>;
2024-05-02 23:18:12 -03:00
const newProps = props as string[];
const filter = filters.byProps(...newProps);
let InnerComponent = null as LazyComponentType<T> | null;
2024-05-06 23:22:36 -03:00
let findFailedLogged = false;
const WrapperComponent = (props: T) => {
if (InnerComponent === null && !findFailedLogged) {
findFailedLogged = true;
2024-05-02 23:18:12 -03:00
logger.error(`Webpack find matched no module. Filter: ${printFilter(filter)}`);
}
2024-05-06 23:22:36 -03:00
return InnerComponent && <InnerComponent {...props} />;
2024-05-02 23:18:12 -03:00
};
WrapperComponent[SYM_LAZY_COMPONENT_INNER] = () => InnerComponent;
2024-05-02 23:18:12 -03:00
waitFor(filter, (v: any) => {
const parsedComponent = parse(v[newProps[0]]);
InnerComponent = parsedComponent;
Object.assign(WrapperComponent, parsedComponent);
2024-05-02 23:18:12 -03:00
}, { isIndirect: true });
if (IS_REPORTER) {
webpackSearchHistory.push(["findExportedComponent", [WrapperComponent, ...newProps]]);
}
2024-05-06 23:22:36 -03:00
if (InnerComponent !== null) return InnerComponent;
2024-05-02 23:18:12 -03:00
return WrapperComponent as LazyComponentType<T>;
2024-05-02 23:18:12 -03:00
}
/**
2024-06-21 02:23:04 -03:00
* Find the first component in an export that includes all the given code.
2024-05-02 23:18:12 -03:00
*
* @example findComponentByCode(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR")
* @example findComponentByCode(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)", ColorPicker => React.memo(ColorPicker))
*
* @param code A list of code to search each export for
* @param parse A function that takes the found component as its first argument and returns a component. Useful if you want to wrap the found component in something. Defaults to the original component
* @returns The component if found, or a noop component
*/
2024-06-12 17:45:54 -03:00
export function findComponentByCode<T extends object = any>(...code: string[] | [...string[], (component: any) => LazyComponentType<T>]) {
const parse = (typeof code.at(-1) === "function" ? code.pop() : m => m) as (component: any) => LazyComponentType<T>;
2024-05-02 23:18:12 -03:00
const newCode = code as string[];
2024-06-21 02:23:04 -03:00
const ComponentResult = findComponent<T>(filters.byComponentCode(...newCode), parse, { isIndirect: true });
if (IS_REPORTER) {
webpackSearchHistory.push(["findComponentByCode", [ComponentResult, ...newCode]]);
}
2024-05-02 23:18:12 -03:00
return ComponentResult;
2024-05-02 23:18:12 -03:00
}
/**
2024-06-21 02:23:04 -03:00
* Find the first module exports or export that includes all the given props.
2024-05-02 23:18:12 -03:00
*
2024-06-21 02:23:04 -03:00
* @param props A list of props to search the module or exports for
* @param parse A function that takes the find result as its first argument and returns something. Useful if you want to use a value from the find result, instead of all of it. Defaults to the find result itself
2024-05-02 23:18:12 -03:00
*/
export function findByProps<T = AnyObject>(...props: string[] | [...string[], (mod: any) => T]) {
const parse = (typeof props.at(-1) === "function" ? props.pop() : m => m) as (mod: any) => T;
const newProps = props as string[];
const result = find<T>(filters.byProps(...newProps), parse, { isIndirect: true });
2024-05-02 23:18:12 -03:00
if (IS_REPORTER) {
webpackSearchHistory.push(["findByProps", [result, ...newProps]]);
}
return result;
}
/**
* Find the first prop value defined by the first prop name, which is in a module exports or export including all the given props.
*
* @example const getUser = findByPropsAndExtract("getUser", "fetchUser")
*
* @param props A list of props to search the module or exports for
* @param parse A function that takes the find result as its first argument and returns something. Useful if you want to use a value from the find result, instead of all of it. Defaults to the find result itself
*/
export function findByPropsAndExtract<T = AnyObject>(...props: string[] | [...string[], (mod: any) => T]) {
const parse = (typeof props.at(-1) === "function" ? props.pop() : m => m) as (mod: any) => T;
const newProps = props as string[];
const result = find<T>(filters.byProps(...newProps), m => parse(m[newProps[0]]), { isIndirect: true });
if (IS_REPORTER) {
webpackSearchHistory.push(["findByPropsAndExtract", [result, ...newProps]]);
}
return result;
2024-05-02 23:18:12 -03:00
}
/**
2024-06-21 02:23:04 -03:00
* Find the first export that includes all the given code.
2024-05-02 23:18:12 -03:00
*
* @param code A list of code to search each export for
* @param parse A function that takes the find result as its first argument and returns something. Useful if you want to use a value from the find result, instead of all of it. Defaults to the find result itself
2024-05-02 23:18:12 -03:00
*/
export function findByCode<T = AnyObject>(...code: string[] | [...string[], (mod: any) => T]) {
const parse = (typeof code.at(-1) === "function" ? code.pop() : m => m) as (mod: any) => T;
const newCode = code as string[];
const result = find<T>(filters.byCode(...newCode), parse, { isIndirect: true });
if (IS_REPORTER) {
webpackSearchHistory.push(["findByCode", [result, ...newCode]]);
}
2024-05-02 23:18:12 -03:00
return result;
2024-05-02 23:18:12 -03:00
}
/**
* Find a store by its name.
2024-05-02 23:18:12 -03:00
*
* @param name The store name
*/
2024-05-11 06:05:20 -03:00
export function findStore<T = GenericStore>(name: string) {
const result = find<T>(filters.byStoreName(name), m => m, { isIndirect: true });
if (IS_REPORTER) {
webpackSearchHistory.push(["findStore", [result, name]]);
}
2024-05-02 23:18:12 -03:00
return result;
2024-05-02 23:18:12 -03:00
}
/**
2024-06-21 02:23:04 -03:00
* Find the module exports of the first module which the factory includes all the given code.
*
* @param code A list of code to search each factory for
* @param parse A function that takes the find result as its first argument and returns something. Useful if you want to use a value from the find result, instead of all of it. Defaults to the find result itself
2024-06-21 02:23:04 -03:00
*/
export function findByFactoryCode<T = AnyObject>(...code: string[] | [...string[], (mod: any) => T]) {
const parse = (typeof code.at(-1) === "function" ? code.pop() : m => m) as (mod: any) => T;
const newCode = code as string[];
const result = find<T>(filters.byFactoryCode(...newCode), parse, { isIndirect: true });
2024-06-21 02:23:04 -03:00
if (IS_REPORTER) {
webpackSearchHistory.push(["findByFactoryCode", [result, ...newCode]]);
2024-06-21 02:23:04 -03:00
}
return result;
}
/**
* Find the module exports of the first module which the factory includes all the given code,
* then map them into an easily usable object via the specified mappers.
*
* @example
* const Modals = mapMangledModule("headerIdIsManaged:", {
* openModal: filters.byCode("headerIdIsManaged:"),
* closeModal: filters.byCode("key==")
* });
*
* @param code The code or list of code to search each factory for
* @param mappers Mappers to create the non mangled exports object
* @returns Unmangled exports as specified in mappers
*/
export function mapMangledModule<S extends PropertyKey>(code: string | string[], mappers: Record<S, FilterFn>) {
const mapping = {} as Record<S, any>;
const setters = {} as Record<S, (innerValue: any) => void>;
2024-06-21 02:23:04 -03:00
for (const newName in mappers) {
// Wrapper to select whether the parent factory filter or child mapper filter failed when the error is thrown
const errorMsgWrapper = lazyString(() => `Webpack mapMangledModule ${callbackCalled ? "mapper" : "factory"} filter matched no module. Filter: ${printFilter(callbackCalled ? mappers[newName] : factoryFilter)}`);
2024-06-21 02:23:04 -03:00
const [proxy, setInnerValue] = proxyInner(errorMsgWrapper, "Webpack find with proxy called on a primitive value.");
mapping[newName] = proxy;
setters[newName] = setInnerValue;
}
const factoryFilter = filters.byFactoryCode(...Array.isArray(code) ? code : [code]);
let callbackCalled = false;
waitFor(factoryFilter, exports => {
callbackCalled = true;
for (const exportKey in exports) {
const exportValue = exports[exportKey];
if (exportValue == null) continue;
2024-06-21 02:23:04 -03:00
for (const newName in mappers) {
const filter = mappers[newName];
if (filter(exportValue)) {
setters[newName](exportValue);
2024-06-21 02:23:04 -03:00
}
}
}
}, { isIndirect: true });
if (IS_REPORTER) {
webpackSearchHistory.push(["mapMangledModule", [mapping, code, mappers]]);
2024-06-21 02:23:04 -03:00
}
if (callbackCalled) {
for (const innerMap in mapping) {
const innerValue = mapping[innerMap];
if (innerValue[SYM_PROXY_INNER_VALUE] != null) {
mapping[innerMap] = innerValue[SYM_PROXY_INNER_VALUE];
}
}
}
return mapping;
2024-06-21 02:23:04 -03:00
}
2024-06-22 02:44:44 -03:00
/**
* Find the first module factory that includes all the given code.
*/
export function findModuleFactory(...code: string[]) {
const filter = filters.byFactoryCode(...code);
const [proxy, setInnerValue] = proxyInner<ModuleFactory>(`Webpack module factory find matched no module. Filter: ${printFilter(filter)}`, "Webpack find with proxy called on a primitive value.");
2024-06-22 02:44:44 -03:00
waitFor(filter, (_, { factory }) => setInnerValue(factory));
if (proxy[SYM_PROXY_INNER_VALUE] != null) return proxy[SYM_PROXY_INNER_VALUE] as ProxyInner<ModuleFactory>;
return proxy;
}
/**
* This is just a wrapper around {@link proxyLazy} to make our reporter test for your webpack finds.
*
* Wraps the result of factory in a Proxy you can consume as if it wasn't lazy.
* On first property access, the factory is evaluated.
*
* @param factory Factory returning the result
* @param attempts How many times to try to evaluate the factory before giving up
* @returns Result of factory function
*/
export function webpackDependantLazy<T = AnyObject>(factory: () => T, attempts?: number) {
if (IS_REPORTER) webpackSearchHistory.push(["webpackDependantLazy", [factory]]);
return proxyLazy<T>(factory, attempts);
}
/**
* This is just a wrapper around {@link LazyComponent} to make our reporter test for your webpack finds.
*
* A lazy component. The factory method is called on first render.
*
* @param factory Function returning a Component
* @param attempts How many times to try to get the component before giving up
* @returns Result of factory function
*/
export function webpackDependantLazyComponent<T extends object = any>(factory: () => any, attempts?: number) {
if (IS_REPORTER) webpackSearchHistory.push(["webpackDependantLazyComponent", [factory]]);
return LazyComponent<T>(factory, attempts);
}
export const DefaultExtractAndLoadChunksRegex = /(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/;
export const ChunkIdsRegex = /\("([^"]+?)"\)/g;
/**
* Extract and load chunks using their entry point.
*
* @param code The code or list of code the module factory containing the lazy chunk loading must include
* @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory
* @returns A function that returns a promise that resolves with a boolean whether the chunks were loaded, on first call
*/
export function extractAndLoadChunksLazy(code: string | string[], matcher: RegExp = DefaultExtractAndLoadChunksRegex) {
const module = findModuleFactory(...Array.isArray(code) ? code : [code]);
const extractAndLoadChunks = makeLazy(async () => {
if (module[SYM_PROXY_INNER_VALUE] == null) {
const err = new Error("extractAndLoadChunks: Couldn't find module factory");
if (!IS_DEV || devToolsOpen) {
logger.warn(err, "Code:", code, "Matcher:", matcher);
return false;
} else {
throw err; // Strict behaviour in DevBuilds to fail early and make sure the issue is found
}
}
const match = String(module).match(canonicalizeMatch(matcher));
if (!match) {
const err = new Error("extractAndLoadChunks: Couldn't find chunk loading in module factory code");
if (!IS_DEV || devToolsOpen) {
logger.warn(err, "Code:", code, "Matcher:", matcher);
return false;
} else {
throw err; // Strict behaviour in DevBuilds to fail early and make sure the issue is found
}
}
const [, rawChunkIds, entryPointId] = match;
if (Number.isNaN(Number(entryPointId))) {
const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number");
if (!IS_DEV || devToolsOpen) {
logger.warn(err, "Code:", code, "Matcher:", matcher);
return false;
} else {
throw err; // Strict behaviour in DevBuilds to fail early and make sure the issue is found
}
}
if (rawChunkIds) {
const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => m[1]);
await Promise.all(chunkIds.map(id => wreq.e(id)));
}
if (wreq.m[entryPointId] == null) {
const err = new Error("extractAndLoadChunks: Entry point is not loaded in the module factories, perhaps one of the chunks failed to load");
if (!IS_DEV || devToolsOpen) {
logger.warn(err, "Code:", code, "Matcher:", matcher);
return false;
} else {
throw err; // Strict behaviour in DevBuilds to fail early and make sure the issue is found
}
}
wreq(entryPointId as any);
return true;
});
if (IS_REPORTER) {
webpackSearchHistory.push(["extractAndLoadChunks", [extractAndLoadChunks, code, matcher]]);
}
return extractAndLoadChunks;
}
2024-06-21 05:05:44 -03:00
export type CacheFindResult = {
2024-06-21 02:23:04 -03:00
/** The find result. `undefined` if nothing was found */
result?: any;
/** The id of the module exporting where the result was found. `undefined` if nothing was found */
id?: PropertyKey;
/** The key exporting the result. `null` if the find result was all the module exports, `undefined` if nothing was found */
exportKey?: PropertyKey | null;
/** The factory of the module exporting the result. `undefined` if nothing was found */
factory?: ModuleFactory;
};
/**
* Find the first export or module exports from an already required module that matches the filter.
*
2024-06-21 02:23:04 -03:00
* @param filter A function that takes an export or module exports and returns a boolean
2024-05-02 23:18:12 -03:00
*/
2024-06-21 02:23:04 -03:00
export const _cacheFind = traceFunction("cacheFind", function _cacheFind(filter: FilterFn): CacheFindResult {
2024-05-02 23:18:12 -03:00
if (typeof filter !== "function")
throw new Error("Invalid filter. Expected a function got " + typeof filter);
for (const key in cache) {
const mod = cache[key];
2024-06-21 02:23:04 -03:00
if (!mod?.loaded || mod?.exports == null) continue;
if (filter.$$vencordIsFactoryFilter && filter(wreq.m[key])) {
2024-06-21 19:47:44 -03:00
return { result: mod.exports, id: key, exportKey: null, factory: wreq.m[key] };
2024-06-21 02:23:04 -03:00
}
2024-05-02 23:18:12 -03:00
if (filter(mod.exports)) {
2024-06-21 19:47:44 -03:00
return { result: mod.exports, id: key, exportKey: null, factory: wreq.m[key] };
2024-06-21 02:23:04 -03:00
}
if (typeof mod.exports !== "object") {
continue;
}
if (mod.exports.default != null && filter(mod.exports.default)) {
2024-06-21 19:47:44 -03:00
return { result: mod.exports.default, id: key, exportKey: "default ", factory: wreq.m[key] };
2024-05-02 23:18:12 -03:00
}
2024-06-21 02:23:04 -03:00
for (const exportKey in mod.exports) if (exportKey.length <= 3) {
const exportValue = mod.exports[exportKey];
if (exportValue != null && filter(exportValue)) {
return { result: exportValue, id: key, exportKey, factory: wreq.m[key] };
}
2024-05-02 23:18:12 -03:00
}
}
2024-06-21 02:23:04 -03:00
return {};
2024-05-02 23:18:12 -03:00
});
2024-06-21 02:23:04 -03:00
/**
* Find the first export or module exports from an already required module that matches the filter.
*
* @param filter A function that takes an export or module exports and returns a boolean
* @returns The found export or module exports, or undefined
*/
export function cacheFind(filter: FilterFn) {
const cacheFindResult = _cacheFind(filter);
2024-05-02 23:18:12 -03:00
2024-06-21 02:23:04 -03:00
return cacheFindResult.result;
}
/**
* Find the the export or module exports from an all the required modules that match the filter.
*
* @param filter A function that takes an export or module exports and returns a boolean
* @returns An array of all the found export or module exports
*/
2024-05-02 23:18:12 -03:00
export function cacheFindAll(filter: FilterFn) {
if (typeof filter !== "function")
throw new Error("Invalid filter. Expected a function got " + typeof filter);
const ret = [] as any[];
for (const key in cache) {
const mod = cache[key];
2024-06-21 02:23:04 -03:00
if (!mod?.loaded || mod?.exports == null) continue;
if (filter.$$vencordIsFactoryFilter && filter(wreq.m[key])) {
ret.push(mod.exports);
}
2024-05-02 23:18:12 -03:00
if (filter(mod.exports)) {
ret.push(mod.exports);
}
2024-06-21 02:23:04 -03:00
if (typeof mod.exports !== "object") {
continue;
}
if (mod.exports.default != null && filter(mod.exports.default)) {
2024-05-02 23:18:12 -03:00
ret.push(mod.exports.default);
}
2024-06-21 02:23:04 -03:00
for (const exportKey in mod.exports) if (exportKey.length <= 3) {
const exportValue = mod.exports[exportKey];
if (exportValue != null && filter(exportValue)) {
ret.push(exportValue);
break;
}
}
2024-05-02 23:18:12 -03:00
}
return ret;
}
/**
* Same as {@link cacheFind} but in bulk.
*
2024-06-21 02:23:04 -03:00
* @param filterFns Array of filters
2024-05-02 23:18:12 -03:00
* @returns Array of results in the same order as the passed filters
*/
2024-05-08 00:25:34 -03:00
export const cacheFindBulk = traceFunction("cacheFindBulk", function cacheFindBulk(...filterFns: FilterFn[]) {
2024-06-21 02:23:04 -03:00
if (!Array.isArray(filterFns)) {
2024-05-02 23:18:12 -03:00
throw new Error("Invalid filters. Expected function[] got " + typeof filterFns);
2024-06-21 02:23:04 -03:00
}
2024-05-02 23:18:12 -03:00
const { length } = filterFns;
2024-06-21 02:23:04 -03:00
if (length === 0) {
2024-05-02 23:18:12 -03:00
throw new Error("Expected at least two filters.");
2024-06-21 02:23:04 -03:00
}
2024-05-02 23:18:12 -03:00
if (length === 1) {
if (IS_DEV) {
throw new Error("bulk called with only one filter. Use find");
}
return [cacheFind(filterFns[0])];
2024-05-02 23:18:12 -03:00
}
let found = 0;
const results = Array(length);
2024-06-21 02:23:04 -03:00
const filters = [...filterFns] as Array<FilterFn | undefined>;
2024-05-02 23:18:12 -03:00
outer:
for (const key in cache) {
const mod = cache[key];
2024-06-21 02:23:04 -03:00
if (!mod?.loaded || mod?.exports == null) continue;
2024-05-02 23:18:12 -03:00
2024-06-21 02:23:04 -03:00
for (let i = 0; i < length; i++) {
const filter = filters[i];
if (filter == null) continue;
if (filter.$$vencordIsFactoryFilter && filter(wreq.m[key])) {
results[i] = mod.exports;
filters[i] = undefined;
2024-05-02 23:18:12 -03:00
if (++found === length) break outer;
break;
}
2024-06-21 02:23:04 -03:00
if (filter(mod.exports)) {
results[i] = mod.exports;
filters[i] = undefined;
2024-05-02 23:18:12 -03:00
if (++found === length) break outer;
break;
}
2024-06-21 02:23:04 -03:00
if (typeof mod.exports !== "object") {
break;
}
if (mod.exports.default != null && filter(mod.exports.default)) {
results[i] = mod.exports.default;
filters[i] = undefined;
if (++found === length) break outer;
continue;
}
for (const exportKey in mod.exports) if (exportKey.length <= 3) {
const exportValue = mod.exports[exportKey];
if (exportValue != null && filter(mod.exports[key])) {
results[i] = exportValue;
filters[i] = undefined;
if (++found === length) break outer;
break;
}
}
2024-05-02 23:18:12 -03:00
}
}
if (found !== length) {
const err = new Error(`Got ${length} filters, but only found ${found} modules!`);
if (!IS_DEV || devToolsOpen) {
2024-05-02 23:18:12 -03:00
logger.warn(err);
return null;
} else {
2024-05-02 23:18:12 -03:00
throw err; // Strict behaviour in DevBuilds to fail early and make sure the issue is found
}
}
return results;
});
/**
2024-06-21 02:23:04 -03:00
* Find the id of the first already loaded module factory that includes all the given code.
2024-05-02 23:18:12 -03:00
*/
2024-06-21 02:23:04 -03:00
export const cacheFindModuleId = traceFunction("cacheFindModuleId", function cacheFindModuleId(...code: string[]) {
2024-05-02 23:18:12 -03:00
outer:
for (const id in wreq.m) {
2024-06-21 02:23:04 -03:00
const str = String(wreq.m[id]);
2024-05-02 23:18:12 -03:00
for (const c of code) {
if (!str.includes(c)) continue outer;
}
2024-06-21 02:23:04 -03:00
2024-05-02 23:18:12 -03:00
return id;
}
const err = new Error("Didn't find module with code(s):\n" + code.join("\n"));
if (!IS_DEV || devToolsOpen) {
2024-05-02 23:18:12 -03:00
logger.warn(err);
} else {
2024-05-02 23:18:12 -03:00
throw err; // Strict behaviour in DevBuilds to fail early and make sure the issue is found
}
});
/**
2024-06-21 02:23:04 -03:00
* Find the first already loaded module factory that includes all the given code.
2024-05-02 23:18:12 -03:00
*/
2024-06-21 02:23:04 -03:00
export function cacheFindModuleFactory(...code: string[]) {
const id = cacheFindModuleId(...code);
if (id == null) return;
2024-05-02 23:18:12 -03:00
return wreq.m[id];
}
2024-06-22 02:37:40 -03:00
/**
* Search modules by keyword. This searches the factory methods,
* meaning you can search all sorts of things, methodName, strings somewhere in the code, etc.
*
* @param filters One or more strings or regexes
* @returns Mapping of found modules
*/
export function search(...filters: Array<string | RegExp>) {
const results = {} as Record<number, Function>;
const factories = wreq.m;
outer:
for (const id in factories) {
const factory = factories[id];
const factoryStr = String(factory);
for (const filter of filters) {
if (typeof filter === "string" && !factoryStr.includes(filter)) continue outer;
if (filter instanceof RegExp && !filter.test(factoryStr)) continue outer;
}
results[id] = factory;
}
return results;
}
/**
* Extract a specific module by id into its own Source File. This has no effect on
* the code, it is only useful to be able to look at a specific module without having
* to view a massive file. extract then returns the extracted module so you can jump to it.
* As mentioned above, note that this extracted module is not actually used,
* so putting breakpoints or similar will have no effect.
*
* @param id The id of the module to extract
*/
export function extract(id: PropertyKey) {
const factory = wreq.m[id] as Function;
if (!factory) return;
const code = `
// [EXTRACTED] WebpackModule${String(id)}
// WARNING: This module was extracted to be more easily readable.
// This module is NOT ACTUALLY USED! This means putting breakpoints will have NO EFFECT!!
0,${String(factory)}
//# sourceURL=ExtractedWebpackModule${String(id)}
`;
const extracted = (0, eval)(code);
return extracted as Function;
}
2024-05-07 21:33:05 -03:00
function deprecatedRedirect<T extends (...args: any[]) => any>(oldMethod: string, newMethod: string, redirect: T): T {
return ((...args: Parameters<T>) => {
logger.warn(`Method ${oldMethod} is deprecated. Use ${newMethod} instead. For more information read https://github.com/Vendicated/Vencord/pull/2409#issue-2277161516`);
return redirect(...args);
}) as T;
}
/**
* @deprecated Use {@link webpackDependantLazy} instead
2024-05-03 19:39:21 -03:00
*
* This is just a wrapper around {@link proxyLazy} to make our reporter test for your webpack finds.
*
* Wraps the result of factory in a Proxy you can consume as if it wasn't lazy.
* On first property access, the factory is evaluated.
*
* @param factory Factory returning the result
* @param attempts How many times to try to evaluate the factory before giving up
* @returns Result of factory function
*/
2024-05-07 21:33:05 -03:00
export const proxyLazyWebpack = deprecatedRedirect("proxyLazyWebpack", "webpackDependantLazy", webpackDependantLazy);
/**
* @deprecated Use {@link webpackDependantLazyComponent} instead
2024-05-03 19:39:21 -03:00
*
* This is just a wrapper around {@link LazyComponent} to make our reporter test for your webpack finds.
*
* A lazy component. The factory method is called on first render.
2024-05-08 06:44:18 -03:00
*
* @param factory Function returning a Component
* @param attempts How many times to try to get the component before giving up
* @returns Result of factory function
*/
2024-05-07 21:33:05 -03:00
export const LazyComponentWebpack = deprecatedRedirect("LazyComponentWebpack", "webpackDependantLazyComponent", webpackDependantLazyComponent);
/**
* @deprecated Use {@link find} instead
2024-05-03 19:39:21 -03:00
*
* Find the first module that matches the filter, lazily
*/
2024-05-07 21:33:05 -03:00
export const findLazy = deprecatedRedirect("findLazy", "find", find);
/**
* @deprecated Use {@link findByProps} instead
2024-05-03 19:39:21 -03:00
*
* Find the first module that has the specified properties, lazily
*/
2024-05-07 21:33:05 -03:00
export const findByPropsLazy = deprecatedRedirect("findByPropsLazy", "findByProps", findByProps);
/**
* @deprecated Use {@link findByCode} instead
2024-05-03 19:39:21 -03:00
*
* Find the first function that includes all the given code, lazily
*/
2024-05-07 21:33:05 -03:00
export const findByCodeLazy = deprecatedRedirect("findByCodeLazy", "findByCode", findByCode);
/**
* @deprecated Use {@link findStore} instead
2024-05-03 19:39:21 -03:00
*
* Find a store by its displayName, lazily
*/
2024-05-07 21:33:05 -03:00
export const findStoreLazy = deprecatedRedirect("findStoreLazy", "findStore", findStore);
/**
* @deprecated Use {@link findComponent} instead
2024-05-03 19:39:21 -03:00
*
* Finds the first component that matches the filter, lazily.
*/
2024-05-07 21:33:05 -03:00
export const findComponentLazy = deprecatedRedirect("findComponentLazy", "findComponent", findComponent);
/**
* @deprecated Use {@link findComponentByCode} instead
2024-05-03 19:39:21 -03:00
*
* Finds the first component that includes all the given code, lazily
*/
2024-05-07 21:33:05 -03:00
export const findComponentByCodeLazy = deprecatedRedirect("findComponentByCodeLazy", "findComponentByCode", findComponentByCode);
/**
* @deprecated Use {@link findExportedComponent} instead
2024-05-03 19:39:21 -03:00
*
* Finds the first component that is exported by the first prop name, lazily
*/
2024-05-07 21:33:05 -03:00
export const findExportedComponentLazy = deprecatedRedirect("findExportedComponentLazy", "findExportedComponent", findExportedComponent);
2024-06-21 02:23:04 -03:00
/**
* @deprecated Use {@link mapMangledModule} instead
*
* {@link mapMangledModule}, lazy.
*
* Finds a mangled module by the provided code "code" (must be unique and can be anywhere in the module)
* then maps it into an easily usable module via the specified mappers.
*
* @param code The code to look for
* @param mappers Mappers to create the non mangled exports
* @returns Unmangled exports as specified in mappers
*
* @example mapMangledModule("headerIdIsManaged:", {
* openModal: filters.byCode("headerIdIsManaged:"),
* closeModal: filters.byCode("key==")
* })
*/
export const mapMangledModuleLazy = deprecatedRedirect("mapMangledModuleLazy", "mapMangledModule", mapMangledModule);
/**
* @deprecated Use {@link cacheFindAll} instead
*/
2024-05-07 21:33:05 -03:00
export const findAll = deprecatedRedirect("findAll", "cacheFindAll", cacheFindAll);
/**
* @deprecated Use {@link cacheFindBulk} instead
2024-05-03 19:39:21 -03:00
*
* Same as {@link cacheFind} but in bulk
*
* @param filterFns Array of filters. Please note that this array will be modified in place, so if you still
* need it afterwards, pass a copy.
* @returns Array of results in the same order as the passed filters
*/
2024-05-07 21:33:05 -03:00
export const findBulk = deprecatedRedirect("findBulk", "cacheFindBulk", cacheFindBulk);
2024-05-02 23:18:12 -03:00
/**
2024-06-21 02:23:04 -03:00
* @deprecated Use {@link cacheFindModuleId} instead
*
2024-06-21 02:23:04 -03:00
* Find the id of the first module factory that includes all the given code
* @returns string or null
2024-05-02 23:18:12 -03:00
*/
2024-06-21 02:23:04 -03:00
export const findModuleId = deprecatedRedirect("findModuleId", "cacheFindModuleId", cacheFindModuleId);