- Solve problem at hand. No refactors unless asked.
- Smallest diff wins. Three similar lines beat premature helper.
- Less code with same behavior always wins. Equivalent solutions: shorter one ships. Compression is intelligence.
- Don't over-guard. Check only conditions that can actually occur. `if (user)` on non-optional store return is wrong. `try/catch` on code that can't throw is wrong. `?.` on type-guaranteed values is wrong. Dead defensive code isn't safety.
- No comments unless asked. Keep existing ones, they carry context.
- Delete dead code. Don't comment out.
- Natural human text in errors, descriptions, toasts. No dashes, no robotic phrasing. Descriptions start capitalized, end with period.
- One-file plugin default. Split only past ~250 lines or when second file is genuinely reusable (`utils.ts`, `settings.ts`, `components/`).
- No abstraction for 1-2 call sites. Inline beats single-use hook. `createX` used once is just a function call.
- Don't reimplement what `@webpack/common` or `@utils/*` exports. Search first.
- Don't cache Discord data. Stores are source of truth, read on demand. Cache only what isn't in a store.
- Don't manage state Discord manages. `useStateFromStores` over `useState` + flux that mirrors a store.
* Copyright (c) <year> Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
```
Header says `Vencord, a Discord client mod` even on new Equicord-only files. Equicord is a Vencord fork, upstream attribution is kept across the whole tree. Don't change it to "Equicord".
Older files carry the full GPL preamble. Leave existing headers alone, don't rewrite them.
- Lock environment via folder suffix. Per-setting platform gating via `target?: "WEB" | "DESKTOP" | "BOTH"` on the setting def (not the plugin). Code-path gating via build constants.
- `requiresRestart` defaults true when `patches` exist. Set only to override.
- `reporterTestable?: ReporterTestable` flags parts the auto-reporter exercises (`Start`, `Patches`, `FluxEvents`, `None`).
- `isModified?: true` marks an upstream Vencord plugin Equicord modified.
- Optional: `tags?: PluginTag[]` (closed list in `src/utils/types.ts`), `required` (forced on, hidden toggle), `enabledByDefault` (on at first run, toggleable), `hidden` (off the UI), `searchTerms?: string[]`.
- Renames at module top before `definePlugin`. Helpers: `migratePluginSettings`, `migratePluginSetting`, `migratePluginToSettings`, `migrateSettingToPlugin`, `migrateSettingsFromPlugin`, `migrateOldSettingToNewPlugin`.
- React: `settings.use([...keys])`. Hoist as `const KEYS = ["a", "b"] as const` at module scope, or memoize. Fresh array literal resubscribes every render.
- Never `settings.use()` with no arg. Subscribes to everything.
- Hot paths: destructure `settings.store.x` once. Never re-read in loops or render rows.
- `OptionType.COMPONENT` components must be module-level. Props: `setValue`, `option`, `closePluginSettings`. No `setError` exists, use `isValid` for validation.
- `settings.plain` is non-reactive snapshot. Use in non-React hot paths and event handlers.
### UX
- Defaults match the choice 80% of users want, not "off".
- SELECT with two options is a BOOLEAN. Don't dress it up.
- If explanation outgrows `description`, move it to `settingsAboutComponent`.
- Plugin-lifetime: `flux: { ACTION_TYPE(payload) { ... } }` on plugin object. `PluginManager` subscribes/unsubscribes, binds `this` to plugin, wraps handler in try/catch that logs throws and rejections. Handlers may be `async`.
- Scoped: `FluxDispatcher.subscribe` inside `useEffect` (cleanup with `unsubscribe(sameRef)`) or plugin `start()` (mirrored `unsubscribe` in `stop()`). Reuse exact function reference.
- Never both `flux` and manual `subscribe` for same event in one plugin.
- Never `try/catch` inside flux handler. Wrapper already logs. Never throw.
- Action types are string literals matching `FluxEvents`. Never import Discord's `ActionTypes` constant.
- `find` is string when possible. Anchor on stable tokens: intl keys (`#{intl::KEY}`), methods with trailing punctuation, action-type literals, distinct CSS/path literals. Prefer intl over English copy.
- `match` is always RegExp. `\i` for every minified identifier. Never letters or named patterns.
- Bound every gap with `.{0,N}?`. Never `.+?`/`.*?` unbounded. Prefer N <= 150. N > 500 needs justification. N > 1000 = wrong anchor.
- Use lookbehind/lookahead to inject without re-consuming context.
- Capture only what you reuse. Reused-only groups become `(?:...)` or move to lookarounds.
- `#{intl::KEY}` expands to `.HASH` or `["HASH"]` (when hash starts with digit or contains `+`/`/`). Never hardcode the hashed form.
- `#{intl::KEY::raw}` emits the key string unchanged. Use ONLY when `intl reverse HASH` returns nothing. Never use to hardcode a known key's hash.
- `#{intl::KEY::hash}` emits just the hash, no `.` or brackets. Use to match object-literal form (`"HASH":[...]`) in the strings-table module.
- Never inject statement where Discord expects expression.
- `group: true` for atomic multi-step rewrites. On any failure (no effect or throw), whole group rolls back.
- `predicate: () => boolean` gates on runtime values. `fromBuild`/`toBuild` gate on Discord build numbers. Both apply per patch and per replacement. Never branch inside `replace`.
- `noWarn: true` suppresses "had no effect" warning. Use only for intentionally optional replacements.
- `all: true` only when one anchor legitimately occurs across multiple modules. Intl keys resolve to one module - do NOT set `all` on intl-anchored patches.
- Keep `replace` small: wrap a single function via patch + `$self`, push logic to plugin TS. A 3-line `$self.handleClick(arguments[0])` beats a 30-line render rebuild.
- `proxyLazy` for same-tick destructuring. `proxyLazyWebpack(() => ...)` when the value depends on webpack. `makeLazy` for plain deferred values.
- `mapMangledModuleLazy(code, mapping)` to extract multiple exports from one mangled module. `extractAndLoadChunksLazy([codes], matcher?)` to force-load a lazy chunk before a `find*`.
- `@webpack/common` exports backed by `waitFor` (stores, `FluxDispatcher`, `Parser`, `Alerts`, hooks) are `undefined` until webpack ready. Reference inside functions, not at module top.
- Import hooks and `React` from `@webpack/common`. Never `import React from "react"`. Only `import type` from `"react"`.
- Forbidden: `React.FC`, `React.memo`, `React.lazy`, `React.Suspense`, `React.Children`, `React.cloneElement`, `React.isValidElement`, `React.PureComponent`. Type props with named `interface`. Use `LazyComponent` over `React.lazy`. Use `ErrorBoundary` over `React.Suspense`.
- Conditional rendering returns `null`, never `undefined`/`false`.
- Wrap every component injected by a patch with `ErrorBoundary.wrap(C, { noop: true })`. Same for any `render`/`wrapper` in declarative fields.
- `LazyComponent(() => findByCode(...))` for webpack-sourced components used before webpack is ready.
- `useStateFromStores([Store1, ...], () => derive(), deps?, areEqual?)`. List every store the selector reads. Pass `areEqual` (e.g. `shallowEqual`) when selector returns objects/arrays/new refs. Pass `deps` when selector closes over props/state. Selector must be pure and synchronous.
- Fragments: prefer `<>...</>`. `React.Fragment` only when fragment needs a `key`.
- List keys: stable identifiers only. Never `key={index}` on mutable lists.
- `ReactDOM.createPortal` from `@webpack/common`. Never `"react-dom"`.
- No `useMemo`/`useCallback` on cheap values that don't feed a dep array.
- Discord mangled classes via `findCssClassesLazy("propA", ...)`. Never hardcode any part: prefix, suffix, or substring. This includes attribute selectors: `[class*="EXAMPLE_"]`, `[class^="EXAMPLE_"]`, `[class$="_EXAMPLE"]`, `[class~="EXAMPLE"]`, `[class|="EXAMPLE"]`. Resolve names with `findCssClassesLazy` and build selectors via `classNameToSelector` from `@utils/css`.
- Static stylesheet: `import "./style.css"`. Runtime-toggleable: `import name from "./style.css?managed"` plus `enableStyle`, `disableStyle`, `toggleStyle`, `setStyleClassNames` from `@api/Styles`.
- Managed CSS variable selectors use `[--varName]`. Runtime rewrites these via `setStyleClassNames` at enable/recompile time (NOT build time). Call `setStyleClassNames(style, classNames)` before `enableStyle(style)`.
- Top-level selectors start with `.vc-<plugin>-`. Descendant element selectors like `.vc-foo button` allowed.
- Handler first arg is `IpcMainInvokeEvent`. All other args are renderer-controlled and untrusted - validate type, length, content. Never throw raw `Error` (leaks main-process paths) - return `{ success: false, error: "..." }` or scrubbed message.
- Filesystem paths: `normalize` and assert `startsWith(<allowed root>)`. Never `rm`/`readFile`/`writeFile` a renderer-supplied path without this guard.
- Every `setInterval`, `setTimeout` (> ~5s), `addEventListener`, `FluxDispatcher.subscribe`, `ResizeObserver`, `IntersectionObserver`, `AbortController`, `MessageEvents.addPreSendListener` started in `start()` dies in `stop()`. In `useEffect`, pair with cleanup return.
- `addEventListener` needs a named function reference. Inline arrow + `removeEventListener` doesn't match and leaks.
- Long-lived component timers use `useFixedTimer`/`useTimer` from `@utils/react`.
- Every `add*` in `start()` pairs with `remove*` in `stop()`.
- Never store tokens, message content, or other user data in keys readable by other plugins.
## Network
- Discord API: `RestAPI.get/post/put/del({ url: Constants.Endpoints.X })`. Never raw `fetch("/api/v9/...")`.
- Third-party: `fetch` only against endpoints allowlisted in `src/main/csp` or routed through `native.ts`. Set `AbortController`, abort in `stop()` or effect cleanup.
- Never embed user-supplied URLs without `parseUrl` validation.
## Logging
- `new Logger("PluginName")` per plugin, hoisted to module scope.
- `debug` for dev traces, `info` for lifecycle, `warn` for recoverable, `error` for unrecoverable. Never log message content, tokens, or auth headers.
- Inside callback: `findGroupChildrenByChildId(id, children)` from `@api/ContextMenu` to locate group, then `children.splice(...)` to insert. `matchSubstring: true` for dynamic ids.
- Imperative `addContextMenuPatch`/`removeContextMenuPatch` only when post-`start()` or conditional. Requires `dependencies: ["ContextMenuAPI"]`.
- Discord CDN URLs (`cdn.discordapp.com/avatars/...`, `/icons/...`, `/emojis/...`) → `IconUtils`. Third-party hosts (badge services, usrbg) fine but must be in `src/main/csp`.
- `useState` + flux subscription mirroring a store. Use `useStateFromStores`.
- Polling Discord state. Subscribe to flux events.
- `JSON.parse(JSON.stringify(x))` deep clone. Use `lodash.cloneDeep`.
- Class wrappers around a single function. Custom event bus. `Strategy`/`Factory`/`Manager`/`Provider`/`Controller` suffix for one impl. State machine for boolean. Wrapping `console.error` in your own reporter.
## Build constants
Replaced at build time. Reference only in plugin TS, never literally inside `replace`.