revenge-bundle-next/lib/utils/src/discord.ts
Palm 5f5fc6f2b2
feat!: module finders refactor (#6)
- Renamed `byX` filters to `withX`.
  It is more correct. For example: You don't lookup, wait for, or get modules **BY** props. You lookup, wait for, and get modules **WITH** props.
- Renamed `*byImportedPath` to `*withImportedPath`
  For consistency with the above change.
- Renamed `getModule` to `getModules`.
  The API allows getting multiple modules and not just a single one. Types are renamed as well.
  `getModuleByImportedPath` remains the same as it only gets one single module with a unique imported path.
- `withX` filter **helpers** removed.
  They can now be replaced with the new capability-based `and` filter.
- `preferExports` and `moduleStateAware` filters removed.
  They can now be replaced with the new capability-based `and` filter.
- Composite finders renamed.
  `some` -> `or`, `every` -> `and`
- Composite finders takes a capability-based approach.
- Filter generators now need to attach runtime flag to let the system know whether the generated filter can be used to filter modules without proper exports or not.

## Misc.

- Removed useless unsub call in Profile Badges
- Use `queueMicrotask` with `asap`

# Features

## Capability-based approach to composite filters

```js
// Old
byProps(...).withDependencies(...)
// Old, same as: preferExports(byProps(...), byDependencies(...))
every(byDependencies(...), byProps(...))
// Old, ordering matters, this would fail unexpectedly!
every(byProps(...), byDependencies(...))

// New
withProps(...).and(withDependencies(...))
// New (as well), ordering doesn't matter, this has the same result as above
withDependencies(...).and(withProps(...))
```
2025-08-30 09:19:57 +07:00

156 lines
4.5 KiB
TypeScript

import { getAssetByName } from '@revenge-mod/assets'
import { TokensModuleId } from '@revenge-mod/discord/common'
import { lookupModule } from '@revenge-mod/modules/finders'
import {
createFilterGenerator,
withDependencies,
} from '@revenge-mod/modules/finders/filters'
import {
ReactJSXRuntimeModuleId,
ReactModuleId,
ReactNativeModuleId,
} from '@revenge-mod/react'
import { FilterFlag } from '#modules/src/finders/filters/constants'
import type {
Filter,
FilterGenerator,
} from '@revenge-mod/modules/finders/filters'
import type { FC } from 'react'
// [React, ReactJsxRuntime, (Tokens), (BaseIconImage), (Asset), 2]
// Asset = relativeDep(1)
const IconComponentFilter = [
ReactModuleId,
ReactJSXRuntimeModuleId,
TokensModuleId,
null,
null,
2,
]
// Generated IconComponents that have multiple assets, there is no specific length, so the filter has to be generated on-the-fly.
// [React, ReactNative, ReactJsxRuntime, (Tokens), (BaseIconImage), (...Assets), 2]
// ...Assets = relativeDep(1..n)
const MultiIconComponentFilterBase = [
ReactModuleId,
ReactNativeModuleId,
ReactJSXRuntimeModuleId,
TokensModuleId,
null,
]
export type WithGeneratedIconComponent = FilterGenerator<
<N extends string>(
name: N,
...assets: string[]
) => Filter<{ [K in N]: FC<any> }>
>
/**
* Filter by icon component name and asset names.
*
* **Make sure to set `uninitialized: true` when using this filter in `lookupModule`!**
*
* @param names The component name, then the asset names if the component has multiple assets. *
* @example
* ```ts
* const [CopyIconModule] = lookupModule(
* withGeneratedIconComponent('CopyIcon'),
* {
* uninitialized: true,
* }
* )
* if (CopyIconModule) {
* const { CopyIcon } = CopyIconModule
* // Use CopyIcon as a React component
* }
* ```
* @example
* ```ts
* const [CircleXIconModule] = lookupModule(
* withGeneratedIconComponent(
* 'CircleXIcon',
* 'CircleXIcon-secondary',
* 'CircleXIcon-primary',
* ),
* {
* uninitialized: true,
* }
* )
* ```
*/
export const withGeneratedIconComponent = createFilterGenerator<
Parameters<WithGeneratedIconComponent>
>(
(names, id, exports) => {
if (typeof exports === 'object') {
if (typeof exports[names[0]] === 'function')
return Object.keys(exports).length === 1
} else {
let filter = IconComponentFilter
if (names.length > 1) {
const mids = []
for (let i = 1; i < names.length; i++) {
const name = names[i]
const mid = getAssetByName(name)?.moduleId
// Module ID can never be zero, so if it's falsy, it means the asset was not found.
if (!mid) return false
mids.push(mid)
}
filter = [...MultiIconComponentFilterBase, ...mids, 2]
} else {
const [name] = names
const mid = getAssetByName(name)?.moduleId
// Module ID can never be zero, so if it's falsy, it means the asset was not found.
if (!mid) return false
IconComponentFilter[4] = mid
}
return withDependencies(filter)(id, exports)
}
return false
},
names => `revenge.utils.discord.generatedIconComponent(${names.join(',')})`,
FilterFlag.Dynamic,
) as WithGeneratedIconComponent
/**
* Looks up a generated icon component by its name and asset names.
*
* @param names The component name, then the asset names if the component has multiple assets.
* @returns The icon component, or `undefined` if it could not be found.
*/
export function lookupGeneratedIconComponent<N extends string>(
...names: [N, ...string[]]
) {
for (const name of names) {
let badFind = false
if (__DEV__) {
if (!getAssetByName(name)) {
badFind = true
warnUnregisteredAsset(name)
}
} else if (!getAssetByName(name)) return
if (__DEV__ && badFind) return
}
const [module] = lookupModule(withGeneratedIconComponent(...names), {
uninitialized: true,
})
return module?.[names[0]] as FC<any> | undefined
}
function warnUnregisteredAsset(name: string) {
nativeLoggingHook(
`\u001b[31mAsset "${name}" is not registered. Cannot get module ID to filter by.\u001b[0m`,
2,
)
}