diff --git a/lib/modules/src/metro/_internal.ts b/lib/modules/src/metro/_internal.ts index 8390ee1..04b6cf4 100644 --- a/lib/modules/src/metro/_internal.ts +++ b/lib/modules/src/metro/_internal.ts @@ -1,6 +1,5 @@ import { global, - define as metroDefine, metroImportAll, metroImportDefault, metroRequire, @@ -11,7 +10,7 @@ import { executeInitializeSubscriptions, executeRequireSubscriptions, } from './subscriptions/_internal' -import type { Metro } from '../types' +import type { Metro, RevengeMetro } from '../types' /** Initializing ID */ export let mInitializingId: Metro.ModuleID | null = null @@ -23,10 +22,30 @@ export const mInitialized = new Set() export const mImportedPaths = new Map() export const mDeps: Metro.DependencyMap[] = [] -export let mList: Metro.ModuleList +export const mList: RevengeMetro.ModuleList = new Map() + +const metroDefine = ( + factory: Metro.FactoryFn, + id: Metro.ModuleID, + dependencyMap: Metro.DependencyMap, +) => { + mDeps[id] = dependencyMap! + mUninitialized.add(id) + + const moduleObject = { exports: {} } + + mList.set(id, [ + 0, + moduleObject, + () => { + handleFactoryCall(factory, moduleObject) + }, + ]) +} /** * Patching Metro's `__d` function to handle module definitions. + * We roll our own implementation of Metro's core functions. * * Here's how Metro sets itself up: * 1. `__METRO_GLOBAL_PREFIX__ = ""` @@ -36,36 +55,34 @@ export let mList: Metro.ModuleList * - We chose to patch it in 4. because we can access all the global functions at that point. * 3. `__r = function metroRequire(...) {}` * 4. `__c = function clear() {}` + * - **PATCH**: Set `__d` to our own implementation. * * 5. `clear()` - * - Metro clears the module list via clear(), and not __c() which makes it unpatchable. - * 6. `__d(..., 0, [...])` + * - Metro clears the module list directly with `clear()` and not `__c()`. + * 6. `metroRequire.importDefault = ...`, `metroRequire.importAll = ...` + * 7. `__d(..., 0, [...])` * - The first module is defined with ID 0, which is the index module. - * - Patch: - * - We clear the module list again, so our module list is in sync with Metro's + * - **PATCH**: Override the `importDefault` and `importAll` functions in `__r`. * #. `__d` is called with subsequent module definitions */ -export function patchMetroDefine() { - const defineKey = `${__METRO_GLOBAL_PREFIX__}__d` as const +const defineKey = `${__METRO_GLOBAL_PREFIX__}__d` as const - // First __d call - globalThis[defineKey] = function define(origFactory, id, deps) { - // Clear the module list so we can keep in sync with Metro's - mList = __c() +// First __d call +globalThis[defineKey] = function define(origFactory, id, deps) { + // Set own implementation of metroImportDefault and metroImportAll + metroRequire.importDefault = metroImportDefault + metroRequire.importAll = metroImportAll - // Set own implementation of metroImportDefault and metroImportAll - metroRequire.importDefault = metroImportDefault - metroRequire.importAll = metroImportAll - - // Set to the our own implementation function - // And then call the function to define the first module - ;(globalThis[defineKey] = metroDefine)(origFactory, id, deps) - } + // Set to the actual custom implementation + globalThis[defineKey] = metroDefine + // Call the custom implementation + metroDefine(origFactory, id, deps) } // Why don't we use all the arguments from Metro.FactoryFn? -// Because there's too many for Hermes to be able to its dedicated CallN function which only supports up to 4 arguments. (Call, Call1, Call2, Call3, Call4) -export function handleFactoryCall( +// Because there's too many for Hermes to be able to use its dedicated CallN operation +// which only supports up to 4 arguments. (Call, Call1, Call2, Call3, Call4) +function handleFactoryCall( factory: Metro.FactoryFn, moduleObject: Metro.Module, ) { diff --git a/lib/modules/src/metro/custom.ts b/lib/modules/src/metro/custom.ts index 65c00a2..9c6a200 100644 --- a/lib/modules/src/metro/custom.ts +++ b/lib/modules/src/metro/custom.ts @@ -1,65 +1,78 @@ /** - * Custom Metro-compatible methods. + * A more performant implementation of Metro's core functions. + * Making initialization faster and use less resources. */ -import { handleFactoryCall, mDeps, mList, mUninitialized } from './_internal' -import type { Nullish } from '@revenge-mod/utils/types' +import { mList } from './_internal' import type { Metro } from '../types' +export const Initialized = 1 << 0 +const HasError = 1 << 1 +const HasImportedDefault = 1 << 2 +const HasImportedAll = 1 << 3 + +const NotInitializedMask = ~Initialized + +export const FlagKey = 0 +export const ModuleObjectKey = 1 +const FactoryKey = 2 +const ImportedDefaultKey = 3 +const ImportedAllKey = 4 +const ErrorKey = 5 + export const global = globalThis -export const metroRequire = global.__r -// Roll our own define function implementation that replaces Metro's -// This is much more performant than Metro's own define function, which makes initialization faster -export function define( - factory: Metro.FactoryFn, - id: Metro.ModuleID, - dependencyMap: Metro.DependencyMap | Nullish, -) { - mDeps[id] = dependencyMap! - mUninitialized.add(id) +export const metroRequire = (moduleId => { + const module = mList.get(moduleId)! + const flags = module[FlagKey] + const moduleObject = module[ModuleObjectKey] - const moduleObject = { exports: {} } + if (flags & Initialized) return moduleObject.exports + if (flags & HasError) throw module[ErrorKey] - mList.set(id, { - dependencyMap: dependencyMap!, - factory: () => { - handleFactoryCall(factory, moduleObject) - }, - publicModule: moduleObject, - } satisfies Omit< - Metro.ModuleDefinition, - 'importedDefault' | 'importedAll' | 'isInitialized' | 'hasError' - > as unknown as Metro.ModuleDefinition) -} + // Optimistically mark module as initialized before running the + // factory to keep any require cycles inside the factory from causing an + // infinite require loop. + module[FlagKey] |= Initialized + moduleObject.id = moduleId -export function metroImportDefault( - moduleId: Metro.ModuleID, -): Metro.ModuleExports { - const mod = mList.get(moduleId)! - if (mod.importedDefault) return mod.importedDefault + try { + const factory = module[FactoryKey] + module[FactoryKey] = undefined - const exports: Metro.ModuleExports = metroRequire(moduleId) - const importedDefault: Metro.ModuleExports = exports?.__esModule - ? exports.default - : exports + factory!() - return (mod.importedDefault = importedDefault) -} + return moduleObject.exports + } catch (e) { + module[FlagKey] = (flags & NotInitializedMask) | HasError + module[ErrorKey] = e + // @ts-expect-error: We never access this key again + module[ModuleObjectKey] = undefined -export function metroImportAll(moduleId: Metro.ModuleID): Metro.ModuleExports { - const mod = mList.get(moduleId)! - if (mod.importedAll) return mod.importedAll - - const exports: Metro.ModuleExports = metroRequire(moduleId) - let importedAll: Metro.ModuleExports - - if (exports?.__esModule) importedAll = exports - else { - importedAll = {} - if (exports) for (const key in exports) importedAll[key] = exports[key] - importedAll.default = exports + // @ts-expect-error: Not documented, but used by React Native + if (global.ErrorUtils) global.ErrorUtils.reportFatalError(e) + else throw e } +}) as Metro.Require - return (mod.importedAll = importedAll) +global.__r = metroRequire + +export const metroImportDefault: Metro.RequireFn = moduleId => { + const mod = mList.get(moduleId)! + if (mod[FlagKey] & HasImportedDefault) return mod[ImportedDefaultKey] + + const exports = metroRequire(moduleId) + return (mod[ImportedDefaultKey] = exports?.__esModule + ? exports.default + : exports) +} + +export const metroImportAll: Metro.RequireFn = moduleId => { + const mod = mList.get(moduleId)! + if (mod[FlagKey] & HasImportedAll) return mod[ImportedAllKey] + + const exports = metroRequire(moduleId) + if (!exports?.__esModule) exports.default = exports + + return (mod[ImportedAllKey] = exports) } diff --git a/lib/modules/src/metro/utils.ts b/lib/modules/src/metro/utils.ts index aa71b91..ac17d00 100644 --- a/lib/modules/src/metro/utils.ts +++ b/lib/modules/src/metro/utils.ts @@ -1,5 +1,6 @@ import { isProxy } from '@revenge-mod/utils/proxy' import { mDeps, mList } from './_internal' +import { FlagKey, Initialized, ModuleObjectKey } from './custom' import type { Metro } from '../types' /** @@ -16,10 +17,10 @@ export function getModuleDependencies( /** * Returns whether a module is initialized. * @param id The module ID. - * @returns `true` if the module is initialized, `false` if it is not initialized, or `undefined` if the module does not exist. + * @returns Non-zero number if the module is initialized, `0` if it is not initialized or does not exist. */ -export function isModuleInitialized(id: Metro.ModuleID): boolean | undefined { - return mList.get(id)?.isInitialized +export function isModuleInitialized(id: Metro.ModuleID): number | undefined { + return mList.get(id)?.[FlagKey]! & Initialized } /** @@ -33,7 +34,7 @@ export function isModuleInitialized(id: Metro.ModuleID): boolean | undefined { export function getInitializedModuleExports( id: Metro.ModuleID, ): Metro.ModuleExports | undefined { - return mList.get(id)?.publicModule?.exports + return mList.get(id)?.[ModuleObjectKey]?.exports } /** diff --git a/lib/modules/src/types.ts b/lib/modules/src/types.ts index 7b102d3..f9d6961 100644 --- a/lib/modules/src/types.ts +++ b/lib/modules/src/types.ts @@ -1,4 +1,4 @@ -import type { If, Nullish } from '@revenge-mod/utils/types' +import type { If } from '@revenge-mod/utils/types' /** * Metro is a bundler for React Native. @@ -15,7 +15,7 @@ export namespace Metro { metroImportAll: RequireFn, moduleObject: Module, exports: ModuleExports, - dependencyMap: DependencyMap | Nullish, + dependencyMap: DependencyMap, ) => void export type ModuleID = number @@ -58,11 +58,9 @@ export namespace Metro { export type DefineFn = ( factory: FactoryFn, moduleId: ModuleID, - dependencyMap?: DependencyMap | undefined, + dependencyMap: DependencyMap, ) => void - export type ModuleDefiner = (moduleId: ModuleID) => void - export type ClearFn = () => ModuleList export interface Require extends RequireFn { @@ -73,6 +71,19 @@ export namespace Metro { export type ModuleExports = any } +export namespace RevengeMetro { + export type ModuleDefinition = [ + flags: number, + module: Metro.Module, + factory: If void>, + importedDefault?: Metro.ModuleExports, + importedAll?: Metro.ModuleExports, + error?: If, + ] + + export type ModuleList = Map +} + /** * Maybe the default export matched instead of the namespace, because you're using `options.returnNamespace`. */ diff --git a/src/index.ts b/src/index.ts index 75fdf5f..2427e71 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,15 +3,12 @@ import type { Metro } from '@revenge-mod/modules/types' Object.defineProperty(globalThis, '__c', { configurable: true, set(clear: Metro.ClearFn) { - // @ts-expect-error - // biome-ignore lint/performance/noDelete: We need to reset the property descriptor - delete globalThis.__c - globalThis.__c = clear + Object.defineProperty(globalThis, '__c', { + value: clear, + }) - const { patchMetroDefine } = - require('@revenge-mod/modules/_/metro') as typeof import('@revenge-mod/modules/_/metro') - - patchMetroDefine() + // Patch Metro's core functions + require('@revenge-mod/modules/_/metro') require('./preinit') },