diff --git a/src/core/ui/settings/pages/Developer/AssetBrowser.tsx b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx index 0b32115..aec2901 100644 --- a/src/core/ui/settings/pages/Developer/AssetBrowser.tsx +++ b/src/core/ui/settings/pages/Developer/AssetBrowser.tsx @@ -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 ( @@ -15,7 +17,7 @@ export default function AssetBrowser() { onChangeText={(v: string) => setSearch(v)} /> a.name.includes(search) || a.id.toString() === search)} + data={all.filter(a => a.name.includes(search) || a.id.toString() === search)} renderItem={({ item }) => } ItemSeparatorComponent={LegacyFormDivider} keyExtractor={item => item.name} diff --git a/src/core/vendetta/api.tsx b/src/core/vendetta/api.tsx index 456c063..53b8dba 100644 --- a/src/core/vendetta/api.tsx +++ b/src/core/vendetta/api.tsx @@ -168,7 +168,26 @@ export const initVendettaObject = (): any => { showInputAlert: (options: any) => alerts.showInputAlert(options) }, assets: { - all: assets.assetsMap, + all: new Proxy({}, { + 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(); + + 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), diff --git a/src/lib/api/assets.ts b/src/lib/api/assets.ts deleted file mode 100644 index 76ca29b..0000000 --- a/src/lib/api/assets.ts +++ /dev/null @@ -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 = new Proxy({}, { - 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; - 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; -} - diff --git a/src/lib/api/assets/index.ts b/src/lib/api/assets/index.ts new file mode 100644 index 0000000..821b7a3 --- /dev/null +++ b/src/lib/api/assets/index.ts @@ -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; + +export function* iterateAssets() { + const { flagsIndex } = getMetroCache(); + const yielded = new Set(); + + 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; + + 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; +} diff --git a/src/lib/api/assets/patches.ts b/src/lib/api/assets/patches.ts new file mode 100644 index 0000000..1a204cd --- /dev/null +++ b/src/lib/api/assets/patches.ts @@ -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; +} diff --git a/src/lib/utils/types.ts b/src/lib/utils/types.ts new file mode 100644 index 0000000..649532b --- /dev/null +++ b/src/lib/utils/types.ts @@ -0,0 +1 @@ +export type Nullish = null | undefined; diff --git a/src/metro/common/index.ts b/src/metro/common/index.ts index 04618a5..5bb4226 100644 --- a/src/metro/common/index.ts +++ b/src/metro/common/index.ts @@ -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"); diff --git a/src/metro/common/stores.ts b/src/metro/common/stores.ts new file mode 100644 index 0000000..bef2ebf --- /dev/null +++ b/src/metro/common/stores.ts @@ -0,0 +1,3 @@ +import { findByStoreNameLazy } from "@metro/wrappers"; + +export const UserStore = findByStoreNameLazy("UserStore"); diff --git a/src/metro/common/types/components.ts b/src/metro/common/types/components.ts index 42ac3b3..c9bdff4 100644 --- a/src/metro/common/types/components.ts +++ b/src/metro/common/types/components.ts @@ -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; trailingPressableProps?: PressableProps; trailingText?: string; - value?: string | Falsey; + value?: string | Nullish; } export type TextInput = React.FC; @@ -141,6 +142,7 @@ interface FABProps { style?: Style; onPress: () => void; positionBottom?: number; + } export type FloatingActionButton = React.FC; diff --git a/src/metro/finders.ts b/src/metro/finders.ts index b803efd..744160b 100644 --- a/src/metro/finders.ts +++ b/src/metro/finders.ts @@ -63,6 +63,7 @@ export function findModuleId(filter: FilterFn) { export function findExports(filter: FilterFn) { const { id, defaultExport } = findModule(filter); if (id == null) return; + return defaultExport ? requireModule(id).default : requireModule(id); } diff --git a/src/metro/internals/caches.ts b/src/metro/internals/caches.ts index c803de7..81adee3 100644 --- a/src/metro/internals/caches.ts +++ b/src/metro/internals/caches.ts @@ -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, + flagsIndex: {} as Record, findIndex: {} as Record, - polyfillIndex: {} as Record, - assetsIndex: {} as Record + polyfillIndex: {} as Record } 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(); - } -} diff --git a/src/metro/internals/enums.ts b/src/metro/internals/enums.ts index 613043b..71cf9de 100644 --- a/src/metro/internals/enums.ts +++ b/src/metro/internals/enums.ts @@ -1,6 +1,7 @@ export enum ModuleFlags { EXISTS = 1 << 0, - BLACKLISTED = 1 << 1 + BLACKLISTED = 1 << 1, + ASSET = 1 << 2, } export enum ModulesMapInternal { diff --git a/src/metro/internals/modules.ts b/src/metro/internals/modules.ts index d2a0cf8..e9f6e02 100644 --- a/src/metro/internals/modules.ts +++ b/src/metro/internals/modules.ts @@ -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; diff --git a/src/metro/types.ts b/src/metro/types.ts index e380833..82f1c25 100644 --- a/src/metro/types.ts +++ b/src/metro/types.ts @@ -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 { diff --git a/src/metro/wrappers.ts b/src/metro/wrappers.ts index ede0915..c3a1914 100644 --- a/src/metro/wrappers.ts +++ b/src/metro/wrappers.ts @@ -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));