From 84b9e3fec173e2fd2bba33a2f900ba02b0ee1077 Mon Sep 17 00:00:00 2001 From: vee Date: Tue, 28 May 2024 22:31:58 +0200 Subject: [PATCH] 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); }