diff --git a/src/plugins/betterSettings/index.tsx b/src/plugins/betterSettings/index.tsx index 39be4d764..ed28a4943 100644 --- a/src/plugins/betterSettings/index.tsx +++ b/src/plugins/betterSettings/index.tsx @@ -8,7 +8,7 @@ import { definePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; -import { proxyInnerValue } from "@utils/proxyInner"; +import { SYM_PROXY_INNER_VALUE } from "@utils/proxyInner"; import { NoopComponent } from "@utils/react"; import definePlugin, { OptionType } from "@utils/types"; import { findByProps } from "@webpack"; @@ -133,7 +133,7 @@ export default definePlugin({ // try catch will only catch errors in the Layer function (hence why it's called as a plain function rather than a component), but // not in children Layer(props: LayerProps) { - if (FocusLock === NoopComponent || ComponentDispatch[proxyInnerValue] == null || Classes[proxyInnerValue] == null) { + if (FocusLock === NoopComponent || ComponentDispatch[SYM_PROXY_INNER_VALUE] == null || Classes[SYM_PROXY_INNER_VALUE] == null) { new Logger("BetterSettings").error("Failed to find some components"); return props.children; } diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index fdf060553..32f8dd4ef 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -17,138 +17,204 @@ */ 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 { SYM_PROXY_INNER_GET, SYM_PROXY_INNER_VALUE } from "@utils/proxyInner"; +import definePlugin, { PluginNative, StartAt } from "@utils/types"; import * as Webpack from "@webpack"; import { cacheFindAll, extract, filters, 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 = cacheFindAll(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: cacheFindAll, + findByProps, + 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)), + 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; + + let value: any; + if (currentVal[SYM_LAZY_GET]) { + value = forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED]; + } else if (currentVal[SYM_PROXY_INNER_GET]) { + value = forceLoad ? currentVal[SYM_PROXY_INNER_GET]() : currentVal[SYM_PROXY_INNER_VALUE]; + } else { + value = 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 = cacheFindAll(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: cacheFindAll, - findByProps, - 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)), - 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 cdd63015b..842c64d18 100644 --- a/src/utils/lazy.ts +++ b/src/utils/lazy.ts @@ -7,12 +7,12 @@ import { AnyObject } from "./types"; export type ProxyLazy = T & { - [proxyLazyGet]: () => T; - [proxyLazyCache]: T | undefined; + [SYM_LAZY_GET]: () => T; + [SYM_LAZY_CACHED]: T | undefined; }; -export const proxyLazyGet = Symbol.for("vencord.lazy.get"); -export const proxyLazyCache = 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"); export type LazyFunction = (() => T) & { $$vencordLazyFailed: () => boolean; @@ -45,14 +45,14 @@ const unconfigurable = ["arguments", "caller", "prototype"]; const handler: ProxyHandler = { ...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName => - [propName, (target: any, ...args: any[]) => Reflect[propName](target[proxyLazyGet](), ...args)] + [propName, (target: any, ...args: any[]) => Reflect[propName](target[SYM_LAZY_GET](), ...args)] )), set: (target, p, newValue) => { - const lazyTarget = target[proxyLazyGet](); + const lazyTarget = target[SYM_LAZY_GET](); return Reflect.set(lazyTarget, p, newValue, lazyTarget); }, ownKeys: target => { - const keys = Reflect.ownKeys(target[proxyLazyGet]()); + const keys = Reflect.ownKeys(target[SYM_LAZY_GET]()); for (const key of unconfigurable) { if (!keys.includes(key)) keys.push(key); } @@ -62,7 +62,7 @@ const handler: ProxyHandler = { if (typeof p === "string" && unconfigurable.includes(p)) return Reflect.getOwnPropertyDescriptor(target, p); - const descriptor = Reflect.getOwnPropertyDescriptor(target[proxyLazyGet](), p); + const descriptor = Reflect.getOwnPropertyDescriptor(target[SYM_LAZY_GET](), p); if (descriptor) Object.defineProperty(target, p, descriptor); return descriptor; } @@ -84,31 +84,32 @@ export function proxyLazy(factory: () => T, attempts = 5, isChild // Define the function in an object to preserve the name after minification const proxyDummy = ({ ProxyDummy() { } }).ProxyDummy; Object.assign(proxyDummy, { - [proxyLazyGet]() { - if (!proxyDummy[proxyLazyCache]) { + [SYM_LAZY_GET]() { + if (!proxyDummy[SYM_LAZY_CACHED]) { if (!get.$$vencordLazyFailed()) { - proxyDummy[proxyLazyCache] = get(); + proxyDummy[SYM_LAZY_CACHED] = get(); } - if (!proxyDummy[proxyLazyCache]) { + if (!proxyDummy[SYM_LAZY_CACHED]) { throw new Error(`proxyLazy factory failed:\n\n${factory}`); } else { - if (typeof proxyDummy[proxyLazyCache] === "function") { - proxy.toString = proxyDummy[proxyLazyCache].toString.bind(proxyDummy[proxyLazyCache]); + if (typeof proxyDummy[SYM_LAZY_CACHED] === "function") { + proxy.toString = proxyDummy[SYM_LAZY_CACHED].toString.bind(proxyDummy[SYM_LAZY_CACHED]); } } } - return proxyDummy[proxyLazyCache]; + return proxyDummy[SYM_LAZY_CACHED]; }, - [proxyLazyCache]: void 0 as T | undefined + [SYM_LAZY_CACHED]: void 0 as T | undefined }); const proxy = new Proxy(proxyDummy, { ...handler, - get(target, p) { - if (p === proxyLazyGet) return target[proxyLazyGet]; - if (p === proxyLazyCache) return target[proxyLazyCache]; + get(target, p, receiver) { + if (p === SYM_LAZY_GET || p === SYM_LAZY_CACHED) { + 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 @@ -117,7 +118,7 @@ export function proxyLazy(factory: () => T, attempts = 5, isChild if (!isChild && isSameTick) { return proxyLazy( () => { - const lazyTarget = target[proxyLazyGet](); + const lazyTarget = target[SYM_LAZY_GET](); return Reflect.get(lazyTarget, p, lazyTarget); }, attempts, @@ -125,7 +126,7 @@ export function proxyLazy(factory: () => T, attempts = 5, isChild ); } - const lazyTarget = target[proxyLazyGet](); + const lazyTarget = target[SYM_LAZY_GET](); if (typeof lazyTarget === "object" || typeof lazyTarget === "function") { return Reflect.get(lazyTarget, p, lazyTarget); } diff --git a/src/utils/proxyInner.ts b/src/utils/proxyInner.ts index a6b36d816..ff68c724f 100644 --- a/src/utils/proxyInner.ts +++ b/src/utils/proxyInner.ts @@ -7,12 +7,12 @@ import { AnyObject } from "./types"; export type ProxyInner = T & { - [proxyInnerGet]?: () => T; - [proxyInnerValue]?: T | undefined; + [SYM_PROXY_INNER_GET]?: () => T; + [SYM_PROXY_INNER_VALUE]?: T | undefined; }; -export const proxyInnerGet = Symbol.for("vencord.proxyInner.get"); -export const proxyInnerValue = Symbol.for("vencord.proxyInner.innerValue"); +export const SYM_PROXY_INNER_GET = Symbol.for("vencord.proxyInner.get"); +export const SYM_PROXY_INNER_VALUE = Symbol.for("vencord.proxyInner.innerValue"); // Proxies demand that these properties be unmodified, so proxyInner // will always return the function default for them. @@ -20,14 +20,14 @@ const unconfigurable = ["arguments", "caller", "prototype"]; const handler: ProxyHandler = { ...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName => - [propName, (target: any, ...args: any[]) => Reflect[propName](target[proxyInnerGet](), ...args)] + [propName, (target: any, ...args: any[]) => Reflect[propName](target[SYM_PROXY_INNER_GET](), ...args)] )), set: (target, p, value) => { - const innerTarget = target[proxyInnerGet](); + const innerTarget = target[SYM_PROXY_INNER_GET](); return Reflect.set(innerTarget, p, value, innerTarget); }, ownKeys: target => { - const keys = Reflect.ownKeys(target[proxyInnerGet]()); + const keys = Reflect.ownKeys(target[SYM_PROXY_INNER_GET]()); for (const key of unconfigurable) { if (!keys.includes(key)) keys.push(key); } @@ -37,7 +37,7 @@ const handler: ProxyHandler = { if (typeof p === "string" && unconfigurable.includes(p)) return Reflect.getOwnPropertyDescriptor(target, p); - const descriptor = Reflect.getOwnPropertyDescriptor(target[proxyInnerGet](), p); + const descriptor = Reflect.getOwnPropertyDescriptor(target[SYM_PROXY_INNER_GET](), p); if (descriptor) Object.defineProperty(target, p, descriptor); return descriptor; } @@ -61,21 +61,22 @@ export function proxyInner( // Define the function in an object to preserve the name after minification const proxyDummy = ({ ProxyDummy() { } }).ProxyDummy; Object.assign(proxyDummy, { - [proxyInnerGet]: function () { - if (proxyDummy[proxyInnerValue] == null) { + [SYM_PROXY_INNER_GET]: function () { + if (proxyDummy[SYM_PROXY_INNER_VALUE] == null) { throw new Error(errMsg); } - return proxyDummy[proxyInnerValue]; + return proxyDummy[SYM_PROXY_INNER_VALUE]; }, - [proxyInnerValue]: void 0 as T | undefined + [SYM_PROXY_INNER_VALUE]: void 0 as T | undefined }); const proxy = new Proxy(proxyDummy, { ...handler, - get(target, p) { - if (p === proxyInnerValue) return target[proxyInnerValue]; - if (p === proxyInnerGet) return target[proxyInnerGet]; + get(target, p, receiver) { + if (p === SYM_PROXY_INNER_GET || p === SYM_PROXY_INNER_VALUE) { + return Reflect.get(target, p, receiver); + } // If we're still in the same tick, it means the proxy was immediately used. // thus, we proxy the get access to make things like destructuring work as expected @@ -92,7 +93,7 @@ export function proxyInner( return recursiveProxy; } - const innerTarget = target[proxyInnerGet](); + const innerTarget = target[SYM_PROXY_INNER_GET](); if (typeof innerTarget === "object" || typeof innerTarget === "function") { return Reflect.get(innerTarget, p, innerTarget); } @@ -107,7 +108,7 @@ export function proxyInner( // Once we set the parent inner value, we will call the setInnerValue functions of the destructured values, // for them to get the proper value from the parent and use as their inner instead function setInnerValue(innerValue: T) { - proxyDummy[proxyInnerValue] = innerValue; + proxyDummy[SYM_PROXY_INNER_VALUE] = innerValue; recursiveSetInnerValues.forEach(setInnerValue => setInnerValue(innerValue)); if (typeof innerValue === "function") { diff --git a/src/webpack/webpack.tsx b/src/webpack/webpack.tsx index 01d628172..1d7396f37 100644 --- a/src/webpack/webpack.tsx +++ b/src/webpack/webpack.tsx @@ -8,7 +8,7 @@ import { makeLazy, proxyLazy } from "@utils/lazy"; import { LazyComponent } from "@utils/lazyReact"; import { Logger } from "@utils/Logger"; import { canonicalizeMatch } from "@utils/patches"; -import { ProxyInner, proxyInner, proxyInnerValue } from "@utils/proxyInner"; +import { ProxyInner, proxyInner, SYM_PROXY_INNER_VALUE } from "@utils/proxyInner"; import { AnyObject } from "@utils/types"; import type { WebpackInstance } from "discord-types/other"; @@ -182,7 +182,7 @@ export function find(filter: FilterFn, callback: (mod: any) => an webpackSearchHistory.push(["find", [proxy, filter]]); } - if (proxy[proxyInnerValue] != null) return proxy[proxyInnerValue] as ProxyInner; + if (proxy[SYM_PROXY_INNER_VALUE] != null) return proxy[SYM_PROXY_INNER_VALUE] as ProxyInner; return proxy; }