add some cool stuff (#1086)

This commit is contained in:
prism 2026-05-15 20:19:13 +02:00 • committed by GitHub
parent 0a36f5cb1e
commit 8632a27f78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 346 additions and 161 deletions

33
.github/workflows/autofix.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: autofix.ci
on:
pull_request:
permissions:
contents: read
jobs:
autofix:
name: autofix
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- name: Use Node.js 24
uses: actions/setup-node@v6
with:
node-version: 24
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run lint fix
run: pnpm lint:fix
- name: Run stylelint fix
run: pnpm lint-styles:fix
- uses: autofix-ci/action@v1

341
.rules
View file

@ -1,213 +1,238 @@
# Equicord Development Guidelines
# Equicord Development Rules
## Critical Rules
## Philosophy
- **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.
- 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
- **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
- `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.
## React
## Plugin Policy
- 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`.
What does NOT get merged:
## Forbidden
- 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.
### Code
## Plugin Structure
- **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.
- 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.
### Plugins
### Declarative over imperative
- **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.
Use these plugin object fields instead of calling `add*` or `remove*` manually. `PluginManager` handles lifecycle and auto enables the matching API plugin.
## Anti-Patterns
`commands`, `contextMenus`, `flux`, `onBeforeMessageSend`, `onBeforeMessageEdit`, `onMessageClick`, `chatBarButton`, `chatBarButtonWrapper`, `messagePopoverButton`, `renderMessageAccessory`, `renderMessageDecoration`, `renderMemberListDecorator`, `renderNicknameIcon`, `renderProfileCollection`, `renderProfileSection`, `headerBarButton`, `userAreaButton`, `userProfileBadge`, `managedStyle`, `toolboxActions`, `audioProcessor`.
- `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`
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
### Core Principles
- 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.
- 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:**
### Examples
```js
// clean replacement
find: "#{intl::PIN_MESSAGE}),icon:"
match: /#{intl::PIN_MESSAGE}\)/
replace: "$self.getPinLabel(arguments[0]))"
```
**Appending:**
```js
// appending
find: "#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}"
match: /#{intl::VIEW_AS_ROLES_MENTIONS_WARNING}.{0,100}(?=])/
replace: "$&,$self.renderTooltip(arguments[0].guild)"
```
**Wrapping:**
```js
// wrapping with lookbehind
find: "#{intl::SEVERAL_USERS_TYPING_STRONG}"
match: /(?<="aria-atomic":!0,children:)\i/
replace: "$self.renderTypingUsers({ children: $& })"
```
## Plugin Interop
## Webpack access
- `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.
- 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
import { isPluginEnabled } from "@api/PluginManager";
import otherPlugin from "@equicordplugins/otherPlugin";
if (!isPluginEnabled(otherPlugin.name)) return null;
otherPlugin.someFunction();
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
### Types from `@vencord/discord-types`
`Channel`, `Guild`, `GuildMember`, `User`, `Role`, `Message`, `Command`, `FluxEvents`, store types.
`Channel`, `Guild`, `GuildMember`, `User`, `Role`, `Message` from `@vencord/discord-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`.
### Components
Deprecated: `@api/Themes`, and the `classNameFactory` re export from `@api/Styles`.
`Paragraph`, `BaseText`, `Flex`, `Button`, `ErrorCard` from `@components/`
### `@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`.
### Settings
### `@components/*`
`Paragraph`, `BaseText`, `Flex`, `Button`, `ErrorCard`, `ErrorBoundary`.
Use `definePluginSettings` from `@api/Settings`, not inline settings object.
### `@webpack/common`
### Utilities
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`.
- **@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`
React and hooks. `React`, `useState`, `useEffect`, `useLayoutEffect`, `useMemo`, `useRef`, `useReducer`, `useCallback`, `useStateFromStores`.
### Webpack Common
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`.
#### IconUtils
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`.
From `@webpack/common`. **NEVER** hardcode `cdn.discordapp.com` URLs.
Routers. `NavigationRouter`, `ChannelRouter`, `SettingsRouter`.
- `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)`
Action creators. `MessageActions`, `ChannelActionCreators`, `UserProfileActions`, `InviteActions`, `GuildActions`, `VoiceActions`, `DraftActions`, `PinActions`, `MessageCache`.
#### Stores
Menu. `Menu`, `ContextMenuApi` (`openContextMenu`, `closeContextMenu`, `openContextMenuLazy`).
`UserStore`, `GuildStore`, `ChannelStore`, `GuildMemberStore`, `SelectedChannelStore`, `SelectedGuildStore`, `PresenceStore`, `RelationshipStore`, `MessageStore`, `EmojiStore`, `ThemeStore`, `PermissionStore`, `VoiceStateStore` from `@webpack/common`
### Lazy webpack imports
From `@webpack`. `findByPropsLazy`, `findByCodeLazy`, `findComponentLazy`, `findComponentByCodeLazy`, `findExportedComponentLazy`, `findStoreLazy`, `findCssClassesLazy`, `findLazy`, `proxyLazyWebpack`, `LazyComponentWebpack`, `mapMangledModuleLazy`, `extractAndLoadChunksLazy`, `waitFor`.
#### Actions
`waitForStore` and `waitForComponent` live in `@webpack/common/internal.tsx`. They're used by `@webpack/common` itself, not directly by plugins.
`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`
### `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`.

View file

@ -33,8 +33,11 @@
"repair": "node scripts/runInstaller.mjs -- --repair",
"lint": "eslint",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint-styles:fix": "pnpm lint-styles --fix",
"lint:fix": "pnpm lint --fix",
"test": "pnpm buildStandalone && pnpm testTsc && pnpm lint:fix && pnpm lint-styles && pnpm generatePluginJson",
"lint:intl": "node scripts/lintIntl.mjs",
"lint:patches": "node scripts/lintPatches.mjs",
"test": "pnpm buildStandalone && pnpm testTsc && pnpm lint:fix && pnpm lint-styles && pnpm lint:intl && pnpm lint:patches && pnpm generatePluginJson",
"testWeb": "pnpm lint:fix && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit",
"depCheck": "node scripts/depCheck.mjs"

54
scripts/lintIntl.mjs Normal file
View file

@ -0,0 +1,54 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { join, sep } from "node:path";
const ROOT = new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1");
const VALID_MODIFIERS = new Set(["raw", "hash"]);
const MARKER_RE = /#\{intl::([\w$+/]*)(?:::(\w+))?\}/g;
const HASH_RE = /["'`]\s*\.([A-Za-z][A-Za-z0-9+/]{5})\b/g;
const tracked = execFileSync("git", ["ls-files", "src"], { cwd: ROOT, encoding: "utf8" })
.split("\n")
.filter(p => /\.(ts|tsx|js|jsx|mjs)$/.test(p))
.map(p => p.replace(/\//g, sep));
let errors = 0;
let warnings = 0;
for (const rel of tracked) {
const text = readFileSync(join(ROOT, rel), "utf8");
if (!text.includes("#{intl::") && !text.includes("find:") && !text.includes("match:")) continue;
const lines = text.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const at = `${rel.replace(/\\/g, "/")}:${i + 1}`;
if (/#\{intl::[^}]*$/.test(line)) {
console.error(`${at}: ERROR unclosed #{intl:: marker`);
errors++;
}
for (const [full, key, modifier] of line.matchAll(MARKER_RE)) {
if (!key) {
console.error(`${at}: ERROR empty intl key: ${full}`);
errors++;
}
if (modifier && !VALID_MODIFIERS.has(modifier)) {
console.error(`${at}: ERROR unknown intl modifier "${modifier}": ${full}`);
errors++;
}
}
if (/\b(find|match)\s*:/.test(line) && !line.includes("#{intl::")) {
for (const [, hash] of line.matchAll(HASH_RE)) {
if (!/[0-9+/]/.test(hash)) continue;
console.warn(`${at}: WARN baked intl hash ".${hash}", prefer #{intl::KEY}`);
warnings++;
}
}
}
}
console.log(`\nintl lint: ${errors} error(s), ${warnings} warning(s)`);
process.exit(errors > 0 ? 1 : 0);

70
scripts/lintPatches.mjs Normal file
View file

@ -0,0 +1,70 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { join, sep } from "node:path";
const ROOT = new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1");
const VERBOSE = process.env.LINT_PATCHES_VERBOSE === "1" || process.argv.includes("--verbose");
const tracked = execFileSync("git", ["ls-files", "src"], { cwd: ROOT, encoding: "utf8" })
.split("\n")
.filter(p => /^src\/(plugins|equicordplugins)\/.*\.(ts|tsx)$/.test(p))
.map(p => p.replace(/\//g, sep));
let errors = 0;
let warnings = 0;
function fail(at, rule, msg) {
console.error(`${at}: ERROR ${rule} ${msg}`);
errors++;
}
function warn(at, rule, msg) {
if (VERBOSE) console.warn(`${at}: WARN ${rule} ${msg}`);
warnings++;
}
for (const rel of tracked) {
const text = readFileSync(join(ROOT, rel), "utf8");
if (!text.includes("match:") && !text.includes("find:")) continue;
const lines = text.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const at = `${rel.replace(/\\/g, "/")}:${i + 1}`;
const matchM = line.match(/\bmatch\s*:\s*\/((?:\\.|[^/\\\n])+)\/[gimsuy]*/);
if (matchM) {
const src = matchM[1];
const unbounded = src.match(/\.[+*]\??/);
if (unbounded) warn(at, "P002", `unbounded ${unbounded[0]} in match`);
const minified = src.match(/\b[a-z]\.[a-z]\b/);
if (minified) fail(at, "P001", `hardcoded minified var "${minified[0]}", use \\i.\\i`);
}
const findM = line.match(/\bfind\s*:\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\/(?:\\.|[^/\\\n])+\/[gimsuy]*)/);
if (findM) {
const window = lines.slice(i, i + 12).join("\n");
if (/\b(replacement|match)\s*:/.test(window)) {
const v = findM[1];
if (v === '""' || v === "''" || v === "``") {
fail(at, "P003", "find is empty");
} else if (v.startsWith("/")) {
const src = v.slice(1, v.lastIndexOf("/"));
if (/\\i/.test(src) && !/[A-Za-z]{3,}/.test(src) && !src.includes("#{intl::")) {
warn(at, "P004", "find regex has no string anchor");
}
}
}
}
const replaceM = line.match(/\breplace\s*:\s*(["'`])((?:\\.|(?!\1).)*)\1/);
if (replaceM && /\btry\s*\{[\s\S]*?\}\s*catch\b/.test(replaceM[2])) {
warn(at, "P005", "replace uses try/catch, fix the match instead");
}
}
}
const tail = warnings && !VERBOSE ? " (run with --verbose to list)" : "";
console.log(`\npatch lint: ${errors} error(s), ${warnings} warning(s)${tail}`);
process.exit(errors > 0 ? 1 : 0);