From 2fc814124fa18507d61b6d8cb9a6ba6e4efa93a0 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 21 Jun 2024 02:23:04 -0300 Subject: [PATCH] add new webpack apis --- src/plugins/consoleShortcuts/index.ts | 4 +- src/plugins/decor/ui/components/index.ts | 2 +- src/plugins/devCompanion.dev/index.tsx | 2 +- src/webpack/common/components.ts | 4 +- src/webpack/common/types/utils.d.ts | 9 - src/webpack/common/utils.ts | 9 +- src/webpack/patchWebpack.ts | 70 ++- src/webpack/webpack.tsx | 515 ++++++++++++++++------- 8 files changed, 432 insertions(+), 183 deletions(-) diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index 9d7c193de..2ce4f0696 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -91,8 +91,8 @@ function makeShortcuts() { findAllByProps: (...props: string[]) => cacheFindAll(filters.byProps(...props)), findByCode: newFindWrapper(filters.byCode), findAllByCode: (code: string) => cacheFindAll(filters.byCode(code)), - findComponentByCode: newFindWrapper(filters.componentByCode), - findAllComponentsByCode: (...code: string[]) => cacheFindAll(filters.componentByCode(...code)), + findComponentByCode: newFindWrapper(filters.byComponentCode), + findAllComponentsByCode: (...code: string[]) => cacheFindAll(filters.byComponentCode(...code)), findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]], findStore: newFindWrapper(filters.byStoreName), PluginsApi: { getter: () => Vencord.Plugins }, diff --git a/src/plugins/decor/ui/components/index.ts b/src/plugins/decor/ui/components/index.ts index be66fc6d4..5cb182e18 100644 --- a/src/plugins/decor/ui/components/index.ts +++ b/src/plugins/decor/ui/components/index.ts @@ -19,7 +19,7 @@ type DecorationGridItemComponent = ComponentType DecorationGridItem = v; -export const AvatarDecorationModalPreview = findComponent(filters.componentByCode(".shopPreviewBanner"), component => { +export const AvatarDecorationModalPreview = findComponent(filters.byComponentCode(".shopPreviewBanner"), component => { return React.memo(component); }); diff --git a/src/plugins/devCompanion.dev/index.tsx b/src/plugins/devCompanion.dev/index.tsx index 277960cd3..c00dd0786 100644 --- a/src/plugins/devCompanion.dev/index.tsx +++ b/src/plugins/devCompanion.dev/index.tsx @@ -216,7 +216,7 @@ function initWs(isManual = false) { results = Object.keys(search(parsedArgs[0])); break; case "ComponentByCode": - results = cacheFindAll(filters.componentByCode(...parsedArgs)); + results = cacheFindAll(filters.byComponentCode(...parsedArgs)); break; default: return reply("Unknown Find Type " + type); diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index 59304e275..92ff4c3b2 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -44,8 +44,8 @@ export let Avatar: t.Avatar = NoopComponent; export let FocusLock: t.FocusLock = NoopComponent; export let useToken: t.useToken; -export const MaskedLink = findComponent(filters.componentByCode("MASKED_LINK)")); -export const Timestamp = findComponent(filters.componentByCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format")); +export const MaskedLink = findComponent(filters.byComponentCode("MASKED_LINK)")); +export const Timestamp = findComponent(filters.byComponentCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format")); export const Flex = findComponent(filters.byProps("Justify", "Align", "Wrap")) as t.Flex; export const OAuth2AuthorizeModal = findExportedComponent("OAuth2AuthorizeModal"); diff --git a/src/webpack/common/types/utils.d.ts b/src/webpack/common/types/utils.d.ts index be43a54b1..16dce6d84 100644 --- a/src/webpack/common/types/utils.d.ts +++ b/src/webpack/common/types/utils.d.ts @@ -172,17 +172,8 @@ export interface Clipboard { export interface NavigationRouter { back(): void; forward(): void; - hasNavigated(): boolean; - getHistory(): { - action: string; - length: 50; - [key: string]: any; - }; transitionTo(path: string, ...args: unknown[]): void; transitionToGuild(guildId: string, ...args: unknown[]): void; - replaceWith(...args: unknown[]): void; - getLastRouteChangeSource(): any; - getLastRouteChangeSourceLocationStack(): any; } export interface IconUtils { diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index d8616122f..5958974d6 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -17,7 +17,7 @@ */ // eslint-disable-next-line path-alias/no-relative -import { _resolveDiscordLoaded, filters, find, findByCode, findByProps, waitFor } from "../webpack"; +import { _resolveDiscordLoaded, filters, find, findByCode, findByProps, mapMangledModule, waitFor } from "../webpack"; import type * as t from "./types/utils"; export const FluxDispatcher = find(filters.byProps("dispatch", "subscribe"), (m: t.FluxDispatcher) => { @@ -115,7 +115,12 @@ export const ApplicationAssetUtils = findByProps("fetch export const Clipboard = findByProps("SUPPORTS_COPY", "copy"); -export const NavigationRouter = findByProps("transitionTo", "replaceWith", "transitionToGuild"); +export const NavigationRouter: t.NavigationRouter = mapMangledModule("aTransitioning to ", { + transitionTo: filters.byCode("transitionTo -"), + transitionToGuild: filters.byCode("transitionToGuild -"), + back: filters.byCode("goBack()"), + forward: filters.byCode("goForward()"), +}); export const SettingsRouter = findByProps("open", "saveAccountChanges"); diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index a1887e8db..9797acc60 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -192,24 +192,42 @@ function patchFactories(factories: Record(r => _resolveDiscordLoaded = export let wreq: WebpackInstance; export let cache: WebpackInstance["c"]; -export type FilterFn = ((mod: any) => boolean) & { +export type FilterFn = ((module: any) => boolean) & { $$vencordProps?: string[]; + $$vencordIsFactoryFilter?: boolean; }; export const filters = { @@ -44,7 +45,7 @@ export const filters = { byCode: (...code: string[]): FilterFn => { const filter: FilterFn = m => { if (typeof m !== "function") return false; - const s = Function.prototype.toString.call(m); + const s = String(m); for (const c of code) { if (!s.includes(c)) return false; } @@ -62,7 +63,7 @@ export const filters = { return filter; }, - componentByCode: (...code: string[]): FilterFn => { + byComponentCode: (...code: string[]): FilterFn => { const byCodeFilter = filters.byCode(...code); const filter: FilterFn = m => { let inner = m; @@ -80,17 +81,39 @@ export const filters = { filter.$$vencordProps = ["componentByCode", ...code]; return filter; + }, + + byFactoryCode: (...code: string[]): FilterFn => { + const byCodeFilter = filters.byCode(...code); + + byCodeFilter.$$vencordProps = ["byFactoryCode", ...code]; + byCodeFilter.$$vencordIsFactoryFilter = true; + + return byCodeFilter; } }; -export type ModCallbackFn = ((mod: any) => void) & { +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 type ModCallbackFnWithId = (mod: any, id: string) => void; +export const factoryListeners = new Set<(factory: ModuleFactory) => void>(); +export const moduleListeners = new Set(); export const waitForSubscriptions = new Map(); -export const moduleListeners = new Set(); -export const factoryListeners = new Set<(factory: (module: any, exports: any, require: WebpackInstance) => void) => void>(); export const beforeInitListeners = new Set<(wreq: WebpackInstance) => void>(); export function _initWebpack(webpackRequire: WebpackInstance) { @@ -106,25 +129,25 @@ if (IS_DEV && IS_DISCORD_DESKTOP) { }, 0); } -export const webpackSearchHistory = [] as Array<["waitFor" | "find" | "findComponent" | "findExportedComponent" | "findComponentByCode" | "findByProps" | "findByCode" | "findStore" | "extractAndLoadChunks" | "webpackDependantLazy" | "webpackDependantLazyComponent", any[]]>; +export const webpackSearchHistory = [] as Array<["waitFor" | "find" | "findComponent" | "findExportedComponent" | "findComponentByCode" | "findByProps" | "findByCode" | "findStore" | "findByFactoryCode" | "mapMangledModule" | "extractAndLoadChunks" | "webpackDependantLazy" | "webpackDependantLazyComponent", any[]]>; function printFilter(filter: FilterFn) { if (filter.$$vencordProps != null) { const props = filter.$$vencordProps; - return `${props[0]}(${props.slice(1).map(arg => `"${arg}"`).join(", ")})`; + return `${props[0]}(${props.slice(1).map(arg => JSON.stringify(arg)).join(", ")})`; } return filter.toString(); } /** - * Wait for the first module that matches the provided filter to be required, - * then call the callback with the module as the first argument. + * 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. * - * If the module is already required, the callback will be called immediately. + * If the module containing the export(s) is already required, the callback will be called immediately. * - * @param filter A function that takes a module and returns a boolean - * @param callback A function that takes the found module as its first argument + * @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 */ export function waitFor(filter: FilterFn, callback: ModCallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) { if (typeof filter !== "function") @@ -147,28 +170,28 @@ export function waitFor(filter: FilterFn, callback: ModCallbackFn, { isIndirect } if (cache != null) { - const existing = cacheFind(filter); - if (existing) return callback(existing); + const { result, id, exportKey, factory } = _cacheFind(filter); + if (result != null) return callback(result, { id: id!, exportKey: exportKey as PropertyKey | null, factory: factory! }); } waitForSubscriptions.set(filter, callback); } /** - * Find the first module that matches the filter. + * Find the first export or module exports that matches the filter. * * The way this works internally is: - * Wait for the first module that matches the provided filter to be required, - * then call the callback with the module as the first argument. + * 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. * - * If the module is already required, the callback will be called immediately. + * If the module containing the export(s) is already required, the callback will be called immediately. * * The callback must return a value that will be used as the proxy inner value. * - * If no callback is specified, the default callback will assign the proxy inner value to all the module. + * If no callback is specified, the default callback will assign the proxy inner value to the plain find result * - * @param filter A function that takes a module and returns a boolean - * @param callback A function that takes the found module as its first argument and returns something to use as the proxy inner value. Useful if you want to use a value from the module, instead of all of it. Defaults to the module itself + * @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 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 callback return value as its true value, or the callback return value if the callback was called when the function was called */ export function find(filter: FilterFn, callback: (mod: any) => any = m => m, { isIndirect = false }: { isIndirect?: boolean; } = {}) { @@ -178,7 +201,7 @@ export function find(filter: FilterFn, callback: (mod: any) => an throw new Error("Invalid callback. Expected a function got " + typeof callback); const [proxy, setInnerValue] = proxyInner(`Webpack 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, mod => setInnerValue(callback(mod)), { isIndirect: true }); + waitFor(filter, m => setInnerValue(callback(m)), { isIndirect: true }); if (IS_REPORTER && !isIndirect) { webpackSearchHistory.push(["find", [proxy, filter]]); @@ -190,9 +213,9 @@ export function find(filter: FilterFn, callback: (mod: any) => an } /** - * Find the first component that matches the filter. + * Find the first exported component that matches the filter. * - * @param filter A function that takes a module and returns a boolean + * @param filter A function that takes an export or module exports and returns a boolean * @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 */ @@ -277,7 +300,7 @@ export function findExportedComponent(...props: string[] } /** - * Find the first component in a default export that includes all the given code. + * Find the first component in an export that includes all the given code. * * @example findComponentByCode(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR") * @example findComponentByCode(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)", ColorPicker => React.memo(ColorPicker)) @@ -290,7 +313,7 @@ export function findComponentByCode(...code: string[] | const parse = (typeof code.at(-1) === "function" ? code.pop() : m => m) as (component: any) => LazyComponentType; const newCode = code as string[]; - const ComponentResult = findComponent(filters.componentByCode(...newCode), parse, { isIndirect: true }); + const ComponentResult = findComponent(filters.byComponentCode(...newCode), parse, { isIndirect: true }); if (IS_REPORTER) { webpackSearchHistory.push(["findComponentByCode", [ComponentResult, ...newCode]]); @@ -300,9 +323,9 @@ export function findComponentByCode(...code: string[] | } /** - * Find the first module or default export that includes all the given props. + * Find the first module exports or export that includes all the given props. * - * @param props A list of props to search the exports for + * @param props A list of props to search the module or exports for */ export function findByProps(...props: string[]) { const result = find(filters.byProps(...props), m => m, { isIndirect: true }); @@ -315,7 +338,7 @@ export function findByProps(...props: string[]) { } /** - * Find the first default export that includes all the given code. + * Find the first export that includes all the given code. * * @param code A list of code to search each export for */ @@ -345,32 +368,152 @@ export function findStore(name: string) { } /** - * Find the first already required module that matches the filter. + * Find the module exports of the first module which the factory includes all the given code. * - * @param filter A function that takes a module and returns a boolean - * @returns The found module or null + * @param code A list of code to search each factory for */ -export const cacheFind = traceFunction("cacheFind", function cacheFind(filter: FilterFn) { +export function findByFactoryCode(...code: string[]) { + const result = find(filters.byFactoryCode(...code), m => m, { isIndirect: true }); + + if (IS_REPORTER) { + webpackSearchHistory.push(["findByFactoryCode", [result, ...code]]); + } + + 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. + * + * @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(code: string | string[], mappers: Record) { + const result = find>(filters.byFactoryCode(...Array.isArray(code) ? code : [code]), exports => { + const mapping = {} as Record; + + outer: + for (const newName in mappers) { + const filter = mappers[newName]; + + if (typeof exports === "object") { + for (const exportKey in exports) { + const exportValue = exports[exportKey]; + + if (exportValue != null && filter(exportValue)) { + mapping[newName] = exportValue; + continue outer; + } + } + } + + const [proxy] = proxyInner(`Webpack mapMangled mapper filter 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."); + // Use the proxy to throw errors because no export matched the filter + mapping[newName] = proxy; + } + + return mapping; + }, { isIndirect: true }); + + if (IS_REPORTER) { + webpackSearchHistory.push(["mapMangledModule", [result, code, mappers]]); + } + + return result; +} + +type CacheFindResult = { + /** 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. + * + * @param filter A function that takes an export or module exports and returns a boolean + */ +export const _cacheFind = traceFunction("cacheFind", function _cacheFind(filter: FilterFn): CacheFindResult { if (typeof filter !== "function") throw new Error("Invalid filter. Expected a function got " + typeof filter); for (const key in cache) { const mod = cache[key]; - if (!mod?.exports) continue; + if (!mod?.loaded || mod?.exports == null) continue; - if (filter(mod.exports)) { - return mod.exports; + if (filter.$$vencordIsFactoryFilter && filter(wreq.m[key])) { + return { result: exports, id: key, exportKey: null, factory: wreq.m[key] }; } - if (mod.exports.default && filter(mod.exports.default)) { - return mod.exports.default; + if (filter(mod.exports)) { + return { result: exports, id: key, exportKey: null, factory: wreq.m[key] }; + } + + if (typeof mod.exports !== "object") { + continue; + } + + if (mod.exports.default != null && filter(mod.exports.default)) { + return { result: exports.default, id: key, exportKey: "default ", factory: wreq.m[key] }; + } + + 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] }; + } } } - return null; + return {}; }); +/** + * 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); + 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 + */ export function cacheFindAll(filter: FilterFn) { if (typeof filter !== "function") throw new Error("Invalid filter. Expected a function got " + typeof filter); @@ -378,15 +521,32 @@ export function cacheFindAll(filter: FilterFn) { const ret = [] as any[]; for (const key in cache) { const mod = cache[key]; - if (!mod?.exports) continue; + if (!mod?.loaded || mod?.exports == null) continue; + + if (filter.$$vencordIsFactoryFilter && filter(wreq.m[key])) { + ret.push(mod.exports); + } if (filter(mod.exports)) { ret.push(mod.exports); } - if (mod.exports.default && filter(mod.exports.default)) { + if (typeof mod.exports !== "object") { + continue; + } + + if (mod.exports.default != null && filter(mod.exports.default)) { ret.push(mod.exports.default); } + + for (const exportKey in mod.exports) if (exportKey.length <= 3) { + const exportValue = mod.exports[exportKey]; + + if (exportValue != null && filter(exportValue)) { + ret.push(exportValue); + break; + } + } } return ret; @@ -395,18 +555,19 @@ export function cacheFindAll(filter: FilterFn) { /** * 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. + * @param filterFns Array of filters * @returns Array of results in the same order as the passed filters */ export const cacheFindBulk = traceFunction("cacheFindBulk", function cacheFindBulk(...filterFns: FilterFn[]) { - if (!Array.isArray(filterFns)) + if (!Array.isArray(filterFns)) { throw new Error("Invalid filters. Expected function[] got " + typeof filterFns); + } const { length } = filterFns; - if (length === 0) + if (length === 0) { throw new Error("Expected at least two filters."); + } if (length === 1) { if (IS_DEV) { @@ -419,27 +580,56 @@ export const cacheFindBulk = traceFunction("cacheFindBulk", function cacheFindBu let found = 0; const results = Array(length); + const filters = [...filterFns] as Array; + outer: for (const key in cache) { const mod = cache[key]; - if (!mod?.exports) continue; + if (!mod?.loaded || mod?.exports == null) continue; - for (let j = 0; j < length; j++) { - const filter = filterFns[j]; + 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; - if (filter(mod.exports)) { - results[j] = mod.exports; - filterFns.splice(j--, 1); if (++found === length) break outer; break; } - if (mod.exports.default && filter(mod.exports.default)) { - results[j] = mod.exports.default; - filterFns.splice(j--, 1); + if (filter(mod.exports)) { + results[i] = mod.exports; + filters[i] = undefined; + if (++found === length) break outer; break; } + + 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; + } + } } } @@ -458,16 +648,17 @@ export const cacheFindBulk = traceFunction("cacheFindBulk", function cacheFindBu }); /** - * Find the id of the first module factory that includes all the given code. + * Find the id of the first already loaded module factory that includes all the given code. */ -export const findModuleId = traceFunction("findModuleId", function findModuleId(...code: string[]) { +export const cacheFindModuleId = traceFunction("cacheFindModuleId", function cacheFindModuleId(...code: string[]) { outer: for (const id in wreq.m) { - const str = wreq.m[id].toString(); + const str = String(wreq.m[id]); for (const c of code) { if (!str.includes(c)) continue outer; } + return id; } @@ -475,18 +666,17 @@ export const findModuleId = traceFunction("findModuleId", function findModuleId( if (!IS_DEV || devToolsOpen) { logger.warn(err); - return null; } else { throw err; // Strict behaviour in DevBuilds to fail early and make sure the issue is found } }); /** - * Find the first module factory that includes all the given code. + * Find the first already loaded module factory that includes all the given code. */ -export function findModuleFactory(...code: string[]) { - const id = findModuleId(...code); - if (!id) return null; +export function cacheFindModuleFactory(...code: string[]) { + const id = cacheFindModuleId(...code); + if (id == null) return; return wreq.m[id]; } @@ -522,6 +712,82 @@ export function 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]); + + async function extractAndLoadChunks() { + if (module[SYM_PROXY_INNER_VALUE]) { + 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]]); + } + + return extractAndLoadChunks; +} + function deprecatedRedirect any>(oldMethod: string, newMethod: string, redirect: T): T { return ((...args: Parameters) => { logger.warn(`Method ${oldMethod} is deprecated. Use ${newMethod} instead. For more information read https://github.com/Vendicated/Vencord/pull/2409#issue-2277161516`); @@ -606,6 +872,25 @@ export const findComponentByCodeLazy = deprecatedRedirect("findComponentByCodeLa */ export const findExportedComponentLazy = deprecatedRedirect("findExportedComponentLazy", "findExportedComponent", findExportedComponent); +/** + * @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 */ @@ -622,87 +907,13 @@ export const findAll = deprecatedRedirect("findAll", "cacheFindAll", cacheFindAl */ export const findBulk = deprecatedRedirect("findBulk", "cacheFindBulk", cacheFindBulk); -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. + * @deprecated Use {@link cacheFindModuleId} instead * - * @param code An array of all the 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 promise that resolves with a boolean whether the chunks were loaded + * Find the id of the first module factory that includes all the given code + * @returns string or null */ -export async function extractAndLoadChunks(code: string[], matcher: RegExp = DefaultExtractAndLoadChunksRegex) { - const module = findModuleFactory(...code); - if (!module) { - 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 = module.toString().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); - return true; -} - -/** - * This is just a wrapper around {@link extractAndLoadChunks} to make our reporter test for your webpack finds. - * - * Extract and load chunks using their entry point. - * - * @param code An array of all the 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[], matcher = DefaultExtractAndLoadChunksRegex) { - if (IS_REPORTER) webpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]); - - return makeLazy(() => extractAndLoadChunks(code, matcher)); -} +export const findModuleId = deprecatedRedirect("findModuleId", "cacheFindModuleId", cacheFindModuleId); /** * Search modules by keyword. This searches the factory methods, @@ -717,10 +928,10 @@ export function search(...filters: Array) { outer: for (const id in factories) { const factory = factories[id]; - const str: string = factory.toString(); + const factoryStr = String(factory); for (const filter of filters) { - if (typeof filter === "string" && !str.includes(filter)) continue outer; - if (filter instanceof RegExp && !filter.test(str)) continue outer; + if (typeof filter === "string" && !factoryStr.includes(filter)) continue outer; + if (filter instanceof RegExp && !filter.test(factoryStr)) continue outer; } results[id] = factory; } @@ -737,17 +948,17 @@ export function search(...filters: Array) { * * @param id The id of the module to extract */ -export function extract(id: string | number) { - const mod = wreq.m[id] as Function; - if (!mod) return null; +export function extract(id: PropertyKey) { + const factory = wreq.m[id] as Function; + if (!factory) return; const code = ` -// [EXTRACTED] WebpackModule${id} +// [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,${mod.toString()} -//# sourceURL=ExtractedWebpackModule${id} +0,${String(factory)} +//# sourceURL=ExtractedWebpackModule${String(id)} `; const extracted = (0, eval)(code); return extracted as Function;