From 4a89fadedd75ba2b5931581b3dbe240eb3b92e42 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 7 May 2024 01:13:46 -0300 Subject: [PATCH] Make Webpack finds not need to re-search --- scripts/generateReport.ts | 70 +++++++++++++++++----------------- src/utils/lazy.ts | 14 ++++--- src/utils/lazyReact.tsx | 36 +++++++++--------- src/utils/react.tsx | 2 + src/webpack/webpack.tsx | 79 +++++++++++++++++++++++++++++---------- 5 files changed, 123 insertions(+), 78 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 9875fb749..d53cbb48f 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -471,26 +471,19 @@ async function runtime(token: string) { } } - // Must evaluate the len outside of the loop, as the array will be modified by the find methods called inside of it - // This will avoid an infinite loop - const len = Vencord.Webpack.webpackSearchHistory.length; - for (let i = 0; i < len; i++) { - const [searchType, args] = Vencord.Webpack.webpackSearchHistory[i]; + const WEBPACK_SEARCH_HISTORY_WITH_FILTER_NAME_PROP = ["find", "findComponent", "waitFor"]; - let method = searchType as string; - if (searchType === "waitFor") method = "cacheFind"; + // eslint-disable-next-line prefer-const + for (let [searchType, args] of Vencord.Webpack.webpackSearchHistory) { + args = [...args]; try { - let result: any; + let result = null as any; - if (method === "webpackDependantLazy" || method === "webpackDependantLazyComponent") { + if (searchType === "webpackDependantLazy" || searchType === "webpackDependantLazyComponent") { const [factory] = args; result = factory(); - - if (result != null && "$$vencordGetter" in result) { - result = result.$$vencordGetter(); - } - } else if (method === "extractAndLoadChunks") { + } else if (searchType === "extractAndLoadChunks") { const [code, matcher] = args; const module = Vencord.Webpack.findModuleFactory(...code); @@ -498,14 +491,20 @@ async function runtime(token: string) { result = module.toString().match(Vencord.Util.canonicalizeMatch(matcher)); } } else { - result = Vencord.Webpack[method](...args); + result = args.shift(); - // If the result is our Proxy or ComponentWrapper, this means the search failed - if ( - result != null && - (result[Vencord.Util.proxyInnerGet] != null || "$$vencordGetter" in result) - ) { - result = undefined; + if (result != null) { + if (result.$$vencordCallbackCalled != null && !result.$$vencordCallbackCalled()) { + result = null; + } + + if (result[Vencord.Util.proxyInnerGet] != null) { + result = result[Vencord.Util.proxyInnerValue]; + } + + if (result.$$vencordInner != null) { + result = result.$$vencordInner(); + } } } @@ -517,28 +516,27 @@ async function runtime(token: string) { let filterName = ""; let parsedArgs = args; - if ("$$vencordProps" in args[0]) { - if ( - searchType === "find" || - searchType === "findComponent" || - searchType === "waitFor" - ) { - filterName = args[0].$$vencordProps[0]; + + if (args[0].$$vencordProps != null) { + if (WEBPACK_SEARCH_HISTORY_WITH_FILTER_NAME_PROP.includes(searchType)) { + filterName = args[0].$$vencordProps.shift(); } - parsedArgs = args[0].$$vencordProps.slice(1); + parsedArgs = args[0].$$vencordProps; } + // if parsedArgs is the same as args, it means vencordProps of the filter was not available (like in normal filter functions), + // so log the filter function instead if ( - parsedArgs === args && searchType === "waitFor" || - searchType === "find" || - searchType === "findComponent" || - searchType === "webpackDependantLazy" || - searchType === "webpackDependantLazyComponent" + parsedArgs === args && (searchType === "waitFor" || + searchType === "find" || + searchType === "findComponent" || + searchType === "webpackDependantLazy" || + searchType === "webpackDependantLazyComponent") ) { - logMessage += `(${parsedArgs[0].toString().slice(0, 147)}...)`; + logMessage += `(${args[0].toString().slice(0, 147)}...)`; } else if (searchType === "extractAndLoadChunks") { - logMessage += `([${parsedArgs[0].map((arg: any) => `"${arg}"`).join(", ")}], ${parsedArgs[1].toString()})`; + logMessage += `([${args[0].map((arg: any) => `"${arg}"`).join(", ")}], ${args[1].toString()})`; } else { logMessage += `(${filterName.length ? `${filterName}(` : ""}${parsedArgs.map(arg => `"${arg}"`).join(", ")})${filterName.length ? ")" : ""}`; } diff --git a/src/utils/lazy.ts b/src/utils/lazy.ts index a98e56f45..4edc56d0d 100644 --- a/src/utils/lazy.ts +++ b/src/utils/lazy.ts @@ -4,17 +4,18 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -export function makeLazy(factory: () => T, attempts = 5): () => T { +export function makeLazy(factory: () => T, attempts = 5, { isIndirect = false }: { isIndirect?: boolean; } = {}): () => T { let tries = 0; let cache: T; const getter = () => { if (!cache && attempts > tries) { cache = factory(); - if (!cache && attempts === ++tries) { + if (!cache && attempts === ++tries && !isIndirect) { console.error(`Lazy factory failed:\n\n${factory}`); } } + return cache; }; @@ -48,8 +49,8 @@ const handler: ProxyHandler = { } }; -const proxyLazyGet = Symbol.for("vencord.lazy.get"); -const proxyLazyCache = Symbol.for("vencord.lazy.cached"); +export const proxyLazyGet = Symbol.for("vencord.lazy.get"); +export const proxyLazyCache = Symbol.for("vencord.lazy.cached"); /** * Wraps the result of factory in a Proxy you can consume as if it wasn't lazy. @@ -59,7 +60,7 @@ const proxyLazyCache = Symbol.for("vencord.lazy.cached"); * @returns Result of factory function */ export function proxyLazy(factory: () => T, attempts = 5, isChild = false): T { - const get = makeLazy(factory, attempts) as any; + const get = makeLazy(factory, attempts, { isIndirect: true }) as any; let isSameTick = true; if (!isChild) setTimeout(() => isSameTick = false, 0); @@ -84,6 +85,9 @@ export function proxyLazy(factory: () => T, attempts = 5, isChild = fal return new Proxy(proxyDummy, { ...handler, get(target, p, receiver) { + if (p === proxyLazyGet) return target[proxyLazyGet]; + if (p === proxyLazyCache) return target[proxyLazyCache]; + // If we're still in the same tick, it means the lazy was immediately used. // thus, we lazy proxy the get access to make things like destructuring work as expected // meow here will also be a lazy diff --git a/src/utils/lazyReact.tsx b/src/utils/lazyReact.tsx index f7f6b0ff0..acc538ce1 100644 --- a/src/utils/lazyReact.tsx +++ b/src/utils/lazyReact.tsx @@ -4,12 +4,8 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { ComponentType } from "react"; - import { makeLazy } from "./lazy"; -export const NoopComponent = () => null; - /** * A lazy component. The factory method is called on first render. * @param factory Function returning a component @@ -17,25 +13,31 @@ export const NoopComponent = () => null; * @returns Result of factory function */ export function LazyComponent(factory: () => React.ComponentType, attempts = 5) { - const get = makeLazy(factory, attempts); + const get = makeLazy(factory, attempts, { isIndirect: true }); + let InnerComponent = null as React.ComponentType | null; + + let lazyFailedLogged = false; const LazyComponent = (props: T) => { - let Component = (() => { - console.error(`LazyComponent factory failed:\n\n${factory}`); - - return null; - }) as React.ComponentType; - // @ts-ignore if (!get.$$vencordLazyFailed()) { - const result = get(); - if (result != null) Component = result; + const ResultComponent = get(); + if (ResultComponent != null) { + InnerComponent = ResultComponent; + } } - return ; + if (InnerComponent === null && !lazyFailedLogged) { + // @ts-ignore + if (get.$$vencordLazyFailed()) { + lazyFailedLogged = true; + } + + console.error(`LazyComponent factory failed:\n\n${factory}`); + } + + return InnerComponent && ; }; - LazyComponent.$$vencordGetter = get; - - return LazyComponent as ComponentType; + return LazyComponent as React.ComponentType; } diff --git a/src/utils/react.tsx b/src/utils/react.tsx index abae97c53..f31549f19 100644 --- a/src/utils/react.tsx +++ b/src/utils/react.tsx @@ -22,6 +22,8 @@ import { checkIntersecting } from "./misc"; export * from "./lazyReact"; +export const NoopComponent = () => null; + /** * Check if an element is on screen * @param intersectOnly If `true`, will only update the state when the element comes into view diff --git a/src/webpack/webpack.tsx b/src/webpack/webpack.tsx index f8945c28b..ce0d41b99 100644 --- a/src/webpack/webpack.tsx +++ b/src/webpack/webpack.tsx @@ -133,7 +133,22 @@ export function waitFor(filter: FilterFn, callback: ModCallbackFn, { isIndirect if (typeof callback !== "function") throw new Error("Invalid callback. Expected a function got " + typeof callback); - if (IS_DEV && !isIndirect) webpackSearchHistory.push(["waitFor", [filter]]); + if (IS_DEV && !isIndirect) { + const originalCallback = callback; + + let callbackCalled = false; + callback = function () { + callbackCalled = true; + + // @ts-ignore + originalCallback(...arguments); + }; + + // @ts-ignore + callback.$$vencordCallbackCalled = () => callbackCalled; + + webpackSearchHistory.push(["waitFor", [callback, filter]]); + } if (cache != null) { const existing = cacheFind(filter); @@ -165,11 +180,13 @@ export function find(filter: FilterFn, callback: (mod: any) => any = m if (typeof callback !== "function") throw new Error("Invalid callback. Expected a function got " + typeof callback); - if (IS_DEV && !isIndirect) webpackSearchHistory.push(["find", [filter]]); - 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 }); + if (IS_DEV && !isIndirect) { + webpackSearchHistory.push(["find", [proxy, filter]]); + } + if (proxy[proxyInnerValue] != null) return proxy[proxyInnerValue] as T; return proxy; @@ -187,9 +204,7 @@ export function findComponent(filter: FilterFn, parse: ( if (typeof parse !== "function") throw new Error("Invalid component parse. Expected a function got " + typeof parse); - if (IS_DEV && !isIndirect) webpackSearchHistory.push(["findComponent", [filter]]); - - let InnerComponent = null as null | React.ComponentType; + let InnerComponent = null as React.ComponentType | null; let findFailedLogged = false; const WrapperComponent = (props: T) => { @@ -201,14 +216,20 @@ export function findComponent(filter: FilterFn, parse: ( return InnerComponent && ; }; - WrapperComponent.$$vencordGetter = () => InnerComponent; - waitFor(filter, (v: any) => { const parsedComponent = parse(v); InnerComponent = parsedComponent; Object.assign(InnerComponent, parsedComponent); }, { isIndirect: true }); + if (IS_DEV) { + WrapperComponent.$$vencordInner = () => InnerComponent; + + if (!isIndirect) { + webpackSearchHistory.push(["findComponent", [WrapperComponent, filter]]); + } + } + if (InnerComponent !== null) return InnerComponent; return WrapperComponent; @@ -230,9 +251,7 @@ export function findExportedComponent(...props: string[] const filter = filters.byProps(...newProps); - if (IS_DEV) webpackSearchHistory.push(["findExportedComponent", props]); - - let InnerComponent = null as null | React.ComponentType; + let InnerComponent = null as React.ComponentType | null; let findFailedLogged = false; const WrapperComponent = (props: T) => { @@ -244,7 +263,6 @@ export function findExportedComponent(...props: string[] return InnerComponent && ; }; - WrapperComponent.$$vencordGetter = () => InnerComponent; waitFor(filter, (v: any) => { const parsedComponent = parse(v[newProps[0]]); @@ -252,6 +270,11 @@ export function findExportedComponent(...props: string[] Object.assign(InnerComponent, parsedComponent); }, { isIndirect: true }); + if (IS_DEV) { + WrapperComponent.$$vencordInner = () => InnerComponent; + webpackSearchHistory.push(["findExportedComponent", [WrapperComponent, ...props]]); + } + if (InnerComponent !== null) return InnerComponent; return WrapperComponent as React.ComponentType; @@ -271,9 +294,13 @@ export function findComponentByCode(...code: string[] | const parse = (typeof code.at(-1) === "function" ? code.pop() : m => m) as (component: any) => React.ComponentType; const newCode = code as string[]; - if (IS_DEV) webpackSearchHistory.push(["findComponentByCode", code]); + const ComponentResult = findComponent(filters.componentByCode(...newCode), parse, { isIndirect: true }); - return findComponent(filters.componentByCode(...newCode), parse, { isIndirect: true }); + if (IS_DEV) { + webpackSearchHistory.push(["findComponentByCode", [ComponentResult, ...code]]); + } + + return ComponentResult; } /** @@ -282,9 +309,13 @@ export function findComponentByCode(...code: string[] | * @param props A list of props to search the exports for */ export function findByProps(...props: string[]) { - if (IS_DEV) webpackSearchHistory.push(["findByProps", props]); + const result = find(filters.byProps(...props), m => m, { isIndirect: true }); - return find(filters.byProps(...props), m => m, { isIndirect: true }); + if (IS_DEV) { + webpackSearchHistory.push(["findByProps", [result, ...props]]); + } + + return result; } /** @@ -293,9 +324,13 @@ export function findByProps(...props: string[]) { * @param code A list of code to search each export for */ export function findByCode(...code: string[]) { - if (IS_DEV) webpackSearchHistory.push(["findByCode", code]); + const result = find(filters.byCode(...code), m => m, { isIndirect: true }); - return find(filters.byCode(...code), m => m, { isIndirect: true }); + if (IS_DEV) { + webpackSearchHistory.push(["findByCode", [result, ...code]]); + } + + return result; } /** @@ -304,9 +339,13 @@ export function findByCode(...code: string[]) { * @param name The store name */ export function findStore(name: string) { - if (IS_DEV) webpackSearchHistory.push(["findStore", [name]]); + const result = find(filters.byStoreName(name), m => m, { isIndirect: true }); - return find(filters.byStoreName(name), m => m, { isIndirect: true }); + if (IS_DEV) { + webpackSearchHistory.push(["findStore", [result, name]]); + } + + return result; } /**