feat: adopt dev metro
This commit is contained in:
parent
e56ec778e0
commit
cc879485ac
15 changed files with 168 additions and 129 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
78
src/lib/api/assets/index.ts
Normal file
78
src/lib/api/assets/index.ts
Normal 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;
|
||||
}
|
||||
25
src/lib/api/assets/patches.ts
Normal file
25
src/lib/api/assets/patches.ts
Normal 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
1
src/lib/utils/types.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export type Nullish = null | undefined;
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
3
src/metro/common/stores.ts
Normal file
3
src/metro/common/stores.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { findByStoreNameLazy } from "@metro/wrappers";
|
||||
|
||||
export const UserStore = findByStoreNameLazy("UserStore");
|
||||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export enum ModuleFlags {
|
||||
EXISTS = 1 << 0,
|
||||
BLACKLISTED = 1 << 1
|
||||
BLACKLISTED = 1 << 1,
|
||||
ASSET = 1 << 2,
|
||||
}
|
||||
|
||||
export enum ModulesMapInternal {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue