From 534565db256e9aaf47f3b3092cbe6f4e0bc982be Mon Sep 17 00:00:00 2001 From: V Date: Sat, 25 Nov 2023 01:32:21 +0100 Subject: [PATCH] Add webpack find testing (#2016) Co-authored-by: V Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> --- .github/workflows/reportBrokenPlugins.yml | 2 +- scripts/build/build.mjs | 4 +- scripts/build/buildWeb.mjs | 4 +- scripts/build/common.mjs | 1 + scripts/generateReport.ts | 212 +++++++++++++++--- src/plugins/fakeNitro/index.ts | 9 +- .../components/UserPermissions.tsx | 5 +- src/plugins/spotifyControls/SpotifyStore.ts | 9 +- src/plugins/typingIndicator/index.tsx | 5 +- src/plugins/vencordToolbox/index.tsx | 7 +- src/utils/lazy.ts | 2 +- src/utils/lazyReact.tsx | 6 +- src/webpack/common/internal.tsx | 10 +- src/webpack/common/utils.ts | 44 ++-- src/webpack/webpack.ts | 66 +++++- 15 files changed, 289 insertions(+), 97 deletions(-) diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml index 8bc93618..13202200 100644 --- a/.github/workflows/reportBrokenPlugins.yml +++ b/.github/workflows/reportBrokenPlugins.yml @@ -29,7 +29,7 @@ jobs: sudo apt-get install -y chromium-browser - name: Build web - run: pnpm buildWeb --standalone + run: pnpm buildWeb --standalone --dev - name: Create Report timeout-minutes: 10 diff --git a/scripts/build/build.mjs b/scripts/build/build.mjs index 89cca7e4..a2e0e002 100755 --- a/scripts/build/build.mjs +++ b/scripts/build/build.mjs @@ -21,11 +21,11 @@ import esbuild from "esbuild"; import { readdir } from "fs/promises"; import { join } from "path"; -import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs"; +import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isDev, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs"; const defines = { IS_STANDALONE: isStandalone, - IS_DEV: JSON.stringify(watch), + IS_DEV: JSON.stringify(isDev), IS_UPDATER_DISABLED: updaterDisabled, IS_WEB: false, IS_EXTENSION: false, diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index 353f4e06..b4c72606 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -23,7 +23,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises import { join } from "path"; import Zip from "zip-local"; -import { BUILD_TIMESTAMP, commonOpts, globPlugins, VERSION, watch } from "./common.mjs"; +import { BUILD_TIMESTAMP, commonOpts, globPlugins, isDev, VERSION } from "./common.mjs"; /** * @type {esbuild.BuildOptions} @@ -43,7 +43,7 @@ const commonOptions = { IS_WEB: "true", IS_EXTENSION: "false", IS_STANDALONE: "true", - IS_DEV: JSON.stringify(watch), + IS_DEV: JSON.stringify(isDev), IS_DISCORD_DESKTOP: "false", IS_VESKTOP: "false", IS_UPDATER_DISABLED: "true", diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index 5488b1b3..5c34ad03 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -33,6 +33,7 @@ export const VERSION = PackageJSON.version; // https://reproducible-builds.org/docs/source-date-epoch/ export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now(); export const watch = process.argv.includes("--watch"); +export const isDev = watch || process.argv.includes("--dev"); export const isStandalone = JSON.stringify(process.argv.includes("--standalone")); export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater")); export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim(); diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 4e044c94..cadf0c2a 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -34,7 +34,7 @@ for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) { const CANARY = process.env.USE_CANARY === "true"; const browser = await pup.launch({ - headless: true, + headless: "new", executablePath: process.env.CHROMIUM_BIN }); @@ -58,14 +58,16 @@ const report = { plugin: string; error: string; }[], - otherErrors: [] as string[] + otherErrors: [] as string[], + badWebpackFinds: [] as string[] }; const IGNORED_DISCORD_ERRORS = [ "KeybindStore: Looking for callback action", "Unable to process domain list delta: Client revision number is null", "Downloading the full bad domains file", - /\[GatewaySocket\].{0,110}Cannot access '/ + /\[GatewaySocket\].{0,110}Cannot access '/, + "search for 'name' in undefined" ] as Array; function toCodeBlock(s: string) { @@ -74,7 +76,10 @@ function toCodeBlock(s: string) { } async function printReport() { + console.log(); + console.log("# Vencord Report" + (CANARY ? " (Canary)" : "")); + console.log(); console.log("## Bad Patches"); @@ -87,12 +92,19 @@ async function printReport() { console.log(); + console.log("## Bad Webpack Finds"); + report.badWebpackFinds.forEach(p => console.log("- " + p)); + + console.log(); + console.log("## Bad Starts"); report.badStarts.forEach(p => { console.log(`- ${p.plugin}`); console.log(` - Error: ${toCodeBlock(p.error)}`); }); + console.log(); + report.otherErrors = report.otherErrors.filter(e => !IGNORED_DISCORD_ERRORS.some(regex => e.match(regex))); console.log("## Discord Errors"); @@ -100,8 +112,9 @@ async function printReport() { console.log(`- ${toCodeBlock(e)}`); }); + console.log(); + if (process.env.DISCORD_WEBHOOK) { - // this code was written almost entirely by Copilot xD await fetch(process.env.DISCORD_WEBHOOK, { method: "POST", headers: { @@ -110,7 +123,7 @@ async function printReport() { body: JSON.stringify({ description: "Here's the latest Vencord Report!", username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""), - avatar_url: "https://cdn.discordapp.com/icons/1015060230222131221/f0204a918c6c9c9a43195997e97d8adf.webp", + avatar_url: "https://cdn.discordapp.com/icons/1015060230222131221/6101cff21e241cebb60c4a01563d0c01.webp?size=512", embeds: [ { title: "Bad Patches", @@ -125,6 +138,11 @@ async function printReport() { }).join("\n\n") || "None", color: report.badPatches.length ? 0xff0000 : 0x00ff00 }, + { + title: "Bad Webpack Finds", + description: report.badWebpackFinds.map(toCodeBlock).join("\n") || "None", + color: report.badWebpackFinds.length ? 0xff0000 : 0x00ff00 + }, { title: "Bad Starts", description: report.badStarts.map(p => { @@ -153,29 +171,35 @@ async function printReport() { page.on("console", async e => { const level = e.type(); - const args = e.args(); + const rawArgs = e.args(); - const firstArg = (await args[0]?.jsonValue()); - if (firstArg === "PUPPETEER_TEST_DONE_SIGNAL") { + const firstArg = await rawArgs[0]?.jsonValue(); + if (firstArg === "[PUPPETEER_TEST_DONE_SIGNAL]") { await browser.close(); await printReport(); process.exit(); } - const isVencord = (await args[0]?.jsonValue()) === "[Vencord]"; - const isDebug = (await args[0]?.jsonValue()) === "[PUP_DEBUG]"; + const isVencord = firstArg === "[Vencord]"; + const isDebug = firstArg === "[PUP_DEBUG]"; + const isWebpackFindFail = firstArg === "[PUP_WEBPACK_FIND_FAIL]"; + + if (isWebpackFindFail) { + process.exitCode = 1; + report.badWebpackFinds.push(await rawArgs[1].jsonValue() as string); + } if (isVencord) { - // make ci fail - process.exitCode = 1; + const args = await Promise.all(e.args().map(a => a.jsonValue())); - const jsonArgs = await Promise.all(args.map(a => a.jsonValue())); - const [, tag, message] = jsonArgs; - const cause = await maybeGetError(args[3]); + const [, tag, message] = args as Array; + const cause = await maybeGetError(e.args()[3]); switch (tag) { case "WebpackInterceptor:": - const [, plugin, type, id, regex] = (message as string).match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!; + process.exitCode = 1; + + const [, plugin, type, id, regex] = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!; report.badPatches.push({ plugin, type, @@ -183,17 +207,26 @@ page.on("console", async e => { match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"), error: cause }); + break; case "PluginManager:": - const [, name] = (message as string).match(/Failed to start (.+)/)!; + const failedToStartMatch = message.match(/Failed to start (.+)/); + if (!failedToStartMatch) break; + + process.exitCode = 1; + + const [, name] = failedToStartMatch; report.badStarts.push({ plugin: name, error: cause }); + break; } - } else if (isDebug) { - console.error(e.text()); + } + + if (isDebug) { + console.log(e.text()); } else if (level === "error") { const text = await Promise.all( e.args().map(async a => { @@ -206,8 +239,8 @@ page.on("console", async e => { ).then(a => a.join(" ").trim()); - if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of")) { - console.error("Got unexpected error", text); + if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("found no module Filter:")) { + console.error("[Unexpected Error]", text); report.otherErrors.push(text); } } @@ -219,17 +252,16 @@ page.on("pageerror", e => console.error("[Page Error]", e)); await page.setBypassCSP(true); function runTime(token: string) { - console.error("[PUP_DEBUG]", "Starting test..."); + console.log("[PUP_DEBUG]", "Starting test..."); try { - // spoof languages to not be suspicious + // Spoof languages to not be suspicious Object.defineProperty(navigator, "languages", { get: function () { return ["en-US", "en"]; }, }); - // Monkey patch Logger to not log with custom css // @ts-ignore Vencord.Util.Logger.prototype._log = function (level, levelColor, args) { @@ -237,7 +269,7 @@ function runTime(token: string) { console[level]("[Vencord]", this.name + ":", ...args); }; - // force enable all plugins and patches + // Force enable all plugins and patches Vencord.Plugins.patches.length = 0; Object.values(Vencord.Plugins.plugins).forEach(p => { // Needs native server to run @@ -247,8 +279,14 @@ function runTime(token: string) { p.patches?.forEach(patch => { patch.plugin = p.name; delete patch.predicate; + if (!Array.isArray(patch.replacement)) patch.replacement = [patch.replacement]; + + patch.replacement.forEach(r => { + delete r.predicate; + }); + Vencord.Plugins.patches.push(patch); }); }); @@ -256,41 +294,141 @@ function runTime(token: string) { Vencord.Webpack.waitFor( "loginToken", m => { - console.error("[PUP_DEBUG]", "Logging in with token..."); + console.log("[PUP_DEBUG]", "Logging in with token..."); m.loginToken(token); } ); - // force load all chunks + // Force load all chunks Vencord.Webpack.onceReady.then(() => setTimeout(async () => { - console.error("[PUP_DEBUG]", "Webpack is ready!"); + console.log("[PUP_DEBUG]", "Webpack is ready!"); const { wreq } = Vencord.Webpack; - console.error("[PUP_DEBUG]", "Loading all chunks..."); - const ids = Function("return" + wreq.u.toString().match(/(?<=\()\{.+?\}/s)![0])(); - for (const id in ids) { + console.log("[PUP_DEBUG]", "Loading all chunks..."); + + let chunks = null as Record | null; + const sym = Symbol("Vencord.chunksExtract"); + + Object.defineProperty(Object.prototype, sym, { + get() { + chunks = this; + }, + set() { }, + configurable: true, + }); + + await (wreq as any).el(sym); + delete Object.prototype[sym]; + + const validChunksEntryPoints = [] as string[]; + const validChunks = [] as string[]; + const invalidChunks = [] as string[]; + + if (!chunks) throw new Error("Failed to get chunks"); + + chunksLoop: + for (const entryPoint in chunks) { + const chunkIds = chunks[entryPoint]; + + for (const id of chunkIds) { + if (!wreq.u(id)) continue; + + const isWasm = await fetch(wreq.p + wreq.u(id)) + .then(r => r.text()) + .then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); + + await new Promise(r => setTimeout(r, 150)); + + if (isWasm) { + invalidChunks.push(id); + continue chunksLoop; + } + + validChunks.push(id); + } + + validChunksEntryPoints.push(entryPoint); + } + + for (const entryPoint of validChunksEntryPoints) { + try { + // Loads all chunks required for an entry point + await (wreq as any).el(entryPoint); + } catch (err) { } + } + + const allChunks = Function("return " + (wreq.u.toString().match(/(?<=\()\{.+?\}/s)?.[0] ?? "null"))() as Record | null; + if (!allChunks) throw new Error("Failed to get all chunks"); + const chunksLeft = Object.keys(allChunks).filter(id => { + return !(validChunks.includes(id) || invalidChunks.includes(id)); + }); + + for (const id of chunksLeft) { const isWasm = await fetch(wreq.p + wreq.u(id)) .then(r => r.text()) .then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); - if (!isWasm) - await wreq.e(id as any); + // Loads a chunk + if (!isWasm) await wreq.e(id as any); await new Promise(r => setTimeout(r, 150)); } - console.error("[PUP_DEBUG]", "Finished loading chunks!"); + + // Make sure every chunk has finished loading + await new Promise(r => setTimeout(r, 1000)); + + for (const entryPoint of validChunksEntryPoints) { + try { + if (wreq.m[entryPoint]) wreq(entryPoint as any); + } catch (err) { + console.error(err); + } + } + + console.log("[PUP_DEBUG]", "Finished loading all chunks!"); for (const patch of Vencord.Plugins.patches) { if (!patch.all) { new Vencord.Util.Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`); } } - setTimeout(() => console.log("PUPPETEER_TEST_DONE_SIGNAL"), 1000); + + for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) { + let method = searchType; + + if (searchType === "findComponent") method = "find"; + if (searchType === "findExportedComponent") method = "findByProps"; + if (searchType === "waitFor" || searchType === "waitForComponent" || searchType === "waitForStore") { + if (typeof args[0] === "string") method = "findByProps"; + else method = "find"; + } + + try { + let result: any; + + if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") { + const [factory] = args; + result = factory(); + } else { + // @ts-ignore + result = Vencord.Webpack[method](...args); + } + + if (result == null || ("$$get" in result && result.$$get() == null)) throw "a rock at ben shapiro"; + } catch (e) { + let logMessage = searchType; + if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`; + else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`; + + console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage); + } + } + + setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000); }, 1000)); } catch (e) { - console.error("[PUP_DEBUG]", "A fatal error occurred"); - console.error("[PUP_DEBUG]", e); + console.log("[PUP_DEBUG]", "A fatal error occurred:", e); process.exit(1); } } diff --git a/src/plugins/fakeNitro/index.ts b/src/plugins/fakeNitro/index.ts index 4d6b7957..3ac75556 100644 --- a/src/plugins/fakeNitro/index.ts +++ b/src/plugins/fakeNitro/index.ts @@ -21,10 +21,9 @@ import { definePluginSettings, Settings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies"; import { getCurrentGuild } from "@utils/discord"; -import { proxyLazy } from "@utils/lazy"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy, findStoreLazy } from "@webpack"; +import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack"; import { ChannelStore, EmojiStore, FluxDispatcher, lodash, Parser, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common"; import type { Message } from "discord-types/general"; import { applyPalette, GIFEncoder, quantize } from "gifenc"; @@ -48,9 +47,9 @@ function searchProtoClassField(localName: string, protoClass: any) { return fieldGetter?.(); } -const PreloadedUserSettingsActionCreators = proxyLazy(() => UserSettingsActionCreators.PreloadedUserSettingsActionCreators); -const AppearanceSettingsActionCreators = proxyLazy(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass)); -const ClientThemeSettingsActionsCreators = proxyLazy(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators)); +const PreloadedUserSettingsActionCreators = proxyLazyWebpack(() => UserSettingsActionCreators.PreloadedUserSettingsActionCreators); +const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass)); +const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators)); const USE_EXTERNAL_EMOJIS = 1n << 18n; const USE_EXTERNAL_STICKERS = 1n << 37n; diff --git a/src/plugins/permissionsViewer/components/UserPermissions.tsx b/src/plugins/permissionsViewer/components/UserPermissions.tsx index aeb97664..b75bafdc 100644 --- a/src/plugins/permissionsViewer/components/UserPermissions.tsx +++ b/src/plugins/permissionsViewer/components/UserPermissions.tsx @@ -18,9 +18,8 @@ import ErrorBoundary from "@components/ErrorBoundary"; import ExpandableHeader from "@components/ExpandableHeader"; -import { proxyLazy } from "@utils/lazy"; import { classes } from "@utils/misc"; -import { filters, findBulk } from "@webpack"; +import { filters, findBulk, proxyLazyWebpack } from "@webpack"; import { i18n, PermissionsBits, Text, Tooltip, useMemo, UserStore } from "@webpack/common"; import type { Guild, GuildMember } from "discord-types/general"; @@ -36,7 +35,7 @@ interface UserPermission { type UserPermissions = Array; -const Classes = proxyLazy(() => { +const Classes = proxyLazyWebpack(() => { const modules = findBulk( filters.byProps("roles", "rolePill", "rolePillBorder"), filters.byProps("roleCircle", "dotBorderBase", "dotBorderColor"), diff --git a/src/plugins/spotifyControls/SpotifyStore.ts b/src/plugins/spotifyControls/SpotifyStore.ts index f940d8d5..b3cd0b28 100644 --- a/src/plugins/spotifyControls/SpotifyStore.ts +++ b/src/plugins/spotifyControls/SpotifyStore.ts @@ -17,8 +17,7 @@ */ import { Settings } from "@api/Settings"; -import { proxyLazy } from "@utils/lazy"; -import { findByPropsLazy } from "@webpack"; +import { findByProps, proxyLazyWebpack } from "@webpack"; import { Flux, FluxDispatcher } from "@webpack/common"; export interface Track { @@ -66,12 +65,12 @@ interface Device { type Repeat = "off" | "track" | "context"; // Don't wanna run before Flux and Dispatcher are ready! -export const SpotifyStore = proxyLazy(() => { +export const SpotifyStore = proxyLazyWebpack(() => { // For some reason ts hates extends Flux.Store const { Store } = Flux; - const SpotifySocket = findByPropsLazy("getActiveSocketAndDevice"); - const SpotifyUtils = findByPropsLazy("SpotifyAPI"); + const SpotifySocket = findByProps("getActiveSocketAndDevice"); + const SpotifyUtils = findByProps("SpotifyAPI"); const API_BASE = "https://api.spotify.com/v1/me/player"; diff --git a/src/plugins/typingIndicator/index.tsx b/src/plugins/typingIndicator/index.tsx index 86bfbb4f..e35eb9b6 100644 --- a/src/plugins/typingIndicator/index.tsx +++ b/src/plugins/typingIndicator/index.tsx @@ -19,14 +19,13 @@ import { definePluginSettings, Settings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import { LazyComponent } from "@utils/react"; import definePlugin, { OptionType } from "@utils/types"; -import { find, findStoreLazy } from "@webpack"; +import { find, findStoreLazy, LazyComponentWebpack } from "@webpack"; import { ChannelStore, GuildMemberStore, i18n, RelationshipStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; import { buildSeveralUsers } from "../typingTweaks"; -const ThreeDots = LazyComponent(() => { +const ThreeDots = LazyComponentWebpack(() => { // This doesn't really need to explicitly find Dots' own module, but it's fine const res = find(m => m.Dots && !m.Menu); diff --git a/src/plugins/vencordToolbox/index.tsx b/src/plugins/vencordToolbox/index.tsx index cd266c6f..bb63a86b 100644 --- a/src/plugins/vencordToolbox/index.tsx +++ b/src/plugins/vencordToolbox/index.tsx @@ -22,15 +22,14 @@ import { openNotificationLogModal } from "@api/Notifications/notificationLog"; import { Settings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import { LazyComponent } from "@utils/react"; import definePlugin from "@utils/types"; -import { filters, find } from "@webpack"; +import { filters, find, LazyComponentWebpack } from "@webpack"; import { Menu, Popout, useState } from "@webpack/common"; import type { ReactNode } from "react"; -const HeaderBarIcon = LazyComponent(() => { +const HeaderBarIcon = LazyComponentWebpack(() => { const filter = filters.byCode(".HEADER_BAR_BADGE"); - return find(m => m.Icon && filter(m.Icon)).Icon; + return find(m => m.Icon && filter(m.Icon))?.Icon; }); function VencordPopout(onClose: () => void) { diff --git a/src/utils/lazy.ts b/src/utils/lazy.ts index 1c89d511..32336fb4 100644 --- a/src/utils/lazy.ts +++ b/src/utils/lazy.ts @@ -76,7 +76,7 @@ handler.getOwnPropertyDescriptor = (target, p) => { }; /** - * Wraps the result of {@see makeLazy} in a Proxy you can consume as if it wasn't lazy. + * Wraps the result of {@link makeLazy} in a Proxy you can consume as if it wasn't lazy. * On first property access, the lazy is evaluated * @param factory lazy factory * @param attempts how many times to try to evaluate the lazy before giving up diff --git a/src/utils/lazyReact.tsx b/src/utils/lazyReact.tsx index abd300a9..e45ca079 100644 --- a/src/utils/lazyReact.tsx +++ b/src/utils/lazyReact.tsx @@ -16,8 +16,12 @@ const NoopComponent = () => null; */ export function LazyComponent(factory: () => React.ComponentType, attempts = 5) { const get = makeLazy(factory, attempts); - return (props: T) => { + const LazyComponent = (props: T) => { const Component = get() ?? NoopComponent; return ; }; + + LazyComponent.$$get = get; + + return LazyComponent; } diff --git a/src/webpack/common/internal.tsx b/src/webpack/common/internal.tsx index 66c52de0..9a89af36 100644 --- a/src/webpack/common/internal.tsx +++ b/src/webpack/common/internal.tsx @@ -19,9 +19,11 @@ import { LazyComponent } from "@utils/react"; // eslint-disable-next-line path-alias/no-relative -import { FilterFn, filters, waitFor } from "../webpack"; +import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "../webpack"; export function waitForComponent = React.ComponentType & Record>(name: string, filter: FilterFn | string | string[]): T { + if (IS_DEV) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]); + let myValue: T = function () { throw new Error(`Vencord could not find the ${name} Component`); } as any; @@ -30,11 +32,13 @@ export function waitForComponent = React.Comp waitFor(filter, (v: any) => { myValue = v; Object.assign(lazyComponent, v); - }); + }, { isIndirect: true }); return lazyComponent; } export function waitForStore(name: string, cb: (v: any) => void) { - waitFor(filters.byStoreName(name), cb); + if (IS_DEV) lazyWebpackSearchHistory.push(["waitForStore", [name]]); + + waitFor(filters.byStoreName(name), cb, { isIndirect: true }); } diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index 2a3d4e67..cef4d51d 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -16,14 +16,23 @@ * along with this program. If not, see . */ -import { proxyLazy } from "@utils/lazy"; import type { Channel, User } from "discord-types/general"; // eslint-disable-next-line path-alias/no-relative -import { _resolveReady, find, findByPropsLazy, findLazy, waitFor } from "../webpack"; +import { _resolveReady, findByPropsLazy, findLazy, waitFor } from "../webpack"; import type * as t from "./types/utils"; export let FluxDispatcher: t.FluxDispatcher; + +waitFor(["dispatch", "subscribe"], m => { + FluxDispatcher = m; + const cb = () => { + m.unsubscribe("CONNECTION_OPEN", cb); + _resolveReady(); + }; + m.subscribe("CONNECTION_OPEN", cb); +}); + export let ComponentDispatch; waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m.ComponentDispatch); @@ -41,7 +50,9 @@ export let SnowflakeUtils: t.SnowflakeUtils; waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m); export let Parser: t.Parser; +waitFor("parseTopic", m => Parser = m); export let Alerts: t.Alerts; +waitFor(["show", "close"], m => Alerts = m); const ToastType = { MESSAGE: 0, @@ -82,6 +93,13 @@ export const Toasts = { } }; +// This is the same module but this is easier +waitFor("showToast", m => { + Toasts.show = m.showToast; + Toasts.pop = m.popToast; +}); + + /** * Show a simple toast. If you need more options, use Toasts.show manually */ @@ -106,26 +124,8 @@ export const Clipboard: t.Clipboard = findByPropsLazy("SUPPORTS_COPY", "copy"); export const NavigationRouter: t.NavigationRouter = findByPropsLazy("transitionTo", "replaceWith", "transitionToGuild"); -waitFor(["dispatch", "subscribe"], m => { - FluxDispatcher = m; - const cb = () => { - m.unsubscribe("CONNECTION_OPEN", cb); - _resolveReady(); - }; - m.subscribe("CONNECTION_OPEN", cb); -}); - - -// This is the same module but this is easier -waitFor("showToast", m => { - Toasts.show = m.showToast; - Toasts.pop = m.popToast; -}); - -waitFor(["show", "close"], m => Alerts = m); -waitFor("parseTopic", m => Parser = m); - export let SettingsRouter: any; waitFor(["open", "saveAccountChanges"], m => SettingsRouter = m); -export const PermissionsBits: t.PermissionsBits = proxyLazy(() => find(m => typeof m.Permissions?.ADMINISTRATOR === "bigint").Permissions); +const { Permissions } = findLazy(m => typeof m.Permissions?.ADMINISTRATOR === "bigint") as { Permissions: t.PermissionsBits; }; +export { Permissions as PermissionsBits }; diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 980288e9..4bdec707 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -127,13 +127,6 @@ export const find = traceFunction("find", function find(filter: FilterFn, { isIn return isWaitFor ? [null, null] : null; }); -/** - * find but lazy - */ -export function findLazy(filter: FilterFn) { - return proxyLazy(() => find(filter)); -} - export function findAll(filter: FilterFn) { if (typeof filter !== "function") throw new Error("Invalid filter. Expected a function got " + typeof filter); @@ -244,6 +237,49 @@ export const findModuleId = traceFunction("findModuleId", function findModuleId( return null; }); +export const lazyWebpackSearchHistory = [] as Array<["find" | "findByProps" | "findByCode" | "findStore" | "findComponent" | "findComponentByCode" | "findExportedComponent" | "waitFor" | "waitForComponent" | "waitForStore" | "proxyLazyWebpack" | "LazyComponentWebpack", any[]]>; + +/** + * This is just a wrapper around {@link proxyLazy} to make our reporter test for your webpack finds. + * + * Wraps the result of {@link makeLazy} in a Proxy you can consume as if it wasn't lazy. + * On first property access, the lazy is evaluated + * @param factory lazy factory + * @param attempts how many times to try to evaluate the lazy before giving up + * @returns Proxy + * + * Note that the example below exists already as an api, see {@link findByPropsLazy} + * @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah); + */ +export function proxyLazyWebpack(factory: () => any, attempts?: number) { + if (IS_DEV) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [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 LazyComponentWebpack(factory: () => any, attempts?: number) { + if (IS_DEV) lazyWebpackSearchHistory.push(["LazyComponentWebpack", [factory]]); + + return LazyComponent(factory, attempts); +} + +/** + * find but lazy + */ +export function findLazy(filter: FilterFn) { + if (IS_DEV) lazyWebpackSearchHistory.push(["find", [filter]]); + + return proxyLazy(() => find(filter)); +} + /** * Find the first module that has the specified properties */ @@ -258,6 +294,8 @@ export function findByProps(...props: string[]) { * findByProps but lazy */ export function findByPropsLazy(...props: string[]) { + if (IS_DEV) lazyWebpackSearchHistory.push(["findByProps", props]); + return proxyLazy(() => findByProps(...props)); } @@ -275,6 +313,8 @@ export function findByCode(...code: string[]) { * findByCode but lazy */ export function findByCodeLazy(...code: string[]) { + if (IS_DEV) lazyWebpackSearchHistory.push(["findByCode", code]); + return proxyLazy(() => findByCode(...code)); } @@ -292,6 +332,8 @@ export function findStore(name: string) { * findStore but lazy */ export function findStoreLazy(name: string) { + if (IS_DEV) lazyWebpackSearchHistory.push(["findStore", [name]]); + return proxyLazy(() => findStore(name)); } @@ -309,6 +351,8 @@ export function findComponentByCode(...code: string[]) { * Finds the first component that matches the filter, lazily. */ export function findComponentLazy(filter: FilterFn) { + if (IS_DEV) lazyWebpackSearchHistory.push(["findComponent", [filter]]); + return LazyComponent(() => find(filter)); } @@ -316,6 +360,8 @@ export function findComponentLazy(filter: FilterFn) { * Finds the first component that includes all the given code, lazily */ export function findComponentByCodeLazy(...code: string[]) { + if (IS_DEV) lazyWebpackSearchHistory.push(["findComponentByCode", code]); + return LazyComponent(() => findComponentByCode(...code)); } @@ -323,6 +369,8 @@ export function findComponentByCodeLazy(...code: string[ * Finds the first component that is exported by the first prop name, lazily */ export function findExportedComponentLazy(...props: string[]) { + if (IS_DEV) lazyWebpackSearchHistory.push(["findExportedComponent", props]); + return LazyComponent(() => findByProps(...props)?.[props[0]]); } @@ -330,7 +378,9 @@ export function findExportedComponentLazy(...props: stri * Wait for a module that matches the provided filter to be registered, * then call the callback with the module as the first argument */ -export function waitFor(filter: string | string[] | FilterFn, callback: CallbackFn) { +export function waitFor(filter: string | string[] | FilterFn, callback: CallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) { + if (IS_DEV && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]); + if (typeof filter === "string") filter = filters.byProps(filter); else if (Array.isArray(filter))