feat: see commit description

- Shim React and React Native
- Move src/components to lib/components
- Exposed lib/components to `unscoped.components`
- scipts/build: Properly set footer sourceURL
- plugins/developer-settings: Debounce searching in Asset Browser
- plugins/settings: Debounce searching in Plugins
This commit is contained in:
PalmDevs 2025-06-08 19:01:33 +07:00
parent f453a82dab
commit 5b3a8bc5ab
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
42 changed files with 174 additions and 71 deletions

View file

@ -29,6 +29,9 @@
"lib/assets": {
"name": "@revenge-mod/assets",
},
"lib/components": {
"name": "@revenge-mod/components",
},
"lib/discord": {
"name": "@revenge-mod/discord",
},
@ -437,6 +440,8 @@
"@revenge-mod/assets": ["@revenge-mod/assets@workspace:lib/assets"],
"@revenge-mod/components": ["@revenge-mod/components@workspace:lib/components"],
"@revenge-mod/discord": ["@revenge-mod/discord@workspace:lib/discord"],
"@revenge-mod/externals": ["@revenge-mod/externals@workspace:lib/externals"],

View file

@ -1,4 +1,4 @@
import { ReactNative } from '@revenge-mod/react'
import { Platform } from 'react-native'
import { _customs, _overrides } from './_internal'
import { cache } from './caches'
import { AssetsRegistry } from './preinit'
@ -16,8 +16,7 @@ export {
} from './preinit'
// iOS cannot display SVGs
let _preferredType: Asset['type'] =
ReactNative.Platform.OS === 'ios' ? 'png' : 'svg'
let _preferredType: Asset['type'] = Platform.OS === 'ios' ? 'png' : 'svg'
/**
* Set the preferred asset type. This is used to determine which asset to use when multiple types are available.

View file

@ -0,0 +1,25 @@
{
"name": "@revenge-mod/components",
"private": true,
"type": "module",
"exports": {
"./FormSwitch": {
"default": "./src/FormSwitch.tsx"
},
"./Page": {
"default": "./src/Page.tsx"
},
"./SearchInput": {
"default": "./src/SearchInput.tsx"
},
"./TableRowAssetIcon": {
"default": "./src/TableRowAssetIcon.tsx"
},
"./init": {
"default": "./src/init.ts"
},
"./types": {
"types": "./src/types.ts"
}
}
}

View file

@ -1,5 +1,5 @@
import { FormSwitch as Switch } from '@revenge-mod/discord/design'
import { ReactNative } from '@revenge-mod/react'
import { StyleSheet, View } from 'react-native'
import type { DiscordModules } from '@revenge-mod/discord/types'
/**
@ -9,13 +9,13 @@ export default function FormSwitch(
props: DiscordModules.Components.FormSwitchProps,
) {
return (
<ReactNative.View style={props.disabled && styles.disabled}>
<View style={props.disabled && styles.disabled}>
<Switch {...props} />
</ReactNative.View>
</View>
)
}
const styles = ReactNative.StyleSheet.create({
const styles = StyleSheet.create({
disabled: {
opacity: 0.5,
},

View file

@ -1,5 +1,5 @@
import { Design } from '@revenge-mod/discord/design'
import { ReactNative } from '@revenge-mod/react'
import { StyleSheet } from 'react-native'
import type { DiscordModules } from '@revenge-mod/discord/types'
export default function Page(props: DiscordModules.Components.StackProps) {
@ -10,7 +10,7 @@ export default function Page(props: DiscordModules.Components.StackProps) {
)
}
const styles = ReactNative.StyleSheet.create({
const styles = StyleSheet.create({
page: {
flex: 1,
paddingHorizontal: 16,

View file

@ -0,0 +1,4 @@
export { default as FormSwitch } from './FormSwitch'
export { default as Page } from './Page'
export { default as SearchInput } from './SearchInput'
export { default as TableRowAssetIcon } from './TableRowAssetIcon'

View file

@ -0,0 +1,11 @@
/** biome-ignore-all lint/complexity/useArrowFunction: Rolldown issue: Bug requires us to use normal functions instead of arrow functions */
import { _uapi } from '@revenge-mod/plugins/_'
import { defineLazyProperty } from '@revenge-mod/utils/objects'
import type { UnscopedInitPluginApi } from '@revenge-mod/plugins/types'
const uapi = _uapi as UnscopedInitPluginApi
defineLazyProperty(uapi, 'components', function () {
return require('./index')
})

View file

@ -0,0 +1,12 @@
export interface PluginApiComponents {
FormSwitch: typeof import('@revenge-mod/components/FormSwitch').default
Page: typeof import('@revenge-mod/components/Page').default
SearchInput: typeof import('@revenge-mod/components/SearchInput').default
TableRowAssetIcon: typeof import('@revenge-mod/components/TableRowAssetIcon').default
}
declare module '@revenge-mod/plugins/types' {
export interface UnscopedInitPluginApi {
components: PluginApiComponents
}
}

View file

@ -1,6 +1,7 @@
// TODO(storage): Implement using NativeModules interop instead
import { FileModule } from '@revenge-mod/discord/native'
// This file is usually imported way too early, so we can't import our shim
import { React } from '@revenge-mod/react'
import { mergeDeep } from '@revenge-mod/utils/objects'
import { useReRender } from '@revenge-mod/utils/react'

View file

@ -1,3 +1,4 @@
// @revenge-mod/storage depends on this file, and it is imported way too early, so we can't import our shim
import { React } from '@revenge-mod/react'
import { findInTree } from './trees'
import type { ReactNode } from 'react'

View file

@ -61,10 +61,21 @@ export default async function build(dev = false, log = true) {
find: 'react/jsx-runtime',
replacement: `${ShimsDir}/react~jsx-runtime.ts`,
},
// Do not move React to the top!
// If you do that, react/jsx-runtime would resolve to ${ShimsDir}/react.ts/jsx-runtime instead.
{
find: 'react',
replacement: `${ShimsDir}/react.ts`,
},
{
find: 'react-native',
replacement: `${ShimsDir}/react-native.ts`,
},
],
}),
importGlobPlugin(),
swcPlugin(),
sourceURLPlugin(),
hermesCPlugin({
flags: [
dev ? '-Og' : '-O',
@ -102,7 +113,6 @@ export default async function build(dev = false, log = true) {
minify: {},
file: 'dist/revenge.js',
format: 'iife',
footer: '//# sourceURL=Revenge',
})
if (log)
@ -113,6 +123,18 @@ export default async function build(dev = false, log = true) {
)
}
function sourceURLPlugin() {
return {
name: 'footer',
renderChunk(code) {
return {
code: `${code}\n//# sourceURL=Revenge`,
map: null,
}
},
} satisfies RolldownPlugin
}
function swcPlugin() {
return {
name: 'swc',

3
shims/react-native.ts Normal file
View file

@ -0,0 +1,3 @@
import { ReactNative } from '@revenge-mod/react'
module.exports = ReactNative

3
shims/react.ts Normal file
View file

@ -0,0 +1,3 @@
import { React } from '@revenge-mod/react'
module.exports = React

View file

@ -19,4 +19,4 @@ const jsxs = ((type, ...args) =>
...args,
)) as typeof _jsxs
export { jsx, jsxs, Fragment }
module.exports = { jsx, jsxs, Fragment }

View file

@ -37,6 +37,7 @@ onModuleFirstRequired(IndexModuleId, function onIndexRequired() {
// Initialize init libraries
require('@revenge-mod/externals/init')
require('@revenge-mod/discord/init')
require('@revenge-mod/components/init')
// Run all init plugins
require('~/plugins/init')

View file

@ -1,4 +1,4 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { MobileSetting, UserSettingsSections } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'

View file

@ -1,9 +1,9 @@
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 { React } from '@revenge-mod/react'
import { getErrorStack } from '@revenge-mod/utils/errors'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { useRef, useState } from 'react'
import { api } from '..'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
@ -34,11 +34,11 @@ const {
} = Design
function EvaluateJavaScriptAlert() {
const code = React.useRef('')
const [awaitResult, setAwaitResult] = React.useState(true)
const [showHidden, setShowHidden] = React.useState(true)
const [inspectDepth, setInspectDepth] = React.useState(3)
const [loading, setLoading] = React.useState(false)
const code = useRef('')
const [awaitResult, setAwaitResult] = useState(true)
const [showHidden, setShowHidden] = useState(true)
const [inspectDepth, setInspectDepth] = useState(3)
const [loading, setLoading] = useState(false)
return (
<AlertModal

View file

@ -1,4 +1,4 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { api } from '..'
import { MobileSetting } from '../constants'
import { DevToolsContext } from '../react-devtools'

View file

@ -1,4 +1,4 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import {
connectToDevTools,

View file

@ -1,4 +1,4 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import { disconnectFromDevTools, useIsDevToolsOpen } from '../react-devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'

View file

@ -1,4 +1,4 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import { DevToolsContext } from '../react-devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'

View file

@ -1,4 +1,4 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { MobileSetting, UserSettingsSections } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'

View file

@ -1,8 +1,8 @@
import { ToastActionCreators } from '@revenge-mod/discord/actions'
import { React } from '@revenge-mod/react'
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
import { getErrorStack } from '@revenge-mod/utils/errors'
import { useReRender } from '@revenge-mod/utils/react'
import { useEffect } from 'react'
import { api } from '.'
export const DevToolsContext: {
@ -80,14 +80,14 @@ export function disconnectFromDevTools() {
export function useIsDevToolsOpen() {
const rerender = useReRender()
React.useEffect(() => {
useEffect(() => {
const sub: Subscription = e => (e === 1 || e === 2) && rerender()
subscriptions.add(sub)
return () => {
subscriptions.delete(sub)
}
}, [])
}, [rerender])
return DevToolsContext.open
}

View file

@ -1,4 +1,7 @@
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,
@ -6,15 +9,13 @@ import {
import { Design } from '@revenge-mod/discord/design'
import { Clipboard } from '@revenge-mod/externals/react-native-clipboard'
import { FlashList } from '@revenge-mod/externals/shopify'
import { React, ReactNative } from '@revenge-mod/react'
import { debounce } from '@revenge-mod/utils/callbacks'
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
import Page from '~/components/Page'
import SearchInput from '~/components/SearchInput'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { useCallback, useMemo, useState } from 'react'
import { Image, StyleSheet } from 'react-native'
import type { Asset, AssetId } from '@revenge-mod/assets/types'
import type { Metro } from '@revenge-mod/modules/types'
const { Image, StyleSheet } = ReactNative
const { AlertModal, AlertActionButton, TableRow, TableRowGroup, Text } = Design
const Displayable = new Set(['png', 'jpg', 'svg', 'webp'])
@ -27,10 +28,11 @@ const UndisplayableFallback = {
}
export default function AssetBrowserSettingScreen() {
const [search, setSearch] = React.useState('')
const assets = React.useMemo(() => [...getAssets()], [getAssets])
const [search, setSearch] = useState('')
const debouncedSetSearch = useCallback(debounce(setSearch, 100), [])
const assets = useMemo(() => [...getAssets()], [])
const filteredAssets = React.useMemo(
const filteredAssets = useMemo(
() =>
!search.length
? assets
@ -42,7 +44,10 @@ export default function AssetBrowserSettingScreen() {
return (
<Page spacing={16}>
<SearchInput onChange={(v: string) => setSearch(v)} size="md" />
<SearchInput
onChange={(v: string) => debouncedSetSearch(v)}
size="md"
/>
<FlashList.FlashList
data={filteredAssets}
contentContainerStyle={styles.listContainer}

View file

@ -1,8 +1,8 @@
import { ToastActionCreators } from '@revenge-mod/discord/actions'
import { Design } from '@revenge-mod/discord/design'
import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/renderer'
import { ReactNative } from '@revenge-mod/react'
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
import { View } from 'react-native'
import { api } from '..'
import { MobileSetting } from '../constants'
import { DevToolsContext, useIsDevToolsOpen } from '../react-devtools'
@ -49,7 +49,7 @@ export function ReactDevToolsAddressSetting() {
const settings = api.storage.use(s => s.devtools?.address)
return (
<ReactNative.View style={{ paddingHorizontal: 16, paddingTop: 16 }}>
<View style={{ paddingHorizontal: 16, paddingTop: 16 }}>
<Design.TextInput
defaultValue={
settings?.devtools.address ?? DevToolsContext.address
@ -76,6 +76,6 @@ export function ReactDevToolsAddressSetting() {
onChange={text => (DevToolsContext.address = text)}
returnKeyType="done"
/>
</ReactNative.View>
</View>
)
}

View file

@ -1,4 +1,4 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'

View file

@ -1,10 +1,10 @@
import { ReactNative } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { Platform } from 'react-native'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const { major, minor, patch, prerelease } =
ReactNative.Platform.constants.reactNativeVersion
Platform.constants.reactNativeVersion
const version = `${major}.${minor}.${patch}${prerelease ? `-${prerelease}` : ''}`
const ReactNativeVersionSetting: SettingsItem = {

View file

@ -1,5 +1,5 @@
import { React } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { version } from 'react'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
@ -7,7 +7,7 @@ const ReactVersionSetting: SettingsItem = {
parent: MobileSetting.REVENGE,
IconComponent: () => <TableRowAssetIcon name="ScienceIcon" />,
title: () => 'React',
useDescription: () => React.version,
useDescription: () => version,
type: 'static',
}

View file

@ -1,5 +1,5 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { BundleUpdaterManager } from '@revenge-mod/discord/native'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'

View file

@ -1,5 +1,5 @@
import { ReactNative } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { Linking } from 'react-native'
import { DiscordServerURL } from '~/constants'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
@ -8,7 +8,7 @@ const RevengeDiscordServerSetting: SettingsItem = {
parent: MobileSetting.REVENGE,
IconComponent: () => <TableRowAssetIcon name="Discord" />,
title: () => 'Discord Server',
onPress: () => ReactNative.Linking.openURL(DiscordServerURL),
onPress: () => Linking.openURL(DiscordServerURL),
type: 'pressable',
}

View file

@ -1,4 +1,4 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { MobileSetting, UserSettingsSections } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'

View file

@ -1,5 +1,5 @@
import { ReactNative } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { Linking } from 'react-native'
import { GitHubOrganizationURL } from '~/constants'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
@ -10,7 +10,7 @@ const RevengeGitHubOrganizationSetting: SettingsItem = {
<TableRowAssetIcon name="img_account_sync_github_white" />
),
title: () => 'GitHub',
onPress: () => ReactNative.Linking.openURL(GitHubOrganizationURL),
onPress: () => Linking.openURL(GitHubOrganizationURL),
type: 'pressable',
}

View file

@ -1,4 +1,4 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const RevengeNotImplementedSetting: SettingsItem = {

View file

@ -1,6 +1,6 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { _plugins } from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting, UserSettingsSections } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'

View file

@ -1,4 +1,4 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import RevengeIcon from '~assets/RevengeIcon'
import { MobileSetting, UserSettingsSections } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'

View file

@ -1,4 +1,4 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { MobileSetting, UserSettingsSections } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'

View file

@ -1,4 +1,4 @@
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import RevengeIcon from '~assets/RevengeIcon'
import { MobileSetting } from '../constants'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'

View file

@ -1,4 +1,4 @@
import { React } from '@revenge-mod/react'
import { useEffect } from 'react'
import type { StackScreenProps } from '@react-navigation/stack'
import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation'
import type { MobileSetting, UserSettingsSections } from '../constants'
@ -12,7 +12,8 @@ export default function RevengeCustomPageScreen({
>) {
const { render: PageComponent, options } = route.params
React.useEffect(() => {
// biome-ignore lint/correctness/useExhaustiveDependencies: We only want to set options once
useEffect(() => {
if (options) navigation.setOptions(options)
}, [])

View file

@ -1,4 +1,7 @@
import { getAssetIdByName } from '@revenge-mod/assets'
import FormSwitch from '@revenge-mod/components/FormSwitch'
import Page from '@revenge-mod/components/Page'
import SearchInput from '@revenge-mod/components/SearchInput'
import { Tokens } from '@revenge-mod/discord/common'
import { Design } from '@revenge-mod/discord/design'
import { FlashList } from '@revenge-mod/externals/shopify'
@ -12,31 +15,30 @@ import {
startPlugin,
} from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
import { React, ReactNative } from '@revenge-mod/react'
import { debounce } from '@revenge-mod/utils/callbacks'
import { useReRender } from '@revenge-mod/utils/react'
import FormSwitch from '~/components/FormSwitch'
import Page from '~/components/Page'
import SearchInput from '~/components/SearchInput'
import { useCallback, useMemo, useState } from 'react'
import { Image, useWindowDimensions } from 'react-native'
import type { InternalPlugin } from '@revenge-mod/plugins/_'
const { Image, useWindowDimensions } = ReactNative
const { Card, Text, Stack } = Design
export default function RevengePluginsSettingScreen() {
const [search, setSearch] = React.useState('')
const [search, setSearch] = useState('')
const debouncedSetSearch = useCallback(debounce(setSearch, 100), [])
const { width } = useWindowDimensions()
const numColumns = Math.floor((width - 16) / 448)
const plugins = React.useMemo(
const plugins = useMemo(
() =>
[..._plugins.values()].map(
plugin => [plugin, _metas.get(plugin.manifest.id)![2]] as const,
),
[_plugins],
[],
)
const filteredPlugins = React.useMemo(
const filteredPlugins = useMemo(
() =>
plugins.filter(([plugin]) => {
const { name, description, author } = plugin.manifest
@ -52,7 +54,10 @@ export default function RevengePluginsSettingScreen() {
return (
<Page spacing={16}>
<SearchInput onChange={(v: string) => setSearch(v)} size="md" />
<SearchInput
onChange={(v: string) => debouncedSetSearch(v)}
size="md"
/>
<FlashList.MasonryFlashList
data={filteredPlugins}
estimatedItemSize={108}

5
types.d.ts vendored
View file

@ -8,6 +8,11 @@ declare global {
const __BUILD_FLAG_LOG_PROMISE_REJECTIONS__: boolean
// CommonJS modules, we don't want to depend on @types/node
const module: {
exports: any
}
export interface ImportMeta {
glob<T = any>(
pattern: ImportMetaGlobPattern,