forked from nightcord/mac
214 lines
9.1 KiB
Plaintext
214 lines
9.1 KiB
Plaintext
# Equicord Development Guidelines
|
|
|
|
## Critical Rules
|
|
|
|
- **NEVER** edit existing license headers. New files use 2026.
|
|
- Push back when the user idea is wrong or not correct.
|
|
- **NEVER** git checkout files without explicit permission.
|
|
- Always verify components/functions arent deprecated before using.
|
|
- **NEVER** use `any` for Discord objects. Import proper types from `@vencord/discord-types`.
|
|
- Fix with smallest possible change. No refactoring unless explicitly asked.
|
|
- Solve the problem at hand only. No patterns for patterns sake.
|
|
- **NEVER** overengineer. No premature abstractions, no helpers for one-time operations, no building for hypothetical futures. Three similar lines is better than a premature abstraction.
|
|
- Write modern, robust, professional code. Use current language features, proper error handling, and clean structure.
|
|
- Write natural human text in errors, descriptions, messages. No dashes or robotic formatting. Write like "Module not found" not "Module - not found". Keep it simple and conversational.
|
|
|
|
## Code Rules
|
|
|
|
- **DELETE** dead code, dont comment it. Git preserves history.
|
|
- No hardcoded values, unused imports, magic numbers.
|
|
- Import `classNameFactory` from `@utils/css` (NOT `@api/Styles` which is deprecated). **ALWAYS** use `cl()` for class names, never hardcode strings like `"vc-plugin-name-class"`.
|
|
- When combining multiple class names use `classes()` from `@utils/misc`, not template strings like `` `${a} ${b}` ``.
|
|
- Use utilities from `@utils/`, `@api/`, `@components/`.
|
|
- Use `Logger` from `@utils/Logger`, not `console.log`.
|
|
- Descriptions: capital first letter, end with period.
|
|
- Less code with same functionality is **ALWAYS** better. If it can be done in fewer lines without losing clarity, do it.
|
|
- Keep logic flat. Avoid deep nesting, prefer early returns, guard clauses, and simple control flow.
|
|
- **NEVER** add comments unless explicitly asked.
|
|
- Keep existing comments, they contain important context.
|
|
- Use `Map`/`Set`, `.some`/`.find`, no spread in loops, `Promise.all`.
|
|
- KISS over clever, flat over nested, explicit over implicit.
|
|
|
|
## TypeScript
|
|
|
|
- **Prefer:** optional chaining (`?.`), nullish coalescing (`??`), `const`, arrow functions
|
|
- **Use:** destructuring, template literals, object shorthand, array methods
|
|
- **Style:** early returns, trust inference, inline single-use variables
|
|
|
|
## React
|
|
|
|
- Wrap complex components with `ErrorBoundary.wrap(Component, { noop: true })`.
|
|
- Return `null` for conditional rendering, not `undefined`.
|
|
- **Forbidden:** `React.cloneElement`, `React.isValidElement`, `React.memo()`, `React.lazy`, `React.Children`
|
|
- Always return cleanup functions in `useEffect`.
|
|
|
|
## Forbidden
|
|
|
|
### Code
|
|
|
|
- **No raw DOM manipulation.** No `document.querySelector`, no `MutationObserver`, no `element.style`. Always use webpack patches and React.
|
|
- Empty catch blocks.
|
|
- Hardcoded minified vars in patches (`e,t,n`).
|
|
- **NEVER** use `settings.use()` with arrays. Mutate then reassign.
|
|
|
|
### Plugins
|
|
|
|
- **No selfbot or API abuse.** Nothing that automates user actions, spoofs client state, or abuses Discord's API in any way.
|
|
- No plugins that only do something trivially achievable with existing built-in plugins or Discord features.
|
|
- No CSS-only plugins that only hide or redesign UI elements.
|
|
- No plugins targeting specific third-party bots (official Discord apps are fine).
|
|
- No untrusted third-party APIs. No plugins requiring users to supply their own API keys.
|
|
- Do not introduce new dependencies unless strictly necessary and well justified.
|
|
|
|
## Anti-Patterns
|
|
|
|
- `value !== null && value !== undefined` ⟹ `value != null`
|
|
- `array && array.length > 0` ⟹ `array.length` (only when array is guaranteed to exist)
|
|
- `settings?.store?.value` ⟹ `settings.store.value`
|
|
- `value || defaultValue` ⟹ `value ?? defaultValue`
|
|
|
|
## Patching
|
|
|
|
### Core Principles
|
|
|
|
- Minimum code touched. Stability over cleverness. One patch per concern.
|
|
- Try multiple approaches, keep simplifying until you find the cleanest solution.
|
|
- Remove any regex character that doesnt change the match result.
|
|
- Not overengineered, not overlooked, just clean and future-proof.
|
|
|
|
### Find Strings
|
|
|
|
- Use `#{intl::KEY}` in find when possible, most stable anchor.
|
|
- If find matches multiple modules, add nearby stable string like `"),icon:"` or function name.
|
|
- `"#{intl::PIN_MESSAGE}),icon:"` is better than generic strings.
|
|
|
|
### Match Patterns
|
|
|
|
- Match only what you need to replace, let find do the targeting.
|
|
- Can use `#{intl::KEY}` in match too, gets canonicalized to hash.
|
|
- `/#{intl::KEY}\)/` beats `/label:\i\.pinned\?.{0,60}#{intl::KEY}\)/`
|
|
- `$&` keeps original, append/prepend to it.
|
|
- Use bounded gaps: `.{0,50}` not `.+?` or `.*?`
|
|
- Only use capture groups if reusing in replace.
|
|
|
|
### NEVER Do
|
|
|
|
- **Hardcoded minified vars:** `e,t,n,r,i,o,s,l,c,u,$_,xx,eD,eH,eW` — use `\i` instead
|
|
- **Minified chains:** `\i\.\i` alone — surround with stable strings
|
|
- **Unbounded gaps:** `.+?` or `.*?` — use `.{0,N}`
|
|
- **Generic patterns:** `/className:\i/` alone — add anchor
|
|
- **Raw intl hashes:** `.aA4Vce` — use `#{intl::KEY_NAME}`
|
|
|
|
### Patch Examples
|
|
|
|
**Clean replacement:**
|
|
|
|
```js
|
|
find: "#{intl::PIN_MESSAGE}),icon:"
|
|
match: /#{intl::PIN_MESSAGE}\)/
|
|
replace: "$self.getPinLabel(arguments[0]))"
|
|
```
|
|
|
|
**Appending:**
|
|
|
|
```js
|
|
find: "#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}"
|
|
match: /#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}.{0,100}(?=])/
|
|
replace: "$&,$self.renderTooltip(arguments[0].guild)"
|
|
```
|
|
|
|
**Wrapping:**
|
|
|
|
```js
|
|
find: "#{intl::SEVERAL_USERS_TYPING_STRONG}"
|
|
match: /(?<="aria-atomic":!0,children:)\i/
|
|
replace: "$self.renderTypingUsers({ children: $& })"
|
|
```
|
|
|
|
## Plugin Interop
|
|
|
|
- `isPluginEnabled(name)` from `@api/PluginManager` takes a string, checks required/isDependency/enabled.
|
|
- Import the plugin directly from its path to access `.name` and functions.
|
|
- Dont use `Vencord.Plugins.plugins` or `plugin.started`. Dont use `"as unknown as"` casting.
|
|
|
|
```ts
|
|
import { isPluginEnabled } from "@api/PluginManager";
|
|
import otherPlugin from "@equicordplugins/otherPlugin";
|
|
if (!isPluginEnabled(otherPlugin.name)) return null;
|
|
otherPlugin.someFunction();
|
|
```
|
|
|
|
## Reference
|
|
|
|
### Types
|
|
|
|
`Channel`, `Guild`, `GuildMember`, `User`, `Role`, `Message` from `@vencord/discord-types`
|
|
|
|
### Components
|
|
|
|
`Paragraph`, `BaseText`, `Flex`, `Button`, `ErrorCard` from `@components/`
|
|
|
|
### Settings
|
|
|
|
Use `definePluginSettings` from `@api/Settings`, not inline settings object.
|
|
|
|
### Utilities
|
|
|
|
- **@utils/clipboard** — `copyToClipboard`
|
|
- **@utils/discord** — `insertTextIntoChatInputBox`, `getCurrentChannel`, `getCurrentGuild`, `getIntlMessage`, `openUserProfile`, `openPrivateChannel`, `sendMessage`, `copyWithToast`, `getUniqueUsername`, `fetchUserProfile`
|
|
- **@utils/css** — `classNameFactory`, `classNameToSelector`
|
|
- **@utils/misc** — `classes`, `sleep`, `isObject`, `isObjectEmpty`, `pluralise`, `parseUrl`, `identity`
|
|
- **@utils/text** — `formatDuration`, `formatDurationMs`, `humanFriendlyJoin`, `makeCodeblock`, `toInlineCode`, `escapeRegExp`
|
|
- **@utils/modal** — `openModal`, `closeModal`, `ModalRoot`, `ModalHeader`, `ModalContent`, `ModalCloseButton`
|
|
- **@utils/margins** — `Margins` (`Margins.top8`, `Margins.bottom16`, etc.)
|
|
- **@utils/guards** — `isTruthy`, `isNonNullish`
|
|
- **@utils/web** — `saveFile`, `chooseFile`
|
|
|
|
### Webpack Common
|
|
|
|
#### IconUtils
|
|
|
|
From `@webpack/common`. **NEVER** hardcode `cdn.discordapp.com` URLs.
|
|
|
|
- `IconUtils.getUserAvatarURL(user, canAnimate?, size?)`
|
|
- `IconUtils.getDefaultAvatarURL(id)`
|
|
- `IconUtils.getUserBannerURL({ id, banner, canAnimate?, size })`
|
|
- `IconUtils.getGuildIconURL({ id, icon, size?, canAnimate? })`
|
|
- `IconUtils.getGuildBannerURL(guild, canAnimate?)`
|
|
- `IconUtils.getChannelIconURL({ id, icon })`
|
|
- `IconUtils.getEmojiURL({ id, animated, size })`
|
|
- `IconUtils.getApplicationIconURL(data)`
|
|
- `IconUtils.getGameAssetURL(data)`
|
|
|
|
#### Stores
|
|
|
|
`UserStore`, `GuildStore`, `ChannelStore`, `GuildMemberStore`, `SelectedChannelStore`, `SelectedGuildStore`, `PresenceStore`, `RelationshipStore`, `MessageStore`, `EmojiStore`, `ThemeStore`, `PermissionStore`, `VoiceStateStore` from `@webpack/common`
|
|
|
|
#### Actions
|
|
|
|
`RestAPI`, `FluxDispatcher`, `MessageActions`, `NavigationRouter`, `ChannelRouter`, `ChannelActionCreators`, `SettingsRouter` from `@webpack/common`
|
|
|
|
#### Utils
|
|
|
|
`Constants` (`Constants.Endpoints`), `SnowflakeUtils`, `Parser`, `PermissionsBits`, `moment`, `lodash`, `ColorUtils`, `ImageUtils`, `DateUtils`, `UsernameUtils`, `DisplayProfileUtils`, `URLUtils`, `Humanize`, `EmojiUtils` from `@webpack/common`
|
|
|
|
#### Components
|
|
|
|
`Tooltip`, `TextInput`, `TextArea`, `Select`, `Slider`, `Avatar`, `Menu`, `Popout`, `ScrollerThin`, `Timestamp` from `@webpack/common`
|
|
|
|
#### Toasts
|
|
|
|
`Toasts`, `showToast` from `@webpack/common`
|
|
|
|
### Imports
|
|
|
|
- **Lazy** from `@webpack` — `findByPropsLazy`, `findStoreLazy`, `findComponentByCodeLazy`, `findExportedComponentLazy`
|
|
- **Common** from `@webpack/common` — `useState`, `useEffect`, `useCallback`, `useStateFromStores`
|
|
|
|
### Never Hardcode
|
|
|
|
- `"cdn.discordapp.com/avatars/"` etc. ⟹ `IconUtils`
|
|
- `"/api/v9"`, `"/users/@me"` ⟹ `Constants.Endpoints` or `RestAPI`
|
|
- `console.log/warn/error` ⟹ `Logger` from `@utils/Logger`
|
|
- `` `${a} ${b}` `` for class names ⟹ `classes(a, b)` from `@utils/misc`
|