diff --git a/src/webpack/api.tsx b/src/webpack/api.tsx index 2362c8601..06e0a979f 100644 --- a/src/webpack/api.tsx +++ b/src/webpack/api.tsx @@ -27,7 +27,49 @@ export const onceDiscordLoaded = new Promise(r => _resolveDiscordLoaded = export let wreq: WebpackRequire; export let cache: WebpackRequire["c"]; -export type FilterFn = ((module: ModuleExports) => boolean) & { +export function _initWebpack(webpackRequire: WebpackRequire) { + wreq = webpackRequire; + + if (webpackRequire.c == null) return; + cache = webpackRequire.c; + + Reflect.defineProperty(webpackRequire.c, Symbol.toStringTag, { + value: "ModuleCache", + configurable: true, + writable: true, + enumerable: false + }); +} + +export type ModListenerInfo = { + id: PropertyKey; + factory: AnyModuleFactory; +}; + +export type ModCallbackInfo = { + id: PropertyKey; + exportKey: PropertyKey | null; + factory: AnyModuleFactory; +}; + +export type ModListenerFn = (module: ModuleExports, info: ModListenerInfo) => void; +export type ModCallbackFn = ((module: ModuleExports, info: ModCallbackInfo) => void) & { + $$vencordCallbackCalled?: () => boolean; +}; + +export const factoryListeners = new Set<(factory: AnyModuleFactory) => void>(); +export const moduleListeners = new Set(); +export const waitForSubscriptions = new Map(); + +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); +} + +export type FilterFn = ((module: any) => boolean) & { $$vencordProps?: string[]; $$vencordIsFactoryFilter?: boolean; }; @@ -93,48 +135,6 @@ export const filters = { } }; -export type ModListenerInfo = { - id: PropertyKey; - factory: AnyModuleFactory; -}; - -export type ModCallbackInfo = { - id: PropertyKey; - exportKey: PropertyKey | null; - factory: AnyModuleFactory; -}; - -export type ModListenerFn = (module: ModuleExports, info: ModListenerInfo) => void; -export type ModCallbackFn = ((module: ModuleExports, info: ModCallbackInfo) => void) & { - $$vencordCallbackCalled?: () => boolean; -}; - -export const factoryListeners = new Set<(factory: AnyModuleFactory) => void>(); -export const moduleListeners = new Set(); -export const waitForSubscriptions = new Map(); - -export function _initWebpack(webpackRequire: WebpackRequire) { - wreq = webpackRequire; - - if (webpackRequire.c == null) return; - cache = webpackRequire.c; - - Reflect.defineProperty(webpackRequire.c, Symbol.toStringTag, { - value: "ModuleCache", - configurable: true, - writable: true, - enumerable: false - }); -} - -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); -} - export const webpackSearchHistory = [] as Array<["waitFor" | "find" | "findComponent" | "findExportedComponent" | "findComponentByCode" | "findByProps" | "findByCode" | "findStore" | "findByFactoryCode" | "mapMangledModule" | "extractAndLoadChunks" | "webpackDependantLazy" | "webpackDependantLazyComponent", any[]]>; function printFilter(filter: FilterFn) { @@ -388,20 +388,6 @@ export function findByFactoryCode(...code: string[]) { return result; } -/** - * 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(`Webpack module factory find matched no module. Filter: ${printFilter(filter)}`, "Webpack find with proxy called on a primitive value. This can happen if you try to destructure a primitive in the top level definition of the find."); - waitFor(filter, (_, { factory }) => setInnerValue(factory)); - - if (proxy[SYM_PROXY_INNER_VALUE] != null) return proxy[SYM_PROXY_INNER_VALUE] as ProxyInner; - - return proxy; -} - /** * 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. @@ -450,6 +436,127 @@ export function mapMangledModule(code: string | string[], return result; } +/** + * 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(`Webpack module factory find matched no module. Filter: ${printFilter(filter)}`, "Webpack find with proxy called on a primitive value. This can happen if you try to destructure a primitive in the top level definition of the find."); + waitFor(filter, (_, { factory }) => setInnerValue(factory)); + + if (proxy[SYM_PROXY_INNER_VALUE] != null) return proxy[SYM_PROXY_INNER_VALUE] as ProxyInner; + + 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(factory: () => T, attempts?: number) { + if (IS_REPORTER) webpackSearchHistory.push(["webpackDependantLazy", [factory]]); + + return proxyLazy(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(factory: () => any, attempts?: number) { + if (IS_REPORTER) webpackSearchHistory.push(["webpackDependantLazyComponent", [factory]]); + + return LazyComponent(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; +} + export type CacheFindResult = { /** The find result. `undefined` if nothing was found */ result?: ModuleExports; @@ -687,113 +794,6 @@ export function cacheFindModuleFactory(...code: string[]) { return wreq.m[id]; } -/** - * 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(factory: () => T, attempts?: number) { - if (IS_REPORTER) webpackSearchHistory.push(["webpackDependantLazy", [factory]]); - - return proxyLazy(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(factory: () => any, attempts?: number) { - if (IS_REPORTER) webpackSearchHistory.push(["webpackDependantLazyComponent", [factory]]); - - return LazyComponent(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; -} - /** * Search modules by keyword. This searches the factory methods, * meaning you can search all sorts of things, methodName, strings somewhere in the code, etc. @@ -891,7 +891,6 @@ export const findLazy = deprecatedRedirect("findLazy", "find", find); */ export const findByPropsLazy = deprecatedRedirect("findByPropsLazy", "findByProps", findByProps); - /** * @deprecated Use {@link findByCode} instead *