238 lines
16 KiB
Text
238 lines
16 KiB
Text
# Equicord Development Rules
|
|
|
|
## Philosophy
|
|
|
|
- Solve the problem at hand. No refactors unless asked.
|
|
- Smallest diff that fixes it. Three similar lines beats a premature helper.
|
|
- Less code with the same behavior wins.
|
|
- Never add comments unless asked. Keep existing comments, they carry context.
|
|
- Delete dead code. Don't comment it out.
|
|
- Natural human text in errors, descriptions, toasts. No dashes, no robotic phrasing. Descriptions start capitalized and end with a period.
|
|
|
|
## TypeScript
|
|
|
|
- `strict: true`, `noImplicitAny: false`, `noUncheckedIndexedAccess: false`. Annotate params explicitly. Array index access is not auto `undefined`.
|
|
- Never `any`. Use `unknown` and narrow, or type webpack results via the `find*` generic.
|
|
- Never `as unknown as`. Write a type guard or use `satisfies`.
|
|
- Never `@ts-ignore`. Use `@ts-expect-error` with a one line reason only when unavoidable.
|
|
- Never `!` non null assertion. Narrow with `if` or early return.
|
|
- Never plain `enum`. Use `const enum` for internal closed sets. Use `as const` object plus literal union for cross boundary values.
|
|
- Never `namespace`. Modules only.
|
|
- Never relative `../../` when a `@alias` exists.
|
|
- Don't optional chain through values guaranteed to exist (`settings.store`, plugin proxies, find proxies). Optional chaining is for actually optional values.
|
|
- `import type` for type only imports.
|
|
|
|
## Plugin Policy
|
|
|
|
What does NOT get merged:
|
|
|
|
- Selfbot, automation of user actions, client state spoofing, anything abusing the API.
|
|
- Plugins requiring user supplied API keys or untrusted third party endpoints.
|
|
- Plugins targeting specific third party bots. Official Discord apps are fine.
|
|
- CSS only plugins that just hide or restyle UI.
|
|
- Functionality already trivial via built in plugins or Discord features.
|
|
- New dependencies without strong justification.
|
|
|
|
## Plugin Structure
|
|
|
|
- Folder per plugin. `src/plugins/<name>/index.tsx` or `src/equicordplugins/<name>/index.tsx`. Single file plugins still get a folder.
|
|
- Folder suffixes the build system filters on: `.desktop`, `.web`, `.dev`, `.discordDesktop`, `.vesktop`, `.equibop`.
|
|
- `export default definePlugin({ ... })`. Never named.
|
|
- Required fields: `name`, `description`, `authors`. Use `Devs` or `EquicordDevs`, never inline objects.
|
|
- List `dependencies` for every API plugin used.
|
|
- Prefer the folder suffix to lock environment. The `target?: "WEB" | "DESKTOP" | "BOTH"` field exists but is rarely used. Runtime `IS_DISCORD_DESKTOP`, `IS_WEB`, `IS_VESKTOP` (build time replaced) for partial gating inside a mixed plugin.
|
|
- `requiresRestart` defaults to true when `patches` exist. Set it only to override.
|
|
|
|
### Declarative over imperative
|
|
|
|
Use these plugin object fields instead of calling `add*` or `remove*` manually. `PluginManager` handles lifecycle and auto enables the matching API plugin.
|
|
|
|
`commands`, `contextMenus`, `flux`, `onBeforeMessageSend`, `onBeforeMessageEdit`, `onMessageClick`, `chatBarButton`, `chatBarButtonWrapper`, `messagePopoverButton`, `renderMessageAccessory`, `renderMessageDecoration`, `renderMemberListDecorator`, `renderNicknameIcon`, `renderProfileCollection`, `renderProfileSection`, `headerBarButton`, `userAreaButton`, `userProfileBadge`, `managedStyle`, `toolboxActions`, `audioProcessor`.
|
|
|
|
Wrapper style fields take `{ render | wrapper, priority }`, never a bare function.
|
|
|
|
Imperative `add*` and `remove*` is only valid when registering multiple items of the same kind, registering conditionally, or registering after `start()`. Always pair with removal in `stop()`. Never mix the two for the same item.
|
|
|
|
### Lifecycle
|
|
|
|
- `start()` runs at `StartAt.WebpackReady` by default.
|
|
- `stop()` must reverse everything `start()` did.
|
|
- Never throw from `start` or `stop`.
|
|
- Plugin methods called via `$self` must be on the plugin object. Closures over module locals aren't callable.
|
|
- Never `Vencord.Plugins.plugins["Name"]`. Import the plugin directly and use `isPluginEnabled(name)` from `@api/PluginManager`.
|
|
|
|
## Settings
|
|
|
|
- `definePluginSettings({...})` from `@api/Settings`. Assign to `settings`.
|
|
- Every non SELECT setting needs `default`. SELECT puts `default: true` on one option.
|
|
- `restartNeeded: true` only when the value feeds a patch replacement or eager load decision.
|
|
- `hidden` and `disabled` can be functions for conditional UI. Never delete keys to "remove" them. Write a migration.
|
|
- `withPrivateSettings<T>()` for internal state. Never widen the global `Settings` interface.
|
|
- Renames go at module top before `definePlugin`. Use `migratePluginSettings("New", "Old", ...)` and `migratePluginSetting(...)`.
|
|
|
|
### Reading
|
|
|
|
- React: `settings.use([...specificKeys])`. Hoist the keys array to module scope or memoize it. A fresh array literal resubscribes every render.
|
|
- Never `settings.use()` with no argument inside list rendered components.
|
|
- Hot paths: destructure `settings.store.x` once. Never re read in loops or render rows.
|
|
- `OptionType.COMPONENT` components must be module level. Use the `setValue` and `setError` props. Never write `settings.store.x` directly inside them.
|
|
|
|
## Flux
|
|
|
|
- Plugin lifetime subscriptions: `flux: { ACTION_TYPE(payload) { ... } }` on the plugin object.
|
|
- Component scoped or one shot: `FluxDispatcher.subscribe` inside `useEffect`, paired with `unsubscribe(sameRef)` in cleanup.
|
|
- Never declare both `flux` and manual `subscribe` for the same event in one plugin.
|
|
|
|
## Patching
|
|
|
|
- One patch per concern. Minimum code touched.
|
|
- `find` is a string when possible. Anchor on stable tokens: intl keys (`#{intl::KEY}`), method names with trailing punctuation, action type literals, distinct CSS or path literals.
|
|
- Prefer intl key anchors over English copy.
|
|
- `match` is always a RegExp. Use `\i` for every minified identifier. Never letters or named patterns.
|
|
- Bound every gap with `.{0,N}?`. Never `.+?` or `.*?` unbounded. Cap at the smallest N that works.
|
|
- Use lookbehind and lookahead to inject without re consuming context.
|
|
- Capture only what you reuse. Convert reused only groups to `(?:...)` or move to lookarounds.
|
|
- `#{intl::KEY}` in match. The build hashes it. Never hardcode hashed forms like `.aA4Vce`.
|
|
- `/g` only when intentionally hitting multiple sites in one module.
|
|
- Replace: `$&` keeps the match, `$1` and `$2` for captures. Call into plugin code via `$self.fn(...)`.
|
|
- Never inject a statement where Discord expects an expression.
|
|
- `group: true` when multiple replacements in one patch must land together.
|
|
- Scope conditional patches with `predicate`, `fromBuild`, `toBuild`. Never branch inside `replace`.
|
|
- `all: true` for intl patches that may match across multiple bundles (English fallback plus user locale).
|
|
- Never `try/catch` to silence a failing replacement. Fix the match.
|
|
- Never depend on argument names or object key order. Use `arguments[i]`.
|
|
- Prefer multiple small patches with tight `find`s over one patch with a broad find and many replacements.
|
|
|
|
### Examples
|
|
|
|
```js
|
|
// clean replacement
|
|
find: "#{intl::PIN_MESSAGE}),icon:"
|
|
match: /#{intl::PIN_MESSAGE}\)/
|
|
replace: "$self.getPinLabel(arguments[0]))"
|
|
|
|
// appending
|
|
find: "#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}"
|
|
match: /#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}.{0,100}(?=])/
|
|
replace: "$&,$self.renderTooltip(arguments[0].guild)"
|
|
|
|
// wrapping with lookbehind
|
|
find: "#{intl::SEVERAL_USERS_TYPING_STRONG}"
|
|
match: /(?<="aria-atomic":!0,children:)\i/
|
|
replace: "$self.renderTypingUsers({ children: $& })"
|
|
```
|
|
|
|
## Webpack access
|
|
|
|
- Top level: lazy finders only. `findByPropsLazy`, `findByCodeLazy`, `findComponentByCodeLazy`, `findStoreLazy`, `findCssClassesLazy`, `getUserSettingLazy`, `proxyLazyWebpack`.
|
|
- Direct `find(...)` only inside `start()` or later runtime callbacks.
|
|
- Use `findComponentByCode` or `waitForComponent` for Discord components, not `findByProps`. Survives bundle reshuffles.
|
|
- `proxyLazy` for values needing same tick destructuring. `makeLazy` for plain deferred values.
|
|
- `@webpack/common` exports backed by `waitFor` (stores, `FluxDispatcher`, `Parser`, `Alerts`, hooks) are `undefined` until webpack is ready. Reference inside functions, not at module top.
|
|
- No `eagerPatches` or `wreq(id)` brute forcing.
|
|
|
|
## React
|
|
|
|
- Import hooks and `React` from `@webpack/common`. Never `import React from "react"`. Only `import type` from `"react"` (`ReactNode`, `ComponentType`, etc.).
|
|
- Forbidden: `React.FC`, `React.memo`, `React.lazy`, `React.Children`, `React.cloneElement`, `React.isValidElement`. Type props with a named `interface`.
|
|
- Conditional rendering returns `null`, never `undefined` or `false`.
|
|
- Wrap every component injected by a patch with `ErrorBoundary.wrap(C, { noop: true })`. Same for components exported as plugin surface.
|
|
- `LazyComponent(() => findByCode(...))` for webpack sourced components used before webpack is ready.
|
|
- `useStateFromStores([Store1, ...], () => derive(), deps?, areEqual?)`. List every store the selector reads. Pass `areEqual` when the selector returns objects or arrays. Selector must be pure and synchronous.
|
|
|
|
## CSS / Classes
|
|
|
|
- `classNameFactory` from `@utils/css`. Never from `@api/Styles`. That re export is deprecated, kept for Vesktop backcompat.
|
|
- Prefix is `vc-<plugin>-`. Usage: `cl("base", ["a", "b"], { active: cond })`.
|
|
- Combine classes with `classes(a, b)` from `@utils/misc`. Never template strings, never `+`.
|
|
- Discord mangled classes via `findCssClassesLazy("propA", ...)`. Never hardcode mangled suffixes.
|
|
- Static stylesheet: `import "./style.css"`. Runtime toggleable: `import name from "./style.css?managed"` plus `enableStyle`, `disableStyle`, `toggleStyle`, `setStyleClassNames` from `@api/Styles`.
|
|
- CSS variable selectors in managed CSS use `[--varName]`. The build rewrites these via the `setStyleClassNames` map. Never hardcode mangled selectors in CSS.
|
|
- Every selector in plugin CSS starts with `.vc-<plugin>-`. No bare element selectors, no `:root`, no `@import`.
|
|
- Prefer Discord CSS vars like `var(--background-primary)` over literal colors.
|
|
- Inline `style={{}}` only for dynamic per render values.
|
|
- No `!important` except to override a Discord rule unbeatable by specificity.
|
|
|
|
## Native / Desktop
|
|
|
|
- Desktop only plugin: folder suffix `.desktop`. Mixed target plugins with a `native.ts` do not use the suffix.
|
|
- `native.ts` next to `index.tsx`. Exports auto register as IPC handlers via `~pluginNatives`.
|
|
- Handler first arg is `IpcMainInvokeEvent`. Renderer side:
|
|
```ts
|
|
const Native = VencordNative.pluginHelpers.MyPlugin as PluginNative<typeof import("./native")>;
|
|
```
|
|
- `pluginHelpers` key matches `definePlugin.name`.
|
|
- Renderer must never import `fs`, `path`, `net`, `https`, `child_process`, `electron`. Go through `native.ts` and validate input there.
|
|
- File pickers: `saveFile` and `chooseFile` from `@utils/web`. Cross platform, no IPC needed.
|
|
- Only `src/preload.ts` may `contextBridge.exposeInMainWorld`.
|
|
|
|
## Never hardcode
|
|
|
|
- CDN URLs (`cdn.discordapp.com/...`) → `IconUtils`.
|
|
- API paths (`/api/v9`, `/users/@me`) → `Constants.Endpoints` or `RestAPI`.
|
|
- Intl strings → `#{intl::KEY}`.
|
|
- Mangled CSS classes → `findCssClassesLazy`.
|
|
- Minified vars in patches (`e,t,n,r,i,o,s,...`) → `\i`.
|
|
- `console.log`, `warn`, `error` → `new Logger("name")` from `@utils/Logger`.
|
|
- Toast type strings → `Toasts.Type.*`.
|
|
- Class name concatenation → `classes(...)`.
|
|
|
|
## Forbidden
|
|
|
|
- Raw DOM. No `document.querySelector`, `MutationObserver`, `element.style`. Use webpack patches and React.
|
|
- Empty `catch` blocks.
|
|
- `settings.use()` with arrays mutated in place. Reassign with a new array.
|
|
|
|
## Reference
|
|
|
|
### Types from `@vencord/discord-types`
|
|
`Channel`, `Guild`, `GuildMember`, `User`, `Role`, `Message`, `Command`, `FluxEvents`, store types.
|
|
|
|
### `@api/*`
|
|
`Settings`, `Commands`, `DataStore`, `ContextMenu`, `Badges`, `Notices`, `Notifications`, `MessageEvents`, `MessageUpdater`, `MessageAccessories`, `MessageDecorations`, `MessagePopover`, `MemberListDecorators`, `ServerList`, `ChatButtons`, `HeaderBar`, `UserArea`, `NicknameIcons`, `ProfileCollections`, `ProfileSections`, `AudioPlayer`, `UserSettings`, `Styles`, `PluginManager`.
|
|
|
|
Deprecated: `@api/Themes`, and the `classNameFactory` re export from `@api/Styles`.
|
|
|
|
### `@utils/*`
|
|
- `Logger`. `new Logger(name)`.
|
|
- `css`. `classNameFactory`, `classNameToSelector`.
|
|
- `misc`. `classes`, `sleep`, `isObject`, `isObjectEmpty`, `pluralise`, `parseUrl`, `identity`, `interpolateIfDefined`, `tryOrElse`, `isPluginDev`, `removeFromArray`.
|
|
- `text`. `formatDuration`, `formatDurationMs`, `humanFriendlyJoin`, `makeCodeblock`, `toInlineCode`, `escapeRegExp`, `stripIndent`, `ZWSP`.
|
|
- `react`. `useAwaiter`, `useForceUpdater`, `useIntersection`, `useTimer`, `useFixedTimer`, `useCleanupEffect`, `NoopComponent`.
|
|
- `lazy`. `makeLazy`, `proxyLazy`.
|
|
- `lazyReact`. `LazyComponent`.
|
|
- `modal`. `openModal`, `openModalLazy`, `closeModal`, `closeAllModals`, `openMediaModal`, `ModalRoot`, `ModalHeader`, `ModalContent`, `ModalFooter`, `ModalCloseButton`, `ModalSize`.
|
|
- `discord`. `getCurrentChannel`, `getCurrentGuild`, `getIntlMessage`, `openUserProfile`, `openPrivateChannel`, `sendMessage`, `insertTextIntoChatInputBox`, `copyWithToast`, `getUniqueUsername`, `fetchUserProfile`, `openImageModal`, `openInviteModal`, `getTheme`, `Theme`, `hasGuildFeature`.
|
|
- `clipboard`. `copyToClipboard`, `readClipboard`.
|
|
- `margins`. `Margins`.
|
|
- `guards`. `isTruthy`, `isNonNullish`.
|
|
- `web`. `saveFile`, `chooseFile`.
|
|
- `constants`. `Devs`, `EquicordDevs`, `IS_WINDOWS`, `IS_MAC`, `IS_LINUX`, `IS_MOBILE`.
|
|
- `types`. `OptionType`, `StartAt`, `ReporterTestable`, `definePlugin`.
|
|
|
|
### `@components/*`
|
|
`Paragraph`, `BaseText`, `Flex`, `Button`, `ErrorCard`, `ErrorBoundary`.
|
|
|
|
### `@webpack/common`
|
|
|
|
Stores. `UserStore`, `GuildStore`, `GuildRoleStore`, `GuildMemberStore`, `GuildChannelStore`, `ChannelStore`, `SelectedChannelStore`, `SelectedGuildStore`, `MessageStore`, `PresenceStore`, `RelationshipStore`, `PermissionStore`, `ReadStateStore`, `EmojiStore`, `StickersStore`, `ThemeStore`, `VoiceStateStore`, `TypingStore`, `DraftStore`, `WindowStore`, `AccessibilityStore`, `PendingReplyStore`, `UserProfileStore`, `StreamerModeStore`, `SpotifyStore`, `NotificationSettingsStore`, `LocaleStore`, `ExperimentStore`, `QuestStore`. Full list in `src/webpack/common/stores.ts`.
|
|
|
|
React and hooks. `React`, `useState`, `useEffect`, `useLayoutEffect`, `useMemo`, `useRef`, `useReducer`, `useCallback`, `useStateFromStores`.
|
|
|
|
Components. `Tooltip`, `TextInput`, `TextArea`, `Select`, `SearchableSelect`, `Slider`, `Checkbox`, `Avatar`, `Menu`, `Popout`, `Dialog`, `TabBar`, `Clickable`, `MaskedLink`, `Timestamp`, `FocusLock`, `UserSummaryItem`, `ColorPicker`, `OAuth2AuthorizeModal`, `ScrollerThin`, `ScrollerAuto`, `ScrollerNone`, `ListScrollerThin`, `ListScrollerAuto`.
|
|
|
|
Utils. `FluxDispatcher`, `RestAPI`, `Constants`, `SnowflakeUtils`, `Parser`, `PermissionsBits`, `Alerts`, `Toasts`, `showToast`, `moment`, `lodash`, `i18n`, `IconUtils`, `ColorUtils`, `ImageUtils`, `DateUtils`, `UsernameUtils`, `DisplayProfileUtils`, `URLUtils`, `Humanize`, `EmojiUtils`, `UserUtils`, `UploadHandler`, `UploadManager`, `ComponentDispatch`, `ExpressionPickerStore`, `ReadStateUtils`, `PopoutActions`, `ApplicationAssetUtils`.
|
|
|
|
Routers. `NavigationRouter`, `ChannelRouter`, `SettingsRouter`.
|
|
|
|
Action creators. `MessageActions`, `ChannelActionCreators`, `UserProfileActions`, `InviteActions`, `GuildActions`, `VoiceActions`, `DraftActions`, `PinActions`, `MessageCache`.
|
|
|
|
Menu. `Menu`, `ContextMenuApi` (`openContextMenu`, `closeContextMenu`, `openContextMenuLazy`).
|
|
|
|
### Lazy webpack imports
|
|
From `@webpack`. `findByPropsLazy`, `findByCodeLazy`, `findComponentLazy`, `findComponentByCodeLazy`, `findExportedComponentLazy`, `findStoreLazy`, `findCssClassesLazy`, `findLazy`, `proxyLazyWebpack`, `LazyComponentWebpack`, `mapMangledModuleLazy`, `extractAndLoadChunksLazy`, `waitFor`.
|
|
|
|
`waitForStore` and `waitForComponent` live in `@webpack/common/internal.tsx`. They're used by `@webpack/common` itself, not directly by plugins.
|
|
|
|
### `IconUtils` methods
|
|
`getUserAvatarURL(user, canAnimate?, size?, format?)`, `getUserBannerURL({ id, banner, canAnimate?, size })`, `getGuildIconURL({ id, icon, size?, canAnimate? })`, `getGuildBannerURL(guild, canAnimate?)`, `getGuildSplashURL`, `getGuildDiscoverySplashURL`, `getGuildHomeHeaderURL`, `getChannelIconURL({ id, icon })`, `getResourceChannelIconURL`, `getEmojiURL({ id, animated, size })`, `getApplicationIconURL`, `getGameAssetURL`, `getVideoFilterAssetURL`, `getAvatarDecorationURL`, `getGuildMemberAvatarURL`, `getGuildMemberAvatarURLSimple`, `getGuildMemberBannerURL`, `getDefaultAvatarURL(id)`, `hasAnimatedGuildIcon`, `isAnimatedIconHash`.
|