fix: support New Architecture

- Retrieve turbo modules from `global.__turboModuleProxy`
- Use `getConstants()` to retrieve constants of native modules
- Defer some calls from the native bridge/JSI that could break the app when bundle index is not initialized properly

Tested on Android 291.0 (291200), not tested on older versions (without New Architecture)
This commit is contained in:
pylixonly 2025-07-21 20:44:48 +08:00
parent c263de39d0
commit b92d2734b3
7 changed files with 144 additions and 36 deletions

View file

@ -46,6 +46,7 @@ const config = {
".png": "dataurl"
},
define: {
window: "globalThis",
__DEV__: dev ?? JSON.stringify(releaseBranch !== "main")
},
inject: ["./shims/asyncIteratorSymbol.js", "./shims/promiseAllSettled.js"],

View file

@ -1,10 +1,10 @@
import type { Metro } from "@metro/types";
import { version } from "bunny-build-info";
const { instead } = require("spitroast");
// @ts-ignore - shut up fr
// @ts-ignore - window is defined later in the bundle, so we assign it early
globalThis.window = globalThis;
async function initializeBunny() {
async function initializeRevenge() {
try {
// Make 'freeze' and 'seal' do nothing
Object.freeze = Object.seal = Object;
@ -18,51 +18,80 @@ async function initializeBunny() {
console.log(stack ?? e?.toString?.() ?? e);
alert([
"Failed to load Revenge!\n",
`Build Number: ${ClientInfoManager.Build}`,
`Build Number: ${ClientInfoManager.getConstants().Build}`,
`Revenge: ${version}`,
stack || e?.toString?.(),
].join("\n"));
}
}
// @ts-ignore
if (typeof globalThis.__r !== "undefined") {
initializeBunny();
} else {
// We hold calls from the native side
function onceIndexRequired(originalRequire: any) {
const batchedBridge = window.__fbBatchedBridge;
if (typeof window.__r === "undefined") {
// Used for storing the current require function for the global.__r getter defined below
var _requireFunc: any;
const callQueue = new Array<any>;
const unpatchHook = instead("callFunctionReturnFlushedQueue", batchedBridge, (args: any, orig: any) => {
if (args[0] === "AppRegistry" || !batchedBridge.getCallableModule(args[0])) {
callQueue.push(args);
return batchedBridge.flushedQueue();
// Calls from the native side are deferred until the index.ts(x) is loaded
// Revenge delays the execution of index.ts(x) because Revenge's initialization is asynchronous
interface DeferredQueue {
object: any;
original: any;
method: string;
args: any[];
}
const deferredCalls: Array<DeferredQueue> = [];
const deferMethodExecution = (object: any, method: string, condition?: (...args: any[]) => boolean) => {
const originalMethod = object[method];
object[method] = (...args: any[]) => {
if (condition && !condition(...args)) {
originalMethod(...args);
return;
}
return orig.apply(batchedBridge, args);
});
deferredCalls.push({ object, original: originalMethod, method, args });
};
}
const resumeDeferredAndRestore = () => {
for (const { object, method, args, original } of deferredCalls) {
object[method] = original;
object[method](...args);
}
deferredCalls.length = 0;
}
const onceIndexRequired = (originalRequire: Metro.RequireFn) => {
// We hold calls from the native side
if (window.__fbBatchedBridge) {
deferMethodExecution(window.__fbBatchedBridge, "callFunctionReturnFlushedQueue", args => {
// If the call is to AppRegistry, we want to defer it because it is not yet registered (Revenge delays it)
// Same goes to the non-callable modules, which are not registered yet, so we ensure that only registered ones can get through
return args[0] !== "AppRegistry" && window.__fbBatchedBridge.getCallableModule(args[0]);
});
}
// Introduced since RN New Architecture
if (window.RN$AppRegistry) {
deferMethodExecution(window.RN$AppRegistry, "runApplication");
}
const startDiscord = async () => {
await initializeBunny();
unpatchHook();
await initializeRevenge();
originalRequire(0);
callQueue.forEach(arg =>
batchedBridge.getCallableModule(arg[0])
&& batchedBridge.__callFunction(...arg));
resumeDeferredAndRestore();
};
startDiscord();
}
var _requireFunc: any; // We can't set properties to 'this' during __r set for some reason
Object.defineProperties(globalThis, {
__r: {
configurable: true,
get: () => _requireFunc,
set(v) {
// _requireFunc is required here, because using 'this' here errors for some unknown reason
_requireFunc = function patchedRequire(a: number) {
// Initializing index.ts(x)
if (a === 0) {
@ -85,4 +114,8 @@ if (typeof globalThis.__r !== "undefined") {
set(v) { this.value = v; }
}
});
}
} else {
// It is too late to late to hook __r, so we just initialize Revenge here
// Likely because of using the legacy loader (from Vendetta)
initializeRevenge();
}

1
src/global.d.ts vendored
View file

@ -6,6 +6,7 @@ declare global {
var globalEvalWithSourceUrl: (script: string, sourceURL: string) => any;
var nativePerformanceNow: typeof performance.now;
var nativeModuleProxy: Record<string, any>;
var __turboModuleProxy: (name: string) => any;
interface Window {
[key: string]: any;

View file

@ -113,8 +113,8 @@ export function getDebugInfo() {
}
},
discord: {
version: NativeClientInfoModule.Version,
build: NativeClientInfoModule.Build,
version: NativeClientInfoModule.getConstants().Version,
build: NativeClientInfoModule.getConstants().Build,
},
react: {
version: React.version,

View file

@ -2,10 +2,34 @@ import { RNModules } from "./types";
const nmp = window.nativeModuleProxy;
export const NativeCacheModule = (nmp.NativeCacheModule ?? nmp.MMKVManager) as RNModules.MMKVManager;
export const NativeFileModule = (nmp.NativeFileModule ?? nmp.RTNFileManager ?? nmp.DCDFileManager) as RNModules.FileManager;
export const NativeClientInfoModule = nmp.NativeClientInfoModule ?? nmp.RTNClientInfoManager ?? nmp.InfoDictionaryManager;
export const NativeDeviceModule = nmp.NativeDeviceModule ?? nmp.RTNDeviceManager ?? nmp.DCDDeviceManager;
export const NativeThemeModule = nmp.NativeThemeModule ?? nmp.RTNThemeManager ?? nmp.DCDTheme;
function getNativeModule<T = any>(...names: string[]): T | undefined {
for (const name of names) {
if (globalThis.__turboModuleProxy) {
const module = globalThis.__turboModuleProxy(name);
if (module) return module as T;
}
export const { BundleUpdaterManager } = nmp;
if (nmp[name]) return nmp[name] as T;
}
return undefined;
}
export const NativeCacheModule = getNativeModule<RNModules.MMKVManager>(
"NativeCacheModule", "MMKVManager"
)!;
export const NativeFileModule = getNativeModule<RNModules.FileManager>(
"NativeFileModule", "RTNFileManager", "DCDFileManager"
)!;
export const NativeClientInfoModule = getNativeModule<RNModules.ClientInfoModule>(
"NativeClientInfoModule", "RTNClientInfoManager", "InfoDictionaryManager"
)!;
export const NativeDeviceModule = getNativeModule(
"NativeDeviceModule", "RTNDeviceManager", "DCDDeviceManager"
)!;
export const NativeThemeModule = getNativeModule(
"NativeThemeModule", "RTNThemeManager", "DCDTheme"
)!;
export const BundleUpdaterManager = getNativeModule(
"BundleUpdaterManager"
)!;

View file

@ -86,4 +86,53 @@ export namespace RNModules {
*/
DocumentsDirPath: string;
}
export interface ClientInfoModule {
getConstants(): {
/**
* Sentry ingestion DSN URL for alpha/beta builds
*/
SentryAlphaBetaDsn: string
/**
* Sentry ingestion DSN URL for staff builds (?)
*/
SentryStaffDsn: string
/**
* Sentry ingestion DSN URL for stable builds
*/
SentryDsn: string
DeviceVendorID: string
Manifest: string
/**
* Version code
*
* Follows the format of `{MINOR}{CHANNEL}{PATCH}` for `{MINOR}.{PATCH} ({CHANNEL})`
* - `248200` for `248.0 (alpha)`
* - `247105` for `247.5 (beta)`
* - `246011` for `246.11 (stable)`
*/
Build: string
/**
* Version string
*
* Eg. `248.0`
*/
Version: string
/**
* Release channel
*/
ReleaseChannel: string
/**
* Matches `Version`
*/
OTABuild: string
/**
* Identifier for the installed client
*
* - **Android**: Package name
* - **iOS**: Bundle ID
*/
Identifier: string
}
}
}

View file

@ -18,7 +18,7 @@ export const getMetroCache = () => _metroCache;
function buildInitCache() {
const cache = {
_v: CACHE_VERSION,
_buildNumber: NativeClientInfoModule.Build as number,
_buildNumber: NativeClientInfoModule.getConstants().Build,
_modulesCount: Object.keys(window.modules).length,
flagsIndex: {} as Record<string, number>,
findIndex: {} as Record<string, ModulesMap | undefined>,
@ -48,7 +48,7 @@ export async function initMetroCache() {
_metroCache = null!;
throw "cache invalidated; cache version outdated";
}
if (_metroCache._buildNumber !== NativeClientInfoModule.Build) {
if (_metroCache._buildNumber !== NativeClientInfoModule.getConstants().Build) {
_metroCache = null!;
throw "cache invalidated; version mismatch";
}