# 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//index.tsx` or `src/equicordplugins//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()` 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--`. 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--`. 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; ``` - `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`.