diff --git a/lib/init/src/index.ts b/lib/init/src/index.ts index a01fb4d..0fe414d 100644 --- a/lib/init/src/index.ts +++ b/lib/init/src/index.ts @@ -3,12 +3,12 @@ import './preinit' import './patches/prevent-immutablize' import './patches/proxy' -import { onModuleInitialized } from '@revenge-mod/modules/metro/subscriptions' +import { onModuleFirstRequired } from '@revenge-mod/modules/metro/subscriptions' import { getErrorStack } from '@revenge-mod/utils/errors' import { logger } from './shared' -onModuleInitialized(0, async function onceIndexModuleRequired() { +onModuleFirstRequired(0, async function onceIndexModuleRequired() { if (!__BUILD_FLAG_INIT_DISABLE_PATCH_LOG_PROMISE_REJECTIONS__) require('./patches/log-promise-rejections') try { diff --git a/lib/modules/src/metro/_internal.ts b/lib/modules/src/metro/_internal.ts index 42d7eb7..13d25f0 100644 --- a/lib/modules/src/metro/_internal.ts +++ b/lib/modules/src/metro/_internal.ts @@ -1,15 +1,13 @@ -import { _executeSubscription } from './subscriptions/_internal' -import { isBlacklisted, isModuleExportsBad } from './utils' +import { _executeInitializedSubscription, _executeRequiredSubscription } from './subscriptions/_internal' +import { isModuleExportsBad } from './utils' import type { Metro } from '../../types/metro' -export const _bl = new Set() - /** Initializing ID */ export let _mInitingId = -1 -/** Uninitialized IDs */ +/** Known uninitialized IDs (not blacklisted) */ export const _mUninited = new Set() -/** Known initialized IDs (exports aren't bad) */ +/** Known initialized IDs (not blacklisted) */ export const _mInited = new Set() export const _mPaths = new Map() @@ -20,53 +18,51 @@ export function patchMetroDefine(metroDefine: Metro.DefineFn) { // deps won't be undefined last time I checked const metadata = [deps!] as typeof _mMd extends Map ? V : never _mMd.set(id, metadata) + _mUninited.add(id) - if (isBlacklisted(id)) metroDefine(origFactory, id, deps) - else { - _mUninited.add(id) + metroDefine( + ((global, req, importDefault, importAll, module, exports, deps) => { + metadata[1] = module - metroDefine( - ((global, req, importDefault, importAll, module, exports, deps) => { - metadata[1] = module + const prevIId = _mInitingId + _mInitingId = id - const prevIId = _mInitingId - _mInitingId = id + _executeRequiredSubscription() - try { - origFactory(global, req, importDefault, importAll, module, exports, deps) - // Don't use exports here, as modules can set module.exports to a different object - if (isModuleExportsBad(module.exports)) throw undefined + try { + origFactory(global, req, importDefault, importAll, module, exports, deps) + // Don't use exports here, as modules can set module.exports to a different object + if (isModuleExportsBad(module.exports)) throw undefined - // Add the module to the initialized set only if the factory doesn't error or the exports aren't bad - _mInited.add(id) - } catch { - _blacklist(id) - } finally { - _mInitingId = prevIId - _mUninited.delete(id) - // Don't use exports here, as modules can set module.exports to a different object - _executeSubscription(id, module.exports) - } - }) satisfies Metro.FactoryFn, - id, - deps, - ) - } + // Add the module to the initialized set only if the factory doesn't error or the exports aren't bad + _mInited.add(id) + } catch { + _blacklist(id) + } finally { + _mInitingId = prevIId + _mUninited.delete(id) + // Don't use exports here, as modules can set module.exports to a different object + _executeInitializedSubscription(module.exports) + } + }) satisfies Metro.FactoryFn, + id, + deps, + ) }) satisfies Metro.DefineFn } /** - * Blacklists a module. + * Blacklists a module. A module can be blacklisted for several reasons: * - * Blacklisting modules prevents Revenge from doing the following things: - * - * - Iterating over the module during `find()`s. + * - If the module is already initialized, it either means: + * - The module factory threw an error during initialization + * - The module exports are bad after initialization + * - Otherwise, this module is known to be bad from previous caches * * @param id The module ID to blacklist. * @internal */ export function _blacklist(id: Metro.ModuleID) { - _bl.add(id) // In case blacklisting happens before the module is initialized _mUninited.delete(id) diff --git a/lib/modules/src/metro/patches.ts b/lib/modules/src/metro/patches.ts index 485587e..484d382 100644 --- a/lib/modules/src/metro/patches.ts +++ b/lib/modules/src/metro/patches.ts @@ -14,7 +14,7 @@ const unsubForImportTracker = waitForModules(byProps('fileFinishedImporting'), ( exports.fileFinishedImporting = (path: string) => { orig(path) _mPaths.set(path, _mInitingId) - _executeImportedPathSubscription(_mInitingId, path) + _executeImportedPathSubscription(path) } } }) diff --git a/lib/modules/src/metro/subscriptions/_internal.ts b/lib/modules/src/metro/subscriptions/_internal.ts index 724a59d..488f127 100644 --- a/lib/modules/src/metro/subscriptions/_internal.ts +++ b/lib/modules/src/metro/subscriptions/_internal.ts @@ -1,15 +1,26 @@ -import type { ModuleInitializedCallback, ModuleWithImportedPathInitializedCallback } from '.' +import { _mInitingId } from '../_internal' + +import type { ModuleInitializedCallback, ModuleRequiredCallback, ModuleWithImportedPathInitializedCallback } from '.' import type { Metro } from '../../../types/metro' -export const _all = new Set() -export const _specific = new Map>() +export const _reqAll = new Set() +export const _reqSpecific = new Map>() +export const _initAll = new Set() +export const _initSpecific = new Map>() export const _importedPath = new Set() -export function _executeSubscription(id: Metro.ModuleID, exports: Metro.ModuleExports) { - for (const cb of _all) cb(id, exports) - if (_specific.has(id)) for (const cb of _specific.get(id)!) cb(id, exports) +export function _executeRequiredSubscription() { + const id = _mInitingId + for (const cb of _reqAll) cb(id) + if (_reqSpecific.has(id)) for (const cb of _reqSpecific.get(id)!) cb(id) } -export function _executeImportedPathSubscription(id: Metro.ModuleID, path: string) { - for (const cb of _importedPath) cb(id, path) +export function _executeInitializedSubscription(exports: Metro.ModuleExports) { + const id = _mInitingId + for (const cb of _initAll) cb(id, exports) + if (_initSpecific.has(id)) for (const cb of _initSpecific.get(id)!) cb(id, exports) +} + +export function _executeImportedPathSubscription(path: string) { + for (const cb of _importedPath) cb(_mInitingId, path) } diff --git a/lib/modules/src/metro/subscriptions/index.ts b/lib/modules/src/metro/subscriptions/index.ts index 52faf24..38f05c9 100644 --- a/lib/modules/src/metro/subscriptions/index.ts +++ b/lib/modules/src/metro/subscriptions/index.ts @@ -1,36 +1,79 @@ -import type { Metro } from '../../../types/metro' -import { _all, _importedPath, _specific } from './_internal' +import { _initAll, _importedPath, _initSpecific, _reqSpecific, _reqAll } from './_internal' +import type { isInitializedModuleBlacklisted } from '../utils' +import type { Metro } from '../../../types/metro' + +export type ModuleRequiredCallback = (id: Metro.ModuleID) => void export type ModuleInitializedCallback = (id: Metro.ModuleID, exports: Metro.ModuleExports) => void export type ModuleWithImportedPathInitializedCallback = (id: Metro.ModuleID, path: string) => void /** - * Registers a callback to be called when any module is initialized. **Some module exports may be bad.** + * Registers a callback to be called when any module is initialized. + * + * @see {@link isInitializedModuleBlacklisted} to avoid bad module exports. + * * @param callback The callback to be called. * @returns A function that unregisters the callback. */ export function onAnyModuleInitialized(callback: ModuleInitializedCallback) { - _all.add(callback) - return () => _all.delete(callback) + _initAll.add(callback) + return () => _initAll.delete(callback) } /** - * Registers a callback to be called when a specific module is initialized. **Module exports may be bad.** + * Registers a callback to be called when a specific module is initialized. + * + * @see {@link isInitializedModuleBlacklisted} to avoid bad module exports. + * * @param id The ID of the module. * @param callback The callback to be called. + * @returns A function that unregisters the callback. */ export function onModuleInitialized(id: Metro.ModuleID, callback: ModuleInitializedCallback) { - if (!_specific.has(id)) _specific.set(id, new Set()) - const set = _specific.get(id)! + if (!_initSpecific.has(id)) _initSpecific.set(id, new Set()) + const set = _initSpecific.get(id)! set.add(callback) return () => set.delete(callback) } /** * Registers a callback to be called when a module with a specific import path is initialized. + * + * @see {@link isInitializedModuleBlacklisted} to avoid bad module exports. + * * @param callback The callback to be called. + * @returns A function that unregisters the callback. */ export function onModuleFinishedImporting(callback: ModuleWithImportedPathInitializedCallback) { _importedPath.add(callback) return () => _importedPath.delete(callback) } + +/** + * Registers a callback to be called when any module is required the first time. + * + * @see {@link isInitializedModuleBlacklisted} to avoid bad module exports. + * + * @param callback The callback to be called. + * @returns A function that unregisters the callback. + */ +export function onAnyModuleFirstRequired(callback: ModuleRequiredCallback) { + _reqAll.add(id => callback(id)) + return () => _reqAll.delete(id => callback(id)) +} + +/** + * Registers a callback to be called when a specific module is required the first time. + * + * @see {@link isInitializedModuleBlacklisted} to avoid bad module exports. + * + * @param id The ID of the module. + * @param callback The callback to be called. + * @returns A function that unregisters the callback. + */ +export function onModuleFirstRequired(id: Metro.ModuleID, callback: ModuleRequiredCallback) { + if (!_reqSpecific.has(id)) _reqSpecific.set(id, new Set()) + const set = _reqSpecific.get(id)! + set.add(callback) + return () => set.delete(callback) +}