From 4057995a86cd84f32b4ad24b7426c8b252f4756a Mon Sep 17 00:00:00 2001 From: amsyarasyiq <82711525+amsyarasyiq@users.noreply.github.com> Date: Thu, 23 May 2024 00:47:21 +0800 Subject: [PATCH] fix(metro): unlazify react components when needed --- build.mjs | 3 ++ src/entry.ts | 8 +++ src/lib/managers/themes.ts | 18 ++++--- src/lib/ui/components/discord/Forms.ts | 3 +- src/lib/ui/components/discord/Redesign.tsx | 9 ++-- src/lib/utils/lazy.ts | 15 ++++-- src/metro/modules.ts | 9 ++-- src/metro/polyfills/redesign.ts | 62 +++++++++++----------- 8 files changed, 78 insertions(+), 49 deletions(-) diff --git a/build.mjs b/build.mjs index 7924e46..63b1239 100644 --- a/build.mjs +++ b/build.mjs @@ -95,6 +95,9 @@ try { version: `"${isRelease ? commitHash : timeString}"` } } + }, + react: { + pragma: "__bunny_createElement" } }, }, diff --git a/src/entry.ts b/src/entry.ts index e795ffc..dfa36f4 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -1,9 +1,17 @@ +import { getFactoryOfProxy } from "@lib/utils/lazy"; import { version } from "bunny-build"; import { instead } from "spitroast"; // @ts-ignore - shut up fr globalThis.window = globalThis; +// @ts-ignore && TODO: Move this to a more sensible place +globalThis.__bunny_createElement = function createElement(type, props, ...children) { + const factory = getFactoryOfProxy(type); + if (factory) type = factory(); + return React.createElement(type, props, ...children); +}; + async function initializeBunny() { try { // Make 'freeze' and 'seal' do nothing diff --git a/src/lib/managers/themes.ts b/src/lib/managers/themes.ts index a9635c0..78573ab 100644 --- a/src/lib/managers/themes.ts +++ b/src/lib/managers/themes.ts @@ -8,11 +8,11 @@ import { ThemeManager } from "@lib/api/native/modules"; import { after, before, instead } from "@lib/api/patcher"; import { awaitSyncWrapper, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage"; import { findInReactTree, safeFetch } from "@lib/utils"; -import { lazyDestructure } from "@lib/utils/lazy"; +import { lazyDestructure, proxyLazy } from "@lib/utils/lazy"; import { Author } from "@lib/utils/types"; import { chroma } from "@metro/common"; import { byMutableProp } from "@metro/filters"; -import { findExports } from "@metro/finders"; +import { createFindProxy } from "@metro/proxy"; import { findByNameProxy, findByProps, findByPropsProxy, findByStoreNameProxy } from "@metro/utils"; import { ImageBackground, Platform, processColor } from "react-native"; @@ -44,10 +44,18 @@ export interface Theme { // Somehow, this is late enough, though? export const color = findByPropsProxy("SemanticColor"); +const mmkvStorage = proxyLazy(() => { + const newModule = findByProps("impl"); + if (typeof newModule?.impl === "object") return newModule.impl; + return findByProps("storage"); +}); + const appearanceManager = findByPropsProxy("updateTheme"); -const mmkvStorage = findByPropsProxy("storage")?.parseResolve ? findByPropsProxy("storage") : findByPropsProxy("impl").impl; const ThemeStore = findByStoreNameProxy("ThemeStore"); const formDividerModule = findByPropsProxy("DIVIDER_COLORS"); +const MessagesWrapperConnected = findByNameProxy("MessagesWrapperConnected", false); +const { MessagesWrapper } = lazyDestructure(() => findByProps("MessagesWrapper")); +const isThemeModule = createFindProxy(byMutableProp("isThemeDark")); export const themes = wrapSync(createStorage>(createMMKVBackend("VENDETTA_THEMES"))); @@ -76,9 +84,6 @@ async function writeTheme(theme: Theme | {}) { * @internal */ export function patchChatBackground() { - const MessagesWrapperConnected = findByNameProxy("MessagesWrapperConnected", false); - const { MessagesWrapper } = lazyDestructure(() => findByProps("MessagesWrapper")); - const patches = [ after("default", MessagesWrapperConnected, (_, ret) => enabled ? React.createElement(ImageBackground, { style: { flex: 1, height: "100%" }, @@ -250,7 +255,6 @@ function isDiscordTheme(name: string) { } function patchColor() { - const isThemeModule = findExports(byMutableProp("isThemeDark")); const callback = ([theme]: any[]) => theme === vdKey ? [vdThemeFallback] : void 0; Object.keys(color.RawColor).forEach(k => { diff --git a/src/lib/ui/components/discord/Forms.ts b/src/lib/ui/components/discord/Forms.ts index ee2bb76..ee6332c 100644 --- a/src/lib/ui/components/discord/Forms.ts +++ b/src/lib/ui/components/discord/Forms.ts @@ -1,3 +1,4 @@ +import { lazyDestructure } from "@lib/utils/lazy"; import { findByPropsProxy } from "@metro/utils"; export const Forms = findByPropsProxy("Form", "FormSection"); @@ -29,4 +30,4 @@ export const { FormTernaryCheckBox, FormText, FormTitle -} = Forms; +} = lazyDestructure(() => Forms); diff --git a/src/lib/ui/components/discord/Redesign.tsx b/src/lib/ui/components/discord/Redesign.tsx index 9baec6f..58cc936 100644 --- a/src/lib/ui/components/discord/Redesign.tsx +++ b/src/lib/ui/components/discord/Redesign.tsx @@ -1,4 +1,5 @@ -import { findExports } from "@metro/finders"; +import { lazyDestructure, proxyLazy } from "@lib/utils/lazy"; +import { findExports } from "@metro"; import { createFilterDefinition, findByPropsProxy } from "@metro/utils"; import { RedesignModuleKeys } from "@ui/types"; import { useEffect } from "react"; @@ -9,7 +10,7 @@ const bySingularProp = createFilterDefinition<[string]>( prop => `bunny.ui.bySingularProp(${prop})` ); -const findSingular = (prop: string) => findExports(bySingularProp(prop))?.[prop]; +const findSingular = (prop: string) => proxyLazy(() => findExports(bySingularProp(prop))?.[prop]); export const Redesign = findByPropsProxy("TableRow") as Record; @@ -117,7 +118,7 @@ export const { useStackNavigation, useTabNavigation, useTooltip -} = Redesign; +} = lazyDestructure(() => Redesign); export const { AccessibilityAnnouncer, @@ -166,4 +167,4 @@ export const { useLegacyClassComponentStyles, useThemeContext, useToken -} = CompatfulRedesign; +} = lazyDestructure(() => CompatfulRedesign); diff --git a/src/lib/utils/lazy.ts b/src/lib/utils/lazy.ts index 96f3e11..eaeb4fe 100644 --- a/src/lib/utils/lazy.ts +++ b/src/lib/utils/lazy.ts @@ -1,9 +1,12 @@ -const factorySymbol = Symbol("lazyFactory"); -const cacheSymbol = Symbol("lazyCache"); +const factorySymbol = Symbol.for("bunny.lazy.factory"); +const cacheSymbol = Symbol.for("bunny.lazy.cache"); const unconfigurable = ["arguments", "caller", "prototype"]; const isUnconfigurable = (key: PropertyKey) => typeof key === "string" && unconfigurable.includes(key); +// eslint-disable-next-line func-call-spacing +const proxyToFactoryMap = new WeakMap any>(); + const lazyHandler: ProxyHandler = { ownKeys: target => { const cacheKeys = Reflect.ownKeys(target[factorySymbol]()); @@ -41,7 +44,9 @@ export function proxyLazy(factory: () => T, asFunction = true): T { return dummy[cacheSymbol] ??= factory(); }; - return new Proxy(dummy, lazyHandler) as any; + const proxy = new Proxy(dummy, lazyHandler) as T; + proxyToFactoryMap.set(proxy, dummy[factorySymbol]); + return proxy; } export function lazyDestructure>(factory: () => T, asFunction = false): T { @@ -62,3 +67,7 @@ export function lazyDestructure>(factory: } }) as T; } + +export function getFactoryOfProxy(obj: T): (() => T) | void { + return proxyToFactoryMap.get(obj) as (() => T) | void; +} diff --git a/src/metro/modules.ts b/src/metro/modules.ts index 37e3019..b18e95e 100644 --- a/src/metro/modules.ts +++ b/src/metro/modules.ts @@ -141,8 +141,11 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) { }); } - moduleSubscriptions.get(Number(id))?.forEach(s => s()) - && moduleSubscriptions.delete(Number(id)); + const subs = moduleSubscriptions.get(Number(id)); + if (subs) { + subs.forEach(s => s()); + moduleSubscriptions.delete(Number(id)); + } } export function getImportingModuleId() { @@ -174,9 +177,7 @@ export function requireModule(id: Metro.ModuleID) { let moduleExports; try { - const forced = !metroModules[id]?.isInitialized; moduleExports = metroRequire(id); - forced && console.log("Force inited", id, moduleExports); } catch { blacklistModule(String(id)); moduleExports = undefined; diff --git a/src/metro/polyfills/redesign.ts b/src/metro/polyfills/redesign.ts index aea8f1f..69ffcd8 100644 --- a/src/metro/polyfills/redesign.ts +++ b/src/metro/polyfills/redesign.ts @@ -1,4 +1,5 @@ import { getPolyfillModuleCacher } from "@metro/caches"; +import { byProps } from "@metro/filters"; import { LiteralUnion } from "type-fest"; const redesignProps = new Set([ @@ -99,41 +100,42 @@ const redesignProps = new Set([ type Keys = LiteralUnion ? U : string, string>; -const redesignPropSource = {} as Record; -const redesignModule = {} as Record; - const cacher = getPolyfillModuleCacher("redesign_module"); -for (const [id, moduleExports] of cacher.getModules()) { - polyfillRedesignModule(moduleExports, Number(id)); -} +const _cache = {} as Record; -function polyfillRedesignModule(moduleExports: any, id: number) { - const propMap = new Map(); +export default new Proxy({}, { + get(_, p) { + if (typeof p !== "string" || !redesignProps.has(p as any)) return; + if (_cache[p]) return _cache[p]; - for (const prop of redesignProps) { - if (moduleExports?.[prop]) { - cacher.cacheId(id); - propMap.set(prop, null); - } else if (moduleExports?.default?.[prop]) { - cacher.cacheId(id); - propMap.set(prop, "default"); - } - } + const prop = p as Keys; + const filter = byProps(prop); + const candidates: any[] = []; - for (const [prop, defaultKey] of propMap) { - const exportsForProp = defaultKey ? moduleExports[defaultKey] : moduleExports; - - if (redesignModule[prop]) { - if (Object.keys(exportsForProp).length < Object.keys(redesignPropSource[prop]).length) { - redesignModule[prop] = exportsForProp[prop]; - redesignPropSource[prop] = exportsForProp; + for (const [id, moduleExports] of cacher.getModules()) { + if (filter(moduleExports, id, false)) { + cacher.cacheId(id); + candidates.push(moduleExports); + } else if ( + moduleExports.__esModule + && moduleExports.default + && filter(moduleExports.default, id, false) + ) { + cacher.cacheId(id); + candidates.push(moduleExports.default); } - } else { - redesignModule[prop] = exportsForProp[prop]; - redesignPropSource[prop] = exportsForProp; } - } -} -export default redesignModule; + if (candidates.length === 0) return undefined; + + const bestCandidate = candidates.reduce( + (c1, c2) => + (Object.keys(c2).length < Object.keys(c1).length) + ? c2 + : c1 + ); + + return _cache[prop] = bestCandidate[prop]; + }, +});