diff --git a/src/lib/ui/components/discord/Redesign.tsx b/src/lib/ui/components/discord/Redesign.tsx index 7553a1c..7ba1fee 100644 --- a/src/lib/ui/components/discord/Redesign.tsx +++ b/src/lib/ui/components/discord/Redesign.tsx @@ -1,6 +1,5 @@ -import { createFilterDefinition } from "@metro/filters"; import { find } from "@metro/finders"; -import { findByProps } from "@metro/utils"; +import { createFilterDefinition, findByProps } from "@metro/utils"; import { RedesignModuleKeys } from "@ui/types"; import { useEffect } from "react"; import { View } from "react-native"; diff --git a/src/metro/caches.ts b/src/metro/caches.ts index b46345f..3b93b9f 100644 --- a/src/metro/caches.ts +++ b/src/metro/caches.ts @@ -1,34 +1,27 @@ import { ClientInfoManager, MMKVManager } from "@lib/api/native/modules"; import { throttle } from "@lib/utils/throttle"; -import SparkMD5 from "spark-md5"; + +const BUNNY_METRO_CACHE_KEY = "__bunny_metro_cache_key_v5__"; + +type ModulesIndexer = { + _?: 1; + [id: number]: 1 | void; +}; interface MetroCacheStore { v: 5; buildNumber: number; - findIndex: Record; - polyfillCache: Record; + findIndex: Record; + polyfillCache: Record; assetsCache: Record; } -const BUNNY_METRO_CACHE_KEY = "__bunny_metro_cache_key_v5__"; - let metroCache = null as unknown as MetroCacheStore; export function getMetroCache() { return metroCache; } -export function getFuncUniqCall() { - const { stack } = new Error(); - return SparkMD5.hash(stack!); -} - function buildInitCache() { metroCache = { v: 5, @@ -62,9 +55,7 @@ export async function initMetroCache() { } } -const saveCache = throttle(() => { - MMKVManager.setItem(BUNNY_METRO_CACHE_KEY, JSON.stringify(metroCache)); -}); +const saveCache = throttle(() => MMKVManager.setItem(BUNNY_METRO_CACHE_KEY, JSON.stringify(metroCache))); export function registerModuleFindCacheId(uniqueId: string, moduleId: number, all: boolean) { (metroCache.findIndex[uniqueId] ??= { _: undefined })[moduleId] = 1; diff --git a/src/metro/common/index.ts b/src/metro/common/index.ts index 0b74751..67fb9ae 100644 --- a/src/metro/common/index.ts +++ b/src/metro/common/index.ts @@ -1,7 +1,6 @@ -import { createSimpleFilter } from "@metro/filters"; import { find } from "@metro/finders"; import { Dispatcher } from "@metro/types"; -import { findByProps } from "@metro/utils"; +import { createSimpleFilter, findByProps } from "@metro/utils"; // Discord export const constants = findByProps("Fonts", "Permissions"); diff --git a/src/metro/filters.ts b/src/metro/filters.ts index 190810d..7b46e36 100644 --- a/src/metro/filters.ts +++ b/src/metro/filters.ts @@ -1,63 +1,5 @@ - -type Indexable = Record; -type ModuleExports = any; - -type FilterCheckDef = (args: A, m: M) => boolean; - -export interface FilterFn { - (m: any): boolean; - filter: FilterCheckDef; - args: A; - isDefault: boolean; - serialized: string; -} - -export interface FilterDef { - (...args: A): FilterFn; - byDefault(...args: A): FilterFn; - serializer(args: A): string; -} - -export function createFilterDefinition( - fn: FilterCheckDef, - serializer: (args: A) => string -): FilterDef { - function createHolder(func: T, args: A, isDefault: boolean) { - return Object.assign(func, { - filter: fn, - args, - isDefault, - serialized: [ - isDefault && "default::", - serializer(args) - ].filter(Boolean).join("") - }); - } - - const curried = (...args: A) => createHolder((m: ModuleExports) => fn(args, m), args, false); - const curriedDefault = (...args: A) => { - function filter(m: ModuleExports) { - return m.__esModule && m.default ? fn(args, m.default) : false; - } - return createHolder(filter, args, true); - }; - - return Object.assign(curried, { - byDefault: curriedDefault, - serializer - }); -} - -export function createSimpleFilter( - filter: (m: ModuleExports) => boolean, - id: string -) { - return createFilterDefinition( - (_, m) => filter(m), - () => `dynamic::${id}` - )(); -} - +import { metroModules } from "./modules"; +import { createFilterDefinition } from "./utils"; export const byProps = createFilterDefinition( (props, m) => props.length === 0 ? m[props[0]] : props.every(p => m[p]), @@ -84,6 +26,11 @@ export const byStoreName = createFilterDefinition<[string]>( name => `bunny.metro.byStoreName(${name})` ); +export const byFilePath = createFilterDefinition<[string]>( + ([path], _, id) => metroModules[id]?.__filePath === path, + path => `bunny.metro.byFilePath(${path})` +); + export const byMutableProp = createFilterDefinition<[string]>( ([prop], m) => m?.[prop] && !Object.getOwnPropertyDescriptor(m, prop)?.get, prop => `bunny.metro.byMutableProp(${prop})` diff --git a/src/metro/finders.ts b/src/metro/finders.ts index d1a95c1..a45f8cc 100644 --- a/src/metro/finders.ts +++ b/src/metro/finders.ts @@ -1,11 +1,11 @@ import { registerModuleFindCacheId, registerModuleFindFinished } from "./caches"; -import { FilterFn } from "./filters"; import { getModules } from "./modules"; +import { FilterFn } from "./utils"; -function testExports(moduleExports: any, filter: FilterFn) { - if (moduleExports.default && moduleExports.__esModule && filter(moduleExports.default)) +function testExports(moduleExports: any, moduleId: number, filter: FilterFn) { + if (moduleExports.default && moduleExports.__esModule && filter(moduleExports.default, moduleId)) return moduleExports.default; - if (filter(moduleExports)) + if (filter(moduleExports, moduleId)) return moduleExports; } @@ -15,7 +15,7 @@ function testExports(moduleExports: any, filter: FilterFn(filter: FilterFn) { for (const [id, moduleExports] of getModules(filter.serialized, false)) { - const testedExports = testExports(moduleExports, filter); + const testedExports = testExports(moduleExports, id, filter); if (testedExports !== undefined) { registerModuleFindCacheId(filter.serialized, id, false); return testedExports; @@ -33,7 +33,7 @@ export function findAll(filter: FilterFn) { const foundExports: any[] = []; for (const [id, moduleExports] of getModules(filter.serialized, true)) { - const testedExports = testExports(moduleExports, filter); + const testedExports = testExports(moduleExports, id, filter); if (testedExports !== undefined) { foundExports.push(testedExports); registerModuleFindCacheId(filter.serialized, id, true); diff --git a/src/metro/modules.ts b/src/metro/modules.ts index 9bc85a0..b1bb887 100644 --- a/src/metro/modules.ts +++ b/src/metro/modules.ts @@ -1,23 +1,21 @@ import { getMetroCache } from "@metro/caches"; import { Metro } from "@metro/types"; -import { instead } from "spitroast"; +import { before, instead } from "spitroast"; -// eslint-disable-next-line prefer-destructuring -const modules: Metro.ModuleList = window.modules; +export const metroModules: Metro.ModuleList = window.modules; const metroRequire: Metro.Require = window.__r; -metroRequire(0); - const blacklistedIds = new Set(); const noopHandler = () => undefined; const functionToString = Function.prototype.toString; let patchedInspectSource = false; +let patchedImportTracker = false; let _importingModuleId: string | null = null; /** Makes the module associated with the specified ID non-enumberable. */ function blacklistModule(id: string) { - Object.defineProperty(modules, id, { enumerable: false }); + Object.defineProperty(metroModules, id, { enumerable: false }); blacklistedIds.add(id); } @@ -25,8 +23,8 @@ function isBadExports(exports: any) { return !exports || exports === window || exports[""] === null; } -for (const id in modules) { - const metroModule = modules[id]; +for (const id in metroModules) { + const metroModule = metroModules[id]; if (metroModule!.factory) { instead("factory", metroModule, ((args: Parameters, origFunc: Metro.FactoryFn) => { @@ -95,6 +93,14 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) { }); } + if (!patchedImportTracker && moduleExports.fileFinishedImporting) { + before("fileFinishedImporting", moduleExports, ([filePath]) => { + if (_importingModuleId == null || !filePath) return; + metroModules[_importingModuleId]!.__filePath = filePath; + }); + patchedImportTracker = true; + } + // Funny infinity recursion caused by a race condition if (!patchedInspectSource && window["__core-js_shared__"]) { const inspect = (f: unknown) => typeof f === "function" && functionToString.apply(f, []); @@ -105,9 +111,9 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) { // Explosion (no, I can't explain this, don't ask) ((hi rosie)) if (moduleExports.findHostInstance_DEPRECATED) { const numberedId = Number(id); - const prevExports = modules[numberedId - 1]?.publicModule.exports; + const prevExports = metroModules[numberedId - 1]?.publicModule.exports; const inc = prevExports.default?.reactProfilingEnabled ? 1 : -1; - if (!modules[numberedId + inc]?.isInitialized) { + if (!metroModules[numberedId + inc]?.isInitialized) { blacklistModule(String(numberedId + inc)); } } @@ -127,9 +133,10 @@ export function getImportingModuleId() { } export function requireModule(id: Metro.ModuleID) { + if (!metroModules[0]?.isInitialized) metroRequire(0); if (blacklistedIds.has(String(id))) return undefined; - if (modules[id]?.isInitialized && !modules[id]?.hasError) { + if (metroModules[id]?.isInitialized && !metroModules[id]?.hasError) { return metroRequire(id); } @@ -157,7 +164,7 @@ export function* getModules(uniqueId: string, all = false) { let cache = getMetroCache().findIndex[uniqueId]; if (all && !cache?._) cache = undefined; - for (const id in cache ?? modules) { + for (const id in cache ?? metroModules) { const exports = requireModule(id); if (isBadExports(exports)) continue; yield [id, exports]; @@ -166,7 +173,7 @@ export function* getModules(uniqueId: string, all = false) { export function* getCachedPolyfillModules(name: string) { const cache = getMetroCache().polyfillCache[name]; - for (const id in cache ?? modules) { + for (const id in cache ?? metroModules) { const exports = requireModule(id); if (isBadExports(exports)) continue; yield [id, exports]; diff --git a/src/metro/types.ts b/src/metro/types.ts index 7e5f356..c15b618 100644 --- a/src/metro/types.ts +++ b/src/metro/types.ts @@ -45,22 +45,43 @@ export namespace Metro { error?: any; /** Set to undefined once module is initialized */ factory: FactoryFn | undefined; - /** If factory thrown any error */ + /** + * If factory thrown any error + * */ hasError: boolean; - /** Only available on Discord's development environment, will never be defined on release builds */ + /** + * Only available on Discord's development environment, will never be defined on release builds + * */ hot?: HotModuleReloadingData | undefined; - /** Cached `import *` imports in Metro, always an empty object as Bunny prevents outdated import cache */ + /** + * Cached `import *` imports in Metro, always an empty object as Bunny prevents outdated import cache + * */ importedAll: any; - /** Cached `import module from "./module"` imports in Metro, always an empty object as Bunny prevents outdated import cache */ + /** + * Cached `import module from "./module"` imports in Metro, always an empty object as Bunny prevents outdated import cache + * */ importedDefault: any; - /** If factory has been successfully called */ + /** + * Whether factory has been successfully called + * */ isInitialized: boolean; - /** Only available on Discord's development environment, will never be defined on release builds */ + /** + * Only available on Discord's development environment, will never be defined on release builds + * */ path?: string | undefined; - /** Acts as CJS module in the bundler */ + /** + * Acts as CJS module in the bundler + * */ publicModule: Module; - /** Only available on Discord's development environment, will never be defined on release builds */ + /** + * Only available on Discord's development environment, will never be defined on release builds + * */ verboseName?: string | undefined; + + /** + * This is set by us. Should be available for all Discord's tsx modules! + */ + __filePath?: string; } export type ModuleList = Record; diff --git a/src/metro/utils.ts b/src/metro/utils.ts index 7509b16..e9964f4 100644 --- a/src/metro/utils.ts +++ b/src/metro/utils.ts @@ -1,6 +1,63 @@ -import { byDisplayName, byName, byProps, byStoreName, byTypeName } from "./filters"; +import { byDisplayName, byFilePath, byName, byProps, byStoreName, byTypeName } from "./filters"; import { find, findAll } from "./finders"; +type ModuleExports = any; +type FilterCheckDef = (args: A, m: any, id: number) => boolean; + +export interface FilterFn { + (m: any, id: number): boolean; + filter: FilterCheckDef; + args: A; + isDefault: boolean; + serialized: string; +} + +export interface FilterDef { + (...args: A): FilterFn; + byDefault(...args: A): FilterFn; + serializer(args: A): string; +} + +export function createFilterDefinition( + fn: FilterCheckDef, + serializer: (args: A) => string +): FilterDef { + function createHolder(func: T, args: A, isDefault: boolean) { + return Object.assign(func, { + filter: fn, + args, + isDefault, + serialized: [ + isDefault && "default::", + serializer(args) + ].filter(Boolean).join("") + }); + } + + const curried = (...args: A) => createHolder((m: ModuleExports, id: number) => fn(args, m, id), args, false); + const curriedDefault = (...args: A) => { + function filter(m: ModuleExports, id: number) { + return m.__esModule && m.default ? fn(args, m.default, id) : false; + } + return createHolder(filter, args, true); + }; + + return Object.assign(curried, { + byDefault: curriedDefault, + serializer + }); +} + +export function createSimpleFilter( + filter: (m: ModuleExports) => boolean, + id: string +) { + return createFilterDefinition( + (_, m) => filter(m), + () => `dynamic::${id}` + )(); +} + export const findByProps = (...props: string[]) => find(byProps(...props)); export const findByPropsAll = (...props: string[]) => findAll(byProps(...props)); @@ -14,3 +71,4 @@ export const findByTypeName = (name: string, expDefault = true) => find(expDefau export const findByTypeNameAll = (name: string, expDefault = true) => findAll(expDefault ? byTypeName(name) : byTypeName.byDefault(name)); export const findByStoreName = (name: string) => find(byStoreName(name)); +export const findByFilePath = (path: string) => find(byFilePath(path));