chore: initial commit

This commit is contained in:
PalmDevs 2025-03-21 19:59:16 +07:00
commit 8d84ebe491
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
23 changed files with 1968 additions and 0 deletions

178
.gitignore vendored Normal file
View file

@ -0,0 +1,178 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
# Private
.private

15
README.md Normal file
View file

@ -0,0 +1,15 @@
# @revenge-mod/revenge
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run src/index.ts
```
This project was created using `bun init` in bun v1.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

69
biome.json Normal file
View file

@ -0,0 +1,69 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.2/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"useKeyWithClickEvents": {
"level": "off"
}
},
"style": {
"noCommaOperator": {
"level": "off"
},
"noNonNullAssertion": {
"level": "off"
},
"useNodejsImportProtocol": {
"level": "off"
}
},
"suspicious": {
"noAssignInExpressions": {
"level": "off"
},
"noExplicitAny": {
"level": "off"
},
"noConfusingVoidType": {
"level": "off"
}
}
}
},
"json": {
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"parser": {
"allowComments": true,
"allowTrailingCommas": true
}
},
"javascript": {
"formatter": {
"enabled": true,
"lineEnding": "crlf",
"arrowParentheses": "asNeeded",
"indentStyle": "space",
"indentWidth": 4,
"lineWidth": 120,
"quoteProperties": "asNeeded",
"quoteStyle": "single",
"semicolons": "asNeeded",
"trailingCommas": "all"
}
},
"files": {
"ignoreUnknown": true,
"include": ["*.js", "*.json", "*.ts", "*.tsx", "*.jsx", "*.mjs"],
"ignore": ["dist/**/*", "node_modules/**/*"]
}
}

1117
bun.lock Normal file

File diff suppressed because it is too large Load diff

10
lib/init/package.json Normal file
View file

@ -0,0 +1,10 @@
{
"name": "@revenge-mod/init",
"private": true,
"type": "module",
"exports": {
".": {
"default": "./src/index.ts"
}
}
}

75
lib/init/src/index.ts Normal file
View file

@ -0,0 +1,75 @@
import './patches/prevent-immutablize'
import './patches/proxy'
import { getErrorStack } from '@revenge-mod/utils/errors'
import { logger } from './shared'
import type { Metro } from '@revenge-mod/modules'
let metroRequire: Metro.RequireFn | undefined = globalThis.__r
let metroDefine: Metro.DefineFn | undefined = globalThis.__d
Object.defineProperties(globalThis, {
__r: {
configurable: true,
get: () => metroRequire,
set: originalMetroRequire => {
metroRequire = id =>
id
? // Early modules (React, React Native, polyfills, etc.) are required even before index (module 0) is required
originalMetroRequire(id)
: // Once module 0 is required, we initialize our patches
// We also revert __r to its original value, for performance reasons
// biome-ignore lint/performance/noDelete: This is faster than using Object.defineProperty
(delete globalThis.__r, (globalThis.__r = originalMetroRequire), onceIndexModuleRequired())
},
},
__d: {
configurable: true,
// __d() is used to define a module
// Before the first module is being defined, we clear the modules list (so we can get the module list instance)
// We also revert __d to its original value, for performance reasons
get: () => {
// biome-ignore lint/performance/noDelete: This is faster than using Object.defineProperty
delete globalThis.__d
globalThis.__d = metroDefine
modules = __c!()
return metroDefine
},
set: v => {
metroDefine = v
},
},
})
export let modules: Metro.ModuleList
function onceIndexModuleRequired() {
if (!__BUILD_FLAG_INIT_DISABLE_PATCH_LOG_PROMISE_REJECTIONS__) require('./patches/log-promise-rejections')
initialize().catch(e => {
const { ClientInfoModule, DeviceModule } = require('@revenge-mod/modules/native')
alert(
[
'Failed to load Revenge!',
'',
`Discord: ${ClientInfoModule.Version} (${ClientInfoModule.Build})`,
`Device: ${DeviceModule.deviceManufacturer} ${DeviceModule.deviceModel}`,
'',
getErrorStack(e),
].join('\n'),
)
})
}
/*
! This function is blocking app startup !
We need to make sure it's as fast as possible
*/
async function initialize() {
logger.log('Revenge loaded!')
__r(0)
}

View file

@ -0,0 +1,23 @@
/**
* Hermes doesn't natively support Promises, it instead has a polyfill for it.
* It doesn't throw and never logs rejections in production, so we are patching it to catch errors when using async functions.
*
* See for reference: https://github.com/facebook/hermes/blob/3332fa020cae0bab751f648db7c94e1d687eeec7/lib/InternalBytecode/01-Promise.js#L446
*/
import { getErrorStack } from '@revenge-mod/utils/errors'
import { logger } from '../shared'
const ErrorTypeWhitelist = [ReferenceError, TypeError, RangeError]
Promise._m = (promise, err) => {
if (err)
setTimeout(
() => {
if (promise._h === 0) logger.error(`Unhandled promise rejection: ${getErrorStack(err)}`)
},
ErrorTypeWhitelist.some(it => err instanceof it) ? 0 : 2000,
// The time is completely arbitary. I've picked what Hermes chose.
)
}

View file

@ -0,0 +1,8 @@
/**
* This patch prevents objects from being made immutable.
*/
// Retain reference to the functions so the actual functions can be used later
import '@revenge-mod/utils/functions'
Object.freeze = Object.seal = Object.preventExtensions = o => o

View file

@ -0,0 +1,18 @@
/**
* This patch allows us to store instances of Proxy, so we can check whether a value is created using Proxy or not.
* This is especially useful for blacklisting discord-intl modules exports, and exports that cannot be patched.
*/
export const _instances = new WeakSet<object>()
// export const _targets = new WeakMap<object, object>()
const OriginalProxy = globalThis.Proxy
globalThis.Proxy = new Proxy(OriginalProxy, {
construct(_target, args) {
// @ts-expect-error
const prox = new OriginalProxy(...args)
_instances.add(prox)
// _targets.set(prox, args[0])
return prox
},
})

3
lib/init/src/shared.ts Normal file
View file

@ -0,0 +1,3 @@
import { createLogger } from '@revenge-mod/utils/logger'
export const logger = createLogger('revenge.init')

19
lib/utils/package.json Normal file
View file

@ -0,0 +1,19 @@
{
"name": "@revenge-mod/utils",
"private": true,
"type": "module",
"exports": {
"./errors": {
"default": "./src/errors.ts"
},
"./functions": {
"default": "./src/functions.ts"
},
"./logger": {
"default": "./src/logger.ts"
},
"./types": {
"types": "./src/types.d.ts"
}
}
}

3
lib/utils/src/errors.ts Normal file
View file

@ -0,0 +1,3 @@
export function getErrorStack(e: unknown) {
return e instanceof Error ? e.stack : String(e)
}

View file

@ -0,0 +1,5 @@
// This file is imported before the functions are modified
export const objectFreeze = Object.freeze
export const objectSeal = Object.seal
export const objectPreventExtensions = Object.preventExtensions

35
lib/utils/src/logger.ts Normal file
View file

@ -0,0 +1,35 @@
export function createLogger(tag: string) {
return {
log: (message: string) => console.log(`[${tag}] ${message}`),
warn: (message: string) => console.warn(`[${tag}] ${message}`),
error: (message: string) => console.error(`[${tag}] ${message}`),
wtf: (message: string) => console.error(`\u001b[31m[${tag}] ${message}\u001b[0m`),
} satisfies BasicLogger
}
export interface BasicLogger {
/**
* Log a message to the console.
*/
log(...args: unknown[]): void
/**
* Log a warning message to the console.
*
* On Android, this will print to logcat with the `WARN` level.
*/
warn(...args: unknown[]): void
/**
* Log an error message to the console.
*
* On Android, this will print to logcat with the `ERROR` level.
*/
error(...args: unknown[]): void
/**
* [*What a Terrible Failure!*](https://developer.android.com/reference/android/util/Log#wtf(java.lang.String,%20java.lang.String))
*
* Log an error message to the console in red (ANSI)
*
* On Android, this will print to logcat with the `ERROR` level.
*/
wtf(...args: unknown[]): void
}

13
lib/utils/src/proxy.ts Normal file
View file

@ -0,0 +1,13 @@
import { _instances /* _targets */ } from '../../init/src/patches/proxy'
/**
* Returns whether the object is a proxy
* @param obj The object to check
*/
export function isProxy(obj: object) {
return _instances.has(obj)
}
// export function getProxyTarget(obj: object) {
// return _targets.get(obj)
// }

3
lib/utils/src/types.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
export type Nullish = null | undefined
export type If<T, Then, Else> = T extends true ? Then : Else
export type AnyObject = Record<any, any>

26
package.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "@revenge-mod/revenge",
"private": true,
"module": "src/index.ts",
"type": "module",
"scripts": {
"build": "bun --bun run scripts/build.ts",
"dev": "bun --bun run scripts/dev.ts"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@parcel/watcher": "^2.5.1",
"@swc/core": "^1.11.9",
"@types/bun": "latest",
"@unbound-mod/hermesc": "^1.0.3",
"buffer": "^6.0.3",
"chalk": "^5.4.1",
"react-native": "^0.77.1",
"rolldown": "^1.0.0-beta.4-commit.beda261"
},
"peerDependencies": {
"typescript": "^5.7.3"
},
"workspaces": ["lib/*", "plugins/*"],
"trustedDependencies": ["@biomejs/biome", "@parcel/watcher", "@swc/core"]
}

145
scripts/build.ts Normal file
View file

@ -0,0 +1,145 @@
import { transform } from '@swc/core'
import { main } from 'bun'
import chalk from 'chalk'
import { type OutputChunk, type RolldownPlugin, rolldown } from 'rolldown'
// If this file is being run directly, build the project
if (main === import.meta.filename) build()
export default async function build(dev = false, log = true) {
const start = performance.now()
const bundle = await rolldown({
input: 'src/index.ts',
platform: 'neutral',
treeshake: true,
keepNames: true,
moduleTypes: {
'.webp': 'dataurl',
},
define: {
__BUILD_COMMIT__: JSON.stringify(''),
__BUILD_ENV__: JSON.stringify(dev ? 'development' : 'production'),
__BUILD_FLAG_INIT_DISABLE_PATCH_LOG_PROMISE_REJECTIONS__: String(!dev),
},
plugins: [
swcPlugin(),
hermesCPlugin({
flags: [
dev ? '-Og' : '-O',
'-eager',
'-finline',
'-fno-static-require',
'-Wno-direct-eval',
'-Wno-undefined-variable',
],
afterComplete() {
if (log) console.debug(chalk.gray('\u{1F5CE} Bytecode compilation finished'))
},
}),
],
})
await bundle.write({
file: 'dist/revenge.js',
format: 'iife',
footer: '//# sourceURL=Revenge',
inlineDynamicImports: true,
})
if (log)
console.info(
chalk.greenBright(
`\u{1F5CE} Compiled successfully! ${chalk.gray(`(took ${(performance.now() - start).toFixed(2)}ms)`)}`,
),
)
}
function swcPlugin() {
return {
name: 'swc',
transform: {
filter: {
id: /\.[cm]?[jt]sx?$/,
},
handler(code) {
return transform(code, {
jsc: {
transform: {
react: {
runtime: 'automatic',
},
},
parser: {
syntax: 'typescript',
tsx: true,
},
},
env: {
// https://github.com/discord/hermes/blob/main/doc/Features.md
targets: 'fully supports es6',
include: [
'transform-block-scoping',
'transform-classes',
'transform-async-to-generator',
'transform-async-generator-functions',
'transform-named-capturing-groups-regex',
],
exclude: [
'transform-parameters',
'transform-template-literals',
'transform-exponentiation-operator',
'transform-nullish-coalescing-operator',
'transform-object-rest-spread',
'transform-optional-chaining',
'transform-logical-assignment-operators',
],
},
})
},
},
} satisfies RolldownPlugin
}
function hermesCPlugin({ afterComplete, flags }: { flags?: string[]; afterComplete?: () => void } = {}) {
const paths = {
win32: 'hermesc.exe',
darwin: 'darwin/hermesc',
linux: 'linux/hermesc',
}
if (!(process.platform in paths)) throw new Error(`Unsupported platform: ${process.platform}`)
const binPath = paths[process.platform as keyof typeof paths]
return {
name: 'hermesc',
generateBundle(_, bundle) {
const file = bundle['revenge.js'] as OutputChunk
if (!file || !file.code) throw new Error('No code to compile')
const cmd = Bun.spawnSync(
[
`./node_modules/@unbound-mod/hermesc/${process.platform}/${binPath}`,
'-emit-binary',
...(flags ?? []),
],
{
stdin: new Blob([file.code]),
stdout: 'pipe',
},
)
const buf = cmd.stdout
if (!buf.length) throw new Error('No output from hermesc')
this.emitFile({
type: 'asset',
fileName: `${file.fileName.split('.')[0]!}.bundle`,
source: buf,
})
if (afterComplete) afterComplete()
},
} satisfies RolldownPlugin
}

51
scripts/dev.ts Normal file
View file

@ -0,0 +1,51 @@
import { join } from 'path'
import watcher from '@parcel/watcher'
import chalk from 'chalk'
import build from './build'
const cwdify = (path: string) => join(process.cwd(), path)
const Sources = ['src', 'lib', 'plugins'].map(cwdify)
const ExitTriggers = ['scripts', 'package.json', 'bun.lock'].map(cwdify)
function debounce<F extends (...args: any[]) => any>(func: F, timeout: number) {
let timer: NodeJS.Timer
return (...args: Parameters<F>) =>
new Promise(rs => {
clearTimeout(timer)
timer = setTimeout(() => {
rs(func(...args))
}, timeout)
})
}
const buildDevWithDebounceAndCatchErrors = debounce(async () => {
try {
return await build(true)
} catch (e) {
console.error(e)
}
}, 250)
watcher.subscribe(process.cwd(), async (err, events) => {
if (err) return console.error(err)
if (events.some(it => ExitTriggers.some(se => it.path.startsWith(se)))) {
console.error(chalk.redBright('\u26A0 Scripts or packages has changed, exiting!'))
process.exit()
}
if (events.some(it => Sources.some(src => it.path.startsWith(src)))) await buildDevWithDebounceAndCatchErrors()
})
const server = Bun.serve({
fetch(req, srv) {
console.debug(chalk.gray(`\u{1F79B} Receiving request from ${srv.requestIP(req)!.address}`))
return new Response(Bun.file('./dist/revenge.bundle'))
},
port: 4040,
})
console.info(chalk.cyanBright(`\u24D8 Serving on port ${server.port}`))
await buildDevWithDebounceAndCatchErrors()

1
src/index.ts Normal file
View file

@ -0,0 +1 @@
import '@revenge-mod/init'

28
tsconfig.json Normal file
View file

@ -0,0 +1,28 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
},
"exclude": ["scripts/**"]
}

107
types.d.ts vendored Normal file
View file

@ -0,0 +1,107 @@
// All of these typings are exported, be careful what you export here!
import type { ErrorUtils as RNErrorUtils } from 'react-native'
/// HERMES
declare global {
declare function setTimeout(cb: (...args: unknown[]) => unknown, timeout?: number): number
/**
* Calls the garbage collector
*/
declare function gc(): void
interface HermesInternalObject {
getRuntimeProperties(): Record<string, string>
// biome-ignore lint/complexity/noBannedTypes: You can pass any function here
getFunctionLocation(fn: Function): {
fileName: string
lineNumber: number
columnNumber: number
segmentID: number
virtualOffset: number
isNative: boolean
}
}
}
/// HERMES PROMISES
declare global {
interface Promise {
/// PROMISE POLYFILLS FROM: https://github.com/then/promise
/// AND: https://github.com/facebook/hermes/blob/main/lib/InternalBytecode/01-Promise.js
_h: 0 | 1 | 2
}
type HermesPromiseRejectionHandler = (promise: Promise<any>, error: any) => void
interface PromiseConstructor {
_m: HermesPromiseRejectionHandler
}
}
/// REACT DEVTOOLS
declare global {
var __REACT_DEVTOOLS_GLOBAL_HOOK__: unknown | undefined
var __reactDevTools:
| {
version: string
exports: {
connectToDevTools(opts: {
host?: string
port?: number
websocket?: WebSocket
}): void
}
}
| undefined
}
/// METRO
declare global {
var nativeModuleProxy: Record<string, unknown>
var __r: Metro.RequireFn
var __c: Metro.ClearFn
var __d: Metro.DefineFn
}
/// REACT NATIVE
declare global {
declare function nativeLoggingHook(str: string, level: number): void
declare function alert(message: unknown): void
var nativePerformanceNow: typeof performance.now
var performance: {
now(): number
}
var globalEvalWithSourceUrl: (code: string, sourceURL: string) => any
const ErrorUtils: RNErrorUtils
}
export namespace ReactNativeInternals {
namespace AssetsRegistry {
// export type AssetDestPathResolver = 'android' | 'generic'
export type PackagerAsset = {
__packager_asset: boolean
fileSystemLocation?: string
httpServerLocation?: string
width?: number
height?: number
scales: number[]
hash: string
name: string
type: string
// resolver?: AssetDestPathResolver
}
export function registerAsset(asset: PackagerAsset): number
export function getAssetByID(assetId: number): PackagerAsset
}
}

16
types.internal.d.ts vendored Normal file
View file

@ -0,0 +1,16 @@
// Type definitions for things in this codebase ONLY!
import type { Buffer as _Buffer } from 'buffer'
import type MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue'
declare global {
// Discord has a Buffer polyfill that gets initialized a little later, be careful with this
var Buffer: typeof _Buffer
}
declare global {
const __BUILD_ENV__: 'development' | 'production'
const __BUILD_COMMIT__: string
const __BUILD_FLAG_INIT_DISABLE_PATCH_LOG_PROMISE_REJECTIONS__: boolean
}