add some cool stuff (#1086)
This commit is contained in:
parent
0a36f5cb1e
commit
8632a27f78
5 changed files with 346 additions and 161 deletions
33
.github/workflows/autofix.yml
vendored
Normal file
33
.github/workflows/autofix.yml
vendored
Normal 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
|
||||
345
.rules
345
.rules
|
|
@ -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.
|
||||
|
||||
```ts
|
||||
import { isPluginEnabled } from "@api/PluginManager";
|
||||
import otherPlugin from "@equicordplugins/otherPlugin";
|
||||
if (!isPluginEnabled(otherPlugin.name)) return null;
|
||||
otherPlugin.someFunction();
|
||||
```
|
||||
## 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
|
||||
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`.
|
||||
|
|
|
|||
|
|
@ -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
54
scripts/lintIntl.mjs
Normal 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
70
scripts/lintPatches.mjs
Normal 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);
|
||||
Loading…
Add table
Reference in a new issue