feat: adopt dev metro

This commit is contained in:
pylixonly 2024-11-16 13:08:47 +08:00
parent e56ec778e0
commit cc879485ac
15 changed files with 168 additions and 129 deletions

View file

@ -1,11 +1,13 @@
import AssetDisplay from "@core/ui/settings/pages/Developer/AssetDisplay";
import { assetsMap } from "@lib/api/assets";
import { iterateAssets } from "@lib/api/assets";
import { LegacyFormDivider } from "@metro/common/components";
import { ErrorBoundary, Search } from "@ui/components";
import { useMemo } from "react";
import { FlatList, View } from "react-native";
export default function AssetBrowser() {
const [search, setSearch] = React.useState("");
const all = useMemo(() => Array.from(iterateAssets()), []);
return (
<ErrorBoundary>
@ -15,7 +17,7 @@ export default function AssetBrowser() {
onChangeText={(v: string) => setSearch(v)}
/>
<FlatList
data={Object.values(assetsMap).filter(a => a.name.includes(search) || a.id.toString() === search)}
data={all.filter(a => a.name.includes(search) || a.id.toString() === search)}
renderItem={({ item }) => <AssetDisplay asset={item} />}
ItemSeparatorComponent={LegacyFormDivider}
keyExtractor={item => item.name}

View file

@ -168,7 +168,26 @@ export const initVendettaObject = (): any => {
showInputAlert: (options: any) => alerts.showInputAlert(options)
},
assets: {
all: assets.assetsMap,
all: new Proxy<any>({}, {
get(cache, p) {
if (typeof p !== "string") return undefined;
if (cache[p]) return cache[p];
for (const asset of assets.iterateAssets()) {
if (asset.name) return cache[p] = asset;
}
},
ownKeys(cache) {
const keys = new Set<string>();
for (const asset of assets.iterateAssets()) {
cache[asset.name] = asset;
keys.add(asset.name);
}
return [...keys];
},
}),
find: (filter: (a: any) => boolean) => assets.findAsset(filter),
getAssetByName: (name: string) => assets.findAsset(name),
getAssetByID: (id: number) => assets.findAsset(id),

View file

@ -1,94 +0,0 @@
import { after } from "@lib/api/patcher";
import { getMetroCache, indexAssetName } from "@metro/internals/caches";
import { getImportingModuleId, requireModule } from "@metro/internals/modules";
// TODO: Deprecate this map, make another that maps to an array of assets (Asset[]) instead
/**
* @internal
* Pitfall: Multiple assets may have the same name, this is fine if we require the asset only for display,\
* but not when used to get the registered id/index. In some condition, this would break some plugins like HideGiftButton that gets id by name for patching.\
*/
export const assetsMap: Record<string, Asset> = new Proxy<any>({}, {
get(cache, p) {
if (typeof p !== "string") return undefined;
if (cache[p]) return cache[p];
const moduleIds = getMetroCache().assetsIndex[p];
if (moduleIds == null) return undefined;
for (const id in moduleIds) {
const assetIndex = requireModule(Number(id));
if (typeof assetIndex !== "number") continue;
const assetDefinition = assetsModule.getAssetByID(assetIndex);
if (!assetDefinition) continue;
assetDefinition.index ??= assetDefinition.id ??= assetIndex;
assetDefinition.moduleId ??= id;
// ??= is intended, we only assign to the first asset registered
cache[p] ??= assetDefinition;
}
return cache[p];
},
ownKeys(cache) {
const keys = [] as Array<string>;
for (const key in getMetroCache().assetsIndex) {
cache[key] = this.get!(cache, key, {});
if (cache[key]) keys.push(key);
}
return keys;
},
});
export interface Asset {
/** @deprecated */
id: number;
index: number;
name: string;
moduleId: number;
}
interface AssetModule {
registerAsset(assetDefinition: any): number;
getAssetByID(id: number): any;
}
let assetsModule: AssetModule;
/**
* @internal
*/
export function patchAssets(module: AssetModule) {
if (assetsModule) return;
assetsModule = module;
const unpatch = after("registerAsset", assetsModule, ([asset]: Asset[]) => {
const moduleId = getImportingModuleId();
if (moduleId !== -1) indexAssetName(asset.name, moduleId);
});
return unpatch;
}
/**
* Returns the first asset registry by its registry id (number), name (string) or given filter (function)
*/
export function findAsset(id: number): Asset | undefined;
export function findAsset(name: string): Asset | undefined;
export function findAsset(filter: (a: Asset) => boolean): Asset | undefined;
export function findAsset(param: number | string | ((a: Asset) => boolean)) {
if (typeof param === "number") return assetsModule.getAssetByID(param);
if (typeof param === "string") return assetsMap[param];
return Object.values(assetsMap).find(param);
}
/**
* Returns the first asset ID in the registry with the given name
*/
export function findAssetId(name: string) {
return assetsMap[name]?.index;
}

View file

@ -0,0 +1,78 @@
import { getMetroCache } from "@metro/internals/caches";
import { ModuleFlags } from "@metro/internals/enums";
import { requireModule } from "@metro/internals/modules";
import { assetsModule } from "./patches";
export interface Asset {
id: number;
name: string;
moduleId: number;
}
// Cache common usage
const _nameToAssetCache = {} as Record<string, Asset>;
export function* iterateAssets() {
const { flagsIndex } = getMetroCache();
const yielded = new Set<number>();
for (const id in flagsIndex) {
if (flagsIndex[id] & ModuleFlags.ASSET) {
const assetId = requireModule(Number(id));
if (typeof assetId !== "number" || yielded.has(assetId)) continue;
yield getAssetById(assetId);
yielded.add(assetId);
}
}
}
// Apply additional properties for convenience
function getAssetById(id: number): Asset {
const asset = assetsModule.getAssetByID(id);
if (!asset) return asset;
return Object.assign(asset, { id });
}
/**
* Returns the first asset registry by its registry id (number), name (string) or given filter (function)
*/
export function findAsset(id: number): Asset | undefined;
export function findAsset(name: string): Asset | undefined;
export function findAsset(filter: (a: Asset) => boolean): Asset | undefined;
export function findAsset(param: number | string | ((a: Asset) => boolean)) {
if (typeof param === "number") return getAssetById(param);
if (typeof param === "string" && _nameToAssetCache[param]) {
return _nameToAssetCache[param];
}
for (const asset of iterateAssets()) {
if (typeof param === "string" && asset.name === param) {
_nameToAssetCache[param] = asset;
return asset;
} else if (typeof param === "function" && param(asset)) {
return asset;
}
}
}
export function filterAssets(param: string | ((a: Asset) => boolean)) {
const filteredAssets = [] as Array<Asset>;
for (const asset of iterateAssets()) {
if (typeof param === "string" ? asset.name === param : param(asset)) {
filteredAssets.push(asset);
}
}
return filteredAssets;
}
/**
* Returns the first asset ID in the registry with the given name
*/
export function findAssetId(name: string) {
return findAsset(name)?.id;
}

View file

@ -0,0 +1,25 @@
import { after } from "@lib/api/patcher";
import { indexAssetModuleFlag } from "@metro/internals/caches";
import { getImportingModuleId } from "@metro/internals/modules";
interface AssetModule {
registerAsset(assetDefinition: any): number;
getAssetByID(id: number): any;
}
export let assetsModule: AssetModule;
/**
* @internal
*/
export function patchAssets(module: AssetModule) {
if (assetsModule) return;
assetsModule = module;
const unpatch = after("registerAsset", assetsModule, () => {
const moduleId = getImportingModuleId();
if (moduleId !== -1) indexAssetModuleFlag(moduleId);
});
return unpatch;
}

1
src/lib/utils/types.ts Normal file
View file

@ -0,0 +1 @@
export type Nullish = null | undefined;

View file

@ -1,3 +1,4 @@
import { lazyDestructure } from "@lib/utils/lazy";
import { findByFilePathLazy, findByProps, findByPropsLazy } from "@metro/wrappers";
import type { Dispatcher } from "./types/flux";
@ -18,13 +19,16 @@ export const toasts = findByFilePathLazy("modules/toast/native/ToastActionCreato
export const messageUtil = findByPropsLazy("sendBotMessage");
export const navigationStack = findByPropsLazy("createStackNavigator");
export const NavigationNative = findByPropsLazy("NavigationContainer");
export const tokens = findByPropsLazy("colors", "unsafe_rawColors");
export const semver = findByPropsLazy("parse", "clean");
export const tokens = findByPropsLazy("unsafe_rawColors", "colors");
export const { useToken } = lazyDestructure(() => findByProps("useToken"));
// Flux
export const Flux = findByPropsLazy("connectStores");
// TODO: Making this a proxy/lazy fuck things up for some reason
export const FluxDispatcher = findByProps("_interceptors") as Dispatcher;
export const FluxUtils = findByProps("useStateFromStores");
// React
export const React = window.React = findByPropsLazy("createElement") as typeof import("react");

View file

@ -0,0 +1,3 @@
import { findByStoreNameLazy } from "@metro/wrappers";
export const UserStore = findByStoreNameLazy("UserStore");

View file

@ -1,4 +1,5 @@
import { TextStyles, ThemeColors } from "@lib/ui/types";
import { Nullish } from "@lib/utils/types";
import { TextStyles, ThemeColors } from "@ui/types";
import { Falsey } from "lodash";
import { FC, MutableRefObject, PropsWithoutRef, ReactNode, RefObject } from "react";
import type * as RN from "react-native";
@ -110,7 +111,7 @@ interface TextInputProps extends Omit<RN.TextInputProps, "onChange" | "onChangeT
trailingIcon?: React.FC<any>;
trailingPressableProps?: PressableProps;
trailingText?: string;
value?: string | Falsey;
value?: string | Nullish;
}
export type TextInput = React.FC<TextInputProps>;
@ -141,6 +142,7 @@ interface FABProps {
style?: Style;
onPress: () => void;
positionBottom?: number;
}
export type FloatingActionButton = React.FC<FABProps>;

View file

@ -63,6 +63,7 @@ export function findModuleId<A extends unknown[]>(filter: FilterFn<A>) {
export function findExports<A extends unknown[]>(filter: FilterFn<A>) {
const { id, defaultExport } = findModule(filter);
if (id == null) return;
return defaultExport ? requireModule(id).default : requireModule(id);
}

View file

@ -1,10 +1,11 @@
import { NativeCacheModule, NativeClientInfoModule } from "@lib/api/native/modules";
import { fileExists, readFile, writeFile } from "@lib/api/native/fs";
import { NativeClientInfoModule } from "@lib/api/native/modules";
import { debounce } from "es-toolkit";
import { ModuleFlags, ModulesMapInternal } from "./enums";
const CACHE_VERSION = 52;
const BUNNY_METRO_CACHE_KEY = "__bunny_metro_cache_key__";
const CACHE_VERSION = 100;
const BUNNY_METRO_CACHE_PATH = "caches/metro_modules.json";
type ModulesMap = {
[flag in number | `_${ModulesMapInternal}`]?: ModuleFlags;
@ -19,14 +20,13 @@ function buildInitCache() {
_v: CACHE_VERSION,
_buildNumber: NativeClientInfoModule.Build as number,
_modulesCount: Object.keys(window.modules).length,
exportsIndex: {} as Record<string, number>,
flagsIndex: {} as Record<string, number>,
findIndex: {} as Record<string, ModulesMap | undefined>,
polyfillIndex: {} as Record<string, ModulesMap | undefined>,
assetsIndex: {} as Record<string, ModulesMap | undefined>
polyfillIndex: {} as Record<string, ModulesMap | undefined>
} as const;
// Force load all modules so useful modules are pre-cached. Delay by a second
// because force loading it all will results in an unexpected crash.
// Force load all modules so useful modules are pre-cached. Add a minor
// delay so the cache is initialized before the modules are loaded.
setTimeout(() => {
for (const id in window.modules) {
require("./modules").requireModule(id);
@ -37,11 +37,10 @@ function buildInitCache() {
return cache;
}
// TODO: Store in file system... is a better idea?
/** @internal */
export async function initMetroCache() {
const rawCache = await NativeCacheModule.getItem(BUNNY_METRO_CACHE_KEY);
if (rawCache == null) return void buildInitCache();
if (!await fileExists(BUNNY_METRO_CACHE_PATH)) return void buildInitCache();
const rawCache = await readFile(BUNNY_METRO_CACHE_PATH);
try {
_metroCache = JSON.parse(rawCache);
@ -63,7 +62,7 @@ export async function initMetroCache() {
}
const saveCache = debounce(() => {
NativeCacheModule.setItem(BUNNY_METRO_CACHE_KEY, JSON.stringify(_metroCache));
writeFile(BUNNY_METRO_CACHE_PATH, JSON.stringify(_metroCache));
}, 1000);
function extractExportsFlags(moduleExports: any) {
@ -77,13 +76,18 @@ function extractExportsFlags(moduleExports: any) {
export function indexExportsFlags(moduleId: number, moduleExports: any) {
const flags = extractExportsFlags(moduleExports);
if (flags && flags !== ModuleFlags.EXISTS) {
_metroCache.exportsIndex[moduleId] = flags;
_metroCache.flagsIndex[moduleId] = flags;
}
}
/** @internal */
export function indexBlacklistFlag(id: number) {
_metroCache.exportsIndex[id] |= ModuleFlags.BLACKLISTED;
_metroCache.flagsIndex[id] |= ModuleFlags.BLACKLISTED;
}
/** @internal */
export function indexAssetModuleFlag(id: number) {
_metroCache.flagsIndex[id] |= ModuleFlags.ASSET;
}
/** @internal */
@ -124,11 +128,3 @@ export function getPolyfillModuleCacher(name: string) {
}
};
}
/** @internal */
export function indexAssetName(name: string, moduleId: number) {
if (!isNaN(moduleId)) {
(_metroCache.assetsIndex[name] ??= {})[moduleId] = 1;
saveCache();
}
}

View file

@ -1,6 +1,7 @@
export enum ModuleFlags {
EXISTS = 1 << 0,
BLACKLISTED = 1 << 1
BLACKLISTED = 1 << 1,
ASSET = 1 << 2,
}
export enum ModulesMapInternal {

View file

@ -23,7 +23,7 @@ for (const key in metroModules) {
const id = Number(key);
const metroModule = metroModules[id];
const cache = getMetroCache().exportsIndex[id];
const cache = getMetroCache().flagsIndex[id];
if (cache & ModuleFlags.BLACKLISTED) {
blacklistModule(id);
continue;
@ -86,7 +86,7 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) {
moduleExports.default.track = () => Promise.resolve();
if (moduleExports.registerAsset) {
require("@lib/api/assets").patchAssets(moduleExports);
require("@lib/api/assets/patches").patchAssets(moduleExports);
}
// There are modules registering the same native component
@ -100,6 +100,7 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) {
patchedNativeComponentRegistry = true;
}
// Hook DeveloperExperimentStore
if (moduleExports?.default?.constructor?.displayName === "DeveloperExperimentStore") {
moduleExports.default = new Proxy(moduleExports.default, {
@ -130,7 +131,7 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) {
patchedInspectSource = true;
}
// Explosion (no, I can't explain this, don't ask) ((hi rosie))
//
if (moduleExports.findHostInstance_DEPRECATED) {
const prevExports = metroModules[id - 1]?.publicModule.exports;
const inc = prevExports.default?.reactProfilingEnabled ? 1 : -1;

View file

@ -1,4 +1,4 @@
type Nullish = null | undefined;
import { Nullish } from "@lib/utils/types";
/** @see {@link https://github.com/facebook/metro/blob/c2d7539dfc10aacb2f99fcc2f268a3b53e867a90/packages/metro-runtime/src/polyfills/require.js} */
export namespace Metro {

View file

@ -1,5 +1,5 @@
import { byDisplayName, byFilePath,byName, byProps, byStoreName, byTypeName } from "./filters";
import { findAllExports,findExports } from "./finders";
import { byDisplayName, byFilePath, byName, byProps, byStoreName, byTypeName } from "./filters";
import { findAllExports, findExports } from "./finders";
import { createLazyModule } from "./lazy";
export const findByProps = (...props: string[]) => findExports(byProps(...props));