fix(metro): unlazify react components when needed

This commit is contained in:
amsyarasyiq 2024-05-23 00:47:21 +08:00
parent dcb0408e31
commit 4057995a86
8 changed files with 78 additions and 49 deletions

View file

@ -95,6 +95,9 @@ try {
version: `"${isRelease ? commitHash : timeString}"`
}
}
},
react: {
pragma: "__bunny_createElement"
}
},
},

View file

@ -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

View file

@ -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<Record<string, Theme>>(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 => {

View file

@ -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);

View file

@ -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<RedesignModuleKeys, any>;
@ -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);

View file

@ -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, () => any>();
const lazyHandler: ProxyHandler<any> = {
ownKeys: target => {
const cacheKeys = Reflect.ownKeys(target[factorySymbol]());
@ -41,7 +44,9 @@ export function proxyLazy<T>(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<T extends Record<PropertyKey, unknown>>(factory: () => T, asFunction = false): T {
@ -62,3 +67,7 @@ export function lazyDestructure<T extends Record<PropertyKey, unknown>>(factory:
}
}) as T;
}
export function getFactoryOfProxy<T>(obj: T): (() => T) | void {
return proxyToFactoryMap.get(obj) as (() => T) | void;
}

View file

@ -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;

View file

@ -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<typeof redesignProps extends Set<infer U> ? U : string, string>;
const redesignPropSource = {} as Record<Keys, any>;
const redesignModule = {} as Record<Keys, any>;
const cacher = getPolyfillModuleCacher("redesign_module");
for (const [id, moduleExports] of cacher.getModules()) {
polyfillRedesignModule(moduleExports, Number(id));
}
const _cache = {} as Record<Keys, any>;
function polyfillRedesignModule(moduleExports: any, id: number) {
const propMap = new Map<string, string | null>();
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];
},
});