From 1eeadbcd97aeb3caa2fcfd80b99a3a3b94dfddf2 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 17:11:17 -0300 Subject: [PATCH 1/6] export all webpack instances --- src/Vencord.ts | 2 +- src/webpack/patchWebpack.ts | 150 +++++++++++++++++------------------- 2 files changed, 71 insertions(+), 81 deletions(-) diff --git a/src/Vencord.ts b/src/Vencord.ts index 72541148e..ea769c789 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -23,10 +23,10 @@ export * as Util from "./utils"; export * as QuickCss from "./utils/quickCss"; export * as Updater from "./utils/updater"; export * as Webpack from "./webpack"; +export * as WebpackPatcher from "./webpack/patchWebpack"; export { PlainSettings, Settings }; import "./utils/quickCss"; -import "./webpack/patchWebpack"; import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab"; import { StartAt } from "@utils/types"; diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index d7e363065..25d0d20f1 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -14,6 +14,8 @@ import { traceFunction } from "../debug/Tracer"; import { patches } from "../plugins"; import { _initWebpack, factoryListeners, ModuleFactory, moduleListeners, subscriptions, WebpackRequire, wreq } from "."; +type AnyWebpackRequire = Partial & Pick; + type PatchedModuleFactory = ModuleFactory & { $$vencordOriginal?: ModuleFactory; }; @@ -22,83 +24,87 @@ type PatchedModuleFactories = Record; const logger = new Logger("WebpackInterceptor", "#8caaee"); -/** A set with all the module factories objects */ -const allModuleFactories = new Set(); +/** A set with all the Webpack instances */ +export const allWebpackInstances = new Set(); /** Whether we tried to fallback to factory WebpackRequire, or disabled patches */ let wreqFallbackApplied = false; +type Define = typeof Reflect.defineProperty; +const define: Define = (target, p, attributes) => { + if (Object.hasOwn(attributes, "value")) { + attributes.writable = true; + } + + return Reflect.defineProperty(target, p, { + configurable: true, + enumerable: true, + ...attributes + }); +}; + // wreq.m is the Webpack object containing module factories. // We wrap it with our proxy, which is responsible for patching the module factories when they are set, or definining getters for the patched versions. // If this is the main Webpack, we also set up the internal references to WebpackRequire. // wreq.m is pre-populated with module factories, and is also populated via webpackGlobal.push // The sentry module also has their own Webpack with a pre-populated wreq.m, so this also patches the sentry module factories. -Reflect.defineProperty(Function.prototype, "m", { - configurable: true, +define(Function.prototype, "m", { + enumerable: false, set(this: WebpackRequire, moduleFactories: PatchedModuleFactories) { // When using React DevTools or other extensions, we may also catch their Webpack here. // This ensures we actually got the right ones. const { stack } = new Error(); - if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(moduleFactories)) { - const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1]; - logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`); - - // Define a setter for the bundlePath property of WebpackRequire. Only the main Webpack has this property. - // So if the setter is called, this means we can initialize the internal references to WebpackRequire. - Reflect.defineProperty(this, "p", { - configurable: true, - - set(this: WebpackRequire, bundlePath: WebpackRequire["p"]) { - if (bundlePath !== "/assets/") return; - - logger.info("Main Webpack found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); - _initWebpack(this); - clearTimeout(setterTimeout); - - Reflect.defineProperty(this, "p", { - value: bundlePath, - configurable: true, - enumerable: true, - writable: true - }); - } - }); - // setImmediate to clear this property setter if this is not the main Webpack. - // If this is the main Webpack, wreq.m will always be set before the timeout runs. - const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); - - // This needs to be added before the loop below - allModuleFactories.add(moduleFactories); - - // Patch the pre-populated factories - for (const id in moduleFactories) { - defineModulesFactoryGetter(id, Settings.eagerPatches ? patchFactory(id, moduleFactories[id]) : moduleFactories[id]); - } - - Reflect.defineProperty(moduleFactories, Symbol.toStringTag, { - value: "ModuleFactories", - configurable: true, - writable: true, - enumerable: false - }); - - // The proxy responsible for patching the module factories when they are set, or definining getters for the patched versions - moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); - /* - If Discord ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype - Reflect.setPrototypeOf(moduleFactories, new Proxy(moduleFactories, moduleFactoriesHandler)); - */ + if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) && Array.isArray(moduleFactories)) { + define(this, "m", { value: moduleFactories }); + return; } - Reflect.defineProperty(this, "m", { - value: moduleFactories, - configurable: true, - enumerable: true, - writable: true + const fileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; + logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`); + + allWebpackInstances.add(this); + + // Define a setter for the bundlePath property of WebpackRequire. Only the main Webpack has this property. + // So if the setter is called, this means we can initialize the internal references to WebpackRequire. + define(this, "p", { + enumerable: false, + + set(this: WebpackRequire, bundlePath: WebpackRequire["p"]) { + if (bundlePath !== "/assets/") return; + + logger.info("Main Webpack found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); + _initWebpack(this); + clearTimeout(setterTimeout); + + define(this, "p", { value: bundlePath }); + } }); + // setImmediate to clear this property setter if this is not the main Webpack. + // If this is the main Webpack, wreq.m will always be set before the timeout runs. + const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); + + define(moduleFactories, Symbol.toStringTag, { + value: "ModuleFactories", + enumerable: false + }); + + // The proxy responsible for patching the module factories when they are set, or definining getters for the patched versions + moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); + /* + If Discord ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype + Reflect.setPrototypeOf(moduleFactories, new Proxy(moduleFactories, moduleFactoriesHandler)); + */ + + define(this, "m", { value: moduleFactories }); + + // Patch the pre-populated factories + for (const id in moduleFactories) { + defineModulesFactoryGetter(id, Settings.eagerPatches ? patchFactory(id, moduleFactories[id]) : moduleFactories[id]); + } } }); + /** * Define the getter for returning the patched version of the module factory. * @@ -111,11 +117,8 @@ Reflect.defineProperty(Function.prototype, "m", { function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFactory) { // Define the getter in all the module factories objects. Patches are only executed once, so make sure all module factories object // have the patched version - for (const moduleFactories of allModuleFactories) { - Reflect.defineProperty(moduleFactories, id, { - configurable: true, - enumerable: true, - + for (const wreq of allWebpackInstances) { + define(wreq.m, id, { get() { // $$vencordOriginal means the factory is already patched if (factory.$$vencordOriginal != null) { @@ -155,13 +158,7 @@ const moduleFactoriesHandler: ProxyHandler = { set: (target, p, newValue, receiver) => { // If the property is not a number, we are not dealing with a module factory if (Number.isNaN(Number(p))) { - Reflect.defineProperty(target, p, { - value: newValue, - configurable: true, - enumerable: true, - writable: true - }); - return true; + return define(target, p, { value: newValue }); } const existingFactory = Reflect.get(target, p, receiver); @@ -332,13 +329,8 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { PatchedFactory(...args: Parameters) { // Restore the original factory in all the module factories objects, // because we want to make sure the original factory is restored properly, no matter what is the Webpack instance - for (const moduleFactories of allModuleFactories) { - Reflect.defineProperty(moduleFactories, id, { - value: patchedFactory.$$vencordOriginal, - configurable: true, - enumerable: true, - writable: true - }); + for (const wreq of allWebpackInstances) { + define(wreq.m, id, { value: patchedFactory.$$vencordOriginal }); } // eslint-disable-next-line prefer-const @@ -387,10 +379,8 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { // There are (at the time of writing) 11 modules exporting the window // Make these non enumerable to improve webpack search performance if (exports === window && typeof require === "function" && require.c != null) { - Reflect.defineProperty(require.c, id, { + define(require.c, id, { value: require.c[id], - configurable: true, - writable: true, enumerable: false }); return factoryReturn; From 6b648f3d381dbfc9027d8f85839a6fd26f4c7830 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 17:12:36 -0300 Subject: [PATCH 2/6] oops! --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 25d0d20f1..40c428e65 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -54,7 +54,7 @@ define(Function.prototype, "m", { // When using React DevTools or other extensions, we may also catch their Webpack here. // This ensures we actually got the right ones. const { stack } = new Error(); - if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) && Array.isArray(moduleFactories)) { + if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || Array.isArray(moduleFactories)) { define(this, "m", { value: moduleFactories }); return; } From 64262001e8a4d6203864fcf76906238980a31df1 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 17:14:35 -0300 Subject: [PATCH 3/6] boop --- src/webpack/patchWebpack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 40c428e65..6cbcd2a46 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -89,13 +89,13 @@ define(Function.prototype, "m", { }); // The proxy responsible for patching the module factories when they are set, or definining getters for the patched versions - moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); + const proxiedModuleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); /* If Discord ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype Reflect.setPrototypeOf(moduleFactories, new Proxy(moduleFactories, moduleFactoriesHandler)); */ - define(this, "m", { value: moduleFactories }); + define(this, "m", { value: proxiedModuleFactories }); // Patch the pre-populated factories for (const id in moduleFactories) { From d78f1c80004e0558dd5d520316aaa0d423dec420 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 17:15:25 -0300 Subject: [PATCH 4/6] where did this come from? --- src/webpack/patchWebpack.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 6cbcd2a46..b19b16b9a 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -104,7 +104,6 @@ define(Function.prototype, "m", { } }); - /** * Define the getter for returning the patched version of the module factory. * From 84b9e3fec173e2fd2bba33a2f900ba02b0ee1077 Mon Sep 17 00:00:00 2001 From: vee Date: Tue, 28 May 2024 22:31:58 +0200 Subject: [PATCH 5/6] ConsoleShortcuts: Fix autocomplete on lazies, add more utils (#2519) --- src/plugins/consoleShortcuts/index.ts | 260 +++++++++++++++---------- src/plugins/consoleShortcuts/native.ts | 16 ++ src/utils/lazy.ts | 29 +-- 3 files changed, 192 insertions(+), 113 deletions(-) create mode 100644 src/plugins/consoleShortcuts/native.ts diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index b0efe8a08..ee86b5fcf 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -17,138 +17,198 @@ */ import { Devs } from "@utils/constants"; +import { getCurrentChannel, getCurrentGuild } from "@utils/discord"; +import { SYM_LAZY_CACHED, SYM_LAZY_GET } from "@utils/lazy"; import { relaunch } from "@utils/native"; import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches"; -import definePlugin, { StartAt } from "@utils/types"; +import definePlugin, { PluginNative, StartAt } from "@utils/types"; import * as Webpack from "@webpack"; import { extract, filters, findAll, findModuleId, search } from "@webpack"; import * as Common from "@webpack/common"; import type { ComponentType } from "react"; -const WEB_ONLY = (f: string) => () => { +const DESKTOP_ONLY = (f: string) => () => { throw new Error(`'${f}' is Discord Desktop only.`); }; +const define: typeof Object.defineProperty = + (obj, prop, desc) => { + if (Object.hasOwn(desc, "value")) + desc.writable = true; + + return Object.defineProperty(obj, prop, { + configurable: true, + enumerable: true, + ...desc + }); + }; + +function makeShortcuts() { + function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) { + const cache = new Map(); + + return function (...filterProps: unknown[]) { + const cacheKey = String(filterProps); + if (cache.has(cacheKey)) return cache.get(cacheKey); + + const matches = findAll(filterFactory(...filterProps)); + + const result = (() => { + switch (matches.length) { + case 0: return null; + case 1: return matches[0]; + default: + const uniqueMatches = [...new Set(matches)]; + if (uniqueMatches.length > 1) + console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches); + + return matches[0]; + } + })(); + if (result && cacheKey) cache.set(cacheKey, result); + return result; + }; + } + + let fakeRenderWin: WeakRef | undefined; + const find = newFindWrapper(f => f); + const findByProps = newFindWrapper(filters.byProps); + + return { + ...Object.fromEntries(Object.keys(Common).map(key => [key, { getter: () => Common[key] }])), + wp: Webpack, + wpc: { getter: () => Webpack.cache }, + wreq: { getter: () => Webpack.wreq }, + wpsearch: search, + wpex: extract, + wpexs: (code: string) => extract(findModuleId(code)!), + find, + findAll: findAll, + findByProps, + findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)), + findByCode: newFindWrapper(filters.byCode), + findAllByCode: (code: string) => findAll(filters.byCode(code)), + findComponentByCode: newFindWrapper(filters.componentByCode), + findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)), + findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]], + findStore: newFindWrapper(filters.byStoreName), + PluginsApi: { getter: () => Vencord.Plugins }, + plugins: { getter: () => Vencord.Plugins.plugins }, + Settings: { getter: () => Vencord.Settings }, + Api: { getter: () => Vencord.Api }, + Util: { getter: () => Vencord.Util }, + reload: () => location.reload(), + restart: IS_WEB ? DESKTOP_ONLY("restart") : relaunch, + canonicalizeMatch, + canonicalizeReplace, + canonicalizeReplacement, + fakeRender: (component: ComponentType, props: any) => { + const prevWin = fakeRenderWin?.deref(); + const win = prevWin?.closed === false + ? prevWin + : window.open("about:blank", "Fake Render", "popup,width=500,height=500")!; + fakeRenderWin = new WeakRef(win); + win.focus(); + + const doc = win.document; + doc.body.style.margin = "1em"; + + if (!win.prepared) { + win.prepared = true; + + [...document.querySelectorAll("style"), ...document.querySelectorAll("link[rel=stylesheet]")].forEach(s => { + const n = s.cloneNode(true) as HTMLStyleElement | HTMLLinkElement; + + if (s.parentElement?.tagName === "HEAD") + doc.head.append(n); + else if (n.id?.startsWith("vencord-") || n.id?.startsWith("vcd-")) + doc.documentElement.append(n); + else + doc.body.append(n); + }); + } + + Common.ReactDOM.render(Common.React.createElement(component, props), doc.body.appendChild(document.createElement("div"))); + }, + + preEnable: (plugin: string) => (Vencord.Settings.plugins[plugin] ??= { enabled: true }).enabled = true, + + channel: { getter: () => getCurrentChannel(), preload: false }, + channelId: { getter: () => Common.SelectedChannelStore.getChannelId(), preload: false }, + guild: { getter: () => getCurrentGuild(), preload: false }, + guildId: { getter: () => Common.SelectedGuildStore.getGuildId(), preload: false }, + me: { getter: () => Common.UserStore.getCurrentUser(), preload: false }, + meId: { getter: () => Common.UserStore.getCurrentUser().id, preload: false }, + messages: { getter: () => Common.MessageStore.getMessages(Common.SelectedChannelStore.getChannelId()), preload: false } + }; +} + +function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) { + const currentVal = val.getter(); + if (!currentVal || val.preload === false) return currentVal; + + const value = currentVal[SYM_LAZY_GET] + ? forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED] + : currentVal; + + if (value) define(window.shortcutList, key, { value }); + + return value; +} + export default definePlugin({ name: "ConsoleShortcuts", description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.", authors: [Devs.Ven], - getShortcuts(): Record { - function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) { - const cache = new Map(); - - return function (...filterProps: unknown[]) { - const cacheKey = String(filterProps); - if (cache.has(cacheKey)) return cache.get(cacheKey); - - const matches = findAll(filterFactory(...filterProps)); - - const result = (() => { - switch (matches.length) { - case 0: return null; - case 1: return matches[0]; - default: - const uniqueMatches = [...new Set(matches)]; - if (uniqueMatches.length > 1) - console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches); - - return matches[0]; - } - })(); - if (result && cacheKey) cache.set(cacheKey, result); - return result; - }; - } - - let fakeRenderWin: WeakRef | undefined; - const find = newFindWrapper(f => f); - const findByProps = newFindWrapper(filters.byProps); - - return { - ...Object.fromEntries(Object.keys(Common).map(key => [key, { getter: () => Common[key] }])), - wp: Webpack, - wpc: { getter: () => Webpack.cache }, - wreq: { getter: () => Webpack.wreq }, - wpsearch: search, - wpex: extract, - wpexs: (code: string) => extract(findModuleId(code)!), - find, - findAll: findAll, - findByProps, - findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)), - findByCode: newFindWrapper(filters.byCode), - findAllByCode: (code: string) => findAll(filters.byCode(code)), - findComponentByCode: newFindWrapper(filters.componentByCode), - findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)), - findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]], - findStore: newFindWrapper(filters.byStoreName), - PluginsApi: { getter: () => Vencord.Plugins }, - plugins: { getter: () => Vencord.Plugins.plugins }, - Settings: { getter: () => Vencord.Settings }, - Api: { getter: () => Vencord.Api }, - reload: () => location.reload(), - restart: IS_WEB ? WEB_ONLY("restart") : relaunch, - canonicalizeMatch, - canonicalizeReplace, - canonicalizeReplacement, - fakeRender: (component: ComponentType, props: any) => { - const prevWin = fakeRenderWin?.deref(); - const win = prevWin?.closed === false ? prevWin : window.open("about:blank", "Fake Render", "popup,width=500,height=500")!; - fakeRenderWin = new WeakRef(win); - win.focus(); - - const doc = win.document; - doc.body.style.margin = "1em"; - - if (!win.prepared) { - win.prepared = true; - - [...document.querySelectorAll("style"), ...document.querySelectorAll("link[rel=stylesheet]")].forEach(s => { - const n = s.cloneNode(true) as HTMLStyleElement | HTMLLinkElement; - - if (s.parentElement?.tagName === "HEAD") - doc.head.append(n); - else if (n.id?.startsWith("vencord-") || n.id?.startsWith("vcd-")) - doc.documentElement.append(n); - else - doc.body.append(n); - }); - } - - Common.ReactDOM.render(Common.React.createElement(component, props), doc.body.appendChild(document.createElement("div"))); - } - }; - }, - startAt: StartAt.Init, start() { - const shortcuts = this.getShortcuts(); + const shortcuts = makeShortcuts(); window.shortcutList = {}; for (const [key, val] of Object.entries(shortcuts)) { - if (val.getter != null) { - Object.defineProperty(window.shortcutList, key, { - get: val.getter, - configurable: true, - enumerable: true + if ("getter" in val) { + define(window.shortcutList, key, { + get: () => loadAndCacheShortcut(key, val, true) }); - Object.defineProperty(window, key, { - get: () => window.shortcutList[key], - configurable: true, - enumerable: true + define(window, key, { + get: () => window.shortcutList[key] }); } else { window.shortcutList[key] = val; window[key] = val; } } + + // unproxy loaded modules + Webpack.onceReady.then(() => { + setTimeout(() => this.eagerLoad(false), 1000); + + if (!IS_WEB) { + const Native = VencordNative.pluginHelpers.ConsoleShortcuts as PluginNative; + Native.initDevtoolsOpenEagerLoad(); + } + }); + }, + + async eagerLoad(forceLoad: boolean) { + await Webpack.onceReady; + + const shortcuts = makeShortcuts(); + + for (const [key, val] of Object.entries(shortcuts)) { + if (!Object.hasOwn(val, "getter") || (val as any).preload === false) continue; + + try { + loadAndCacheShortcut(key, val, forceLoad); + } catch { } // swallow not found errors in DEV + } }, stop() { delete window.shortcutList; - for (const key in this.getShortcuts()) { + for (const key in makeShortcuts()) { delete window[key]; } } diff --git a/src/plugins/consoleShortcuts/native.ts b/src/plugins/consoleShortcuts/native.ts new file mode 100644 index 000000000..763b239a4 --- /dev/null +++ b/src/plugins/consoleShortcuts/native.ts @@ -0,0 +1,16 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { IpcMainInvokeEvent } from "electron"; + +export function initDevtoolsOpenEagerLoad(e: IpcMainInvokeEvent) { + const handleDevtoolsOpened = () => e.sender.executeJavaScript("Vencord.Plugins.plugins.ConsoleShortcuts.eagerLoad(true)"); + + if (e.sender.isDevToolsOpened()) + handleDevtoolsOpened(); + else + e.sender.once("devtools-opened", () => handleDevtoolsOpened()); +} diff --git a/src/utils/lazy.ts b/src/utils/lazy.ts index b05855fa0..b86533d4d 100644 --- a/src/utils/lazy.ts +++ b/src/utils/lazy.ts @@ -33,8 +33,8 @@ export function makeLazy(factory: () => T, attempts = 5): () => T { const handler: ProxyHandler = {}; -const kGET = Symbol.for("vencord.lazy.get"); -const kCACHE = Symbol.for("vencord.lazy.cached"); +export const SYM_LAZY_GET = Symbol.for("vencord.lazy.get"); +export const SYM_LAZY_CACHED = Symbol.for("vencord.lazy.cached"); for (const method of [ "apply", @@ -51,11 +51,11 @@ for (const method of [ "setPrototypeOf" ]) { handler[method] = - (target: any, ...args: any[]) => Reflect[method](target[kGET](), ...args); + (target: any, ...args: any[]) => Reflect[method](target[SYM_LAZY_GET](), ...args); } handler.ownKeys = target => { - const v = target[kGET](); + const v = target[SYM_LAZY_GET](); const keys = Reflect.ownKeys(v); for (const key of UNCONFIGURABLE_PROPERTIES) { if (!keys.includes(key)) keys.push(key); @@ -67,7 +67,7 @@ handler.getOwnPropertyDescriptor = (target, p) => { if (typeof p === "string" && UNCONFIGURABLE_PROPERTIES.includes(p)) return Reflect.getOwnPropertyDescriptor(target, p); - const descriptor = Reflect.getOwnPropertyDescriptor(target[kGET](), p); + const descriptor = Reflect.getOwnPropertyDescriptor(target[SYM_LAZY_GET](), p); if (descriptor) Object.defineProperty(target, p, descriptor); return descriptor; @@ -90,31 +90,34 @@ export function proxyLazy(factory: () => T, attempts = 5, isChild = false): T let tries = 0; const proxyDummy = Object.assign(function () { }, { - [kCACHE]: void 0 as T | undefined, - [kGET]() { - if (!proxyDummy[kCACHE] && attempts > tries++) { - proxyDummy[kCACHE] = factory(); - if (!proxyDummy[kCACHE] && attempts === tries) + [SYM_LAZY_CACHED]: void 0 as T | undefined, + [SYM_LAZY_GET]() { + if (!proxyDummy[SYM_LAZY_CACHED] && attempts > tries++) { + proxyDummy[SYM_LAZY_CACHED] = factory(); + if (!proxyDummy[SYM_LAZY_CACHED] && attempts === tries) console.error("Lazy factory failed:", factory); } - return proxyDummy[kCACHE]; + return proxyDummy[SYM_LAZY_CACHED]; } }); return new Proxy(proxyDummy, { ...handler, get(target, p, receiver) { + if (p === SYM_LAZY_CACHED || p === SYM_LAZY_GET) + return Reflect.get(target, p, receiver); + // 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 // `const { meow } = findByPropsLazy("meow");` if (!isChild && isSameTick) return proxyLazy( - () => Reflect.get(target[kGET](), p, receiver), + () => Reflect.get(target[SYM_LAZY_GET](), p, receiver), attempts, true ); - const lazyTarget = target[kGET](); + const lazyTarget = target[SYM_LAZY_GET](); if (typeof lazyTarget === "object" || typeof lazyTarget === "function") { return Reflect.get(lazyTarget, p, receiver); } From a4073703fd628622e2b22f59a2b9252cce4db9d7 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 17:44:34 -0300 Subject: [PATCH 6/6] explain UNCONFIGURABLE_PROPERTIES better --- src/utils/misc.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index 089bd541c..424386a26 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -100,7 +100,7 @@ export function pluralise(amount: number, singular: string, plural = singular + return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`; } -/** Unconfigurable properties for proxies */ +/** Proxies which have an internal target but use a function as the main target require these properties to be unconfigurable */ export const UNCONFIGURABLE_PROPERTIES = ["arguments", "caller", "prototype"]; export function interpolateIfDefined(strings: TemplateStringsArray, ...args: any[]) {