Skip to content

Commit

Permalink
feat: add debugger (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
PalmDevs authored Dec 31, 2024
2 parents 457a47b + b7729c0 commit b348697
Show file tree
Hide file tree
Showing 9 changed files with 453 additions and 53 deletions.
Binary file modified bun.lockb
Binary file not shown.
12 changes: 11 additions & 1 deletion libraries/modules/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,17 @@ export namespace DiscordModules {
}

// Other
export type Slider = FC
export type Slider = FC<{
value: number
step: number
minimumValue: number
maximumValue: number
onValueChange?: (value: number) => void
onSlidingStart?: () => void
onSlidingComplete?: () => void
startIcon?: ReactNode
endIcon?: ReactNode
}>
export type FlashList = FC
export type Text = FC<
TextProps & {
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"license": "BSD-3-Clause",
"scripts": {
"serve": "bun run ./scripts/serve.mjs",
"build": "bun run ./scripts/build.mjs"
"build": "bun run ./scripts/build.mjs",
"debugger": "node ./scripts/debugger.mjs"
},
"dependencies": {
"@revenge-mod/app": "workspace:*",
Expand All @@ -26,10 +27,12 @@
"@swc/helpers": "^0.5.15",
"@tsconfig/strictest": "^2.0.5",
"chalk": "^5.3.0",
"clipboardy": "^4.0.0",
"esbuild": "^0.24.0",
"esbuild-plugin-globals": "^0.2.0",
"react-native": "^0.76.3",
"typescript": "^5.7.2",
"ws": "^8.18.0",
"yargs-parser": "^21.1.1"
},
"workspaces": ["libraries/*"],
Expand Down
147 changes: 147 additions & 0 deletions scripts/debugger.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env node
import * as repl from 'node:repl'
import { WebSocketServer } from 'ws'
import os from 'os'
import chalk from 'chalk'
import clipboardy from 'clipboardy'
import { join, resolve } from 'path'
import { existsSync } from 'fs'
import { mkdir, writeFile } from 'fs/promises'

const debuggerHistoryPath = resolve(join('node_modules', 'debugger'))

if ('Bun' in globalThis)
throw new Error(
`Bun is unsupported due to it lacking compatibility with node:repl. Please run "node ./scripts/debugger.mjs" or "bun debugger"`,
)

let isPrompting = false

const debuggerColorify = message => (isPrompting ? '\n' : '') + chalk.bold.blue('[Debugger] ') + message

const clientColorify = (style, message) =>
(isPrompting ? '\n' : '') +
(style === 'error'
? chalk.bold.red('[Revenge] ERR! ') + chalk.red(message)
: style === 'warn'
? chalk.bold.yellow('[Revenge] ') + chalk.yellow(message)
: chalk.bold.green('[Revenge] ') + message)

const logAsDebugger = message => console.info(debuggerColorify(message))

const logAsClient = message => console.info(clientColorify(null, message))
const logAsClientWarn = message => console.warn(clientColorify('warn', message))
const logAsClientError = message => console.error(clientColorify('error', message))

const copyPrompt = '--copy'
const clearHistoryPrompt = '--clear'

export function serve() {
let websocketOpen = false
let nextReply

const wss = new WebSocketServer({
port: 9090,
})
wss.on('connection', ws => {
if (websocketOpen) return
websocketOpen = true

logAsDebugger('Client connected')

ws.on('message', data => {
try {
/** @type {{ level: "info" | "warn" | "error", message: string, nonce?: string }} */
const json = JSON.parse(data.toString())

if (nextReply?.cb && nextReply?.nonce && nextReply.nonce === json.nonce) {
if (json.level === 'info' && nextReply.toCopy) {
clipboardy.write(json.message)
nextReply.cb(null, debuggerColorify('Copied result to clipboard'))
} else
nextReply.cb(
null,
json.level === 'error'
? clientColorify('error', json.message)
: clientColorify(null, json.message),
)
nextReply = null
isPrompting = true
} else {
if (json.level === 'error') logAsClientError(json.message)
else if (json.level === 'warn') logAsClientWarn(json.message)
else logAsClient(json.message)

if (isPrompting) rl.displayPrompt(true)
}
} catch {}
})

isPrompting = true
const rl = repl.start({
eval(input, _, __, cb) {
if (!isPrompting) return
if (!input.trim()) return cb()

try {
isPrompting = false

const code = input.trim()
if (code === clearHistoryPrompt) {
writeFile(join(debuggerHistoryPath, 'history.txt'), '')
logAsDebugger('Cleared REPL history')
return cb()
}

nextReply = {
nonce: crypto.randomUUID(),
cb,
toCopy: code.endsWith(copyPrompt),
}
ws.send(
JSON.stringify({
code: code.endsWith(copyPrompt) ? code.slice(0, -copyPrompt.length) : code,
nonce: nextReply.nonce,
}),
)
} catch (e) {
cb(e)
}
},
writer(msg) {
return msg
},
})
rl.setupHistory(join(debuggerHistoryPath, 'history.txt'), () => void 0)

rl.on('close', () => {
isPrompting = false
ws.close()
})

ws.on('close', () => {
logAsDebugger('Client disconnected')
rl.close()
websocketOpen = false
})
})

console.info(chalk.red('\nDebugger ready, available on:\n'))
const netInterfaces = os.networkInterfaces()
for (const netinterfaces of Object.values(netInterfaces)) {
for (const details of netinterfaces || []) {
if (details.family !== 'IPv4') continue
const port = chalk.yellowBright(wss.address()?.port.toString())
console.info(` ${chalk.gray('http://')}${details.address}:${port}`)
}
}

console.log(chalk.gray.underline(`\nRun with ${chalk.bold.white(copyPrompt)} to your prompt to copy the result to clipboard`))
console.log(chalk.gray.underline(`Run with ${chalk.bold.white(clearHistoryPrompt)} to clear your REPL history\n`))

return wss
}

if (!existsSync(debuggerHistoryPath)) await mkdir(debuggerHistoryPath, { recursive: true })

serve()
14 changes: 14 additions & 0 deletions src/plugins/developer-settings/debugger.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
declare global {
var dbgr: {
reload(): void
patcher: {
// biome-ignore lint/suspicious/noExplicitAny: The object can be anything
snipe(object: any, key: string, callback?: (args: unknown) => void): void
// biome-ignore lint/suspicious/noExplicitAny: The object can be anything
noop(object: any, key: string): void
wipe(): void
}
} | undefined
}

export {}
96 changes: 96 additions & 0 deletions src/plugins/developer-settings/debugger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { EventEmitter } from 'events'
import type { RevengeLibrary } from '@revenge-mod/revenge'

export const DebuggerEvents = new EventEmitter<DebuggerEventsListeners>()

export type DebuggerEventsListeners = {
connect: () => void
disconnect: () => void
// biome-ignore lint/suspicious/noExplicitAny: Anything can be thrown
error: (err: any) => void
// biome-ignore lint/suspicious/noExplicitAny: Anything can be thrown
'*': (event: keyof DebuggerEventsListeners, err?: any) => void
}

export const DebuggerContext = {
ws: undefined,
connected: false,
} as {
ws: WebSocket | undefined
connected: boolean
}

export function disconnectFromDebugger() {
DebuggerContext.ws!.close()
DebuggerContext.connected = false
}

export function connectToDebugger(addr: string, revenge: RevengeLibrary) {
const ws = (DebuggerContext.ws = new WebSocket(`ws://${addr}`))

ws.addEventListener('open', () => {
DebuggerContext.connected = true
DebuggerEvents.emit('connect')
DebuggerEvents.emit('*', 'connect')
})

ws.addEventListener('close', () => {
DebuggerContext.connected = false
DebuggerEvents.emit('disconnect')
DebuggerEvents.emit('*', 'disconnect')
})

ws.addEventListener('error', e => {
DebuggerContext.connected = false
DebuggerEvents.emit('error', e)
DebuggerEvents.emit('*', 'error', e)
})

ws.addEventListener('message', e => {
try {
const json = JSON.parse(e.data) as {
code: string
nonce: string
}

if (typeof json.code === 'string' && typeof json.nonce === 'string') {
let res: unknown
try {
// biome-ignore lint/security/noGlobalEval: This is intentional
res = globalThis.eval(json.code)
} catch (e) {
res = e
}

const inspect =
revenge.modules.findProp<
(val: unknown, opts?: { depth?: number; showHidden?: boolean; color?: boolean }) => string
>('inspect')!

try {
ws.send(
res instanceof Error
? JSON.stringify({
level: 'error',
message: String(res),
nonce: json.nonce,
})
: JSON.stringify({
level: 'info',
message: inspect(res, { showHidden: true }),
nonce: json.nonce,
}),
)
} catch (e) {
ws.send(
JSON.stringify({
level: 'error',
message: `DebuggerError: ${String(e)}`,
nonce: json.nonce,
}),
)
}
}
} catch {}
})
}
Loading

0 comments on commit b348697

Please sign in to comment.