feat+refactor(metro): implement byFilePath

This commit is contained in:
amsyarasyiq 2024-05-20 19:11:23 +08:00
parent c89dc175e3
commit 259b9399d0
8 changed files with 133 additions and 111 deletions

View file

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

View file

@ -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<string, {
_?: 1;
[id: number]: 1 | void;
} | undefined>;
polyfillCache: Record<string, {
_?: 1;
[id: number]: 1 | void;
} | undefined>;
findIndex: Record<string, ModulesIndexer | undefined>;
polyfillCache: Record<string, ModulesIndexer | undefined>;
assetsCache: Record<string, number>;
}
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;

View file

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

View file

@ -1,63 +1,5 @@
type Indexable = Record<PropertyKey, any>;
type ModuleExports = any;
type FilterCheckDef<A extends unknown[]> = <M extends Indexable>(args: A, m: M) => boolean;
export interface FilterFn<A extends unknown[]> {
(m: any): boolean;
filter: FilterCheckDef<A>;
args: A;
isDefault: boolean;
serialized: string;
}
export interface FilterDef<A extends unknown[]> {
(...args: A): FilterFn<A>;
byDefault(...args: A): FilterFn<A>;
serializer(args: A): string;
}
export function createFilterDefinition<A extends unknown[]>(
fn: FilterCheckDef<A>,
serializer: (args: A) => string
): FilterDef<A> {
function createHolder<T extends Function>(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<string[]>(
(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})`

View file

@ -1,11 +1,11 @@
import { registerModuleFindCacheId, registerModuleFindFinished } from "./caches";
import { FilterFn } from "./filters";
import { getModules } from "./modules";
import { FilterFn } from "./utils";
function testExports<A extends unknown[]>(moduleExports: any, filter: FilterFn<A>) {
if (moduleExports.default && moduleExports.__esModule && filter(moduleExports.default))
function testExports<A extends unknown[]>(moduleExports: any, moduleId: number, filter: FilterFn<A>) {
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<A extends unknown[]>(moduleExports: any, filter: FilterFn<A
*/
export function find<A extends unknown[]>(filter: FilterFn<A>) {
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<A extends unknown[]>(filter: FilterFn<A>) {
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);

View file

@ -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<String>();
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["<!@ pylix was here :fuyusquish: !@>"] === 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<Metro.FactoryFn>, 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];

View file

@ -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<ModuleID, ModuleDefinition | Nullish>;

View file

@ -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<A extends unknown[]> = (args: A, m: any, id: number) => boolean;
export interface FilterFn<A extends unknown[]> {
(m: any, id: number): boolean;
filter: FilterCheckDef<A>;
args: A;
isDefault: boolean;
serialized: string;
}
export interface FilterDef<A extends unknown[]> {
(...args: A): FilterFn<A>;
byDefault(...args: A): FilterFn<A>;
serializer(args: A): string;
}
export function createFilterDefinition<A extends unknown[]>(
fn: FilterCheckDef<A>,
serializer: (args: A) => string
): FilterDef<A> {
function createHolder<T extends Function>(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));