- 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.
Use these plugin object fields instead of calling `add*` or `remove*` manually. `PluginManager` handles lifecycle and auto enables the matching API plugin.
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.
- 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.
- `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.
- 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.
- 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.
- 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")>;