fix(plugins): remove developer-kit

This plugin has been moved to an external repository.
The goal of bundling internal plugins are including what's needed by _the user_ by default.

Developer Kit is not needed by most users, nor is it a "safe" plugin (it exposes a few attack surfaces, even if unlikely to be used).
This commit is contained in:
PalmDevs 2026-07-31 02:50:56 +07:00
parent 0f75551c36
commit b258ac70d6
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
21 changed files with 0 additions and 1137 deletions

View file

@ -7,8 +7,6 @@
"dependencies": { "dependencies": {
"@revenge-mod/assets": "workspace:*", "@revenge-mod/assets": "workspace:*",
"@revenge-mod/components": "workspace:*", "@revenge-mod/components": "workspace:*",
"@revenge-mod/devtools-client": "^1.4.0",
"@revenge-mod/devtools-shared": "^1.3.0",
"@revenge-mod/discord": "workspace:*", "@revenge-mod/discord": "workspace:*",
"@revenge-mod/externals": "workspace:*", "@revenge-mod/externals": "workspace:*",
"@revenge-mod/json-storage": "workspace:*", "@revenge-mod/json-storage": "workspace:*",
@ -210,10 +208,6 @@
"@revenge-mod/components": ["@revenge-mod/components@workspace:lib/components"], "@revenge-mod/components": ["@revenge-mod/components@workspace:lib/components"],
"@revenge-mod/devtools-client": ["@revenge-mod/devtools-client@1.4.0", "", { "dependencies": { "@revenge-mod/devtools-shared": "^1.3.0", "bippy": "^0.3.8" } }, "sha512-hGmdM0RcK3/IrzpW7lDOXl+it3wYcoF+ePI43Ey3AvIPaBGIDIGqqitKtrB8wIGo5jREsr703fcjseE+bt+aCg=="],
"@revenge-mod/devtools-shared": ["@revenge-mod/devtools-shared@1.3.0", "", { "dependencies": { "chalk": "^5.6.2", "devalue": "^5.3.2" } }, "sha512-MLox/xQsYtMG78R4R8sy32P5X7ILUNUPAX8IiWjUsPIclkTweDvwEAUadkvbBo8j+/ZJaJdif+Fg/C9YQNlOqg=="],
"@revenge-mod/discord": ["@revenge-mod/discord@workspace:lib/discord"], "@revenge-mod/discord": ["@revenge-mod/discord@workspace:lib/discord"],
"@revenge-mod/externals": ["@revenge-mod/externals@workspace:lib/externals"], "@revenge-mod/externals": ["@revenge-mod/externals@workspace:lib/externals"],
@ -304,8 +298,6 @@
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
"@types/react-reconciler": ["@types/react-reconciler@0.28.9", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg=="],
"@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
"@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
@ -406,8 +398,6 @@
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"bippy": ["bippy@0.3.34", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": ">=17.0.1" } }, "sha512-vmptmU/20UdIWHHhq7qCSHhHzK7Ro3YJ1utU0fBG7ujUc58LEfTtilKxcF0IOgSjT5XLcm7CBzDjbv4lcKApGQ=="],
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
@ -434,8 +424,6 @@
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devalue": ["devalue@5.8.2", "", {}, "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA=="],
"dts-resolver": ["dts-resolver@3.0.0", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q=="], "dts-resolver": ["dts-resolver@3.0.0", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@ -592,8 +580,6 @@
"yuku-parser": ["yuku-parser@0.8.1", "", { "dependencies": { "@yuku-toolchain/types": "^0.8.1", "yuku-ast": "^0.8.1" }, "optionalDependencies": { "@yuku-parser/binding-darwin-arm64": "0.8.1", "@yuku-parser/binding-darwin-x64": "0.8.1", "@yuku-parser/binding-freebsd-x64": "0.8.1", "@yuku-parser/binding-linux-arm-gnu": "0.8.1", "@yuku-parser/binding-linux-arm-musl": "0.8.1", "@yuku-parser/binding-linux-arm64-gnu": "0.8.1", "@yuku-parser/binding-linux-arm64-musl": "0.8.1", "@yuku-parser/binding-linux-x64-gnu": "0.8.1", "@yuku-parser/binding-linux-x64-musl": "0.8.1", "@yuku-parser/binding-win32-arm64": "0.8.1", "@yuku-parser/binding-win32-x64": "0.8.1" } }, "sha512-YvUz2jdFMIq0QjN8LmI32ox+RBCn3l8VToAXVQ5QFfLTxSxmGZS6JP80ptePEju+CpeTdINLmUm7Ewodzh1QnQ=="], "yuku-parser": ["yuku-parser@0.8.1", "", { "dependencies": { "@yuku-toolchain/types": "^0.8.1", "yuku-ast": "^0.8.1" }, "optionalDependencies": { "@yuku-parser/binding-darwin-arm64": "0.8.1", "@yuku-parser/binding-darwin-x64": "0.8.1", "@yuku-parser/binding-freebsd-x64": "0.8.1", "@yuku-parser/binding-linux-arm-gnu": "0.8.1", "@yuku-parser/binding-linux-arm-musl": "0.8.1", "@yuku-parser/binding-linux-arm64-gnu": "0.8.1", "@yuku-parser/binding-linux-arm64-musl": "0.8.1", "@yuku-parser/binding-linux-x64-gnu": "0.8.1", "@yuku-parser/binding-linux-x64-musl": "0.8.1", "@yuku-parser/binding-win32-arm64": "0.8.1", "@yuku-parser/binding-win32-x64": "0.8.1" } }, "sha512-YvUz2jdFMIq0QjN8LmI32ox+RBCn3l8VToAXVQ5QFfLTxSxmGZS6JP80ptePEju+CpeTdINLmUm7Ewodzh1QnQ=="],
"@revenge-mod/devtools-shared/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"metro-runtime/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], "metro-runtime/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
"pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],

View file

@ -41,8 +41,6 @@
"dependencies": { "dependencies": {
"@revenge-mod/assets": "workspace:*", "@revenge-mod/assets": "workspace:*",
"@revenge-mod/components": "workspace:*", "@revenge-mod/components": "workspace:*",
"@revenge-mod/devtools-client": "^1.4.0",
"@revenge-mod/devtools-shared": "^1.3.0",
"@revenge-mod/discord": "workspace:*", "@revenge-mod/discord": "workspace:*",
"@revenge-mod/externals": "workspace:*", "@revenge-mod/externals": "workspace:*",
"@revenge-mod/json-storage": "workspace:*", "@revenge-mod/json-storage": "workspace:*",

View file

@ -1,26 +0,0 @@
export const Setting = {
// MAIN SETTINGS
RevengeDeveloper: 'RevengeDeveloper',
// SUBSETTINGS
DTAutoConnect: 'DTAutoConnect',
DTConnect: 'DTConnect',
DTDisconnect: 'DTDisconnect',
RDTAutoConnect: 'RDTAutoConnect',
RDTConnect: 'RDTConnect',
RDTDisconnect: 'RDTDisconnect',
EvalJS: 'EvalJS',
AssetBrowser: 'AssetBrowser',
TestErrorBoundary: 'TestErrorBoundary',
} as const
export const RouteNames = {
[Setting.RevengeDeveloper]: 'Revenge Developer',
[Setting.AssetBrowser]: 'Asset Browser',
[Setting.EvalJS]: 'Evaluate JavaScript',
[Setting.TestErrorBoundary]: 'Test ErrorBoundary',
} as const

View file

@ -1,17 +0,0 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { RouteNames, Setting } from '../constants'
import defer * as AssetBrowserSettingScreen from '../screens/AssetBrowserSettingScreen'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const AssetBrowserSetting: SettingsItem = {
parent: Setting.RevengeDeveloper,
type: 'route',
IconComponent: () => <TableRowAssetIcon name="ImageIcon" />,
useTitle: () => 'Asset Browser',
screen: {
route: RouteNames[Setting.AssetBrowser],
getComponent: () => AssetBrowserSettingScreen.default,
},
}
export default AssetBrowserSetting

View file

@ -1,20 +0,0 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { api } from '..'
import { Setting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const DTAutoConnectSetting: SettingsItem = {
parent: Setting.RevengeDeveloper,
IconComponent: () => <TableRowAssetIcon name="LinkIcon" />,
useTitle: () => 'Auto-connect to DevTools',
useDescription: () => 'Automatically connect to DevTools during startup.',
useValue: () =>
api.jsonStorage.use(s => s.devTools?.autoConnect !== undefined)!
.devTools.autoConnect,
onValueChange: v => {
api.jsonStorage.set({ devTools: { autoConnect: v } })
},
type: 'toggle',
}
export default DTAutoConnectSetting

View file

@ -1,17 +0,0 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { DevToolsClient } from '@revenge-mod/devtools-client'
import { Setting } from '../constants'
import { connect, useIsConnected } from '../devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const DTConnectSetting: SettingsItem = {
parent: Setting.RevengeDeveloper,
IconComponent: () => <TableRowAssetIcon name="LinkIcon" />,
useTitle: () => 'Connect to DevTools',
useDescription: () => `Version: ${DevToolsClient.version}`,
usePredicate: () => !useIsConnected(),
onPress: connect,
type: 'pressable',
}
export default DTConnectSetting

View file

@ -1,16 +0,0 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { Setting } from '../constants'
import { disconnect, useIsConnected } from '../devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const DTDisconnectSetting: SettingsItem = {
parent: Setting.RevengeDeveloper,
IconComponent: () => <TableRowAssetIcon name="DenyIcon" variant="danger" />,
variant: 'danger',
useTitle: () => 'Disconnect from DevTools',
usePredicate: useIsConnected,
onPress: disconnect,
type: 'pressable',
}
export default DTDisconnectSetting

View file

@ -1,18 +0,0 @@
import { TableRowAssetIcon } from '@revenge-mod/components'
import { RouteNames, Setting } from '../constants'
import EvalJSSettingScreen from '../screens/EvalJSSettingScreen'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const EvalJSSetting: SettingsItem = {
parent: Setting.RevengeDeveloper,
type: 'route',
IconComponent: () => <TableRowAssetIcon name="FileIcon" />,
useTitle: () => 'Evaluate JavaScript',
useDescription: () => 'Runs a JavaScript code snippet.',
screen: {
route: RouteNames[Setting.EvalJS],
getComponent: () => EvalJSSettingScreen,
},
}
export default EvalJSSetting

View file

@ -1,23 +0,0 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { api } from '..'
import { Setting } from '../constants'
import { RDTContext } from '../react-devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RDTAutoConnectSetting: SettingsItem = {
parent: Setting.RevengeDeveloper,
IconComponent: () => <TableRowAssetIcon name="LinkIcon" />,
useTitle: () => 'Auto-connect to React DevTools',
useDescription: () =>
'Automatically connect to React DevTools during startup.',
usePredicate: () => RDTContext.active,
useValue: () =>
api.jsonStorage.use(s => s.reactDevTools?.autoConnect !== undefined)!
.reactDevTools.autoConnect,
onValueChange: v => {
api.jsonStorage.set({ reactDevTools: { autoConnect: v } })
},
type: 'toggle',
}
export default RDTAutoConnectSetting

View file

@ -1,16 +0,0 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { Setting } from '../constants'
import { connect, RDTContext, useIsConnected } from '../react-devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RDTConnectSetting: SettingsItem = {
parent: Setting.RevengeDeveloper,
IconComponent: () => <TableRowAssetIcon name="LinkIcon" />,
useTitle: () => 'Connect to React DevTools',
useDescription: () => `Version: ${globalThis.__REACT_DEVTOOLS__?.version}`,
usePredicate: () => !useIsConnected() && RDTContext.active,
onPress: connect,
type: 'pressable',
}
export default RDTConnectSetting

View file

@ -1,16 +0,0 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { Setting } from '../constants'
import { disconnect, useIsConnected } from '../react-devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RDTDisconnectSetting: SettingsItem = {
parent: Setting.RevengeDeveloper,
IconComponent: () => <TableRowAssetIcon name="DenyIcon" variant="danger" />,
variant: 'danger',
useTitle: () => 'Disconnect from React DevTools',
usePredicate: useIsConnected,
onPress: disconnect,
type: 'pressable',
}
export default RDTDisconnectSetting

View file

@ -1,17 +0,0 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { RouteNames, Setting } from '../constants'
import defer * as RevengeDeveloperSettingScreen from '../screens/RevengeDeveloperSettingScreen'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RevengeDeveloperSetting: SettingsItem = {
parent: null,
type: 'route',
IconComponent: () => <TableRowAssetIcon name="WrenchIcon" />,
useTitle: () => 'Developer',
screen: {
route: RouteNames[Setting.RevengeDeveloper],
getComponent: () => RevengeDeveloperSettingScreen.default,
},
}
export default RevengeDeveloperSetting

View file

@ -1,20 +0,0 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { RouteNames, Setting } from '../constants'
import defer * as TestErrorBoundarySettingScreen from '../screens/TestErrorBoundarySettingScreen'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const TestErrorBoundarySetting: SettingsItem = {
parent: Setting.RevengeDeveloper,
type: 'route',
variant: 'danger',
IconComponent: () => (
<TableRowAssetIcon name="ScreenXIcon" variant="danger" />
),
useTitle: () => 'Test ErrorBoundary',
screen: {
route: RouteNames[Setting.TestErrorBoundary],
getComponent: () => TestErrorBoundarySettingScreen.default,
},
}
export default TestErrorBoundarySetting

View file

@ -1,141 +0,0 @@
import { DevToolsClient } from '@revenge-mod/devtools-client'
import { LogLevel } from '@revenge-mod/devtools-shared/constants'
import { ToastActionCreators } from '@revenge-mod/discord/actions'
import { TypedEventEmitter } from '@revenge-mod/discord/common/utils'
import { ClientInfoModule } from '@revenge-mod/discord/native'
import { getBridgeInfo } from '@revenge-mod/modules/native'
import { instead } from '@revenge-mod/patcher'
import { noop } from '@revenge-mod/utils/callback'
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
import { useReRender } from '@revenge-mod/utils/react'
import { useEffect } from 'react'
import { api } from '.'
import type { UnpatchFunction } from '@revenge-mod/patcher/types'
export const DTContext: {
client: DevToolsClient | null
addr: string
alias: string
con: boolean
} = {
client: null,
addr: 'localhost:7684',
alias: '',
con: false,
}
const events = new TypedEventEmitter<{
connect: []
disconnect: []
errored: [unknown]
}>()
const CircleErrorIcon = lookupGeneratedIconComponent(
'CircleErrorIcon',
'CircleErrorIcon-secondary',
'CircleErrorIcon-primary',
)
let intercepted = false
function interceptLogging() {
if (intercepted) return noop
intercepted = true
const unpatches: UnpatchFunction[] = []
for (const [key, level] of [
['log', LogLevel.Default],
['warn', LogLevel.Warn],
['error', LogLevel.Error],
['info', LogLevel.Default],
['debug', LogLevel.Debug],
] as const) {
unpatches.push(
instead(console, key, (args, orig) => {
if (DTContext.con && DTContext.client)
DTContext.client.log(level, args)
return Reflect.apply(orig, console, args)
}),
)
}
return () => {
intercepted = false
for (const unpatch of unpatches) unpatch()
}
}
export function connect() {
const Bridge = getBridgeInfo()
const Client = ClientInfoModule.getConstants()
const client = (DTContext.client = new DevToolsClient())
client.expose('api', api)
client.expose('cleanup', api.cleanup)
client.expose('logger', api.logger)
client.expose('revenge', api.unscoped)
client.connect(
`ws://${DTContext.addr}`,
`${Client.Version}${Bridge ? ` (${Bridge.name} ${Bridge.version})` : ''}`,
DTContext.alias || undefined,
)
const ws = client.ws!
ws.addEventListener('open', () => {
if (client.settings.log.interceptConsole) {
const unintercept = interceptLogging()
ws.addEventListener('close', function self() {
unintercept()
ws.removeEventListener('close', self)
})
}
DTContext.con = true
events.emit('connect')
})
ws.addEventListener('close', e_ => {
cleanup()
events.emit('disconnect')
const e = e_ as CloseEvent
if (!e.wasClean) {
api.logger.error('DevTools error:', e.reason)
ToastActionCreators.open({
key: 'DEVTOOLS_ERROR',
IconComponent: CircleErrorIcon,
content: e.reason,
})
}
})
}
function cleanup() {
DTContext.client = null
DTContext.con = false
}
export function disconnect() {
DTContext.client?.disconnect()
}
export function useIsConnected() {
const rerender = useReRender()
useEffect(() => {
events.on('connect', rerender)
events.on('disconnect', rerender)
return () => {
events.off('connect', rerender)
events.off('disconnect', rerender)
}
}, [rerender])
return DTContext.con
}

View file

@ -1,80 +0,0 @@
import {
onSettingsModulesLoaded,
refreshSettings,
} from '@revenge-mod/discord/modules/settings'
import {
InternalPluginFlags,
isPluginStartedLate,
PluginFlags,
registerInternalPlugin,
} from '@revenge-mod/plugins/_'
import pluginSettings from '../settings'
import * as dt from './devtools'
import defer * as rdt from './react-devtools'
import defer * as utils from './utils'
import type { PluginApi } from '@revenge-mod/plugins/types'
interface Storage {
devTools: DevToolsSettings & { alias?: string }
reactDevTools: DevToolsSettings
}
interface DevToolsSettings {
address: string
autoConnect: boolean
}
const defaultStorage: Storage = {
devTools: {
address: 'localhost:7864',
autoConnect: false,
},
reactDevTools: {
address: 'localhost:8097',
autoConnect: false,
},
}
// TODO(PalmDevs): only register in development builds once updates can be made automatic
registerInternalPlugin<{ jsonStorage: Storage }>(
{
id: 'revenge.developer-kit',
name: 'Developer Kit',
description: 'Tools assisting Revenge developers.',
author: 'Revenge',
icon: 'WrenchIcon',
dependencies: { [pluginSettings]: {} },
},
{
jsonStorage: {
load: true,
default: defaultStorage,
},
async start(api_) {
api = api_
onSettingsModulesLoaded(utils.register)
if (isPluginStartedLate(api_.plugin)) {
refreshSettings()
}
const settings = await api.jsonStorage.get()
dt.DTContext.addr = settings.devTools.address
dt.DTContext.alias = settings.devTools.alias ?? ''
rdt.RDTContext.addr = settings.reactDevTools.address
if (settings.devTools.autoConnect) dt.connect()
if (settings.reactDevTools.autoConnect) rdt.connect()
},
stop({ cleanup }) {
cleanup(refreshSettings)
},
},
PluginFlags.Enabled,
InternalPluginFlags.Internal,
)
// Expose to EvalJSSetting
export let api: PluginApi<{ jsonStorage: Storage }>

View file

@ -1,86 +0,0 @@
import { ToastActionCreators } from '@revenge-mod/discord/actions'
import { TypedEventEmitter } from '@revenge-mod/discord/common/utils'
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
import { useReRender } from '@revenge-mod/utils/react'
import { useEffect } from 'react'
import { api } from '.'
export const RDTContext: {
ws: WebSocket | undefined
addr: string
con: boolean
active: boolean
} = {
active: Boolean(globalThis.__REACT_DEVTOOLS__),
ws: undefined,
addr: 'localhost:8097',
con: false,
}
const events = new TypedEventEmitter<{
connect: []
disconnect: []
errored: [unknown]
}>()
const CircleErrorIcon = lookupGeneratedIconComponent(
'CircleErrorIcon',
'CircleErrorIcon-secondary',
'CircleErrorIcon-primary',
)
export function connect() {
if (!RDTContext.active || RDTContext.ws) return
const ws = (RDTContext.ws = new WebSocket(`ws://${RDTContext.addr}`))
ws.addEventListener('open', () => {
RDTContext.con = true
events.emit('connect')
})
ws.addEventListener('close', e_ => {
cleanup()
events.emit('disconnect')
const e = e_ as CloseEvent
if (!e.wasClean) {
api.logger.error('React DevTools error:', e.reason)
ToastActionCreators.open({
key: 'REACT_DEVTOOLS_ERROR',
IconComponent: CircleErrorIcon,
content: e.reason,
})
}
})
__REACT_DEVTOOLS__!.exports.connectToDevTools({
websocket: ws,
})
}
function cleanup() {
RDTContext.con = false
RDTContext.ws = undefined
}
export function disconnect() {
if (RDTContext.ws) RDTContext.ws.close()
}
export function useIsConnected() {
const rerender = useReRender()
useEffect(() => {
events.on('connect', rerender)
events.on('disconnect', rerender)
return () => {
events.off('connect', rerender)
events.off('disconnect', rerender)
}
}, [rerender])
return RDTContext.con
}

View file

@ -1,231 +0,0 @@
import { getAssets } from '@revenge-mod/assets'
import Page from '@revenge-mod/components/Page'
import SearchInput from '@revenge-mod/components/SearchInput'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import {
AlertActionCreators,
ToastActionCreators,
} from '@revenge-mod/discord/actions'
import { Design } from '@revenge-mod/discord/design'
import { Clipboard } from '@revenge-mod/externals/react-native-clipboard'
import { debounce } from '@revenge-mod/utils/callback'
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
import { FlashList } from '@shopify/flash-list'
import { useCallback, useMemo, useState } from 'react'
import { Image, StyleSheet, View } from 'react-native'
import type { Asset, AssetId } from '@revenge-mod/assets/types'
import type { Metro } from '@revenge-mod/modules/types'
const { AlertModal, AlertActionButton, TableRow, TableRowGroup, Text } = Design
const Displayable = new Set(['png', 'jpg', 'svg', 'webp'])
const UndisplayableFallback = {
jsona: 'PaperIcon',
lottie: 'ImageFileIcon',
webm: 'CirclePlayIcon-primary',
ttf: 'LanguageIcon',
default: 'UnknownGameIcon',
}
export default function AssetBrowserSettingScreen() {
const [search, setSearch] = useState('')
const debouncedSetSearch = useCallback(debounce(setSearch, 100), [])
const assets = useMemo(() => [...getAssets()], [])
const filteredAssets = useMemo(
() =>
!search.length
? assets
: assets.filter(asset =>
asset.name.toLowerCase().includes(search.toLowerCase()),
),
[assets, search],
)
return (
<Page spacing={16}>
<SearchInput
onChange={(v: string) => {
debouncedSetSearch(v)
}}
size="md"
/>
<FlashList
data={filteredAssets}
contentContainerStyle={styles.listContainer}
fadingEdgeLength={32}
keyExtractor={asset =>
asset.id
? asset.id.toString()
: `${asset.name}.${asset.type}`
}
renderItem={({ item: asset, index }) => (
<AssetDisplay
asset={asset}
end={index === filteredAssets.length - 1}
id={asset.id}
moduleId={asset.moduleId}
start={!index}
/>
)}
/>
</Page>
)
}
function AssetDisplay({
id,
moduleId,
asset,
start,
end,
}: {
id: AssetId
moduleId?: Metro.ModuleID
asset: Asset
start?: boolean
end?: boolean
}) {
const isDisplayable = Displayable.has(asset.type)
const metadata = [
['ID', id.toString()],
['Type', asset.type],
] as [string, string, unpressable?: boolean, hiddenInText?: boolean][]
metadata.push(
moduleId
? ['Module ID', moduleId.toString()]
: [
'Custom asset',
'This is a custom asset registered by a plugin.',
true,
true,
],
)
return (
<TableRow
end={end}
icon={
isDisplayable ? (
<Image source={id} style={styles.smallPreview} />
) : (
<TableRowAssetIcon
name={
asset.type in UndisplayableFallback
? UndisplayableFallback[
asset.type as keyof typeof UndisplayableFallback
]
: UndisplayableFallback.default
}
variant="danger"
/>
)
}
label={asset.name}
onPress={() => {
openAssetDisplayAlert(asset, id, metadata)
}}
start={start}
subLabel={metadata
.filter(([, , , hiddenInText]) => !hiddenInText)
.map(([name, value]) => `${name}: ${value ?? 'N/A'}`)
.join(' • ')}
variant={isDisplayable ? 'default' : 'danger'}
/>
)
}
const CopyIcon = lookupGeneratedIconComponent('CopyIcon')
function openAssetDisplayAlert(
asset: Asset,
id: AssetId,
metadata: [string, string, unpressable?: boolean, ...unknown[]][],
) {
const isDisplayable = Displayable.has(asset.type)
AlertActionCreators.openAlert(
'asset-display',
<AlertModal
actions={<AlertActionButton text="Close" variant="secondary" />}
title={
<View style={{ flex: 1, width: '100%' }}>
{isDisplayable ? (
<Image source={id} style={styles.preview} />
) : (
<PreviewUnavailable type={asset.type} />
)}
</View>
}
extraContent={
<>
<TableRowGroup>
{[['Name', asset.name], ...metadata].map(
([name, value, unpressable]) => (
<TableRow
key={id}
label={name}
onPress={
unpressable
? undefined
: () => {
Clipboard.setString(value)
ToastActionCreators.open({
key: 'ASSET_BROWSER_COPIED',
IconComponent: CopyIcon,
content: `Copied ${name} to clipboard`,
})
}
}
subLabel={value}
/>
),
)}
</TableRowGroup>
<Text
color="text-feedback-critical"
variant="text-xs/semibold"
>
Note: Asset IDs and module IDs are not consistent
between app launches and app versions respectively and
should only be used when absolutely needed.
</Text>
</>
}
/>,
)
}
function PreviewUnavailable({ type }: { type: string }) {
return (
<Text
color="text-feedback-critical"
style={styles.centeredText}
variant="text-sm/medium"
>
Asset type {type.toUpperCase()} is not supported for preview.
</Text>
)
}
const styles = StyleSheet.create({
listContainer: {
paddingBottom: 16,
},
smallPreview: {
height: 32,
width: 32,
},
preview: {
flex: 1,
width: 'auto',
height: 192,
resizeMode: 'contain',
},
centeredText: {
width: '100%',
textAlign: 'center',
},
})

View file

@ -1,148 +0,0 @@
import { Page } from '@revenge-mod/components'
import { Design } from '@revenge-mod/discord/design'
import { nodeUtil } from '@revenge-mod/externals/browserify'
import { getErrorStack } from '@revenge-mod/utils/error'
import { sleep } from '@revenge-mod/utils/promise'
import { useRef, useState } from 'react'
import { ScrollView } from 'react-native'
import { api } from '..'
const {
Button,
Card,
Slider,
Stack,
TableRow,
TableRowGroup,
TableSwitchRow,
Text,
TextArea,
} = Design
export default function EvalJSSettingScreen() {
const code = useRef('')
const [result, setResult] = useState('')
const [awaitResult, setAwaitResult] = useState(true)
const [showHidden, setShowHidden] = useState(true)
const [inspectDepth, setInspectDepth] = useState(3)
const [wordWrap, setWordWrap] = useState(true)
return (
<ScrollView contentContainerStyle={{ paddingBottom: 32 }}>
<Page spacing={16}>
<TextArea
autoFocus
label="Code"
onChange={v => {
code.current = v
}}
placeholder="revenge.discord.native.BundleUpdaterManager.reload()"
size="md"
/>
<TableRowGroup title="Inspection Options">
<TableSwitchRow
label="Await result"
onValueChange={setAwaitResult}
subLabel="Wait for the result of the code before displaying it."
value={awaitResult}
/>
<TableSwitchRow
label="Show hidden"
onValueChange={setShowHidden}
subLabel="Show hidden properties of the object."
value={showHidden}
/>
<TableRow
label="Inspect depth"
subLabel="The depth of the object to inspect."
trailing={
<Text variant="text-sm/normal">{inspectDepth}</Text>
}
/>
</TableRowGroup>
<Slider
endIcon={<Text variant="text-sm/normal">10</Text>}
maximumValue={10}
minimumValue={0}
onValueChange={setInspectDepth}
startIcon={<Text variant="text-sm/normal">0</Text>}
step={1}
value={inspectDepth}
/>
<TableRowGroup title="Display Options">
<TableSwitchRow
label="Word wrap"
subLabel="Long results will wrap to the next line."
onValueChange={setWordWrap}
value={wordWrap}
/>
</TableRowGroup>
<Stack>
<Text variant="heading-sm/semibold" color="text-strong">
Result
</Text>
<Card>
{/* TODO: Maybe try to make it fill space with minHeight: 128? */}
<ScrollView
nestedScrollEnabled
style={{ maxHeight: 300 }}
>
<ScrollView
nestedScrollEnabled
horizontal={!wordWrap}
>
<Text
selectable
style={{ fontFamily: 'monospace' }}
>
{result || '<empty>'}
</Text>
</ScrollView>
</ScrollView>
</Card>
</Stack>
<Button
onPress={async function onPress() {
const key = `_${Math.random()
.toString(36)
.substring(2, 15)}`
try {
if (!api) {
setResult(
'<Revenge API not available, snippet will be run without it...>',
)
await sleep(1000)
}
// @ts-expect-error
else globalThis[key] = api
// biome-ignore lint/security/noGlobalEval: Intentional
const res = (0, eval)(
`var api=${key},{unscoped:revenge}=api;undefined;${code.current}//# sourceURL=Revenge:EvalJS`,
)
setResult(
nodeUtil.inspect(
awaitResult ? await res : res,
{
depth: inspectDepth,
showHidden,
},
),
)
// @ts-expect-error
delete globalThis[key]
} catch (e) {
setResult(getErrorStack(e))
}
}}
text="Evaluate"
variant="primary"
/>
</Page>
</ScrollView>
)
}

View file

@ -1,183 +0,0 @@
import { styles } from '@revenge-mod/components/_'
import { ToastActionCreators } from '@revenge-mod/discord/actions'
import { Design } from '@revenge-mod/discord/design'
import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/renderer'
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
import { StyleSheet, View } from 'react-native'
import { api } from '..'
import { Setting } from '../constants'
import { DTContext, useIsConnected as useIsDTConnected } from '../devtools'
import {
RDTContext,
useIsConnected as useIsRDTConnected,
} from '../react-devtools'
const CircleCheckIcon = lookupGeneratedIconComponent(
'CircleCheckIcon',
'CircleCheckIcon-secondary',
'CircleCheckIcon-primary',
)
import { ScrollView } from 'react-native'
const listStyles = StyleSheet.create({
container: {
flexGrow: 0,
flexShrink: 0,
height: 'auto',
},
})
export default function RevengeDeveloperSettingScreen() {
return (
<ScrollView style={styles.flex}>
<Design.Stack spacing={8}>
<View>
<SettingListRenderer.SettingsList
containerStyle={listStyles.container}
node={{
type: 'list',
ListHeaderComponent: DTConnectionSetting,
sections: [
{
settings: [
Setting.DTAutoConnect,
Setting.DTConnect,
Setting.DTDisconnect,
],
},
],
}}
/>
</View>
<View>
<SettingListRenderer.SettingsList
containerStyle={listStyles.container}
node={{
type: 'list',
ListHeaderComponent: RDTContext.active
? RDTAddrSetting
: undefined,
sections: [
{
settings: [
Setting.RDTAutoConnect,
Setting.RDTConnect,
Setting.RDTDisconnect,
],
},
{
label: 'Tools',
settings: [
Setting.EvalJS,
Setting.AssetBrowser,
Setting.TestErrorBoundary,
],
},
],
}}
/>
</View>
</Design.Stack>
</ScrollView>
)
}
export function DTConnectionSetting() {
const open = useIsDTConnected()
const settings = api.jsonStorage.use(
s =>
s.devTools?.address !== undefined ||
s.devTools?.alias !== undefined,
)
return (
<>
<Design.TextInput
defaultValue={settings?.devTools.address ?? DTContext.addr}
editable={!open}
isDisabled={open}
label="DevTools"
leadingText="ws://"
onBlur={() =>
api.jsonStorage
.set({
devTools: {
address: DTContext.addr,
},
})
.then(() =>
ToastActionCreators.open({
IconComponent: CircleCheckIcon,
key: 'DEVTOOLS_ADDRESS_SAVED',
content: 'Address saved',
}),
)
}
onChange={text => {
DTContext.addr = text
}}
returnKeyType="done"
/>
<Design.TextInput
defaultValue={settings?.devTools.alias ?? DTContext.alias}
editable={!open}
isDisabled={open}
label="DevTools Alias"
onBlur={() =>
api.jsonStorage
.set({
devTools: {
alias: DTContext.alias,
},
})
.then(() =>
ToastActionCreators.open({
IconComponent: CircleCheckIcon,
key: 'DEVTOOLS_ALIAS_SAVED',
content: 'Alias saved',
}),
)
}
onChange={text => {
DTContext.alias = text
}}
returnKeyType="done"
/>
</>
)
}
export function RDTAddrSetting() {
const open = useIsRDTConnected()
const settings = api.jsonStorage.use(s => s.reactDevTools?.address)
return (
<Design.TextInput
defaultValue={settings?.reactDevTools.address ?? RDTContext.addr}
editable={!open}
isDisabled={open}
label="React DevTools"
leadingText="ws://"
onBlur={() =>
api.jsonStorage
.set({
reactDevTools: {
address: RDTContext.addr,
},
})
.then(() =>
ToastActionCreators.open({
IconComponent: CircleCheckIcon,
key: 'REACT_DEVTOOLS_ADDRESS_SAVED',
content: 'Address saved',
}),
)
}
onChange={text => {
RDTContext.addr = text
}}
returnKeyType="done"
/>
)
}

View file

@ -1,12 +0,0 @@
import { useEffect } from 'react'
export default function TestErrorBoundarySettingScreen() {
let x = 0
useEffect(() => {
x++
})
// @ts-expect-error: Intentional
return x < 2 ? <undefined /> : null
}

View file

@ -1,34 +0,0 @@
import {
addSettingsItemToSection,
registerSettingsItems,
} from '@revenge-mod/discord/modules/settings'
import { api } from '.'
import { Setting } from './constants'
import AssetBrowserSetting from './definitions/AssetBrowserSetting'
import DTAutoConnectSetting from './definitions/DTAutoConnectSetting'
import DTConnectSetting from './definitions/DTConnectSetting'
import DTDisconnectSetting from './definitions/DTDisconnectSetting'
import EvalJSSetting from './definitions/EvalJSSetting'
import RDTAutoConnectSetting from './definitions/RDTAutoConnectSetting'
import RDTConnectSetting from './definitions/RDTConnectSetting'
import RDTDisconnectSetting from './definitions/RDTDisconnectSetting'
import RevengeDeveloperSetting from './definitions/RevengeDeveloperSetting'
import TestErrorBoundarySetting from './definitions/TestErrorBoundarySetting'
export function register() {
api.cleanup(
registerSettingsItems({
[Setting.RevengeDeveloper]: RevengeDeveloperSetting,
[Setting.DTAutoConnect]: DTAutoConnectSetting,
[Setting.DTConnect]: DTConnectSetting,
[Setting.DTDisconnect]: DTDisconnectSetting,
[Setting.RDTAutoConnect]: RDTAutoConnectSetting,
[Setting.RDTConnect]: RDTConnectSetting,
[Setting.RDTDisconnect]: RDTDisconnectSetting,
[Setting.EvalJS]: EvalJSSetting,
[Setting.AssetBrowser]: AssetBrowserSetting,
[Setting.TestErrorBoundary]: TestErrorBoundarySetting,
}),
addSettingsItemToSection('REVENGE', Setting.RevengeDeveloper, -1),
)
}