feat(plugins/developer-kit): improve UI for JS eval

Currently the eval is done in a modal which makes it hard to use and means any code written is discarded after pressing evaluate and also it is hard to read output from an alert. This commit improves it by instead having it as a page instead where the code remains after evaluation and outputs the result in a box in the page rather than in an alert.

Eval results are also selectable now, and have display options. And inspecting with zero depth is now possible.

---------

Co-authored-by: PalmDevs <git@palmdevs.me>
This commit is contained in:
tralwdwd 2025-12-13 16:16:53 +00:00 • committed by GitHub
parent 3284300ef1
commit aa07721269
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 156 additions and 133 deletions

View file

@ -21,5 +21,6 @@ export const Setting = {
export const RouteNames = {
[Setting.RevengeDeveloper]: 'Revenge Developer',
[Setting.AssetBrowser]: 'Asset Browser',
[Setting.EvalJS]: 'Evaluate JavaScript',
[Setting.TestErrorBoundary]: 'Test ErrorBoundary',
} as const

View file

@ -1,144 +1,18 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { AlertActionCreators } from '@revenge-mod/discord/actions'
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 { api } from '..'
import { Setting } from '../constants'
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 AlertKey = 'evaluate-javascript'
const EvalJSSetting: SettingsItem = {
parent: Setting.RevengeDeveloper,
type: 'route',
IconComponent: () => <TableRowAssetIcon name="FileIcon" />,
title: () => 'Evaluate JavaScript',
useDescription: () => 'Runs a JavaScript code snippet.',
onPress: () => {
AlertActionCreators.openAlert(AlertKey, <EvalJSAlert />)
screen: {
route: RouteNames[Setting.EvalJS],
getComponent: () => EvalJSSettingScreen,
},
type: 'pressable',
}
const {
AlertActionButton,
AlertModal,
Button,
Slider,
Stack,
TableRow,
TableRowGroup,
TableSwitchRow,
Text,
TextArea,
} = Design
function EvalJSAlert() {
const code = useRef('')
const [awaitResult, setAwaitResult] = useState(true)
const [inspectDepth, setInspectDepth] = useState(3)
const [showHidden, setShowHidden] = useState(true)
return (
<AlertModal
actions={
<>
<Button
onPress={async function onPress() {
const key = `_${Math.random()
.toString(36)
.substring(2, 15)}`
try {
if (!api) {
alert(
'Unable to provide plugin API. Running snippet in a second...',
)
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`,
)
alert(
nodeUtil.inspect(
awaitResult ? await res : res,
{
depth: inspectDepth,
showHidden,
},
),
)
// @ts-expect-error
delete globalThis[key]
} catch (e) {
alert(getErrorStack(e))
}
AlertActionCreators.dismissAlert(AlertKey)
}}
text="Evaluate"
variant="primary"
/>
<AlertActionButton text="Cancel" variant="secondary" />
</>
}
extraContent={
<Stack>
<TextArea
autoFocus
label="Code"
onChange={v => {
code.current = v
}}
placeholder="revenge.discord.native.BundleUpdaterManager.reload()"
size="md"
/>
<TableRowGroup>
<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={1}
onValueChange={setInspectDepth}
startIcon={<Text variant="text-sm/normal">1</Text>}
step={1}
value={inspectDepth}
/>
</Stack>
}
title="Evaluate JavaScript"
/>
)
}
export default EvalJSSetting

View file

@ -0,0 +1,148 @@
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>
)
}