diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cffc32b..22c5b01 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,22 +14,24 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install pnpm - uses: pnpm/action-setup@v4 + - name: Restore Bun cache + uses: actions/cache@v4 with: - version: latest + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: pnpm - - name: Install dependencies - run: pnpm install --frozen-lockfile + run: bun install --frozen-lockfile - name: Release - run: pnpx semantic-release + run: bunx semantic-release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_BRANCH: ${{ github.ref_name }} diff --git a/.releaserc.js b/.releaserc.js index ca2d876..ea0a030 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -28,7 +28,7 @@ export default { [ "@semantic-release/exec", { - prepareCmd: `pnpm build --release-branch ${process.env.RELEASE_BRANCH} --build-minify`, + prepareCmd: `bun run build --release-branch ${process.env.RELEASE_BRANCH} --build-minify`, }, ], [ diff --git a/CHANGELOG.md b/CHANGELOG.md index 90818ea..ab9cc15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,69 @@ +## [1.8.1-dev.1](https://github.com/revenge-mod/revenge-bundle/compare/v1.8.0...v1.8.1-dev.1) (2025-07-22) + + +### Bug Fixes + +* **api/patcher:** assert delayCallback ([#196](https://github.com/revenge-mod/revenge-bundle/issues/196)) ([5bc8868](https://github.com/revenge-mod/revenge-bundle/commit/5bc886868def333812dfc2b8d3c90d51e7739852)) + +# [1.8.0](https://github.com/revenge-mod/revenge-bundle/compare/v1.7.4...v1.8.0) (2025-07-21) + + +### Bug Fixes + +* Restore original behavior broken by b92d2734b33d2f0e719c364ed5aaefd43c7ae457 ([ec2809f](https://github.com/revenge-mod/revenge-bundle/commit/ec2809fe4c835c84101a8b8525cfc3863d1bc4d8)) +* Restore original behavior broken for old architecture ([#194](https://github.com/revenge-mod/revenge-bundle/issues/194)) ([acb85ba](https://github.com/revenge-mod/revenge-bundle/commit/acb85bacbab8b661c797f3b1609e64f2c724382f)) +* support New Architecture ([b92d273](https://github.com/revenge-mod/revenge-bundle/commit/b92d2734b33d2f0e719c364ed5aaefd43c7ae457)) +* support versions with New Architecture ([#193](https://github.com/revenge-mod/revenge-bundle/issues/193)) ([1b96867](https://github.com/revenge-mod/revenge-bundle/commit/1b96867ac76e522616186e96120c940b9311d659)) + + +### Features + +* compile to bytecode ([18f21fe](https://github.com/revenge-mod/revenge-bundle/commit/18f21feaaea0a4330e2b098c549b2a516cc8d352)) + +# [1.8.0-dev.2](https://github.com/revenge-mod/revenge-bundle/compare/v1.8.0-dev.1...v1.8.0-dev.2) (2025-07-21) + + +### Bug Fixes + +* Restore original behavior broken by b92d2734b33d2f0e719c364ed5aaefd43c7ae457 ([ec2809f](https://github.com/revenge-mod/revenge-bundle/commit/ec2809fe4c835c84101a8b8525cfc3863d1bc4d8)) +* Restore original behavior broken for old architecture ([#194](https://github.com/revenge-mod/revenge-bundle/issues/194)) ([acb85ba](https://github.com/revenge-mod/revenge-bundle/commit/acb85bacbab8b661c797f3b1609e64f2c724382f)) + +# [1.8.0-dev.1](https://github.com/revenge-mod/revenge-bundle/compare/v1.7.5-dev.1...v1.8.0-dev.1) (2025-07-21) + + +### Features + +* compile to bytecode ([18f21fe](https://github.com/revenge-mod/revenge-bundle/commit/18f21feaaea0a4330e2b098c549b2a516cc8d352)) + +# [1.8.0-dev.1](https://github.com/revenge-mod/revenge-bundle/compare/v1.7.5-dev.1...v1.8.0-dev.1) (2025-07-21) + + +### Features + +* compile to bytecode ([18f21fe](https://github.com/revenge-mod/revenge-bundle/commit/18f21feaaea0a4330e2b098c549b2a516cc8d352)) + +# [1.8.0-dev.1](https://github.com/revenge-mod/revenge-bundle/compare/v1.7.5-dev.1...v1.8.0-dev.1) (2025-07-21) + + +### Features + +* compile to bytecode ([18f21fe](https://github.com/revenge-mod/revenge-bundle/commit/18f21feaaea0a4330e2b098c549b2a516cc8d352)) + +# [1.8.0-dev.1](https://github.com/revenge-mod/revenge-bundle/compare/v1.7.5-dev.1...v1.8.0-dev.1) (2025-07-21) + + +### Features + +* compile to bytecode ([18f21fe](https://github.com/revenge-mod/revenge-bundle/commit/18f21feaaea0a4330e2b098c549b2a516cc8d352)) + +## [1.7.5-dev.1](https://github.com/revenge-mod/revenge-bundle/compare/v1.7.4...v1.7.5-dev.1) (2025-07-21) + + +### Bug Fixes + +* support New Architecture ([b92d273](https://github.com/revenge-mod/revenge-bundle/commit/b92d2734b33d2f0e719c364ed5aaefd43c7ae457)) +* support versions with New Architecture ([#193](https://github.com/revenge-mod/revenge-bundle/issues/193)) ([1b96867](https://github.com/revenge-mod/revenge-bundle/commit/1b96867ac76e522616186e96120c940b9311d659)) + ## [1.7.4](https://github.com/revenge-mod/revenge-bundle/compare/v1.7.3...v1.7.4) (2025-05-19) diff --git a/docs/2_building.md b/docs/2_building.md index 9050d9e..9405fa7 100644 --- a/docs/2_building.md +++ b/docs/2_building.md @@ -10,13 +10,13 @@ 4. Install dependencies ``` - pnpm i + bun i ``` 5. Build Revenge's code ``` - pnpm build + bun run build ``` 6. In the newly created `dist` directory, run a HTTP server. I recommend [http-server](https://www.npmjs.com/package/http-server). @@ -24,6 +24,6 @@ 8. Restart Discord. Upon reload, you should notice that your device will download Revenge's bundled code from your server, rather than GitHub. 9. Make your changes, rebuild, reload, go wild! -Alternatively, you can directly *serve* the bundled code by running `pnpm serve`. `revenge.js` will be served on your local address under the port 4040. You will then insert `http://:4040/revenge.js` as a custom URL and reload. Whenever you restart your mobile client, the script will rebuild the bundle as your client fetches it. +Alternatively, you can directly *serve* the bundled code by running `bun serve`. `revenge.js` will be served on your local address under the port 4040. You will then insert `http://:4040/revenge.js` as a custom URL and reload. Whenever you restart your mobile client, the script will rebuild the bundle as your client fetches it. If the bundle keeps being cached and not updated, you can instead tap the **Settings** > **Developer** > **Clear JS bundle** option which will prompt you to reload. diff --git a/package.json b/package.json index 14d3d5b..2c98393 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "build": "node scripts/build.mjs", "serve": "node scripts/serve.mjs", - "serve:adb": "pnpm run serve --adb", + "serve:adb": "bun run serve --adb", "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx" }, "license": "BSD-3-Clause", @@ -17,47 +17,39 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", - "semantic-release": "^24.1.3", - "@swc/core": "^1.7.0", - "@swc/helpers": "^0.5.12", - "@types/node": "^20.14.11", + "semantic-release": "^24.2.7", + "@swc/core": "^1.13.1", + "@swc/helpers": "^0.5.17", + "@types/node": "^20.19.9", "@types/yargs-parser": "^21.0.3", - "@typescript-eslint/eslint-plugin": "^7.16.1", - "@typescript-eslint/parser": "^7.16.1", - "@typescript-eslint/typescript-estree": "^7.16.1", - "chalk": "^5.3.0", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "@typescript-eslint/typescript-estree": "^7.18.0", + "chalk": "^5.4.1", "esbuild": "^0.20.2", "esbuild-plugin-globals": "^0.2.0", - "eslint": "^8.57.0", + "eslint": "^8.57.1", "eslint-plugin-import-alias": "^1.2.0", - "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^3.2.0", - "typescript": "^5.5.3", + "typescript": "^5.8.3", "yargs-parser": "^21.1.1" }, "dependencies": { - "@gullerya/object-observer": "^6.1.3", - "@shopify/react-native-skia": "^1.3.8", - "@tanstack/react-query": "^5.51.16", - "@types/chroma-js": "~2.4.4", - "@types/lodash": "~4.17.7", + "@gullerya/object-observer": "^6.1.4", + "@shopify/react-native-skia": "^1.12.4", + "@tanstack/react-query": "^5.83.0", + "@types/chroma-js": "~2.4.5", + "@types/lodash": "~4.17.20", "@types/react": "19.1.8", "react-native": "0.78.0", - "es-toolkit": "^1.13.1", - "fuzzysort": "^3.0.2", - "intl-messageformat": "^10.5.14", + "es-toolkit": "^1.39.7", + "fuzzysort": "^3.1.0", + "intl-messageformat": "^10.7.16", "moment": "2.22.2", - "react-native-reanimated": "^3.6.2", + "react-native-reanimated": "^3.18.0", "spitroast": "^1.4.4", - "type-fest": "^4.22.1" - }, - "pnpm": { - "peerDependencyRules": { - "ignoreMissing": [ - "react", - "react-native" - ] - } + "type-fest": "^4.41.0" } } \ No newline at end of file diff --git a/src/entry.ts b/src/entry.ts index a283071..cc1d806 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -1,10 +1,11 @@ +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; @@ -25,44 +26,100 @@ async function initializeBunny() { } } -// @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; - 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; + method: string; + resume?: (queue: DeferredQueue) => void; + args: any[]; + } + + const deferredCalls: Array = []; + const unpatches: Array<() => void> = []; + + const deferMethodExecution = ( + object: any, + method: string, + condition?: (...args: any[]) => boolean, + resume?: (queue: DeferredQueue) => void, + returnWith?: (queue: DeferredQueue) => any + ) => { + const restore = instead(method, object, function (this: any, args: any[], original: any) { + if (!condition || condition(...args)) { + const queue: DeferredQueue = { object, method, args, resume }; + deferredCalls.push(queue); + return returnWith ? returnWith(queue) : undefined; } - return orig.apply(batchedBridge, args); + // If the condition is not met, we execute the original method immediately + return original.apply(this, args); }); - const startDiscord = async () => { - await initializeBunny(); - unpatchHook(); - originalRequire(0); + unpatches.push(restore); + } - callQueue.forEach(arg => - batchedBridge.getCallableModule(arg[0]) - && batchedBridge.__callFunction(...arg)); + const resumeDeferred = () => { + for (const queue of deferredCalls) { + const { object, method, args, resume } = queue; + + if (resume) { + resume(queue); + } else { + object[method](...args); + } + } + + deferredCalls.length = 0; + } + + const onceIndexRequired = (originalRequire: Metro.RequireFn) => { + // We hold calls from the native side + if (window.__fbBatchedBridge) { + const batchedBridge = window.__fbBatchedBridge; + deferMethodExecution( + batchedBridge, + "callFunctionReturnFlushedQueue", + // 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 + (...args) => args[0] === "AppRegistry" || !batchedBridge.getCallableModule(args[0]), + ({ args }) => { + if (batchedBridge.getCallableModule(args[0])) { + batchedBridge.__callFunction(...args); + } + }, + () => batchedBridge.flushedQueue() + ); + } + + // Introduced since RN New Architecture + if (window.RN$AppRegistry) { + deferMethodExecution(window.RN$AppRegistry, "runApplication"); + } + + const startDiscord = async () => { + await initializeRevenge(); + + for (const unpatch of unpatches) unpatch(); + unpatches.length = 0; + + originalRequire(0); + resumeDeferred(); }; 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 +142,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(); +} \ No newline at end of file diff --git a/src/global.d.ts b/src/global.d.ts index 7390be4..d8d38b4 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -6,6 +6,7 @@ declare global { var globalEvalWithSourceUrl: (script: string, sourceURL: string) => any; var nativePerformanceNow: typeof performance.now; var nativeModuleProxy: Record; + var __turboModuleProxy: (name: string) => any; interface Window { [key: string]: any; diff --git a/src/lib/api/debug.ts b/src/lib/api/debug.ts index 629b1f0..b0640b6 100644 --- a/src/lib/api/debug.ts +++ b/src/lib/api/debug.ts @@ -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, diff --git a/src/lib/api/native/modules/index.ts b/src/lib/api/native/modules/index.ts index f1165e2..d6919cc 100644 --- a/src/lib/api/native/modules/index.ts +++ b/src/lib/api/native/modules/index.ts @@ -2,11 +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(...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( + "NativeCacheModule", "MMKVManager" +)!; +export const NativeFileModule = getNativeModule( + "NativeFileModule", "RTNFileManager", "DCDFileManager" +)!; +export const NativeClientInfoModule = getNativeModule( + "NativeClientInfoModule", "RTNClientInfoManager", "InfoDictionaryManager" +)!; +export const NativeDeviceModule = getNativeModule( + "NativeDeviceModule", "RTNDeviceManager", "DCDDeviceManager" +)!; +export const NativeThemeModule = getNativeModule( + "NativeThemeModule", "RTNThemeManager", "DCDTheme" +)!; +export const BundleUpdaterManager = getNativeModule( + "BundleUpdaterManager" +)!; diff --git a/src/lib/api/native/modules/types.ts b/src/lib/api/native/modules/types.ts index 970a6af..fdb3a32 100644 --- a/src/lib/api/native/modules/types.ts +++ b/src/lib/api/native/modules/types.ts @@ -84,4 +84,53 @@ export namespace RNModules { */ DocumentsDirPath: string; } -} \ No newline at end of file + + 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 + } + } +} diff --git a/src/lib/api/patcher.ts b/src/lib/api/patcher.ts index df5ae4d..098c831 100644 --- a/src/lib/api/patcher.ts +++ b/src/lib/api/patcher.ts @@ -22,7 +22,7 @@ type AfterFn = PatchFn<(args: any[], ret: any) => unknown>; function create(fn: Function) { function patchFn(this: any, ...args: any[]) { - if (_patcherDelaySymbol in args[1]) { + if (typeof args[1][_patcherDelaySymbol] === "function") { const delayCallback: DelayCallback = args[1][_patcherDelaySymbol]; let cancel = false; diff --git a/src/metro/internals/caches.ts b/src/metro/internals/caches.ts index 34ed45c..b5192b8 100644 --- a/src/metro/internals/caches.ts +++ b/src/metro/internals/caches.ts @@ -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, findIndex: {} as Record, @@ -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"; }