mirror of
https://github.com/discordjs/discord.js.git
synced 2026-05-26 13:30:08 +00:00
Compare commits
58 Commits
@discordjs
...
feat/appli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66fe4bbef6 | ||
|
|
7dff2cd9b4 | ||
|
|
7f29356950 | ||
|
|
230e746b31 | ||
|
|
0c2975e3fd | ||
|
|
fcf7f27fd7 | ||
|
|
aac247cc18 | ||
|
|
a78dd012dc | ||
|
|
af923fee8e | ||
|
|
f1087c7e87 | ||
|
|
7de5b4a349 | ||
|
|
f109fc9b42 | ||
|
|
b80dd8ce7f | ||
|
|
88778df0e5 | ||
|
|
985b525556 | ||
|
|
2c08b0f975 | ||
|
|
fbdec3d828 | ||
|
|
cf88ef91fd | ||
|
|
dee79efbaf | ||
|
|
05224e78ec | ||
|
|
612c49b546 | ||
|
|
ffbb7b6936 | ||
|
|
d251e065cd | ||
|
|
cf89260c98 | ||
|
|
2c750a4e00 | ||
|
|
6431cea24b | ||
|
|
f510b5ffab | ||
|
|
1c5674d9b2 | ||
|
|
5efde1162f | ||
|
|
9201243f32 | ||
|
|
0d76f1149f | ||
|
|
b3705df547 | ||
|
|
2d740d5279 | ||
|
|
beed098bf2 | ||
|
|
7886d9b098 | ||
|
|
5247afe983 | ||
|
|
b66f52f9aa | ||
|
|
126529f460 | ||
|
|
90aac127fa | ||
|
|
4ec03ae517 | ||
|
|
f1bcff46b6 | ||
|
|
5d5a6945e4 | ||
|
|
352c9819b6 | ||
|
|
f7c77a73de | ||
|
|
ddf9f818e8 | ||
|
|
8ca279e0c3 | ||
|
|
5a656b849f | ||
|
|
bb67f3c154 | ||
|
|
90813b33aa | ||
|
|
cc43dadcae | ||
|
|
cde757b7cb | ||
|
|
368edeaaff | ||
|
|
d548a5911e | ||
|
|
50018979ed | ||
|
|
55ab46dbc3 | ||
|
|
4b6060dcd8 | ||
|
|
b1d96e251f | ||
|
|
7710decf6a |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -7,7 +7,7 @@ package.json @discordjs/core
|
||||
pnpm-lock.yaml @discordjs/core
|
||||
|
||||
/apps/guide/ @discordjs/website @discordjs/guide
|
||||
/apps/guide/src/content/ @discordjs/guide
|
||||
/apps/guide/content/ @discordjs/guide
|
||||
/apps/website/ @discordjs/website
|
||||
|
||||
/packages/actions/ @discordjs/actions
|
||||
|
||||
3
.github/workflows/deploy-website.yml
vendored
3
.github/workflows/deploy-website.yml
vendored
@@ -17,9 +17,10 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node.js v22
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./packages/actions/src/pnpmCache
|
||||
|
||||
4
.github/workflows/deprecate-version.yml
vendored
4
.github/workflows/deprecate-version.yml
vendored
@@ -11,6 +11,7 @@ on:
|
||||
- '@discordjs/builders'
|
||||
- '@discordjs/collection'
|
||||
- '@discordjs/core'
|
||||
- 'create-discord-app'
|
||||
- 'create-discord-bot'
|
||||
- '@discordjs/formatters'
|
||||
- 'discord.js'
|
||||
@@ -38,9 +39,10 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node.js v22
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./packages/actions/src/pnpmCache
|
||||
|
||||
16
.github/workflows/documentation.yml
vendored
16
.github/workflows/documentation.yml
vendored
@@ -41,9 +41,10 @@ jobs:
|
||||
ref: ${{ inputs.ref || '' }}
|
||||
|
||||
- name: Install Node.js v22
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./packages/actions/src/pnpmCache
|
||||
@@ -88,6 +89,11 @@ jobs:
|
||||
run: |
|
||||
declare -a PACKAGES=("brokers" "builders" "collection" "core" "discord.js" "formatters" "next" "proxy" "rest" "structures" "util" "voice" "ws")
|
||||
for PACKAGE in "${PACKAGES[@]}"; do
|
||||
if [ ! -d "packages/${PACKAGE}" ]; then
|
||||
echo "::notice::${PACKAGE} does not exist on this ref. Skipping..."
|
||||
continue
|
||||
fi
|
||||
|
||||
cd "packages/${PACKAGE}"
|
||||
sed -i 's!https://github.com/discordjs/discord.js/tree/main!https://github.com/discordjs/discord.js/tree/${{ inputs.ref }}!' api-extractor.json
|
||||
../../main/packages/api-extractor/bin/api-extractor run --local --minify
|
||||
@@ -221,6 +227,11 @@ jobs:
|
||||
run: |
|
||||
declare -a PACKAGES=("brokers" "builders" "collection" "core" "discord.js" "formatters" "next" "proxy" "rest" "structures" "util" "voice" "ws")
|
||||
for PACKAGE in "${PACKAGES[@]}"; do
|
||||
if [ ! -d "packages/${PACKAGE}" ]; then
|
||||
echo "::notice::${PACKAGE} does not exist on this ref. Skipping..."
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "${PACKAGE}" == "discord.js" ]]; then
|
||||
mkdir -p "out/${PACKAGE}"
|
||||
mv "packages/${PACKAGE}/docs/docs.json" "out/${PACKAGE}/${GITHUB_REF_NAME}.json"
|
||||
@@ -253,9 +264,10 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node.js v22
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./packages/actions/src/pnpmCache
|
||||
|
||||
1
.github/workflows/pr-triage.yml
vendored
1
.github/workflows/pr-triage.yml
vendored
@@ -19,7 +19,6 @@ jobs:
|
||||
sync-labels: true
|
||||
validate-title:
|
||||
name: Validate title
|
||||
if: github.event.action != 'synchronize'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate pull request title
|
||||
|
||||
3
.github/workflows/publish-dev-docker.yml
vendored
3
.github/workflows/publish-dev-docker.yml
vendored
@@ -13,9 +13,10 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node.js v22
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./packages/actions/src/pnpmCache
|
||||
|
||||
60
.github/workflows/publish-dev.yml
vendored
60
.github/workflows/publish-dev.yml
vendored
@@ -4,6 +4,14 @@ on:
|
||||
- cron: '0 */12 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pull:
|
||||
description: 'The pull number to check out'
|
||||
required: false
|
||||
default: 'main'
|
||||
tag:
|
||||
description: 'The tag to use, generally a feature name'
|
||||
required: false
|
||||
type: string
|
||||
dry_run:
|
||||
description: 'Perform a dry run that skips publishing and outputs logs indicating what would have happened'
|
||||
type: boolean
|
||||
@@ -19,15 +27,33 @@ jobs:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
if: github.repository_owner == 'discordjs'
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.DISCORDJS_APP_ID }}
|
||||
private-key: ${{ secrets.DISCORDJS_APP_KEY_RELEASE }}
|
||||
|
||||
- name: Decide ref
|
||||
id: ref
|
||||
run: |
|
||||
if [ -n "${{ github.event.inputs.pull }}" ]; then
|
||||
echo "ref=refs/pull/${{ github.event.inputs.pull }}/head" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "ref=refs/heads/main" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
ref: ${{ steps.ref.outputs.ref }}
|
||||
|
||||
- name: Install Node.js v22
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
package-manager-cache: false
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -36,12 +62,42 @@ jobs:
|
||||
- name: Build dependencies
|
||||
run: pnpm run build
|
||||
|
||||
- name: Publish packages
|
||||
- name: Checkout main repository (non-main ref)
|
||||
if: ${{ steps.ref.outputs.ref != 'refs/heads/main' }}
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
path: 'main'
|
||||
|
||||
- name: Install action deps (non-main ref)
|
||||
if: ${{ steps.ref.outputs.ref != 'refs/heads/main' }}
|
||||
shell: bash
|
||||
working-directory: ./main
|
||||
env:
|
||||
COREPACK_ENABLE_STRICT: 0
|
||||
run: |
|
||||
pnpm self-update 10
|
||||
pnpm install --filter @discordjs/actions --frozen-lockfile --prefer-offline --loglevel error
|
||||
|
||||
- name: Publish packages (non-main ref)
|
||||
if: ${{ steps.ref.outputs.ref != 'refs/heads/main' }}
|
||||
uses: ./main/packages/actions/src/releasePackages
|
||||
with:
|
||||
exclude: '@discordjs/docgen'
|
||||
dry: ${{ inputs.dry_run }}
|
||||
dev: true
|
||||
tag: ${{ inputs.tag || 'dev' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish packages (main ref)
|
||||
if: ${{ steps.ref.outputs.ref == 'refs/heads/main' }}
|
||||
uses: ./packages/actions/src/releasePackages
|
||||
with:
|
||||
exclude: '@discordjs/docgen'
|
||||
dry: ${{ inputs.dry_run }}
|
||||
dev: true
|
||||
tag: ${{ inputs.tag || 'dev' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
3
.github/workflows/publish-docker.yml
vendored
3
.github/workflows/publish-docker.yml
vendored
@@ -10,9 +10,10 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node.js v22
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./packages/actions/src/pnpmCache
|
||||
|
||||
3
.github/workflows/publish-release.yml
vendored
3
.github/workflows/publish-release.yml
vendored
@@ -17,9 +17,10 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node.js v22
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
package-manager-cache: false
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
33
.github/workflows/release.yml
vendored
33
.github/workflows/release.yml
vendored
@@ -61,9 +61,10 @@ jobs:
|
||||
ref: ${{ inputs.ref || '' }}
|
||||
|
||||
- name: Install Node.js v22
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
package-manager-cache: false
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -72,7 +73,35 @@ jobs:
|
||||
- name: Build dependencies
|
||||
run: pnpm run build
|
||||
|
||||
- name: Release packages
|
||||
- name: Checkout main repository
|
||||
if: ${{ inputs.ref && inputs.ref != 'main' }}
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
path: 'main'
|
||||
|
||||
- name: Install action deps (non-main ref only)
|
||||
if: ${{ inputs.ref && inputs.ref != 'main' }}
|
||||
shell: bash
|
||||
working-directory: ./main
|
||||
env:
|
||||
COREPACK_ENABLE_STRICT: 0
|
||||
run: |
|
||||
pnpm self-update 10
|
||||
pnpm install --filter @discordjs/actions --frozen-lockfile --prefer-offline --loglevel error
|
||||
|
||||
- name: Release packages (non-main ref)
|
||||
if: ${{ inputs.ref && inputs.ref != 'main' }}
|
||||
uses: ./main/packages/actions/src/releasePackages
|
||||
with:
|
||||
package: ${{ inputs.package }}
|
||||
exclude: ${{ inputs.exclude }}
|
||||
dry: ${{ inputs.dry_run }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Release packages (main ref)
|
||||
if: ${{ !inputs.ref || inputs.ref == 'main' }}
|
||||
uses: ./packages/actions/src/releasePackages
|
||||
with:
|
||||
package: ${{ inputs.package }}
|
||||
|
||||
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@@ -20,9 +20,10 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node.js v22
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./packages/actions/src/pnpmCache
|
||||
|
||||
1
apps/guide/.gitignore
vendored
1
apps/guide/.gitignore
vendored
@@ -23,3 +23,4 @@ pids
|
||||
.tmp
|
||||
.vscode
|
||||
.vercel
|
||||
next-env.d.ts
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"pages": [
|
||||
"[MessageCircleQuestion][FAQ](/guide/legacy/popular-topics/faq)",
|
||||
"[ArrowDownToLine][Updating to v14](/guide/legacy/additional-info/changes-in-v14)",
|
||||
"[MessageCircleQuestion][FAQ](/legacy/popular-topics/faq)",
|
||||
"[ArrowDownToLine][Updating to v14](/legacy/additional-info/changes-in-v14)",
|
||||
"[LibraryBig][Documentation](https://discord.js.org/docs)",
|
||||
"[Info][Introduction](/guide/legacy)",
|
||||
"[Info][Introduction](/legacy)",
|
||||
"---Setup---",
|
||||
"preparations",
|
||||
"---Your App---",
|
||||
@@ -25,8 +25,8 @@
|
||||
"...oauth2",
|
||||
"sharding"
|
||||
],
|
||||
"title": "Legacy Guide",
|
||||
"description": "The legacy discord.js guide",
|
||||
"title": "discord.js",
|
||||
"description": "The discord.js guide",
|
||||
"icon": "Book",
|
||||
"root": true
|
||||
}
|
||||
|
||||
5
apps/guide/next-env.d.ts
vendored
5
apps/guide/next-env.d.ts
vendored
@@ -1,5 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -25,13 +25,13 @@ export default withMDX({
|
||||
},
|
||||
experimental: {
|
||||
ppr: true,
|
||||
reactCompiler: true,
|
||||
useCache: true,
|
||||
dynamicOnHover: true,
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
reactCompiler: true,
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"lint": "pnpm run build:check && prettier --check . && cross-env TIMING=1 eslint --format=pretty src ",
|
||||
"format": "pnpm run build:check && prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src ",
|
||||
"fmt": "pnpm run format",
|
||||
"postinstall": "fumadocs-mdx"
|
||||
"postinstall": "next typegen && fumadocs-mdx"
|
||||
},
|
||||
"type": "module",
|
||||
"directories": {
|
||||
@@ -48,61 +48,61 @@
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"cmdk": "^1.1.1",
|
||||
"cva": "1.0.0-beta.3",
|
||||
"fumadocs-core": "^15.6.3",
|
||||
"fumadocs-mdx": "^11.6.10",
|
||||
"fumadocs-twoslash": "^3.1.4",
|
||||
"fumadocs-ui": "^15.6.3",
|
||||
"geist": "^1.4.2",
|
||||
"immer": "^10.1.1",
|
||||
"jotai": "^2.12.5",
|
||||
"fumadocs-core": "^15.8.4",
|
||||
"fumadocs-mdx": "^12.0.3",
|
||||
"fumadocs-twoslash": "^3.1.8",
|
||||
"fumadocs-ui": "^15.8.4",
|
||||
"geist": "^1.5.1",
|
||||
"immer": "^10.1.3",
|
||||
"jotai": "^2.15.0",
|
||||
"jotai-immer": "^0.4.1",
|
||||
"lucide-react": "^0.525.0",
|
||||
"mermaid": "^11.8.1",
|
||||
"motion": "^12.23.3",
|
||||
"next": "15.4.0-canary.11",
|
||||
"next-mdx-remote-client": "^2.1.2",
|
||||
"lucide-react": "^0.545.0",
|
||||
"mermaid": "^11.12.0",
|
||||
"motion": "^12.23.22",
|
||||
"next": "15.6.0-canary.45",
|
||||
"next-mdx-remote-client": "^2.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.4.3",
|
||||
"react": "^19.1.0",
|
||||
"react-aria": "^3.41.1",
|
||||
"react-aria-components": "^1.10.1",
|
||||
"react-dom": "^19.1.0",
|
||||
"nuqs": "^2.7.1",
|
||||
"react": "^19.2.0",
|
||||
"react-aria": "^3.44.0",
|
||||
"react-aria-components": "^1.13.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"sharp": "^0.34.3",
|
||||
"sharp": "^0.34.4",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"twoslash": "^0.3.2",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"twoslash": "^0.3.4",
|
||||
"usehooks-ts": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/env": "^15.3.5",
|
||||
"@shikijs/rehype": "^3.7.0",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@next/env": "^15.5.4",
|
||||
"@shikijs/rehype": "^3.13.0",
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^22.16.3",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/node": "^22.18.8",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.30.1",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
"cpy-cli": "^6.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"git-describe": "^4.1.1",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"shiki": "^3.7.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tailwindcss-react-aria-components": "^2.0.0",
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vercel": "^44.4.1"
|
||||
"shiki": "^3.13.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tailwindcss-react-aria-components": "^2.0.1",
|
||||
"turbo": "^2.5.8",
|
||||
"typescript": "^5.9.3",
|
||||
"vercel": "^48.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function generateStaticParams() {
|
||||
return source.generateParams();
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: { params: Promise<{ slug?: string[] }> }) {
|
||||
export async function generateMetadata(props: { params: Promise<{ slug?: string[] }> }): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug);
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function generateMetadata(props: { params: Promise<{ slug?: string[
|
||||
notFound();
|
||||
}
|
||||
|
||||
const image = ['/docs-og', ...(params.slug ?? []), 'image.png'].join('/');
|
||||
const image = ['/og', ...(params.slug ?? []), 'image.png'].join('/');
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
@@ -27,7 +27,7 @@ export async function generateMetadata(props: { params: Promise<{ slug?: string[
|
||||
card: 'summary_large_image',
|
||||
images: image,
|
||||
},
|
||||
} satisfies Metadata;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page(props: { readonly params: Promise<{ slug?: string[] }> }) {
|
||||
@@ -1,41 +0,0 @@
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import type { ReactNode } from 'react';
|
||||
import { baseOptions } from '@/app/layout.config';
|
||||
import { source } from '@/lib/source';
|
||||
|
||||
export default function Layout({ children }: { readonly children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout
|
||||
sidebar={{
|
||||
tabs: {
|
||||
transform(option, node) {
|
||||
const meta = source.getNodeMeta(node);
|
||||
if (!meta || !node.icon) return option;
|
||||
|
||||
// category selection color based on path src/styles/base.css
|
||||
const color = `var(--${meta.file.path.split('/')[0]}-color, var(--color-fd-foreground))`;
|
||||
|
||||
return {
|
||||
...option,
|
||||
icon: (
|
||||
<div
|
||||
className="rounded-lg border p-1.5 shadow-lg md:mb-auto md:rounded-md md:p-1 [&_svg]:size-6 md:[&_svg]:size-5"
|
||||
style={{
|
||||
color,
|
||||
backgroundColor: `color-mix(in oklab, ${color} 10%, transparent)`,
|
||||
}}
|
||||
>
|
||||
{node.icon}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
}}
|
||||
tree={source.pageTree}
|
||||
{...baseOptions}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Analytics } from '@vercel/analytics/react';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { RootProvider } from 'fumadocs-ui/provider';
|
||||
import { GeistMono } from 'geist/font/mono';
|
||||
import { GeistSans } from 'geist/font/sans';
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import type { CSSProperties, PropsWithChildren } from 'react';
|
||||
import { Body } from '@/app/layout.client';
|
||||
import { source } from '@/lib/source';
|
||||
import { ENV } from '@/util/env';
|
||||
import { baseOptions } from './layout.config';
|
||||
|
||||
import '@/styles/base.css';
|
||||
|
||||
@@ -18,7 +21,7 @@ export const viewport: Viewport = {
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(ENV.IS_LOCAL_DEV ? `http://localhost:${ENV.PORT}` : 'https://next.discordjs.guide'),
|
||||
metadataBase: new URL(ENV.IS_LOCAL_DEV ? `http://localhost:${ENV.PORT}` : 'https://discordjs.guide'),
|
||||
title: {
|
||||
template: '%s | discord.js',
|
||||
default: 'discord.js',
|
||||
@@ -74,7 +77,41 @@ export default async function RootLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<html className={`${GeistSans.variable} ${GeistMono.variable} antialiased`} lang="en" suppressHydrationWarning>
|
||||
<Body>
|
||||
<RootProvider>{children}</RootProvider>
|
||||
<RootProvider>
|
||||
<DocsLayout
|
||||
sidebar={{
|
||||
tabs: {
|
||||
transform(option, node) {
|
||||
const meta = source.getNodeMeta(node);
|
||||
if (!meta || !node.icon) return option;
|
||||
|
||||
// category selection color based on path src/styles/base.css
|
||||
const color = `var(--${meta.path.split('/')[0]}-color, var(--color-fd-foreground))`;
|
||||
|
||||
return {
|
||||
...option,
|
||||
icon: (
|
||||
<div
|
||||
className="size-full rounded-lg text-(--tab-color) max-md:border max-md:bg-(--tab-color)/10 max-md:p-1.5 [&_svg]:size-full"
|
||||
style={
|
||||
{
|
||||
'--tab-color': color,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
{node.icon}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
}}
|
||||
tree={source.pageTree}
|
||||
{...baseOptions}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
</RootProvider>
|
||||
<Analytics />
|
||||
</Body>
|
||||
</html>
|
||||
|
||||
@@ -12,7 +12,7 @@ export function generateStaticParams() {
|
||||
export async function GET(_req: Request, { params }: { params: Promise<{ slug: string[] }> }) {
|
||||
const { slug } = await params;
|
||||
const page = source.getPage(slug.slice(0, -1));
|
||||
// const fontData = await fetch(new URL('../../../assets/Geist-Regular.ttf', import.meta.url), {
|
||||
// const fontData = await fetch(new URL('../../assets/Geist-Regular.ttf', import.meta.url), {
|
||||
// next: { revalidate: 604_800 },
|
||||
// }).then(async (res) => res.arrayBuffer());
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function Page() {
|
||||
redirect('/guide/legacy');
|
||||
}
|
||||
@@ -15,6 +15,6 @@ export const source = loader({
|
||||
|
||||
return undefined;
|
||||
},
|
||||
baseUrl: '/guide/',
|
||||
baseUrl: '/',
|
||||
source: docs.toFumadocsSource(),
|
||||
});
|
||||
|
||||
16
apps/guide/src/middleware.ts
Normal file
16
apps/guide/src/middleware.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NextResponse, type NextRequest } from 'next/server';
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
// TODO: Remove this eventually
|
||||
if (request.nextUrl.pathname.startsWith('/guide/')) {
|
||||
const newUrl = request.nextUrl.clone();
|
||||
newUrl.pathname = newUrl.pathname.replace('/guide/', '/');
|
||||
return NextResponse.redirect(new URL(newUrl.pathname, request.url));
|
||||
}
|
||||
|
||||
return NextResponse.redirect(new URL('/legacy', request.url));
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/', '/guide/:path*'],
|
||||
};
|
||||
1
apps/website/.gitignore
vendored
1
apps/website/.gitignore
vendored
@@ -25,6 +25,7 @@ src/styles/unocss.css
|
||||
.tmp
|
||||
.vscode
|
||||
lighthouse-results
|
||||
next-env.d.ts
|
||||
|
||||
.vercel
|
||||
|
||||
|
||||
5
apps/website/next-env.d.ts
vendored
5
apps/website/next-env.d.ts
vendored
@@ -1,5 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -21,12 +21,12 @@ export default {
|
||||
},
|
||||
experimental: {
|
||||
ppr: true,
|
||||
reactCompiler: true,
|
||||
dynamicOnHover: true,
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
reactCompiler: true,
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
@@ -39,8 +39,8 @@ export default {
|
||||
},
|
||||
{
|
||||
source: '/guide/:path*',
|
||||
destination: 'https://next.discordjs.guide/guide/:path*',
|
||||
permanent: true,
|
||||
destination: 'https://discordjs.guide/:path*',
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"dev": "next dev --turbopack",
|
||||
"lint": "pnpm run build:check && prettier --check . && cross-env TIMING=1 eslint --format=pretty src ",
|
||||
"format": "pnpm run build:check && prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src ",
|
||||
"fmt": "pnpm run format"
|
||||
"fmt": "pnpm run format",
|
||||
"postinstall": "next typegen"
|
||||
},
|
||||
"type": "module",
|
||||
"directories": {
|
||||
@@ -46,66 +47,66 @@
|
||||
"homepage": "https://discord.js.org",
|
||||
"funding": "https://github.com/discordjs/discord.js?sponsor",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"@vercel/edge-config": "^1.4.0",
|
||||
"@vercel/postgres": "^0.10.0",
|
||||
"cloudflare": "^4.4.1",
|
||||
"cloudflare": "^5.2.0",
|
||||
"cmdk": "^1.1.1",
|
||||
"cva": "1.0.0-beta.3",
|
||||
"geist": "^1.4.2",
|
||||
"immer": "^10.1.1",
|
||||
"jotai": "^2.12.5",
|
||||
"geist": "^1.5.1",
|
||||
"immer": "^10.1.3",
|
||||
"jotai": "^2.15.0",
|
||||
"jotai-immer": "^0.4.1",
|
||||
"lucide-react": "^0.525.0",
|
||||
"lucide-react": "^0.545.0",
|
||||
"meilisearch": "^0.50.0",
|
||||
"motion": "^12.23.3",
|
||||
"next": "15.4.0-canary.35",
|
||||
"next-mdx-remote-client": "^2.1.2",
|
||||
"motion": "^12.23.22",
|
||||
"next": "15.6.0-canary.45",
|
||||
"next-mdx-remote-client": "^2.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.4.3",
|
||||
"overlayscrollbars": "^2.11.4",
|
||||
"nuqs": "^2.7.1",
|
||||
"overlayscrollbars": "^2.12.0",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"react": "^19.1.0",
|
||||
"react-aria": "^3.41.1",
|
||||
"react-aria-components": "^1.10.1",
|
||||
"react-dom": "^19.1.0",
|
||||
"react": "^19.2.0",
|
||||
"react-aria": "^3.44.0",
|
||||
"react-aria-components": "^1.13.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"sharp": "^0.34.3",
|
||||
"sharp": "^0.34.4",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"usehooks-ts": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/env": "^15.3.5",
|
||||
"@shikijs/rehype": "^3.7.0",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/node": "^22.16.3",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@next/env": "^15.5.4",
|
||||
"@shikijs/rehype": "^3.13.0",
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/node": "^22.18.8",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.30.1",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
"cpy-cli": "^6.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"git-describe": "^4.1.1",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"shiki": "^3.7.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tailwindcss-react-aria-components": "^2.0.0",
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vercel": "^44.4.1"
|
||||
"shiki": "^3.13.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tailwindcss-react-aria-components": "^2.0.1",
|
||||
"turbo": "^2.5.8",
|
||||
"typescript": "^5.9.3",
|
||||
"vercel": "^48.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
|
||||
@@ -61,7 +61,7 @@ export default async function Page({
|
||||
const fileContent = await readFile(join(process.cwd(), `src/assets/readme/${packageName}/home-README.md`), 'utf8');
|
||||
|
||||
return (
|
||||
<div className="prose prose-neutral dark:prose-invert prose-a:[&>img]:inline-block prose-a:[&>img]:m-0 prose-a:[&>img[height='44']]:h-11 prose-p:my-2 prose-pre:py-3 prose-pre:rounded-sm prose-pre:px-0 prose-pre:border prose-pre:border-[#d4d4d4] dark:prose-pre:border-[#404040] prose-code:font-normal prose-a:text-[#5865F2] prose-a:no-underline prose-a:hover:text-[#3d48c3] dark:prose-a:hover:text-[#7782fa] mx-auto max-w-screen-xl px-6 py-6 [&_code_span:last-of-type:empty]:hidden [&_div[align='center']_p_a+a]:ml-2">
|
||||
<div className="prose prose-neutral dark:prose-invert prose-a:[&>img]:inline-block prose-a:[&>img]:m-0 prose-a:[&>img[height='44']]:h-11 prose-p:my-2 prose-pre:py-3 prose-pre:rounded-sm prose-pre:px-0 prose-pre:border prose-pre:border-[#d4d4d4] dark:prose-pre:border-[#404040] prose-code:font-normal prose-a:text-[#5865F2] prose-a:no-underline prose-a:hover:text-[#3d48c3] dark:prose-a:hover:text-[#7782fa] mx-auto max-w-screen-xl px-6 py-6 break-words [&_code_span:last-of-type:empty]:hidden [&_div[align='center']_p_a+a]:ml-2">
|
||||
<MDXRemote
|
||||
options={{
|
||||
mdxOptions: {
|
||||
|
||||
22
package.json
22
package.json
@@ -51,33 +51,33 @@
|
||||
"homepage": "https://discord.js.org",
|
||||
"funding": "https://github.com/discordjs/discord.js?sponsor",
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.8.1",
|
||||
"@commitlint/config-angular": "^19.8.1",
|
||||
"@commitlint/cli": "^20.1.0",
|
||||
"@commitlint/config-angular": "^20.0.0",
|
||||
"@favware/cliff-jumper": "^4.1.0",
|
||||
"@favware/npm-deprecate": "^2.0.0",
|
||||
"@types/lodash.merge": "^4.6.9",
|
||||
"@unocss/eslint-plugin": "^66.3.3",
|
||||
"@unocss/eslint-plugin": "^66.5.2",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"conventional-changelog-cli": "^5.0.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"husky": "^9.1.7",
|
||||
"is-ci": "^4.1.0",
|
||||
"lint-staged": "^16.1.2",
|
||||
"lint-staged": "^16.2.3",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"prettier": "^3.6.2",
|
||||
"tsup": "^8.5.0",
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.36.0",
|
||||
"unocss": "^66.3.3",
|
||||
"vercel": "^44.4.1",
|
||||
"turbo": "^2.5.8",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.0",
|
||||
"unocss": "^66.5.2",
|
||||
"vercel": "^48.2.1",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.4"
|
||||
"packageManager": "pnpm@10.18.1"
|
||||
}
|
||||
|
||||
@@ -44,33 +44,33 @@
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@actions/glob": "^0.5.0",
|
||||
"@aws-sdk/client-s3": "^3.844.0",
|
||||
"@aws-sdk/client-s3": "^3.901.0",
|
||||
"@discordjs/scripts": "workspace:^",
|
||||
"@vercel/blob": "^1.1.1",
|
||||
"@vercel/blob": "^2.0.0",
|
||||
"@vercel/postgres": "^0.10.0",
|
||||
"cloudflare": "^4.4.1",
|
||||
"commander": "^14.0.0",
|
||||
"cloudflare": "^5.2.0",
|
||||
"commander": "^14.0.1",
|
||||
"meilisearch": "^0.38.0",
|
||||
"p-limit": "^6.2.0",
|
||||
"p-queue": "^8.1.0",
|
||||
"p-limit": "^7.1.1",
|
||||
"p-queue": "^9.0.0",
|
||||
"tslib": "^2.8.1",
|
||||
"undici": "7.11.0"
|
||||
"undici": "7.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@npm/types": "^2.1.0",
|
||||
"@types/bun": "^1.2.19",
|
||||
"@types/node": "^22.16.3",
|
||||
"@types/bun": "^1.2.23",
|
||||
"@types/node": "^22.18.8",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.30.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"terser": "^5.43.1",
|
||||
"terser": "^5.44.0",
|
||||
"tsup": "^8.5.0",
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "~5.8.3",
|
||||
"turbo": "^2.5.8",
|
||||
"typescript": "~5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -11,14 +11,17 @@ inputs:
|
||||
description: 'The published name of a single package to release'
|
||||
exclude:
|
||||
description: 'Comma separated list of packages to exclude from release (if not depended upon)'
|
||||
tag:
|
||||
description: 'The tag to use, generally a feature name'
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- run: bun packages/actions/src/releasePackages/index.ts
|
||||
- run: bun $GITHUB_ACTION_PATH/index.ts
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_DEV: ${{ inputs.dev }}
|
||||
INPUT_DRY: ${{ inputs.dry }}
|
||||
INPUT_PACKAGE: ${{ inputs.package }}
|
||||
INPUT_EXCLUDE: ${{ inputs.exclude }}
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
|
||||
@@ -26,9 +26,9 @@ export interface ReleaseEntry {
|
||||
version: string;
|
||||
}
|
||||
|
||||
async function fetchDevVersion(pkg: string) {
|
||||
async function fetchDevVersion(pkg: string, tag: string) {
|
||||
try {
|
||||
const res = await fetch(`https://registry.npmjs.org/${pkg}/dev`);
|
||||
const res = await fetch(`https://registry.npmjs.org/${pkg}/${tag}`);
|
||||
if (!res.ok) return null;
|
||||
const packument = (await res.json()) as PackumentVersion;
|
||||
return packument.version;
|
||||
@@ -37,12 +37,13 @@ async function fetchDevVersion(pkg: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getReleaseEntries(dev: boolean, dry: boolean) {
|
||||
async function getReleaseEntries(dry: boolean, devTag?: string) {
|
||||
const releaseEntries: ReleaseEntry[] = [];
|
||||
const packageList: pnpmTree[] =
|
||||
await $`pnpm list --recursive --only-projects --filter {packages/\*} --prod --json`.json();
|
||||
|
||||
const commitHash = (await $`git rev-parse --short HEAD`.text()).trim();
|
||||
const timestamp = Math.round(Date.now() / 1_000);
|
||||
|
||||
for (const pkg of packageList) {
|
||||
// Don't release private packages ever (npm will error anyways)
|
||||
@@ -56,8 +57,8 @@ async function getReleaseEntries(dev: boolean, dry: boolean) {
|
||||
version: pkg.version,
|
||||
};
|
||||
|
||||
if (dev) {
|
||||
const devVersion = await fetchDevVersion(pkg.name);
|
||||
if (devTag) {
|
||||
const devVersion = await fetchDevVersion(pkg.name, devTag);
|
||||
if (devVersion?.endsWith(commitHash)) {
|
||||
// Write the currently released dev version so when pnpm publish runs on dependents they depend on the dev versions
|
||||
if (dry) {
|
||||
@@ -71,9 +72,9 @@ async function getReleaseEntries(dev: boolean, dry: boolean) {
|
||||
release.version = devVersion;
|
||||
} else if (dry) {
|
||||
info(`[DRY] Bumping ${pkg.name} via git-cliff.`);
|
||||
release.version = `${pkg.version}.DRY-dev.${Math.round(Date.now() / 1_000)}-${commitHash}`;
|
||||
release.version = `${pkg.version}.DRY-${devTag}.${timestamp}-${commitHash}`;
|
||||
} else {
|
||||
await $`pnpm --filter=${pkg.name} run release --preid "dev.${Math.round(Date.now() / 1_000)}-${commitHash}" --skip-changelog`;
|
||||
await $`pnpm --filter=${pkg.name} run release --preid "${devTag}.${timestamp}-${commitHash}" --skip-changelog`;
|
||||
// Read again instead of parsing the output to be sure we're matching when checking against npm
|
||||
const pkgJson = (await file(`${pkg.path}/package.json`).json()) as PackageJSON;
|
||||
release.version = pkgJson.version;
|
||||
@@ -128,8 +129,8 @@ async function getReleaseEntries(dev: boolean, dry: boolean) {
|
||||
return releaseEntries;
|
||||
}
|
||||
|
||||
export async function generateReleaseTree(dev: boolean, dry: boolean, packageName?: string, exclude?: string[]) {
|
||||
let releaseEntries = await getReleaseEntries(dev, dry);
|
||||
export async function generateReleaseTree(dry: boolean, devTag?: string, packageName?: string, exclude?: string[]) {
|
||||
let releaseEntries = await getReleaseEntries(dry, devTag);
|
||||
// Try to early return if the package doesn't have deps
|
||||
if (packageName && packageName !== 'all') {
|
||||
const releaseEntry = releaseEntries.find((entry) => entry.name === packageName);
|
||||
|
||||
@@ -30,15 +30,29 @@ program
|
||||
)
|
||||
.option('--dry', 'skips actual publishing and outputs logs instead', dryInput)
|
||||
.option('--dev', 'publishes development versions and skips tagging / github releases', devInput)
|
||||
.option('--tag <tag>', 'tag to use for dev releases (defaults to "dev")', getInput('tag'))
|
||||
.parse();
|
||||
|
||||
const { exclude, dry, dev } = program.opts<{ dev: boolean; dry: boolean; exclude: string[] }>();
|
||||
const [packageName] = program.processedArgs as [string];
|
||||
const {
|
||||
exclude,
|
||||
dry,
|
||||
dev,
|
||||
tag: inputTag,
|
||||
} = program.opts<{ dev: boolean; dry: boolean; exclude: string[]; tag: string }>();
|
||||
|
||||
// All this because getInput('tag') will return empty string when not set :P
|
||||
if (!dev && inputTag.length) {
|
||||
throw new Error('The --tag option can only be used with --dev');
|
||||
}
|
||||
|
||||
const tag = inputTag.length ? inputTag : dev ? 'dev' : undefined;
|
||||
|
||||
const [packageName] = program.processedArgs as [string];
|
||||
const tree = await generateReleaseTree(dry, tag, packageName, exclude);
|
||||
|
||||
const tree = await generateReleaseTree(dev, dry, packageName, exclude);
|
||||
for (const branch of tree) {
|
||||
startGroup(`Releasing ${branch.map((entry) => `${entry.name}@${entry.version}`).join(', ')}`);
|
||||
await Promise.all(branch.map(async (release) => releasePackage(release, dev, dry)));
|
||||
await Promise.all(branch.map(async (release) => releasePackage(release, dry, tag)));
|
||||
endGroup();
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ async function gitTagAndRelease(release: ReleaseEntry, dry: boolean) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commitHash = (await $`git rev-parse --short HEAD`.text()).trim();
|
||||
const commitHash = (await $`git rev-parse HEAD`.text()).trim();
|
||||
|
||||
try {
|
||||
await octokit?.rest.repos.createRelease({
|
||||
@@ -41,7 +41,7 @@ async function gitTagAndRelease(release: ReleaseEntry, dry: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function releasePackage(release: ReleaseEntry, dev: boolean, dry: boolean) {
|
||||
export async function releasePackage(release: ReleaseEntry, dry: boolean, devTag?: string, doGitRelease = !devTag) {
|
||||
// Sanity check against the registry first
|
||||
if (await checkRegistry(release)) {
|
||||
info(`${release.name}@${release.version} already published, skipping.`);
|
||||
@@ -51,10 +51,11 @@ export async function releasePackage(release: ReleaseEntry, dev: boolean, dry: b
|
||||
if (dry) {
|
||||
info(`[DRY] Releasing ${release.name}@${release.version}`);
|
||||
} else {
|
||||
await $`pnpm --filter=${release.name} publish --provenance --no-git-checks ${dev ? '--tag=dev' : ''}`;
|
||||
await $`pnpm --filter=${release.name} publish --provenance --no-git-checks ${devTag ? `--tag=${devTag}` : ''}`;
|
||||
}
|
||||
|
||||
if (!dev) await gitTagAndRelease(release, dry);
|
||||
// && !devTag just to be sure
|
||||
if (doGitRelease && !devTag) await gitTagAndRelease(release, dry);
|
||||
|
||||
if (dry) return;
|
||||
|
||||
@@ -76,11 +77,19 @@ export async function releasePackage(release: ReleaseEntry, dev: boolean, dry: b
|
||||
}, 15_000);
|
||||
});
|
||||
|
||||
if (dev) {
|
||||
if (devTag) {
|
||||
// Send and forget, deprecations are less important than releasing other dev versions and can be done manually
|
||||
void $`pnpm exec npm-deprecate --name "*dev*" --message "This version is deprecated. Please use a newer version." --package ${release.name}`
|
||||
void $`pnpm exec npm-deprecate --name "*${devTag}*" --message "This version is deprecated. Please use a newer version." --package ${release.name}`
|
||||
.nothrow()
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
.then(() => {});
|
||||
}
|
||||
|
||||
// Evil, but I can't think of a cleaner mechanism
|
||||
if (release.name === 'create-discord-bot') {
|
||||
await $`pnpm --filter=create-discord-bot run rename-to-app`;
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
release.name = 'create-discord-app';
|
||||
await releasePackage(release, dry, devTag, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,16 +36,16 @@
|
||||
"@rushstack/node-core-library": "5.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.16.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.30.1",
|
||||
"@types/node": "^22.18.8",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"terser": "^5.43.1",
|
||||
"terser": "^5.44.0",
|
||||
"tsup": "^8.5.0",
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "~5.8.3"
|
||||
"turbo": "^2.5.8",
|
||||
"typescript": "~5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,17 +50,17 @@
|
||||
"@microsoft/tsdoc": "~0.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.16.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.30.1",
|
||||
"@types/node": "^22.18.8",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"terser": "^5.43.1",
|
||||
"terser": "^5.44.0",
|
||||
"tsup": "^8.5.0",
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "~5.8.3"
|
||||
"turbo": "^2.5.8",
|
||||
"typescript": "~5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
|
||||
@@ -64,18 +64,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/node": "^22.16.3",
|
||||
"@types/node": "^22.18.8",
|
||||
"@types/resolve": "^1.20.6",
|
||||
"@types/semver": "^7.7.0",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.30.1",
|
||||
"@types/semver": "^7.7.1",
|
||||
"cpy-cli": "^6.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"terser": "^5.43.1",
|
||||
"terser": "^5.44.0",
|
||||
"tsup": "^8.5.0",
|
||||
"turbo": "^2.5.4"
|
||||
"turbo": "^2.5.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ These examples use [ES modules](https://nodejs.org/api/esm.html#enabling).
|
||||
import { PubSubRedisBroker } from '@discordjs/brokers';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
const broker = new PubSubRedisBroker(new Redis());
|
||||
// Considering this only pushes events, the group and name are not important.
|
||||
const broker = new PubSubRedisBroker(new Redis(), { group: 'noop', name: 'noop' });
|
||||
|
||||
await broker.publish('test', 'Hello World!');
|
||||
await broker.destroy();
|
||||
@@ -52,13 +53,22 @@ await broker.destroy();
|
||||
import { PubSubRedisBroker } from '@discordjs/brokers';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
const broker = new PubSubRedisBroker(new Redis());
|
||||
const broker = new PubSubRedisBroker(new Redis(), {
|
||||
// This is the consumer group name. You should make sure to not re-use this
|
||||
// across different applications in your stack, unless you absolutely know
|
||||
// what you're doing.
|
||||
group: 'subscribers',
|
||||
// With the assumption that this service will scale to more than one instance,
|
||||
// you MUST ensure `UNIQUE_CONSUMER_ID` is unique across all of them and
|
||||
// also deterministic (i.e. if instance-1 restarts, it should still be instance-1)
|
||||
name: `consumer-${UNIQUE_CONSUMER_ID}`,
|
||||
});
|
||||
broker.on('test', ({ data, ack }) => {
|
||||
console.log(data);
|
||||
void ack();
|
||||
});
|
||||
|
||||
await broker.subscribe('subscribers', ['test']);
|
||||
await broker.subscribe(['test']);
|
||||
```
|
||||
|
||||
### RPC
|
||||
@@ -68,7 +78,7 @@ await broker.subscribe('subscribers', ['test']);
|
||||
import { RPCRedisBroker } from '@discordjs/brokers';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
const broker = new RPCRedisBroker(new Redis());
|
||||
const broker = new RPCRedisBroker(new Redis(), { group: 'noop', name: 'noop' });
|
||||
|
||||
console.log(await broker.call('testcall', 'Hello World!'));
|
||||
await broker.destroy();
|
||||
@@ -77,14 +87,18 @@ await broker.destroy();
|
||||
import { RPCRedisBroker } from '@discordjs/brokers';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
const broker = new RPCRedisBroker(new Redis());
|
||||
const broker = new RPCRedisBroker(new Redis(), {
|
||||
// Equivalent to the group/name in pubsub, refer to the previous example.
|
||||
group: 'responders',
|
||||
name: `consumer-${UNIQUE_ID}`,
|
||||
});
|
||||
broker.on('testcall', ({ data, ack, reply }) => {
|
||||
console.log('responder', data);
|
||||
void ack();
|
||||
void reply(`Echo: ${data}`);
|
||||
});
|
||||
|
||||
await broker.subscribe('responders', ['testcall']);
|
||||
await broker.subscribe(['testcall']);
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
@@ -68,25 +68,25 @@
|
||||
"funding": "https://github.com/discordjs/discord.js?sponsor",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.1.2",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"ioredis": "^5.6.1"
|
||||
"@vladfrangu/async_event_emitter": "^2.4.7",
|
||||
"ioredis": "^5.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@discordjs/api-extractor": "workspace:^",
|
||||
"@discordjs/scripts": "workspace:^",
|
||||
"@favware/cliff-jumper": "^4.1.0",
|
||||
"@types/node": "^22.16.3",
|
||||
"@types/node": "^22.18.8",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-env": "^10.1.0",
|
||||
"esbuild-plugin-version-injector": "^1.2.1",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tsup": "^8.5.0",
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "~5.8.3",
|
||||
"turbo": "^2.5.8",
|
||||
"typescript": "~5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -8,13 +8,25 @@ import { ReplyError } from 'ioredis';
|
||||
import type { BaseBrokerOptions, IBaseBroker, ToEventMap } from '../Broker.js';
|
||||
import { DefaultBrokerOptions } from '../Broker.js';
|
||||
|
||||
// For some reason ioredis doesn't have this typed, but it exists
|
||||
type RedisReadGroupData = [Buffer, [Buffer, Buffer[]][]][];
|
||||
|
||||
// For some reason ioredis doesn't have those typed, but they exist
|
||||
declare module 'ioredis' {
|
||||
interface Redis {
|
||||
xreadgroupBuffer(...args: (Buffer | string)[]): Promise<[Buffer, [Buffer, Buffer[]][]][] | null>;
|
||||
xclaimBuffer(
|
||||
key: Buffer | string,
|
||||
group: Buffer | string,
|
||||
consumer: Buffer | string,
|
||||
minIdleTime: number,
|
||||
id: Buffer | string,
|
||||
...args: (Buffer | string)[]
|
||||
): Promise<string[]>;
|
||||
xreadgroupBuffer(...args: (Buffer | string)[]): Promise<RedisReadGroupData | null>;
|
||||
}
|
||||
}
|
||||
|
||||
export const kUseRandomGroupName = Symbol.for('djs.brokers.useRandomGroupName');
|
||||
|
||||
/**
|
||||
* Options specific for a Redis broker
|
||||
*/
|
||||
@@ -23,25 +35,35 @@ export interface RedisBrokerOptions extends BaseBrokerOptions {
|
||||
* How long to block for messages when polling
|
||||
*/
|
||||
blockTimeout?: number;
|
||||
|
||||
/**
|
||||
* Consumer group name to use for this broker
|
||||
* Consumer group name to use for this broker. For fanning out events, use {@link kUseRandomGroupName}
|
||||
*
|
||||
* @see {@link https://redis.io/commands/xreadgroup/}
|
||||
*/
|
||||
group: string;
|
||||
|
||||
group: string | typeof kUseRandomGroupName;
|
||||
/**
|
||||
* Max number of messages to poll at once
|
||||
*/
|
||||
maxChunk?: number;
|
||||
|
||||
/**
|
||||
* How many times a message can be delivered to a consumer before it is considered dead.
|
||||
* This is used to prevent messages from being stuck in the queue forever if a consumer is
|
||||
* unable to process them.
|
||||
*/
|
||||
maxDeliveredTimes?: number;
|
||||
/**
|
||||
* How long a message should be idle for before allowing it to be claimed by another consumer.
|
||||
* Note that too high of a value can lead to a high delay in processing messages during a service downscale,
|
||||
* while too low of a value can lead to messages being too eagerly claimed by other consumers during an instance
|
||||
* restart (which is most likely not actually that problematic)
|
||||
*/
|
||||
messageIdleTime?: number;
|
||||
/**
|
||||
* Unique consumer name.
|
||||
*
|
||||
* @see {@link https://redis.io/commands/xreadgroup/}
|
||||
*/
|
||||
name?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,10 +71,11 @@ export interface RedisBrokerOptions extends BaseBrokerOptions {
|
||||
*/
|
||||
export const DefaultRedisBrokerOptions = {
|
||||
...DefaultBrokerOptions,
|
||||
name: randomBytes(20).toString('hex'),
|
||||
maxChunk: 10,
|
||||
maxDeliveredTimes: 3,
|
||||
messageIdleTime: 3_000,
|
||||
blockTimeout: 5_000,
|
||||
} as const satisfies Required<Omit<RedisBrokerOptions, 'group'>>;
|
||||
} as const satisfies Required<Omit<RedisBrokerOptions, 'group' | 'name'>>;
|
||||
|
||||
/**
|
||||
* Helper class with shared Redis logic
|
||||
@@ -84,6 +107,14 @@ export abstract class BaseRedisBroker<
|
||||
*/
|
||||
protected readonly streamReadClient: Redis;
|
||||
|
||||
/**
|
||||
* The group being used by this broker.
|
||||
*
|
||||
* @privateRemarks
|
||||
* Stored as its own field to do the "use random group" resolution in the constructor.
|
||||
*/
|
||||
protected readonly group: string;
|
||||
|
||||
/**
|
||||
* Whether this broker is currently polling events
|
||||
*/
|
||||
@@ -95,6 +126,7 @@ export abstract class BaseRedisBroker<
|
||||
) {
|
||||
super();
|
||||
this.options = { ...DefaultRedisBrokerOptions, ...options };
|
||||
this.group = this.options.group === kUseRandomGroupName ? randomBytes(16).toString('hex') : this.options.group;
|
||||
redisClient.defineCommand('xcleangroup', {
|
||||
numberOfKeys: 1,
|
||||
lua: readFileSync(resolve(__dirname, '..', 'scripts', 'xcleangroup.lua'), 'utf8'),
|
||||
@@ -111,7 +143,7 @@ export abstract class BaseRedisBroker<
|
||||
events.map(async (event) => {
|
||||
this.subscribedEvents.add(event as string);
|
||||
try {
|
||||
return await this.redisClient.xgroup('CREATE', event as string, this.options.group, 0, 'MKSTREAM');
|
||||
return await this.redisClient.xgroup('CREATE', event as string, this.group, 0, 'MKSTREAM');
|
||||
} catch (error) {
|
||||
if (!(error instanceof ReplyError)) {
|
||||
throw error;
|
||||
@@ -141,7 +173,7 @@ export abstract class BaseRedisBroker<
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins polling for events, firing them to {@link BaseRedisBroker.listen}
|
||||
* Begins polling for events, firing them to {@link BaseRedisBroker.emitEvent}
|
||||
*/
|
||||
protected async listen(): Promise<void> {
|
||||
if (this.listening) {
|
||||
@@ -150,40 +182,24 @@ export abstract class BaseRedisBroker<
|
||||
|
||||
this.listening = true;
|
||||
|
||||
// Enter regular polling
|
||||
while (this.subscribedEvents.size > 0) {
|
||||
try {
|
||||
const data = await this.streamReadClient.xreadgroupBuffer(
|
||||
'GROUP',
|
||||
this.options.group,
|
||||
this.options.name,
|
||||
'COUNT',
|
||||
String(this.options.maxChunk),
|
||||
'BLOCK',
|
||||
String(this.options.blockTimeout),
|
||||
'STREAMS',
|
||||
...this.subscribedEvents,
|
||||
...Array.from({ length: this.subscribedEvents.size }, () => '>'),
|
||||
);
|
||||
await this.claimAndEmitDeadEvents();
|
||||
} catch (error) {
|
||||
// @ts-expect-error: Intended
|
||||
this.emit('error', error);
|
||||
// We don't break here to keep the loop running even if dead event processing fails
|
||||
}
|
||||
|
||||
try {
|
||||
// As per docs, '>' means "give me a new message"
|
||||
const data = await this.readGroup('>', this.options.blockTimeout);
|
||||
if (!data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [event, info] of data) {
|
||||
for (const [id, packet] of info) {
|
||||
const idx = packet.findIndex((value, idx) => value.toString('utf8') === 'data' && idx % 2 === 0);
|
||||
if (idx < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = packet[idx + 1];
|
||||
if (!data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.emitEvent(id, this.options.group, event.toString('utf8'), this.options.decode(data));
|
||||
}
|
||||
}
|
||||
await this.processMessages(data);
|
||||
} catch (error) {
|
||||
// @ts-expect-error: Intended
|
||||
this.emit('error', error);
|
||||
@@ -194,6 +210,103 @@ export abstract class BaseRedisBroker<
|
||||
this.listening = false;
|
||||
}
|
||||
|
||||
private async readGroup(fromId: string, block: number): Promise<RedisReadGroupData> {
|
||||
const data = await this.streamReadClient.xreadgroupBuffer(
|
||||
'GROUP',
|
||||
this.group,
|
||||
this.options.name,
|
||||
'COUNT',
|
||||
String(this.options.maxChunk),
|
||||
'BLOCK',
|
||||
String(block),
|
||||
'STREAMS',
|
||||
...this.subscribedEvents,
|
||||
...Array.from({ length: this.subscribedEvents.size }, () => fromId),
|
||||
);
|
||||
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
private async processMessages(data: RedisReadGroupData): Promise<void> {
|
||||
for (const [event, messages] of data) {
|
||||
const eventName = event.toString('utf8');
|
||||
|
||||
for (const [id, packet] of messages) {
|
||||
const idx = packet.findIndex((value, idx) => value.toString('utf8') === 'data' && idx % 2 === 0);
|
||||
if (idx < 0) continue;
|
||||
|
||||
const payload = packet[idx + 1];
|
||||
if (!payload) continue;
|
||||
|
||||
this.emitEvent(id, this.group, eventName, this.options.decode(payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async claimAndEmitDeadEvents(): Promise<void> {
|
||||
for (const stream of this.subscribedEvents) {
|
||||
// Get up to N oldest pending messages (note: a pending message is a message that has been read, but never ACKed)
|
||||
const pending = (await this.streamReadClient.xpending(
|
||||
stream,
|
||||
this.group,
|
||||
'-',
|
||||
'+',
|
||||
this.options.maxChunk,
|
||||
// See: https://redis.io/docs/latest/commands/xpending/#extended-form-of-xpending
|
||||
)) as [id: string, consumer: string, idleMs: number, deliveredTimes: number][];
|
||||
|
||||
for (const [id, consumer, idleMs, deliveredTimes] of pending) {
|
||||
// Technically xclaim checks for us anyway, but why not avoid an extra call?
|
||||
if (idleMs < this.options.messageIdleTime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (deliveredTimes > this.options.maxDeliveredTimes) {
|
||||
// This message is dead. It has repeatedly failed being processed by a consumer.
|
||||
await this.streamReadClient.xdel(stream, this.group, id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to claim the message if we don't already own it (this may fail if another consumer has already claimed it)
|
||||
if (consumer !== this.options.name) {
|
||||
const claimed = await this.streamReadClient.xclaimBuffer(
|
||||
stream,
|
||||
this.group,
|
||||
this.options.name,
|
||||
Math.max(this.options.messageIdleTime, 1),
|
||||
id,
|
||||
'JUSTID',
|
||||
);
|
||||
|
||||
// Another consumer got the message before us
|
||||
if (!claimed?.length) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch message body
|
||||
const entries = await this.streamReadClient.xrangeBuffer(stream, id, id);
|
||||
// No idea how this could happen, frankly!
|
||||
if (!entries?.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [msgId, fields] = entries[0]!;
|
||||
const idx = fields.findIndex((value, idx) => value.toString('utf8') === 'data' && idx % 2 === 0);
|
||||
if (idx < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const payload = fields[idx + 1];
|
||||
if (!payload) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.emitEvent(msgId, this.group, stream, this.options.decode(payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the broker, closing all connections
|
||||
*/
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface RPCRedisBrokerOptions extends RedisBrokerOptions {
|
||||
export const DefaultRPCRedisBrokerOptions = {
|
||||
...DefaultRedisBrokerOptions,
|
||||
timeout: 5_000,
|
||||
} as const satisfies Required<Omit<RPCRedisBrokerOptions, 'group'>>;
|
||||
} as const satisfies Required<Omit<RPCRedisBrokerOptions, 'group' | 'name'>>;
|
||||
|
||||
/**
|
||||
* RPC broker powered by Redis
|
||||
@@ -121,7 +121,7 @@ export class RPCRedisBroker<TEvents extends Record<string, any[]>, TResponses ex
|
||||
const payload: { ack(): Promise<void>; data: unknown; reply(data: unknown): Promise<void> } = {
|
||||
data,
|
||||
ack: async () => {
|
||||
await this.redisClient.xack(event, this.options.group, id);
|
||||
await this.redisClient.xack(event, this.group, id);
|
||||
},
|
||||
reply: async (data) => {
|
||||
await this.redisClient.publish(`${event}:${id.toString()}`, this.options.encode(data));
|
||||
|
||||
109
packages/builders/__tests__/components/label.test.ts
Normal file
109
packages/builders/__tests__/components/label.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { APILabelComponent, APIStringSelectComponent, APITextInputComponent } from 'discord-api-types/v10';
|
||||
import { ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { LabelBuilder } from '../../src/index.js';
|
||||
|
||||
describe('Label components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid fields THEN builder does not throw', () => {
|
||||
expect(() =>
|
||||
new LabelBuilder()
|
||||
.setLabel('label')
|
||||
.setStringSelectMenuComponent((stringSelectMenu) =>
|
||||
stringSelectMenu
|
||||
.setCustomId('test')
|
||||
.setOptions((stringSelectMenuOption) => stringSelectMenuOption.setLabel('label').setValue('value'))
|
||||
.setRequired(),
|
||||
)
|
||||
.toJSON(),
|
||||
).not.toThrow();
|
||||
|
||||
expect(() =>
|
||||
new LabelBuilder()
|
||||
.setLabel('label')
|
||||
.setId(5)
|
||||
.setTextInputComponent((textInput) =>
|
||||
textInput.setCustomId('test').setStyle(TextInputStyle.Paragraph).setRequired(),
|
||||
)
|
||||
.toJSON(),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
test('GIVEN invalid fields THEN build does throw', () => {
|
||||
expect(() => new LabelBuilder().toJSON()).toThrow();
|
||||
expect(() => new LabelBuilder().setId(5).toJSON()).toThrow();
|
||||
expect(() => new LabelBuilder().setLabel('label').toJSON()).toThrow();
|
||||
|
||||
expect(() =>
|
||||
new LabelBuilder()
|
||||
.setLabel('l'.repeat(1_000))
|
||||
.setStringSelectMenuComponent((stringSelectMenu) => stringSelectMenu)
|
||||
.toJSON(),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('GIVEN valid input THEN valid JSON outputs are given', () => {
|
||||
const labelWithTextInputData = {
|
||||
type: ComponentType.Label,
|
||||
component: {
|
||||
type: ComponentType.TextInput,
|
||||
custom_id: 'custom_id',
|
||||
placeholder: 'placeholder',
|
||||
style: TextInputStyle.Paragraph,
|
||||
} satisfies APITextInputComponent,
|
||||
label: 'label',
|
||||
description: 'description',
|
||||
id: 5,
|
||||
} satisfies APILabelComponent;
|
||||
|
||||
const labelWithStringSelectData = {
|
||||
type: ComponentType.Label,
|
||||
component: {
|
||||
type: ComponentType.StringSelect,
|
||||
custom_id: 'custom_id',
|
||||
placeholder: 'placeholder',
|
||||
options: [
|
||||
{ label: 'first', value: 'first' },
|
||||
{ label: 'second', value: 'second' },
|
||||
],
|
||||
required: true,
|
||||
} satisfies APIStringSelectComponent,
|
||||
label: 'label',
|
||||
description: 'description',
|
||||
id: 5,
|
||||
} satisfies APILabelComponent;
|
||||
|
||||
expect(new LabelBuilder(labelWithTextInputData).toJSON()).toEqual(labelWithTextInputData);
|
||||
expect(new LabelBuilder(labelWithStringSelectData).toJSON()).toEqual(labelWithStringSelectData);
|
||||
|
||||
expect(
|
||||
new LabelBuilder()
|
||||
.setTextInputComponent((textInput) =>
|
||||
textInput.setCustomId('custom_id').setPlaceholder('placeholder').setStyle(TextInputStyle.Paragraph),
|
||||
)
|
||||
.setLabel('label')
|
||||
.setDescription('description')
|
||||
.setId(5)
|
||||
.toJSON(),
|
||||
).toEqual(labelWithTextInputData);
|
||||
|
||||
expect(
|
||||
new LabelBuilder()
|
||||
.setStringSelectMenuComponent((stringSelectMenu) =>
|
||||
stringSelectMenu
|
||||
.setCustomId('custom_id')
|
||||
.setPlaceholder('placeholder')
|
||||
.setOptions(
|
||||
(stringSelectMenuOption) => stringSelectMenuOption.setLabel('first').setValue('first'),
|
||||
(stringSelectMenuOption) => stringSelectMenuOption.setLabel('second').setValue('second'),
|
||||
)
|
||||
.setRequired(),
|
||||
)
|
||||
.setLabel('label')
|
||||
.setDescription('description')
|
||||
.setId(5)
|
||||
.toJSON(),
|
||||
).toEqual(labelWithStringSelectData);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,7 @@ const selectMenuDataWithoutOptions = {
|
||||
min_values: 1,
|
||||
disabled: true,
|
||||
placeholder: 'test',
|
||||
required: false,
|
||||
} as const;
|
||||
|
||||
const selectMenuData: APISelectMenuComponent = {
|
||||
|
||||
@@ -8,13 +8,12 @@ describe('Text Input Components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid fields THEN builder does not throw', () => {
|
||||
expect(() => {
|
||||
textInputComponent().setCustomId('foobar').setLabel('test').setStyle(TextInputStyle.Paragraph).toJSON();
|
||||
textInputComponent().setCustomId('foobar').setStyle(TextInputStyle.Paragraph).toJSON();
|
||||
}).not.toThrowError();
|
||||
|
||||
expect(() => {
|
||||
textInputComponent()
|
||||
.setCustomId('foobar')
|
||||
.setLabel('test')
|
||||
.setMaxLength(100)
|
||||
.setMinLength(1)
|
||||
.setPlaceholder('bar')
|
||||
@@ -24,7 +23,7 @@ describe('Text Input Components', () => {
|
||||
}).not.toThrowError();
|
||||
|
||||
expect(() => {
|
||||
textInputComponent().setCustomId('Custom').setLabel('Guess').setStyle(TextInputStyle.Short).toJSON();
|
||||
textInputComponent().setCustomId('Custom').setStyle(TextInputStyle.Short).toJSON();
|
||||
}).not.toThrowError();
|
||||
});
|
||||
});
|
||||
@@ -33,18 +32,17 @@ describe('Text Input Components', () => {
|
||||
expect(() => textInputComponent().toJSON()).toThrowError();
|
||||
expect(() => {
|
||||
textInputComponent()
|
||||
.setCustomId('test')
|
||||
.setCustomId('a'.repeat(500))
|
||||
.setMaxLength(100)
|
||||
.setPlaceholder('hello')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setPlaceholder('a'.repeat(500))
|
||||
.setStyle(3 as TextInputStyle)
|
||||
.toJSON();
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid input THEN valid JSON outputs are given', () => {
|
||||
const textInputData: APITextInputComponent = {
|
||||
const textInputData = {
|
||||
type: ComponentType.TextInput,
|
||||
label: 'label',
|
||||
custom_id: 'custom id',
|
||||
placeholder: 'placeholder',
|
||||
max_length: 100,
|
||||
@@ -52,17 +50,16 @@ describe('Text Input Components', () => {
|
||||
value: 'value',
|
||||
required: false,
|
||||
style: TextInputStyle.Paragraph,
|
||||
};
|
||||
} satisfies APITextInputComponent;
|
||||
|
||||
expect(new TextInputBuilder(textInputData).toJSON()).toEqual(textInputData);
|
||||
expect(
|
||||
textInputComponent()
|
||||
.setCustomId(textInputData.custom_id)
|
||||
.setLabel(textInputData.label)
|
||||
.setPlaceholder(textInputData.placeholder!)
|
||||
.setMaxLength(textInputData.max_length!)
|
||||
.setMinLength(textInputData.min_length!)
|
||||
.setValue(textInputData.value!)
|
||||
.setPlaceholder(textInputData.placeholder)
|
||||
.setMaxLength(textInputData.max_length)
|
||||
.setMinLength(textInputData.min_length)
|
||||
.setValue(textInputData.value)
|
||||
.setRequired(textInputData.required)
|
||||
.setStyle(textInputData.style)
|
||||
.toJSON(),
|
||||
|
||||
@@ -32,4 +32,11 @@ describe('Separator', () => {
|
||||
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid id', () => {
|
||||
test('GIVEN a separator with a set spacing and an invalid set id THEN throws error', () => {
|
||||
const separator = new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large).setId(-1);
|
||||
expect(() => separator.toJSON()).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,11 @@ describe('TextDisplay', () => {
|
||||
expect(textDisplay.toJSON()).toEqual({ type: ComponentType.TextDisplay, content: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN a text display with a set content with an invalid id THEN throws error', () => {
|
||||
const textDisplay = new TextDisplayBuilder().setContent('foo').setId(5.5);
|
||||
expect(() => textDisplay.toJSON()).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN a text display with a pre-defined content THEN overwritten content THEN return valid toJSON data', () => {
|
||||
const textDisplay = new TextDisplayBuilder({ content: 'foo' });
|
||||
textDisplay.setContent('bar');
|
||||
|
||||
@@ -1,54 +1,60 @@
|
||||
import { ComponentType, TextInputStyle, type APIModalInteractionResponseCallbackData } from 'discord-api-types/v10';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { ActionRowBuilder, ModalBuilder, TextInputBuilder } from '../../src/index.js';
|
||||
import { ModalBuilder, TextInputBuilder, LabelBuilder, TextDisplayBuilder } from '../../src/index.js';
|
||||
|
||||
const modal = () => new ModalBuilder();
|
||||
const textInput = () =>
|
||||
new ActionRowBuilder().addTextInputComponent(
|
||||
new TextInputBuilder().setCustomId('text').setLabel(':3').setStyle(TextInputStyle.Short),
|
||||
);
|
||||
|
||||
const label = () =>
|
||||
new LabelBuilder()
|
||||
.setLabel('label')
|
||||
.setTextInputComponent(new TextInputBuilder().setCustomId('text').setStyle(TextInputStyle.Short));
|
||||
|
||||
const textDisplay = () => new TextDisplayBuilder().setContent('text');
|
||||
|
||||
describe('Modals', () => {
|
||||
test('GIVEN valid fields THEN builder does not throw', () => {
|
||||
expect(() => modal().setTitle('test').setCustomId('foobar').setActionRows(textInput()).toJSON()).not.toThrowError();
|
||||
expect(() => modal().setTitle('test').setCustomId('foobar').addActionRows(textInput()).toJSON()).not.toThrowError();
|
||||
expect(() =>
|
||||
modal().setTitle('test').setCustomId('foobar').addLabelComponents(label()).toJSON(),
|
||||
).not.toThrowError();
|
||||
|
||||
expect(() =>
|
||||
modal().setTitle('test').setCustomId('foobar').addLabelComponents(label()).toJSON(),
|
||||
).not.toThrowError();
|
||||
|
||||
expect(() =>
|
||||
modal().setTitle('test').setCustomId('foobar').addTextDisplayComponents(textDisplay()).toJSON(),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid fields THEN builder does throw', () => {
|
||||
expect(() => modal().setTitle('test').setCustomId('foobar').toJSON()).toThrowError();
|
||||
// @ts-expect-error: CustomId is invalid
|
||||
|
||||
// @ts-expect-error: Custom id is invalid
|
||||
expect(() => modal().setTitle('test').setCustomId(42).toJSON()).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid input THEN valid JSON outputs are given', () => {
|
||||
const modalData: APIModalInteractionResponseCallbackData = {
|
||||
const modalData = {
|
||||
title: 'title',
|
||||
custom_id: 'custom id',
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextInput,
|
||||
label: 'label',
|
||||
style: TextInputStyle.Paragraph,
|
||||
custom_id: 'custom id',
|
||||
},
|
||||
],
|
||||
type: ComponentType.Label,
|
||||
id: 33,
|
||||
label: 'label',
|
||||
description: 'description',
|
||||
component: {
|
||||
type: ComponentType.TextInput,
|
||||
style: TextInputStyle.Paragraph,
|
||||
custom_id: 'custom id',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextInput,
|
||||
label: 'label',
|
||||
style: TextInputStyle.Paragraph,
|
||||
custom_id: 'custom id',
|
||||
},
|
||||
],
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'yooooooooo',
|
||||
},
|
||||
],
|
||||
};
|
||||
} satisfies APIModalInteractionResponseCallbackData;
|
||||
|
||||
expect(new ModalBuilder(modalData).toJSON()).toEqual(modalData);
|
||||
|
||||
@@ -56,16 +62,14 @@ describe('Modals', () => {
|
||||
modal()
|
||||
.setTitle(modalData.title)
|
||||
.setCustomId('custom id')
|
||||
.setActionRows(
|
||||
new ActionRowBuilder().addTextInputComponent(
|
||||
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
|
||||
),
|
||||
.addLabelComponents(
|
||||
new LabelBuilder()
|
||||
.setId(33)
|
||||
.setLabel('label')
|
||||
.setDescription('description')
|
||||
.setTextInputComponent(new TextInputBuilder().setCustomId('custom id').setStyle(TextInputStyle.Paragraph)),
|
||||
)
|
||||
.addActionRows([
|
||||
new ActionRowBuilder().addTextInputComponent(
|
||||
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
|
||||
),
|
||||
])
|
||||
.addTextDisplayComponents((textDisplay) => textDisplay.setContent('yooooooooo'))
|
||||
.toJSON(),
|
||||
).toEqual(modalData);
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('Message', () => {
|
||||
|
||||
test('GIVEN bad action row THEN it throws', () => {
|
||||
const message = new MessageBuilder().addActionRowComponents((row) =>
|
||||
row.addTextInputComponent((input) => input.setCustomId('abc').setLabel('def')),
|
||||
row.addTextInputComponent((input) => input.setCustomId('abc')),
|
||||
);
|
||||
expect(() => message.toJSON()).toThrow();
|
||||
});
|
||||
|
||||
@@ -66,27 +66,27 @@
|
||||
"funding": "https://github.com/discordjs/discord.js?sponsor",
|
||||
"dependencies": {
|
||||
"@discordjs/util": "workspace:^",
|
||||
"discord-api-types": "^0.38.16",
|
||||
"discord-api-types": "^0.38.29",
|
||||
"ts-mixer": "^6.0.4",
|
||||
"tslib": "^2.8.1",
|
||||
"zod": "^4.0.5"
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@discordjs/api-extractor": "workspace:^",
|
||||
"@discordjs/scripts": "workspace:^",
|
||||
"@favware/cliff-jumper": "^4.1.0",
|
||||
"@types/node": "^22.16.3",
|
||||
"@types/node": "^22.18.8",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-env": "^10.1.0",
|
||||
"esbuild-plugin-version-injector": "^1.2.1",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tsup": "^8.5.0",
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "~5.8.3",
|
||||
"turbo": "^2.5.8",
|
||||
"typescript": "~5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Locale } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const idPredicate = z.int().min(0).max(2_147_483_647).optional();
|
||||
export const customIdPredicate = z.string().min(1).max(100);
|
||||
|
||||
export const memberPermissionsPredicate = z.coerce.bigint();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ButtonStyle, ChannelType, ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { customIdPredicate } from '../Assertions.js';
|
||||
import { idPredicate, customIdPredicate } from '../Assertions.js';
|
||||
|
||||
const labelPredicate = z.string().min(1).max(80);
|
||||
|
||||
@@ -52,6 +52,7 @@ export const buttonPredicate = z.discriminatedUnion('style', [
|
||||
]);
|
||||
|
||||
const selectMenuBasePredicate = z.object({
|
||||
id: idPredicate,
|
||||
placeholder: z.string().max(150).optional(),
|
||||
min_values: z.number().min(0).max(25).optional(),
|
||||
max_values: z.number().min(0).max(25).optional(),
|
||||
@@ -116,13 +117,26 @@ export const selectMenuStringPredicate = selectMenuBasePredicate
|
||||
input: minimum,
|
||||
});
|
||||
|
||||
if (ctx.value.max_values !== undefined && ctx.value.options.length < ctx.value.max_values) {
|
||||
addIssue('max_values', ctx.value.max_values);
|
||||
}
|
||||
|
||||
if (ctx.value.min_values !== undefined && ctx.value.options.length < ctx.value.min_values) {
|
||||
addIssue('min_values', ctx.value.min_values);
|
||||
}
|
||||
|
||||
if (
|
||||
ctx.value.min_values !== undefined &&
|
||||
ctx.value.max_values !== undefined &&
|
||||
ctx.value.min_values > ctx.value.max_values
|
||||
) {
|
||||
ctx.issues.push({
|
||||
code: 'too_big',
|
||||
message: `The maximum amount of options must be greater than or equal to the minimum amount of options`,
|
||||
inclusive: true,
|
||||
maximum: ctx.value.max_values,
|
||||
type: 'number',
|
||||
path: ['min_values'],
|
||||
origin: 'number',
|
||||
input: ctx.value.min_values,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const selectMenuUserPredicate = selectMenuBasePredicate.extend({
|
||||
@@ -135,6 +149,7 @@ export const selectMenuUserPredicate = selectMenuBasePredicate.extend({
|
||||
});
|
||||
|
||||
export const actionRowPredicate = z.object({
|
||||
id: idPredicate,
|
||||
type: z.literal(ComponentType.ActionRow),
|
||||
components: z.union([
|
||||
z
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from './button/CustomIdButton.js';
|
||||
import { LinkButtonBuilder } from './button/LinkButton.js';
|
||||
import { PremiumButtonBuilder } from './button/PremiumButton.js';
|
||||
import { LabelBuilder } from './label/Label.js';
|
||||
import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
|
||||
import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
|
||||
import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
|
||||
@@ -54,7 +55,7 @@ export type MessageComponentBuilder =
|
||||
/**
|
||||
* The builders that may be used for modals.
|
||||
*/
|
||||
export type ModalComponentBuilder = ActionRowBuilder | ModalActionRowComponentBuilder;
|
||||
export type ModalComponentBuilder = ActionRowBuilder | LabelBuilder | ModalActionRowComponentBuilder;
|
||||
|
||||
/**
|
||||
* Any button builder
|
||||
@@ -88,6 +89,11 @@ export type ModalActionRowComponentBuilder = TextInputBuilder;
|
||||
*/
|
||||
export type AnyActionRowComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder;
|
||||
|
||||
/**
|
||||
* Any modal component builder.
|
||||
*/
|
||||
export type AnyModalComponentBuilder = LabelBuilder | TextDisplayBuilder;
|
||||
|
||||
/**
|
||||
* Components here are mapped to their respective builder.
|
||||
*/
|
||||
@@ -152,6 +158,10 @@ export interface MappedComponentTypes {
|
||||
* The container component type is associated with a {@link ContainerBuilder}.
|
||||
*/
|
||||
[ComponentType.Container]: ContainerBuilder;
|
||||
/**
|
||||
* The label component type is associated with a {@link LabelBuilder}.
|
||||
*/
|
||||
[ComponentType.Label]: LabelBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,6 +223,8 @@ export function createComponentBuilder(
|
||||
return new SectionBuilder(data);
|
||||
case ComponentType.Container:
|
||||
return new ContainerBuilder(data);
|
||||
case ComponentType.Label:
|
||||
return new LabelBuilder(data);
|
||||
default:
|
||||
// @ts-expect-error This case can still occur if we get a newer unsupported component type
|
||||
throw new Error(`Cannot properly serialize component type: ${data.type}`);
|
||||
|
||||
26
packages/builders/src/components/label/Assertions.ts
Normal file
26
packages/builders/src/components/label/Assertions.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { idPredicate } from '../../Assertions';
|
||||
import {
|
||||
selectMenuChannelPredicate,
|
||||
selectMenuMentionablePredicate,
|
||||
selectMenuRolePredicate,
|
||||
selectMenuStringPredicate,
|
||||
selectMenuUserPredicate,
|
||||
} from '../Assertions';
|
||||
import { textInputPredicate } from '../textInput/Assertions';
|
||||
|
||||
export const labelPredicate = z.object({
|
||||
id: idPredicate,
|
||||
type: z.literal(ComponentType.Label),
|
||||
label: z.string().min(1).max(45),
|
||||
description: z.string().min(1).max(100).optional(),
|
||||
component: z.union([
|
||||
selectMenuStringPredicate,
|
||||
textInputPredicate,
|
||||
selectMenuUserPredicate,
|
||||
selectMenuRolePredicate,
|
||||
selectMenuMentionablePredicate,
|
||||
selectMenuChannelPredicate,
|
||||
]),
|
||||
});
|
||||
200
packages/builders/src/components/label/Label.ts
Normal file
200
packages/builders/src/components/label/Label.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import type {
|
||||
APIChannelSelectComponent,
|
||||
APILabelComponent,
|
||||
APIMentionableSelectComponent,
|
||||
APIRoleSelectComponent,
|
||||
APIStringSelectComponent,
|
||||
APITextInputComponent,
|
||||
APIUserSelectComponent,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { resolveBuilder } from '../../util/resolveBuilder.js';
|
||||
import { validate } from '../../util/validation.js';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
import { createComponentBuilder } from '../Components.js';
|
||||
import { ChannelSelectMenuBuilder } from '../selectMenu/ChannelSelectMenu.js';
|
||||
import { MentionableSelectMenuBuilder } from '../selectMenu/MentionableSelectMenu.js';
|
||||
import { RoleSelectMenuBuilder } from '../selectMenu/RoleSelectMenu.js';
|
||||
import { StringSelectMenuBuilder } from '../selectMenu/StringSelectMenu.js';
|
||||
import { UserSelectMenuBuilder } from '../selectMenu/UserSelectMenu.js';
|
||||
import { TextInputBuilder } from '../textInput/TextInput.js';
|
||||
import { labelPredicate } from './Assertions.js';
|
||||
|
||||
export interface LabelBuilderData extends Partial<Omit<APILabelComponent, 'component'>> {
|
||||
component?:
|
||||
| ChannelSelectMenuBuilder
|
||||
| MentionableSelectMenuBuilder
|
||||
| RoleSelectMenuBuilder
|
||||
| StringSelectMenuBuilder
|
||||
| TextInputBuilder
|
||||
| UserSelectMenuBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for labels.
|
||||
*/
|
||||
export class LabelBuilder extends ComponentBuilder<APILabelComponent> {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected readonly data: LabelBuilderData;
|
||||
|
||||
/**
|
||||
* Creates a new label.
|
||||
*
|
||||
* @param data - The API data to create this label with
|
||||
* @example
|
||||
* Creating a label from an API data object:
|
||||
* ```ts
|
||||
* const label = new LabelBuilder({
|
||||
* label: "label",
|
||||
* component,
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a label using setters and API data:
|
||||
* ```ts
|
||||
* const label = new LabelBuilder({
|
||||
* label: 'label',
|
||||
* component,
|
||||
* }).setContent('new text');
|
||||
* ```
|
||||
*/
|
||||
public constructor(data: Partial<APILabelComponent> = {}) {
|
||||
super();
|
||||
|
||||
const { component, ...rest } = data;
|
||||
|
||||
this.data = {
|
||||
...structuredClone(rest),
|
||||
component: component ? createComponentBuilder(component) : undefined,
|
||||
type: ComponentType.Label,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label for this label.
|
||||
*
|
||||
* @param label - The label to use
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
this.data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description for this label.
|
||||
*
|
||||
* @param description - The description to use
|
||||
*/
|
||||
public setDescription(description: string) {
|
||||
this.data.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the description for this label.
|
||||
*/
|
||||
public clearDescription() {
|
||||
this.data.description = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a string select menu component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setStringSelectMenuComponent(
|
||||
input:
|
||||
| APIStringSelectComponent
|
||||
| StringSelectMenuBuilder
|
||||
| ((builder: StringSelectMenuBuilder) => StringSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, StringSelectMenuBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a user select menu component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setUserSelectMenuComponent(
|
||||
input: APIUserSelectComponent | UserSelectMenuBuilder | ((builder: UserSelectMenuBuilder) => UserSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, UserSelectMenuBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a role select menu component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setRoleSelectMenuComponent(
|
||||
input: APIRoleSelectComponent | RoleSelectMenuBuilder | ((builder: RoleSelectMenuBuilder) => RoleSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, RoleSelectMenuBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a mentionable select menu component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setMentionableSelectMenuComponent(
|
||||
input:
|
||||
| APIMentionableSelectComponent
|
||||
| MentionableSelectMenuBuilder
|
||||
| ((builder: MentionableSelectMenuBuilder) => MentionableSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, MentionableSelectMenuBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a channel select menu component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setChannelSelectMenuComponent(
|
||||
input:
|
||||
| APIChannelSelectComponent
|
||||
| ChannelSelectMenuBuilder
|
||||
| ((builder: ChannelSelectMenuBuilder) => ChannelSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, ChannelSelectMenuBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a text input component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setTextInputComponent(
|
||||
input: APITextInputComponent | TextInputBuilder | ((builder: TextInputBuilder) => TextInputBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, TextInputBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(validationOverride?: boolean): APILabelComponent {
|
||||
const { component, ...rest } = this.data;
|
||||
|
||||
const data = {
|
||||
...structuredClone(rest),
|
||||
// The label predicate validates the component.
|
||||
component: component?.toJSON(false),
|
||||
};
|
||||
|
||||
validate(labelPredicate, data, validationOverride);
|
||||
|
||||
return data as APILabelComponent;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export abstract class BaseSelectMenuBuilder<Data extends APISelectMenuComponent>
|
||||
* @internal
|
||||
*/
|
||||
protected abstract override readonly data: Partial<
|
||||
Pick<Data, 'custom_id' | 'disabled' | 'id' | 'max_values' | 'min_values' | 'placeholder'>
|
||||
Pick<Data, 'custom_id' | 'disabled' | 'id' | 'max_values' | 'min_values' | 'placeholder' | 'required'>
|
||||
>;
|
||||
|
||||
/**
|
||||
@@ -75,4 +75,15 @@ export abstract class BaseSelectMenuBuilder<Data extends APISelectMenuComponent>
|
||||
this.data.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this select menu is required.
|
||||
*
|
||||
* @remarks Only for use in modals.
|
||||
* @param required - Whether this string select menu is required
|
||||
*/
|
||||
public setRequired(required = true) {
|
||||
this.data.required = required;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { StringSelectMenuOptionBuilder } from './StringSelectMenuOption.js';
|
||||
|
||||
export interface StringSelectMenuData extends Partial<Omit<APIStringSelectComponent, 'options'>> {
|
||||
options: StringSelectMenuOptionBuilder[];
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { customIdPredicate } from '../../Assertions.js';
|
||||
import { customIdPredicate, idPredicate } from '../../Assertions.js';
|
||||
|
||||
export const textInputPredicate = z.object({
|
||||
id: idPredicate,
|
||||
type: z.literal(ComponentType.TextInput),
|
||||
custom_id: customIdPredicate,
|
||||
label: z.string().min(1).max(45),
|
||||
style: z.enum(TextInputStyle),
|
||||
min_length: z.number().min(0).max(4_000).optional(),
|
||||
max_length: z.number().min(1).max(4_000).optional(),
|
||||
|
||||
@@ -21,7 +21,7 @@ export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
|
||||
* ```ts
|
||||
* const textInput = new TextInputBuilder({
|
||||
* custom_id: 'a cool text input',
|
||||
* label: 'Type something',
|
||||
* placeholder: 'Type something',
|
||||
* style: TextInputStyle.Short,
|
||||
* });
|
||||
* ```
|
||||
@@ -29,7 +29,7 @@ export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
|
||||
* Creating a text input using setters and API data:
|
||||
* ```ts
|
||||
* const textInput = new TextInputBuilder({
|
||||
* label: 'Type something else',
|
||||
* placeholder: 'Type something else',
|
||||
* })
|
||||
* .setCustomId('woah')
|
||||
* .setStyle(TextInputStyle.Paragraph);
|
||||
@@ -50,16 +50,6 @@ export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label for this text input.
|
||||
*
|
||||
* @param label - The label to use
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
this.data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the style for this text input.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ComponentType, SeparatorSpacingSize } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { idPredicate } from '../../Assertions.js';
|
||||
import { actionRowPredicate } from '../Assertions.js';
|
||||
|
||||
const unfurledMediaItemPredicate = z.object({
|
||||
@@ -7,6 +8,8 @@ const unfurledMediaItemPredicate = z.object({
|
||||
});
|
||||
|
||||
export const thumbnailPredicate = z.object({
|
||||
type: z.literal(ComponentType.Thumbnail),
|
||||
id: idPredicate,
|
||||
media: unfurledMediaItemPredicate,
|
||||
description: z.string().min(1).max(1_024).nullish(),
|
||||
spoiler: z.boolean().optional(),
|
||||
@@ -17,30 +20,41 @@ const unfurledMediaItemAttachmentOnlyPredicate = z.object({
|
||||
});
|
||||
|
||||
export const filePredicate = z.object({
|
||||
type: z.literal(ComponentType.File),
|
||||
id: idPredicate,
|
||||
file: unfurledMediaItemAttachmentOnlyPredicate,
|
||||
spoiler: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const separatorPredicate = z.object({
|
||||
type: z.literal(ComponentType.Separator),
|
||||
id: idPredicate,
|
||||
divider: z.boolean().optional(),
|
||||
spacing: z.enum(SeparatorSpacingSize).optional(),
|
||||
});
|
||||
|
||||
export const textDisplayPredicate = z.object({
|
||||
type: z.literal(ComponentType.TextDisplay),
|
||||
id: idPredicate,
|
||||
content: z.string().min(1).max(4_000),
|
||||
});
|
||||
|
||||
export const mediaGalleryItemPredicate = z.object({
|
||||
id: idPredicate,
|
||||
media: unfurledMediaItemPredicate,
|
||||
description: z.string().min(1).max(1_024).nullish(),
|
||||
spoiler: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const mediaGalleryPredicate = z.object({
|
||||
type: z.literal(ComponentType.MediaGallery),
|
||||
id: idPredicate,
|
||||
items: z.array(mediaGalleryItemPredicate).min(1).max(10),
|
||||
});
|
||||
|
||||
export const sectionPredicate = z.object({
|
||||
type: z.literal(ComponentType.Section),
|
||||
id: idPredicate,
|
||||
components: z.array(textDisplayPredicate).min(1).max(3),
|
||||
accessory: z.union([
|
||||
z.object({ type: z.literal(ComponentType.Button) }),
|
||||
@@ -49,6 +63,8 @@ export const sectionPredicate = z.object({
|
||||
});
|
||||
|
||||
export const containerPredicate = z.object({
|
||||
type: z.literal(ComponentType.Container),
|
||||
id: idPredicate,
|
||||
components: z
|
||||
.array(
|
||||
z.union([
|
||||
|
||||
@@ -4,6 +4,9 @@ export * from './components/button/CustomIdButton.js';
|
||||
export * from './components/button/LinkButton.js';
|
||||
export * from './components/button/PremiumButton.js';
|
||||
|
||||
export * from './components/label/Label.js';
|
||||
export * from './components/label/Assertions.js';
|
||||
|
||||
export * from './components/selectMenu/BaseSelectMenu.js';
|
||||
export * from './components/selectMenu/ChannelSelectMenu.js';
|
||||
export * from './components/selectMenu/MentionableSelectMenu.js';
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { customIdPredicate } from '../../Assertions.js';
|
||||
import { labelPredicate } from '../../components/label/Assertions.js';
|
||||
import { textDisplayPredicate } from '../../components/v2/Assertions.js';
|
||||
|
||||
const titlePredicate = z.string().min(1).max(45);
|
||||
|
||||
@@ -8,13 +10,17 @@ export const modalPredicate = z.object({
|
||||
title: titlePredicate,
|
||||
custom_id: customIdPredicate,
|
||||
components: z
|
||||
.object({
|
||||
type: z.literal(ComponentType.ActionRow),
|
||||
components: z
|
||||
.object({ type: z.literal(ComponentType.TextInput) })
|
||||
.array()
|
||||
.length(1),
|
||||
})
|
||||
.union([
|
||||
z.object({
|
||||
type: z.literal(ComponentType.ActionRow),
|
||||
components: z
|
||||
.object({ type: z.literal(ComponentType.TextInput) })
|
||||
.array()
|
||||
.length(1),
|
||||
}),
|
||||
labelPredicate,
|
||||
textDisplayPredicate,
|
||||
])
|
||||
.array()
|
||||
.min(1)
|
||||
.max(5),
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
APIComponentInModalActionRow,
|
||||
APILabelComponent,
|
||||
APIModalInteractionResponseCallbackData,
|
||||
APITextDisplayComponent,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ActionRowBuilder } from '../../components/ActionRow.js';
|
||||
import type { ActionRowBuilder } from '../../components/ActionRow.js';
|
||||
import type { AnyModalComponentBuilder } from '../../components/Components.js';
|
||||
import { createComponentBuilder } from '../../components/Components.js';
|
||||
import { LabelBuilder } from '../../components/label/Label.js';
|
||||
import { TextDisplayBuilder } from '../../components/v2/TextDisplay.js';
|
||||
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
||||
import { resolveBuilder } from '../../util/resolveBuilder.js';
|
||||
import { validate } from '../../util/validation.js';
|
||||
import { modalPredicate } from './Assertions.js';
|
||||
|
||||
export interface ModalBuilderData extends Partial<Omit<APIModalInteractionResponseCallbackData, 'components'>> {
|
||||
components: ActionRowBuilder[];
|
||||
components: (ActionRowBuilder | AnyModalComponentBuilder)[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,7 +30,7 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
|
||||
/**
|
||||
* The components within this modal.
|
||||
*/
|
||||
public get components(): readonly ActionRowBuilder[] {
|
||||
public get components(): readonly (ActionRowBuilder | AnyModalComponentBuilder)[] {
|
||||
return this.data.components;
|
||||
}
|
||||
|
||||
@@ -66,19 +69,15 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds action rows to this modal.
|
||||
* Adds label components to this modal.
|
||||
*
|
||||
* @param components - The components to add
|
||||
*/
|
||||
public addActionRows(
|
||||
...components: RestOrArray<
|
||||
| ActionRowBuilder
|
||||
| APIActionRowComponent<APIComponentInModalActionRow>
|
||||
| ((builder: ActionRowBuilder) => ActionRowBuilder)
|
||||
>
|
||||
public addLabelComponents(
|
||||
...components: RestOrArray<APILabelComponent | LabelBuilder | ((builder: LabelBuilder) => LabelBuilder)>
|
||||
) {
|
||||
const normalized = normalizeArray(components);
|
||||
const resolved = normalized.map((row) => resolveBuilder(row, ActionRowBuilder));
|
||||
const resolved = normalized.map((label) => resolveBuilder(label, LabelBuilder));
|
||||
|
||||
this.data.components.push(...resolved);
|
||||
|
||||
@@ -86,64 +85,54 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the action rows for this modal.
|
||||
* Adds text display components to this modal.
|
||||
*
|
||||
* @param components - The components to set
|
||||
* @param components - The components to add
|
||||
*/
|
||||
public setActionRows(
|
||||
public addTextDisplayComponents(
|
||||
...components: RestOrArray<
|
||||
| ActionRowBuilder
|
||||
| APIActionRowComponent<APIComponentInModalActionRow>
|
||||
| ((builder: ActionRowBuilder) => ActionRowBuilder)
|
||||
APITextDisplayComponent | TextDisplayBuilder | ((builder: TextDisplayBuilder) => TextDisplayBuilder)
|
||||
>
|
||||
) {
|
||||
const normalized = normalizeArray(components);
|
||||
this.spliceActionRows(0, this.data.components.length, ...normalized);
|
||||
const resolved = normalized.map((row) => resolveBuilder(row, TextDisplayBuilder));
|
||||
|
||||
this.data.components.push(...resolved);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts action rows for this modal.
|
||||
* Removes, replaces, or inserts components for this modal.
|
||||
*
|
||||
* @remarks
|
||||
* This method behaves similarly
|
||||
* to {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
|
||||
* The maximum amount of action rows that can be added is 5.
|
||||
* The maximum amount of components that can be added is 5.
|
||||
*
|
||||
* It's useful for modifying and adjusting order of the already-existing action rows of a modal.
|
||||
* It's useful for modifying and adjusting order of the already-existing components of a modal.
|
||||
* @example
|
||||
* Remove the first action row:
|
||||
* Remove the first component:
|
||||
* ```ts
|
||||
* embed.spliceActionRows(0, 1);
|
||||
* modal.spliceComponents(0, 1);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the first n action rows:
|
||||
* Remove the first n components:
|
||||
* ```ts
|
||||
* const n = 4;
|
||||
* embed.spliceActionRows(0, n);
|
||||
* modal.spliceComponents(0, n);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the last action row:
|
||||
* Remove the last component:
|
||||
* ```ts
|
||||
* embed.spliceActionRows(-1, 1);
|
||||
* modal.spliceComponents(-1, 1);
|
||||
* ```
|
||||
* @param index - The index to start at
|
||||
* @param deleteCount - The number of action rows to remove
|
||||
* @param rows - The replacing action row objects
|
||||
* @param deleteCount - The number of components to remove
|
||||
* @param components - The replacing components
|
||||
*/
|
||||
public spliceActionRows(
|
||||
index: number,
|
||||
deleteCount: number,
|
||||
...rows: (
|
||||
| ActionRowBuilder
|
||||
| APIActionRowComponent<APIComponentInModalActionRow>
|
||||
| ((builder: ActionRowBuilder) => ActionRowBuilder)
|
||||
)[]
|
||||
): this {
|
||||
const resolved = rows.map((row) => resolveBuilder(row, ActionRowBuilder));
|
||||
this.data.components.splice(index, deleteCount, ...resolved);
|
||||
|
||||
public spliceComponents(index: number, deleteCount: number, ...components: AnyModalComponentBuilder[]): this {
|
||||
this.data.components.splice(index, deleteCount, ...components);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,18 +64,18 @@
|
||||
"@discordjs/api-extractor": "workspace:^",
|
||||
"@discordjs/scripts": "workspace:^",
|
||||
"@favware/cliff-jumper": "^4.1.0",
|
||||
"@types/node": "^22.16.3",
|
||||
"@types/node": "^22.18.8",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-env": "^10.1.0",
|
||||
"esbuild-plugin-version-injector": "^1.2.1",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tsup": "^8.5.0",
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "~5.8.3",
|
||||
"turbo": "^2.5.8",
|
||||
"typescript": "~5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { REST } from '@discordjs/rest';
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
APIComponentInModalActionRow,
|
||||
RESTGetAPIChannelThreadMemberResult,
|
||||
RESTPostAPIInteractionCallbackWithResponseResult,
|
||||
} from 'discord-api-types/v10';
|
||||
import { expectTypeOf, describe, test } from 'vitest';
|
||||
@@ -158,3 +159,20 @@ describe('Interaction with_response overloads.', () => {
|
||||
Promise<RESTPostAPIInteractionCallbackWithResponseResult | undefined>
|
||||
>());
|
||||
});
|
||||
|
||||
describe('Thread member overloads.', () => {
|
||||
test('Getting a thread member with with_member makes the guild member present.', () =>
|
||||
expectTypeOf(api.threads.getMember(SNOWFLAKE, SNOWFLAKE, { with_member: true })).toEqualTypeOf<
|
||||
Promise<Required<Pick<RESTGetAPIChannelThreadMemberResult, 'member'>> & RESTGetAPIChannelThreadMemberResult>
|
||||
>());
|
||||
|
||||
test('Getting a thread member without with_member returns RESTGetAPIChannelThreadMemberResult.', () => {
|
||||
expectTypeOf(api.threads.getMember(SNOWFLAKE, SNOWFLAKE, { with_member: false })).toEqualTypeOf<
|
||||
Promise<RESTGetAPIChannelThreadMemberResult>
|
||||
>();
|
||||
|
||||
expectTypeOf(api.threads.getMember(SNOWFLAKE, SNOWFLAKE)).toEqualTypeOf<
|
||||
Promise<RESTGetAPIChannelThreadMemberResult>
|
||||
>();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,25 +69,25 @@
|
||||
"@discordjs/util": "workspace:^",
|
||||
"@discordjs/ws": "workspace:^",
|
||||
"@sapphire/snowflake": "^3.5.5",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"discord-api-types": "^0.38.16"
|
||||
"@vladfrangu/async_event_emitter": "^2.4.7",
|
||||
"discord-api-types": "^0.38.29"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@discordjs/api-extractor": "workspace:^",
|
||||
"@discordjs/scripts": "workspace:^",
|
||||
"@favware/cliff-jumper": "^4.1.0",
|
||||
"@types/node": "^22.16.3",
|
||||
"@types/node": "^22.18.8",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-env": "^10.1.0",
|
||||
"esbuild-plugin-version-injector": "^1.2.1",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tsup": "^8.5.0",
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "~5.8.3",
|
||||
"turbo": "^2.5.8",
|
||||
"typescript": "~5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
34
packages/core/scripts/check-routes.mjs
Normal file
34
packages/core/scripts/check-routes.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Routes } from 'discord-api-types/v10';
|
||||
import { glob, readFile } from 'node:fs/promises';
|
||||
|
||||
const usedRoutes = new Set();
|
||||
|
||||
const ignoredRoutes = new Set([
|
||||
// Deprecated
|
||||
'channelPins',
|
||||
'channelPin',
|
||||
'guilds',
|
||||
'guildCurrentMemberNickname',
|
||||
'guildMFA',
|
||||
'nitroStickerPacks',
|
||||
]);
|
||||
|
||||
for await (const file of glob('src/api/*.ts')) {
|
||||
const content = await readFile(file, 'utf-8');
|
||||
|
||||
const routes = content.matchAll(/Routes\.([\w\d_]+)/g);
|
||||
for (const route of routes) {
|
||||
usedRoutes.add(route[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const unusedRoutes = Object.keys(Routes).filter((route) => !usedRoutes.has(route) && !ignoredRoutes.has(route));
|
||||
|
||||
if (unusedRoutes.length > 0) {
|
||||
console.warn('The following routes are not implemented:');
|
||||
for (const route of unusedRoutes) {
|
||||
console.warn(` - ${route}`);
|
||||
}
|
||||
} else {
|
||||
console.log('No missing routes.');
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
|
||||
import { makeURLSearchParams, type RawFile, type REST, type RequestData } from '@discordjs/rest';
|
||||
import { makeURLSearchParams, type RawFile, type RequestData, type REST } from '@discordjs/rest';
|
||||
import {
|
||||
Routes,
|
||||
type RESTPostAPIChannelWebhookJSONBody,
|
||||
type RESTPostAPIChannelWebhookResult,
|
||||
type APIThreadChannel,
|
||||
type RESTDeleteAPIChannelResult,
|
||||
type RESTGetAPIChannelInvitesResult,
|
||||
type RESTGetAPIChannelMessageReactionUsersQuery,
|
||||
@@ -18,8 +17,8 @@ import {
|
||||
type RESTGetAPIChannelThreadsArchivedQuery,
|
||||
type RESTGetAPIChannelUsersThreadsArchivedResult,
|
||||
type RESTGetAPIChannelWebhooksResult,
|
||||
type RESTPatchAPIChannelMessageJSONBody,
|
||||
type RESTPatchAPIChannelJSONBody,
|
||||
type RESTPatchAPIChannelMessageJSONBody,
|
||||
type RESTPatchAPIChannelMessageResult,
|
||||
type RESTPatchAPIChannelResult,
|
||||
type RESTPostAPIChannelFollowersResult,
|
||||
@@ -28,14 +27,16 @@ import {
|
||||
type RESTPostAPIChannelMessageCrosspostResult,
|
||||
type RESTPostAPIChannelMessageJSONBody,
|
||||
type RESTPostAPIChannelMessageResult,
|
||||
type RESTPutAPIChannelPermissionJSONBody,
|
||||
type Snowflake,
|
||||
type RESTPostAPIChannelThreadsJSONBody,
|
||||
type RESTPostAPIChannelThreadsResult,
|
||||
type APIThreadChannel,
|
||||
type RESTPostAPIChannelWebhookJSONBody,
|
||||
type RESTPostAPIChannelWebhookResult,
|
||||
type RESTPostAPIGuildForumThreadsJSONBody,
|
||||
type RESTPostAPISoundboardSendSoundJSONBody,
|
||||
type RESTPostAPISendSoundboardSoundResult,
|
||||
type RESTPostAPISoundboardSendSoundJSONBody,
|
||||
type RESTPutAPIChannelPermissionJSONBody,
|
||||
type RESTPutAPIChannelRecipientJSONBody,
|
||||
type Snowflake,
|
||||
} from 'discord-api-types/v10';
|
||||
|
||||
export interface StartForumThreadOptions extends RESTPostAPIGuildForumThreadsJSONBody {
|
||||
@@ -708,4 +709,45 @@ export class ChannelsAPI {
|
||||
signal,
|
||||
}) as Promise<RESTPostAPISendSoundboardSoundResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a recipient to a group DM channel
|
||||
*
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#group-dm-add-recipient}
|
||||
* @param channelId - The id of the channel to add the recipient to
|
||||
* @param userId - The id of the user to add as a recipient
|
||||
* @param body - The data for adding the recipient
|
||||
* @param options - The options for adding the recipient
|
||||
*/
|
||||
public async addGroupDMRecipient(
|
||||
channelId: Snowflake,
|
||||
userId: Snowflake,
|
||||
body: RESTPutAPIChannelRecipientJSONBody,
|
||||
{ auth, signal }: Pick<RequestData, 'auth' | 'signal'> = {},
|
||||
) {
|
||||
await this.rest.put(Routes.channelRecipient(channelId, userId), {
|
||||
auth,
|
||||
body,
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a recipient from a group DM channel
|
||||
*
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#group-dm-remove-recipient}
|
||||
* @param channelId - The id of the channel to remove the recipient from
|
||||
* @param userId - The id of the user to remove as a recipient
|
||||
* @param options - The options for removing the recipient
|
||||
*/
|
||||
public async removeGroupDMRecipient(
|
||||
channelId: Snowflake,
|
||||
userId: Snowflake,
|
||||
{ auth, signal }: Pick<RequestData, 'auth' | 'signal'> = {},
|
||||
) {
|
||||
await this.rest.delete(Routes.channelRecipient(channelId, userId), {
|
||||
auth,
|
||||
signal,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
34
packages/core/src/api/gateway.ts
Normal file
34
packages/core/src/api/gateway.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
|
||||
import type { RequestData, REST } from '@discordjs/rest';
|
||||
import { Routes, type RESTGetAPIGatewayBotResult, type RESTGetAPIGatewayResult } from 'discord-api-types/v10';
|
||||
|
||||
export class GatewayAPI {
|
||||
public constructor(private readonly rest: REST) {}
|
||||
|
||||
/**
|
||||
* Gets gateway information.
|
||||
*
|
||||
* @see {@link https://discord.com/developers/docs/events/gateway#get-gateway}
|
||||
* @param options - The options for fetching the gateway information
|
||||
*/
|
||||
public async get({ signal }: Pick<RequestData, 'signal'> = {}) {
|
||||
return this.rest.get(Routes.gateway(), {
|
||||
auth: false,
|
||||
signal,
|
||||
}) as Promise<RESTGetAPIGatewayResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets gateway information with additional metadata.
|
||||
*
|
||||
* @see {@link https://discord.com/developers/docs/events/gateway#get-gateway-bot}
|
||||
* @param options - The options for fetching the gateway information
|
||||
*/
|
||||
public async getBot({ auth, signal }: Pick<RequestData, 'auth' | 'signal'> = {}) {
|
||||
return this.rest.get(Routes.gatewayBot(), {
|
||||
auth,
|
||||
signal,
|
||||
}) as Promise<RESTGetAPIGatewayBotResult>;
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,8 @@ import {
|
||||
type RESTPostAPIGuildTemplatesJSONBody,
|
||||
type RESTPostAPIGuildTemplatesResult,
|
||||
type RESTPutAPIGuildBanJSONBody,
|
||||
type RESTPutAPIGuildIncidentActionsJSONBody,
|
||||
type RESTPutAPIGuildIncidentActionsResult,
|
||||
type RESTPutAPIGuildMemberJSONBody,
|
||||
type RESTPutAPIGuildMemberResult,
|
||||
type RESTPutAPIGuildOnboardingJSONBody,
|
||||
@@ -1439,4 +1441,24 @@ export class GuildsAPI {
|
||||
) {
|
||||
await this.rest.delete(Routes.guildSoundboardSound(guildId, soundId), { auth, reason, signal });
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies incident actions for a guild.
|
||||
*
|
||||
* @see {@link https://discord.com/developers/docs/resources/guild#modify-guild-incident-actions}
|
||||
* @param guildId - The id of the guild
|
||||
* @param body - The data for modifying guild incident actions
|
||||
* @param options - The options for modifying guild incident actions
|
||||
*/
|
||||
public async editIncidentActions(
|
||||
guildId: Snowflake,
|
||||
body: RESTPutAPIGuildIncidentActionsJSONBody,
|
||||
{ auth, signal }: Pick<RequestData, 'auth' | 'signal'> = {},
|
||||
) {
|
||||
return this.rest.put(Routes.guildIncidentActions(guildId), {
|
||||
auth,
|
||||
body,
|
||||
signal,
|
||||
}) as Promise<RESTPutAPIGuildIncidentActionsResult>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { REST } from '@discordjs/rest';
|
||||
import { ApplicationCommandsAPI } from './applicationCommands.js';
|
||||
import { ApplicationsAPI } from './applications.js';
|
||||
import { ChannelsAPI } from './channel.js';
|
||||
import { GatewayAPI } from './gateway.js';
|
||||
import { GuildsAPI } from './guild.js';
|
||||
import { InteractionsAPI } from './interactions.js';
|
||||
import { InvitesAPI } from './invite.js';
|
||||
@@ -20,6 +21,7 @@ import { WebhooksAPI } from './webhook.js';
|
||||
export * from './applicationCommands.js';
|
||||
export * from './applications.js';
|
||||
export * from './channel.js';
|
||||
export * from './gateway.js';
|
||||
export * from './guild.js';
|
||||
export * from './interactions.js';
|
||||
export * from './invite.js';
|
||||
@@ -42,6 +44,8 @@ export class API {
|
||||
|
||||
public readonly channels: ChannelsAPI;
|
||||
|
||||
public readonly gateway: GatewayAPI;
|
||||
|
||||
public readonly guilds: GuildsAPI;
|
||||
|
||||
public readonly interactions: InteractionsAPI;
|
||||
@@ -74,6 +78,7 @@ export class API {
|
||||
this.applicationCommands = new ApplicationCommandsAPI(rest);
|
||||
this.applications = new ApplicationsAPI(rest);
|
||||
this.channels = new ChannelsAPI(rest);
|
||||
this.gateway = new GatewayAPI(rest);
|
||||
this.guilds = new GuildsAPI(rest);
|
||||
this.invites = new InvitesAPI(rest);
|
||||
this.monetization = new MonetizationAPI(rest);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
|
||||
import type { RequestData, REST } from '@discordjs/rest';
|
||||
import { makeURLSearchParams, type RequestData, type REST } from '@discordjs/rest';
|
||||
import {
|
||||
Routes,
|
||||
type APIThreadMember,
|
||||
type RESTGetAPIChannelThreadMemberQuery,
|
||||
type RESTGetAPIChannelThreadMemberResult,
|
||||
type RESTGetAPIChannelThreadMembersResult,
|
||||
type Snowflake,
|
||||
} from 'discord-api-types/v10';
|
||||
@@ -71,14 +72,43 @@ export class ThreadsAPI {
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#get-thread-member}
|
||||
* @param threadId - The id of the thread to fetch the member from
|
||||
* @param userId - The id of the user
|
||||
* @param query - The query for fetching the member
|
||||
* @param options - The options for fetching the member
|
||||
*/
|
||||
public async getMember(
|
||||
threadId: Snowflake,
|
||||
userId: Snowflake,
|
||||
query: RESTGetAPIChannelThreadMemberQuery & { with_member: true },
|
||||
options?: Pick<RequestData, 'auth' | 'signal'>,
|
||||
): Promise<Required<Pick<RESTGetAPIChannelThreadMemberResult, 'member'>> & RESTGetAPIChannelThreadMemberResult>;
|
||||
|
||||
/**
|
||||
* Fetches a member of a thread
|
||||
*
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#get-thread-member}
|
||||
* @param threadId - The id of the thread to fetch the member from
|
||||
* @param userId - The id of the user
|
||||
* @param query - The query for fetching the member
|
||||
* @param options - The options for fetching the member
|
||||
*/
|
||||
public async getMember(
|
||||
threadId: Snowflake,
|
||||
userId: Snowflake,
|
||||
query?: RESTGetAPIChannelThreadMemberQuery,
|
||||
options?: Pick<RequestData, 'auth' | 'signal'>,
|
||||
): Promise<RESTGetAPIChannelThreadMemberResult>;
|
||||
|
||||
public async getMember(
|
||||
threadId: Snowflake,
|
||||
userId: Snowflake,
|
||||
query: RESTGetAPIChannelThreadMemberQuery = {},
|
||||
{ auth, signal }: Pick<RequestData, 'auth' | 'signal'> = {},
|
||||
) {
|
||||
return this.rest.get(Routes.threadMembers(threadId, userId), { auth, signal }) as Promise<APIThreadMember>;
|
||||
return this.rest.get(Routes.threadMembers(threadId, userId), {
|
||||
auth,
|
||||
signal,
|
||||
query: makeURLSearchParams(query),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
type RESTGetAPICurrentUserResult,
|
||||
type RESTGetAPIUserResult,
|
||||
type RESTGetCurrentUserGuildMemberResult,
|
||||
type RESTPatchAPICurrentGuildMemberJSONBody,
|
||||
type RESTPatchAPICurrentUserJSONBody,
|
||||
type RESTPatchAPICurrentUserResult,
|
||||
type RESTPatchAPIGuildMemberJSONBody,
|
||||
type RESTPatchAPIGuildMemberResult,
|
||||
type RESTPostAPICurrentUserCreateDMChannelResult,
|
||||
type RESTPutAPICurrentUserApplicationRoleConnectionJSONBody,
|
||||
@@ -111,7 +111,7 @@ export class UsersAPI {
|
||||
*/
|
||||
public async editCurrentGuildMember(
|
||||
guildId: Snowflake,
|
||||
body: RESTPatchAPIGuildMemberJSONBody = {},
|
||||
body: RESTPatchAPICurrentGuildMemberJSONBody = {},
|
||||
{ auth, reason, signal }: Pick<RequestData, 'auth' | 'reason' | 'signal'> = {},
|
||||
) {
|
||||
return this.rest.patch(Routes.guildMember(guildId, '@me'), {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { styleText } from 'node:util';
|
||||
import { Option, program } from 'commander';
|
||||
import prompts from 'prompts';
|
||||
import validateProjectName from 'validate-npm-package-name';
|
||||
import packageJSON from '../package.json' assert { type: 'json' };
|
||||
import packageJSON from '../package.json' with { type: 'json' };
|
||||
import { createDiscordBot } from '../src/create-discord-bot.js';
|
||||
import { resolvePackageManager } from '../src/helpers/packageManager.js';
|
||||
import { DEFAULT_PROJECT_NAME, PACKAGE_MANAGERS } from '../src/util/constants.js';
|
||||
|
||||
@@ -50,25 +50,25 @@
|
||||
"homepage": "https://discord.js.org",
|
||||
"funding": "https://github.com/discordjs/discord.js?sponsor",
|
||||
"dependencies": {
|
||||
"commander": "^14.0.0",
|
||||
"commander": "^14.0.1",
|
||||
"prompts": "^2.4.2",
|
||||
"validate-npm-package-name": "^6.0.1"
|
||||
"validate-npm-package-name": "^6.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@discordjs/api-extractor": "workspace:^",
|
||||
"@favware/cliff-jumper": "^4.1.0",
|
||||
"@types/node": "^22.16.3",
|
||||
"@types/node": "^22.18.8",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@types/validate-npm-package-name": "^4.0.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.30.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"terser": "^5.43.1",
|
||||
"terser": "^5.44.0",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "~5.8.3"
|
||||
"typescript": "~5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
|
||||
@@ -14,6 +14,11 @@ export type PackageManager = 'bun' | 'deno' | 'npm' | 'pnpm' | 'yarn';
|
||||
export function resolvePackageManager(): PackageManager {
|
||||
const npmConfigUserAgent = process.env.npm_config_user_agent;
|
||||
|
||||
// @ts-expect-error: We're not using Deno's types, so its global is not declared
|
||||
if (typeof Deno !== 'undefined') {
|
||||
return 'deno';
|
||||
}
|
||||
|
||||
// If this is not present, return the default package manager.
|
||||
if (!npmConfigUserAgent) {
|
||||
return DEFAULT_PACKAGE_MANAGER;
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
"start": "bun run src/index.[REPLACE_IMPORT_EXT]"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/core": "^2.2.0",
|
||||
"discord.js": "^14.21.0"
|
||||
"@discordjs/core": "^2.2.2",
|
||||
"discord.js": "^14.22.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"zod": "^3.25.74"
|
||||
"zod": "^3.25.76"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,18 +11,18 @@
|
||||
"start": "bun run src/index.[REPLACE_IMPORT_EXT]"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/core": "^2.2.0",
|
||||
"discord.js": "^14.21.0"
|
||||
"@discordjs/core": "^2.2.2",
|
||||
"discord.js": "^14.22.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sapphire/ts-config": "^5.0.1",
|
||||
"@types/bun": "^1.2.18",
|
||||
"eslint": "^9.30.1",
|
||||
"@types/bun": "^1.2.23",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "~5.8.3",
|
||||
"zod": "^3.25.74"
|
||||
"typescript": "~5.9.3",
|
||||
"zod": "^3.25.76"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc.json",
|
||||
"printWidth": 120,
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"recommendations": ["denoland.vscode-deno", "tamasfe.even-better-toml", "codezombiech.gitignore"]
|
||||
"recommendations": ["denoland.vscode-deno"]
|
||||
}
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true,
|
||||
"source.organizeImports": false
|
||||
"source.fixAll": "always",
|
||||
"source.organizeImports": "always"
|
||||
},
|
||||
"editor.trimAutoWhitespace": false,
|
||||
"files.insertFinalNewline": true,
|
||||
"files.eol": "\n",
|
||||
"npm.packageManager": "[REPLACE_ME]",
|
||||
"deno.enable": "[REPLACE_BOOL]",
|
||||
"deno.lint": "[REPLACE_BOOL]",
|
||||
"deno.unstable": false
|
||||
"deno.enable": "[REPLACE_BOOL]"
|
||||
}
|
||||
|
||||
@@ -2,40 +2,25 @@
|
||||
"$schema": "https://raw.githubusercontent.com/denoland/deno/main/cli/schemas/config-file.v1.json",
|
||||
"tasks": {
|
||||
"lint": "deno lint",
|
||||
"deploy": "deno run --allow-read --allow-env --allow-net src/util/deploy.ts",
|
||||
"deploy": "deno run --env-file --allow-read --allow-env --allow-net src/util/deploy.ts",
|
||||
"format": "deno fmt",
|
||||
"fmt": "deno fmt",
|
||||
"start": "deno run --allow-read --allow-env --allow-net src/index.ts",
|
||||
"start": "deno run --env-file --allow-read --allow-env --allow-net src/index.ts",
|
||||
},
|
||||
"fmt": {
|
||||
"useTabs": true,
|
||||
"lineWidth": 120,
|
||||
"singleQuote": true,
|
||||
},
|
||||
"lint": {
|
||||
"include": ["src/"],
|
||||
"rules": {
|
||||
"tags": ["recommended"],
|
||||
"exclude": ["require-await", "no-await-in-sync-fn"],
|
||||
},
|
||||
},
|
||||
"fmt": {
|
||||
"useTabs": true,
|
||||
"lineWidth": 120,
|
||||
"semiColons": true,
|
||||
"singleQuote": true,
|
||||
"proseWrap": "preserve",
|
||||
"include": ["src/"],
|
||||
},
|
||||
"compilerOptions": {
|
||||
"alwaysStrict": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"lib": ["deno.window"],
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"removeComments": false,
|
||||
"strict": true,
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"noImplicitOverride": true,
|
||||
"imports": {
|
||||
"@discordjs/core": "npm:@discordjs/core@^2.2.2",
|
||||
"discord.js": "npm:discord.js@^14.22.1",
|
||||
"zod": "npm:zod@^3.25.76",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RESTPostAPIApplicationCommandsJSONBody, CommandInteraction } from 'npm:discord.js@^14.20.0';
|
||||
import { z } from 'npm:zod@^3.24.1';
|
||||
import type { CommandInteraction, RESTPostAPIApplicationCommandsJSONBody } from 'discord.js';
|
||||
import { z } from 'zod';
|
||||
import type { StructurePredicate } from '../util/loaders.ts';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import type { ClientEvents } from 'npm:discord.js@^14.20.0';
|
||||
import { z } from 'npm:zod@^3.24.1';
|
||||
import type { ClientEvents } from 'discord.js';
|
||||
import { z } from 'zod';
|
||||
import type { StructurePredicate } from '../util/loaders.ts';
|
||||
|
||||
/**
|
||||
* Defines the structure of an event.
|
||||
*/
|
||||
export type Event<T extends keyof ClientEvents = keyof ClientEvents> = {
|
||||
export type Event<EventName extends keyof ClientEvents = keyof ClientEvents> = {
|
||||
/**
|
||||
* The function to execute when the event is emitted.
|
||||
*
|
||||
* @param parameters - The parameters of the event
|
||||
*/
|
||||
execute(...parameters: ClientEvents[T]): Promise<void> | void;
|
||||
execute(...parameters: ClientEvents[EventName]): Promise<void> | void;
|
||||
/**
|
||||
* The name of the event to listen to
|
||||
*/
|
||||
name: T;
|
||||
name: EventName;
|
||||
/**
|
||||
* Whether or not the event should only be listened to once
|
||||
*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Events } from 'npm:discord.js@^14.20.0';
|
||||
import { Events } from 'discord.js';
|
||||
import type { Event } from './index.ts';
|
||||
import { loadCommands } from '../util/loaders.ts';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Events } from 'npm:discord.js@^14.20.0';
|
||||
import { Events } from 'discord.js';
|
||||
import type { Event } from './index.ts';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'https://deno.land/std@0.223.0/dotenv/load.ts';
|
||||
import { URL } from 'node:url';
|
||||
import { Client, GatewayIntentBits } from 'npm:discord.js@^14.20.0';
|
||||
import { Client, GatewayIntentBits } from 'discord.js';
|
||||
import { loadEvents } from './util/loaders.ts';
|
||||
|
||||
// Initialize the client
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'https://deno.land/std@0.223.0/dotenv/load.ts';
|
||||
import { URL } from 'node:url';
|
||||
import { API } from 'npm:@discordjs/core@^2.1.1/http-only';
|
||||
import { REST } from 'npm:discord.js@^14.20.0';
|
||||
import { API } from '@discordjs/core/http-only';
|
||||
import { REST } from 'discord.js';
|
||||
import { loadCommands } from './loaders.ts';
|
||||
|
||||
const commands = await loadCommands(new URL('../commands/', import.meta.url));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PathLike } from 'node:fs';
|
||||
import { readdir, stat } from 'node:fs/promises';
|
||||
import { URL } from 'node:url';
|
||||
import { glob, stat } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type { Command } from '../commands/index.ts';
|
||||
import { predicate as commandPredicate } from '../commands/index.ts';
|
||||
import type { Event } from '../events/index.ts';
|
||||
@@ -9,7 +10,7 @@ import { predicate as eventPredicate } from '../events/index.ts';
|
||||
/**
|
||||
* A predicate to check if the structure is valid
|
||||
*/
|
||||
export type StructurePredicate<T> = (structure: unknown) => structure is T;
|
||||
export type StructurePredicate<Structure> = (structure: unknown) => structure is Structure;
|
||||
|
||||
/**
|
||||
* Loads all the structures in the provided directory
|
||||
@@ -19,11 +20,11 @@ export type StructurePredicate<T> = (structure: unknown) => structure is T;
|
||||
* @param recursive - Whether to recursively load the structures in the directory
|
||||
* @returns
|
||||
*/
|
||||
export async function loadStructures<T>(
|
||||
export async function loadStructures<Structure>(
|
||||
dir: PathLike,
|
||||
predicate: StructurePredicate<T>,
|
||||
predicate: StructurePredicate<Structure>,
|
||||
recursive = true,
|
||||
): Promise<T[]> {
|
||||
): Promise<Structure[]> {
|
||||
// Get the stats of the directory
|
||||
const statDir = await stat(dir);
|
||||
|
||||
@@ -32,34 +33,24 @@ export async function loadStructures<T>(
|
||||
throw new Error(`The directory '${dir}' is not a directory.`);
|
||||
}
|
||||
|
||||
// Get all the files in the directory
|
||||
const files = await readdir(dir);
|
||||
|
||||
// Create an empty array to store the structures
|
||||
const structures: T[] = [];
|
||||
const structures: Structure[] = [];
|
||||
|
||||
// Loop through all the files in the directory
|
||||
for (const file of files) {
|
||||
const fileUrl = new URL(`${dir}/${file}`, import.meta.url);
|
||||
// Create a glob pattern to match the .ts files
|
||||
const basePath = dir instanceof URL ? fileURLToPath(dir) : dir.toString();
|
||||
const pattern = resolve(basePath, recursive ? '**/*.ts' : '*.ts');
|
||||
|
||||
// Get the stats of the file
|
||||
const statFile = await stat(fileUrl);
|
||||
|
||||
// If the file is a directory and recursive is true, recursively load the structures in the directory
|
||||
if (statFile.isDirectory() && recursive) {
|
||||
structures.push(...(await loadStructures(fileUrl, predicate, recursive)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the file is index.ts or the file does not end with .ts, skip the file
|
||||
if (file === 'index.ts' || !file.endsWith('.ts')) {
|
||||
// Loop through all the matching files in the directory
|
||||
for await (const file of glob(pattern)) {
|
||||
// If the file is index.ts, skip the file
|
||||
if (file.endsWith('/index.ts')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Import the structure dynamically from the file
|
||||
const structure = (await import(fileUrl.toString())).default;
|
||||
const { default: structure } = await import(file);
|
||||
|
||||
// If the structure is a valid structure, add it
|
||||
// If the default export is a valid structure, add it
|
||||
if (predicate(structure)) {
|
||||
structures.push(structure);
|
||||
}
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
"deploy": "node --env-file=.env src/util/deploy.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/core": "^2.2.0",
|
||||
"discord.js": "^14.21.0"
|
||||
"@discordjs/core": "^2.2.2",
|
||||
"discord.js": "^14.22.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"zod": "^3.25.74"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
|
||||
@@ -3,10 +3,10 @@ import { z } from 'zod';
|
||||
/**
|
||||
* Defines the structure of an event.
|
||||
*
|
||||
* @template {keyof import('discord.js').ClientEvents} [T=keyof import('discord.js').ClientEvents]
|
||||
* @template {keyof import('discord.js').ClientEvents} [EventName=keyof import('discord.js').ClientEvents]
|
||||
* @typedef {object} Event
|
||||
* @property {(...parameters: import('discord.js').ClientEvents[T]) => Promise<void> | void} execute The function to execute the command
|
||||
* @property {T} name The name of the event to listen to
|
||||
* @property {(...parameters: import('discord.js').ClientEvents[EventName]) => Promise<void> | void} execute The function to execute the command
|
||||
* @property {EventName} name The name of the event to listen to
|
||||
* @property {boolean} [once] Whether or not the event should only be listened to once
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { readdir, stat } from 'node:fs/promises';
|
||||
import { URL } from 'node:url';
|
||||
import { glob, stat } from 'node:fs/promises';
|
||||
import { fileURLToPath, resolve, URL } from 'node:url';
|
||||
import { predicate as commandPredicate } from '../commands/index.js';
|
||||
import { predicate as eventPredicate } from '../events/index.js';
|
||||
|
||||
/**
|
||||
* A predicate to check if the structure is valid.
|
||||
*
|
||||
* @template T
|
||||
* @typedef {(structure: unknown) => structure is T} StructurePredicate
|
||||
* @template Structure
|
||||
* @typedef {(structure: unknown) => structure is Structure} StructurePredicate
|
||||
*/
|
||||
|
||||
/**
|
||||
* Loads all the structures in the provided directory.
|
||||
*
|
||||
* @template T
|
||||
* @template Structure
|
||||
* @param {import('node:fs').PathLike} dir - The directory to load the structures from
|
||||
* @param {StructurePredicate<T>} predicate - The predicate to check if the structure is valid
|
||||
* @param {StructurePredicate<Structure>} predicate - The predicate to check if the structure is valid
|
||||
* @param {boolean} recursive - Whether to recursively load the structures in the directory
|
||||
* @returns {Promise<T[]>}
|
||||
* @returns {Promise<Structure[]>}
|
||||
*/
|
||||
export async function loadStructures(dir, predicate, recursive = true) {
|
||||
// Get the stats of the directory
|
||||
@@ -28,35 +28,25 @@ export async function loadStructures(dir, predicate, recursive = true) {
|
||||
throw new Error(`The directory '${dir}' is not a directory.`);
|
||||
}
|
||||
|
||||
// Get all the files in the directory
|
||||
const files = await readdir(dir);
|
||||
|
||||
// Create an empty array to store the structures
|
||||
/** @type {T[]} */
|
||||
/** @type {Structure[]} */
|
||||
const structures = [];
|
||||
|
||||
// Loop through all the files in the directory
|
||||
for (const file of files) {
|
||||
const fileUrl = new URL(`${dir}/${file}`, import.meta.url);
|
||||
// Create a glob pattern to match the .js files
|
||||
const basePath = dir instanceof URL ? fileURLToPath(dir) : dir.toString();
|
||||
const pattern = resolve(basePath, recursive ? '**/*.js' : '*.js');
|
||||
|
||||
// Get the stats of the file
|
||||
const statFile = await stat(fileUrl);
|
||||
|
||||
// If the file is a directory and recursive is true, recursively load the structures in the directory
|
||||
if (statFile.isDirectory() && recursive) {
|
||||
structures.push(...(await loadStructures(fileUrl, predicate, recursive)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the file is index.js or the file does not end with .js, skip the file
|
||||
if (file === 'index.js' || !file.endsWith('.js')) {
|
||||
// Loop through all the matching files in the directory
|
||||
for await (const file of glob(pattern)) {
|
||||
// If the file is index.js, skip the file
|
||||
if (file.endsWith('/index.js')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Import the structure dynamically from the file
|
||||
const structure = (await import(fileUrl.toString())).default;
|
||||
const { default: structure } = await import(file);
|
||||
|
||||
// If the structure is a valid structure, add it
|
||||
// If the default export is a valid structure, add it
|
||||
if (predicate(structure)) {
|
||||
structures.push(structure);
|
||||
}
|
||||
@@ -68,7 +58,7 @@ export async function loadStructures(dir, predicate, recursive = true) {
|
||||
/**
|
||||
* @param {import('node:fs').PathLike} dir
|
||||
* @param {boolean} [recursive]
|
||||
* @returns {Promise<Map<string,import('../commands/index.js').Command>>}
|
||||
* @returns {Promise<Map<string, import('../commands/index.js').Command>>}
|
||||
*/
|
||||
export async function loadCommands(dir, recursive = true) {
|
||||
return (await loadStructures(dir, commandPredicate, recursive)).reduce(
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true,
|
||||
"source.organizeImports": false
|
||||
"source.fixAll": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.trimAutoWhitespace": false,
|
||||
"files.insertFinalNewline": true,
|
||||
|
||||
@@ -12,19 +12,19 @@
|
||||
"start": "node --env-file=.env dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/core": "^2.2.0",
|
||||
"discord.js": "^14.21.0"
|
||||
"@discordjs/core": "^2.2.2",
|
||||
"discord.js": "^14.22.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sapphire/ts-config": "^5.0.1",
|
||||
"@types/node": "^22.16.0",
|
||||
"eslint": "^9.30.1",
|
||||
"@types/node": "^22.18.8",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "~5.8.3",
|
||||
"zod": "^3.25.74"
|
||||
"typescript": "~5.9.3",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
|
||||
@@ -5,17 +5,17 @@ import type { StructurePredicate } from '../util/loaders.[REPLACE_IMPORT_EXT]';
|
||||
/**
|
||||
* Defines the structure of an event.
|
||||
*/
|
||||
export type Event<T extends keyof ClientEvents = keyof ClientEvents> = {
|
||||
export type Event<EventName extends keyof ClientEvents = keyof ClientEvents> = {
|
||||
/**
|
||||
* The function to execute when the event is emitted.
|
||||
*
|
||||
* @param parameters - The parameters of the event
|
||||
*/
|
||||
execute(...parameters: ClientEvents[T]): Promise<void> | void;
|
||||
execute(...parameters: ClientEvents[EventName]): Promise<void> | void;
|
||||
/**
|
||||
* The name of the event to listen to
|
||||
*/
|
||||
name: T;
|
||||
name: EventName;
|
||||
/**
|
||||
* Whether or not the event should only be listened to once
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PathLike } from 'node:fs';
|
||||
import { readdir, stat } from 'node:fs/promises';
|
||||
import { URL } from 'node:url';
|
||||
import { glob, stat } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type { Command } from '../commands/index.[REPLACE_IMPORT_EXT]';
|
||||
import { predicate as commandPredicate } from '../commands/index.[REPLACE_IMPORT_EXT]';
|
||||
import type { Event } from '../events/index.[REPLACE_IMPORT_EXT]';
|
||||
@@ -9,7 +10,7 @@ import { predicate as eventPredicate } from '../events/index.[REPLACE_IMPORT_EXT
|
||||
/**
|
||||
* A predicate to check if the structure is valid
|
||||
*/
|
||||
export type StructurePredicate<T> = (structure: unknown) => structure is T;
|
||||
export type StructurePredicate<Structure> = (structure: unknown) => structure is Structure;
|
||||
|
||||
/**
|
||||
* Loads all the structures in the provided directory
|
||||
@@ -19,11 +20,11 @@ export type StructurePredicate<T> = (structure: unknown) => structure is T;
|
||||
* @param recursive - Whether to recursively load the structures in the directory
|
||||
* @returns
|
||||
*/
|
||||
export async function loadStructures<T>(
|
||||
export async function loadStructures<Structure>(
|
||||
dir: PathLike,
|
||||
predicate: StructurePredicate<T>,
|
||||
predicate: StructurePredicate<Structure>,
|
||||
recursive = true,
|
||||
): Promise<T[]> {
|
||||
): Promise<Structure[]> {
|
||||
// Get the stats of the directory
|
||||
const statDir = await stat(dir);
|
||||
|
||||
@@ -32,35 +33,27 @@ export async function loadStructures<T>(
|
||||
throw new Error(`The directory '${dir}' is not a directory.`);
|
||||
}
|
||||
|
||||
// Get all the files in the directory
|
||||
const files = await readdir(dir);
|
||||
|
||||
// Create an empty array to store the structures
|
||||
const structures: T[] = [];
|
||||
const structures: Structure[] = [];
|
||||
|
||||
// Loop through all the files in the directory
|
||||
for (const file of files) {
|
||||
const fileUrl = new URL(`${dir}/${file}`, import.meta.url);
|
||||
// Create a glob pattern to match the .[REPLACE_IMPORT_EXT] files
|
||||
const basePath = dir instanceof URL ? fileURLToPath(dir) : dir.toString();
|
||||
const pattern = resolve(basePath, recursive ? '**/*.[REPLACE_IMPORT_EXT]' : '*.[REPLACE_IMPORT_EXT]');
|
||||
|
||||
// Get the stats of the file
|
||||
const statFile = await stat(fileUrl);
|
||||
|
||||
// If the file is a directory and recursive is true, recursively load the structures in the directory
|
||||
if (statFile.isDirectory() && recursive) {
|
||||
structures.push(...(await loadStructures(fileUrl, predicate, recursive)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the file is index.[REPLACE_IMPORT_EXT] or the file does not end with .[REPLACE_IMPORT_EXT], skip the file
|
||||
if (file === 'index.[REPLACE_IMPORT_EXT]' || !file.endsWith('.[REPLACE_IMPORT_EXT]')) {
|
||||
// Loop through all the matching files in the directory
|
||||
for await (const file of glob(pattern)) {
|
||||
// If the file is index.[REPLACE_IMPORT_EXT], skip the file
|
||||
if (file.endsWith('/index.[REPLACE_IMPORT_EXT]')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Import the structure dynamically from the file
|
||||
const structure = (await import(fileUrl.toString())).default;
|
||||
const { default: structure } = await import(file);
|
||||
|
||||
// If the structure is a valid structure, add it
|
||||
if (predicate(structure)) structures.push(structure);
|
||||
// If the default export is a valid structure, add it
|
||||
if (predicate(structure)) {
|
||||
structures.push(structure);
|
||||
}
|
||||
}
|
||||
|
||||
return structures;
|
||||
|
||||
@@ -73,31 +73,31 @@
|
||||
"@discordjs/util": "workspace:^",
|
||||
"@discordjs/ws": "workspace:^",
|
||||
"@sapphire/snowflake": "3.5.5",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"discord-api-types": "^0.38.16",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.7",
|
||||
"discord-api-types": "^0.38.29",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"magic-bytes.js": "^1.12.1",
|
||||
"tslib": "^2.8.1",
|
||||
"undici": "7.11.0"
|
||||
"undici": "7.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@discordjs/api-extractor": "workspace:^",
|
||||
"@discordjs/docgen": "workspace:^",
|
||||
"@discordjs/scripts": "workspace:^",
|
||||
"@favware/cliff-jumper": "^4.1.0",
|
||||
"@types/node": "^22.16.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.30.1",
|
||||
"@types/node": "^22.18.8",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-neon": "^0.2.7",
|
||||
"eslint-formatter-compact": "^8.40.0",
|
||||
"eslint-formatter-pretty": "^6.0.1",
|
||||
"eslint-formatter-pretty": "^7.0.0",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsdoc": "^51.3.4",
|
||||
"eslint-plugin-jsdoc": "^54.7.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tsd": "^0.32.0",
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "~5.8.3"
|
||||
"tsd": "^0.33.0",
|
||||
"turbo": "^2.5.8",
|
||||
"typescript": "~5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
|
||||
@@ -39,12 +39,12 @@ async function writeClientActionImports() {
|
||||
|
||||
const actionName = file.slice(0, -3);
|
||||
|
||||
lines.push(` this.register(require('./${file}').${actionName}Action);`);
|
||||
lines.push(` this.${actionName} = this.load(require('./${file}').${actionName}Action);`);
|
||||
}
|
||||
|
||||
lines.push(' }\n');
|
||||
lines.push(' register(Action) {');
|
||||
lines.push(" this[Action.name.replace(/Action$/, '')] = new Action(this.client);");
|
||||
lines.push(' load(Action) {');
|
||||
lines.push(' return new Action(this.client);');
|
||||
lines.push(' }');
|
||||
lines.push('}\n');
|
||||
lines.push('exports.ActionsManager = ActionsManager;\n');
|
||||
|
||||
@@ -13,50 +13,62 @@ class ActionsManager {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
|
||||
this.register(require('./ChannelCreate.js').ChannelCreateAction);
|
||||
this.register(require('./ChannelDelete.js').ChannelDeleteAction);
|
||||
this.register(require('./ChannelUpdate.js').ChannelUpdateAction);
|
||||
this.register(require('./GuildChannelsPositionUpdate.js').GuildChannelsPositionUpdateAction);
|
||||
this.register(require('./GuildEmojiCreate.js').GuildEmojiCreateAction);
|
||||
this.register(require('./GuildEmojiDelete.js').GuildEmojiDeleteAction);
|
||||
this.register(require('./GuildEmojiUpdate.js').GuildEmojiUpdateAction);
|
||||
this.register(require('./GuildEmojisUpdate.js').GuildEmojisUpdateAction);
|
||||
this.register(require('./GuildMemberRemove.js').GuildMemberRemoveAction);
|
||||
this.register(require('./GuildMemberUpdate.js').GuildMemberUpdateAction);
|
||||
this.register(require('./GuildRoleCreate.js').GuildRoleCreateAction);
|
||||
this.register(require('./GuildRoleDelete.js').GuildRoleDeleteAction);
|
||||
this.register(require('./GuildRolesPositionUpdate.js').GuildRolesPositionUpdateAction);
|
||||
this.register(require('./GuildScheduledEventDelete.js').GuildScheduledEventDeleteAction);
|
||||
this.register(require('./GuildScheduledEventUserAdd.js').GuildScheduledEventUserAddAction);
|
||||
this.register(require('./GuildScheduledEventUserRemove.js').GuildScheduledEventUserRemoveAction);
|
||||
this.register(require('./GuildSoundboardSoundDelete.js').GuildSoundboardSoundDeleteAction);
|
||||
this.register(require('./GuildStickerCreate.js').GuildStickerCreateAction);
|
||||
this.register(require('./GuildStickerDelete.js').GuildStickerDeleteAction);
|
||||
this.register(require('./GuildStickerUpdate.js').GuildStickerUpdateAction);
|
||||
this.register(require('./GuildStickersUpdate.js').GuildStickersUpdateAction);
|
||||
this.register(require('./GuildUpdate.js').GuildUpdateAction);
|
||||
this.register(require('./InteractionCreate.js').InteractionCreateAction);
|
||||
this.register(require('./MessageCreate.js').MessageCreateAction);
|
||||
this.register(require('./MessageDelete.js').MessageDeleteAction);
|
||||
this.register(require('./MessageDeleteBulk.js').MessageDeleteBulkAction);
|
||||
this.register(require('./MessagePollVoteAdd.js').MessagePollVoteAddAction);
|
||||
this.register(require('./MessagePollVoteRemove.js').MessagePollVoteRemoveAction);
|
||||
this.register(require('./MessageReactionAdd.js').MessageReactionAddAction);
|
||||
this.register(require('./MessageReactionRemove.js').MessageReactionRemoveAction);
|
||||
this.register(require('./MessageReactionRemoveAll.js').MessageReactionRemoveAllAction);
|
||||
this.register(require('./MessageReactionRemoveEmoji.js').MessageReactionRemoveEmojiAction);
|
||||
this.register(require('./MessageUpdate.js').MessageUpdateAction);
|
||||
this.register(require('./StageInstanceCreate.js').StageInstanceCreateAction);
|
||||
this.register(require('./StageInstanceDelete.js').StageInstanceDeleteAction);
|
||||
this.register(require('./StageInstanceUpdate.js').StageInstanceUpdateAction);
|
||||
this.register(require('./ThreadCreate.js').ThreadCreateAction);
|
||||
this.register(require('./ThreadMembersUpdate.js').ThreadMembersUpdateAction);
|
||||
this.register(require('./TypingStart.js').TypingStartAction);
|
||||
this.register(require('./UserUpdate.js').UserUpdateAction);
|
||||
this.ChannelCreate = this.load(require('./ChannelCreate.js').ChannelCreateAction);
|
||||
this.ChannelDelete = this.load(require('./ChannelDelete.js').ChannelDeleteAction);
|
||||
this.ChannelUpdate = this.load(require('./ChannelUpdate.js').ChannelUpdateAction);
|
||||
this.GuildChannelsPositionUpdate = this.load(
|
||||
require('./GuildChannelsPositionUpdate.js').GuildChannelsPositionUpdateAction,
|
||||
);
|
||||
this.GuildEmojiCreate = this.load(require('./GuildEmojiCreate.js').GuildEmojiCreateAction);
|
||||
this.GuildEmojiDelete = this.load(require('./GuildEmojiDelete.js').GuildEmojiDeleteAction);
|
||||
this.GuildEmojiUpdate = this.load(require('./GuildEmojiUpdate.js').GuildEmojiUpdateAction);
|
||||
this.GuildEmojisUpdate = this.load(require('./GuildEmojisUpdate.js').GuildEmojisUpdateAction);
|
||||
this.GuildMemberRemove = this.load(require('./GuildMemberRemove.js').GuildMemberRemoveAction);
|
||||
this.GuildMemberUpdate = this.load(require('./GuildMemberUpdate.js').GuildMemberUpdateAction);
|
||||
this.GuildRoleCreate = this.load(require('./GuildRoleCreate.js').GuildRoleCreateAction);
|
||||
this.GuildRoleDelete = this.load(require('./GuildRoleDelete.js').GuildRoleDeleteAction);
|
||||
this.GuildRolesPositionUpdate = this.load(require('./GuildRolesPositionUpdate.js').GuildRolesPositionUpdateAction);
|
||||
this.GuildScheduledEventDelete = this.load(
|
||||
require('./GuildScheduledEventDelete.js').GuildScheduledEventDeleteAction,
|
||||
);
|
||||
this.GuildScheduledEventUserAdd = this.load(
|
||||
require('./GuildScheduledEventUserAdd.js').GuildScheduledEventUserAddAction,
|
||||
);
|
||||
this.GuildScheduledEventUserRemove = this.load(
|
||||
require('./GuildScheduledEventUserRemove.js').GuildScheduledEventUserRemoveAction,
|
||||
);
|
||||
this.GuildSoundboardSoundDelete = this.load(
|
||||
require('./GuildSoundboardSoundDelete.js').GuildSoundboardSoundDeleteAction,
|
||||
);
|
||||
this.GuildStickerCreate = this.load(require('./GuildStickerCreate.js').GuildStickerCreateAction);
|
||||
this.GuildStickerDelete = this.load(require('./GuildStickerDelete.js').GuildStickerDeleteAction);
|
||||
this.GuildStickerUpdate = this.load(require('./GuildStickerUpdate.js').GuildStickerUpdateAction);
|
||||
this.GuildStickersUpdate = this.load(require('./GuildStickersUpdate.js').GuildStickersUpdateAction);
|
||||
this.GuildUpdate = this.load(require('./GuildUpdate.js').GuildUpdateAction);
|
||||
this.InteractionCreate = this.load(require('./InteractionCreate.js').InteractionCreateAction);
|
||||
this.MessageCreate = this.load(require('./MessageCreate.js').MessageCreateAction);
|
||||
this.MessageDelete = this.load(require('./MessageDelete.js').MessageDeleteAction);
|
||||
this.MessageDeleteBulk = this.load(require('./MessageDeleteBulk.js').MessageDeleteBulkAction);
|
||||
this.MessagePollVoteAdd = this.load(require('./MessagePollVoteAdd.js').MessagePollVoteAddAction);
|
||||
this.MessagePollVoteRemove = this.load(require('./MessagePollVoteRemove.js').MessagePollVoteRemoveAction);
|
||||
this.MessageReactionAdd = this.load(require('./MessageReactionAdd.js').MessageReactionAddAction);
|
||||
this.MessageReactionRemove = this.load(require('./MessageReactionRemove.js').MessageReactionRemoveAction);
|
||||
this.MessageReactionRemoveAll = this.load(require('./MessageReactionRemoveAll.js').MessageReactionRemoveAllAction);
|
||||
this.MessageReactionRemoveEmoji = this.load(
|
||||
require('./MessageReactionRemoveEmoji.js').MessageReactionRemoveEmojiAction,
|
||||
);
|
||||
this.MessageUpdate = this.load(require('./MessageUpdate.js').MessageUpdateAction);
|
||||
this.StageInstanceCreate = this.load(require('./StageInstanceCreate.js').StageInstanceCreateAction);
|
||||
this.StageInstanceDelete = this.load(require('./StageInstanceDelete.js').StageInstanceDeleteAction);
|
||||
this.StageInstanceUpdate = this.load(require('./StageInstanceUpdate.js').StageInstanceUpdateAction);
|
||||
this.ThreadCreate = this.load(require('./ThreadCreate.js').ThreadCreateAction);
|
||||
this.ThreadMembersUpdate = this.load(require('./ThreadMembersUpdate.js').ThreadMembersUpdateAction);
|
||||
this.TypingStart = this.load(require('./TypingStart.js').TypingStartAction);
|
||||
this.UserUpdate = this.load(require('./UserUpdate.js').UserUpdateAction);
|
||||
}
|
||||
|
||||
register(Action) {
|
||||
this[Action.name.replace(/Action$/, '')] = new Action(this.client);
|
||||
load(Action) {
|
||||
return new Action(this.client);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,8 +114,10 @@
|
||||
* @property {'CommandInteractionOptionInvalidChannelType'} CommandInteractionOptionInvalidChannelType
|
||||
* @property {'AutocompleteInteractionOptionNoFocusedOption'} AutocompleteInteractionOptionNoFocusedOption
|
||||
*
|
||||
* @property {'ModalSubmitInteractionFieldNotFound'} ModalSubmitInteractionFieldNotFound
|
||||
* @property {'ModalSubmitInteractionFieldType'} ModalSubmitInteractionFieldType
|
||||
* @property {'ModalSubmitInteractionComponentNotFound'} ModalSubmitInteractionComponentNotFound
|
||||
* @property {'ModalSubmitInteractionComponentType'} ModalSubmitInteractionComponentType
|
||||
* @property {'ModalSubmitInteractionComponentEmpty'} ModalSubmitInteractionComponentEmpty
|
||||
* @property {'ModalSubmitInteractionComponentInvalidChannelType'} ModalSubmitInteractionComponentInvalidChannelType
|
||||
*
|
||||
* @property {'InvalidMissingScopes'} InvalidMissingScopes
|
||||
* @property {'InvalidScopesWithPermissions'} InvalidScopesWithPermissions
|
||||
@@ -248,8 +250,10 @@ const keys = [
|
||||
'CommandInteractionOptionInvalidChannelType',
|
||||
'AutocompleteInteractionOptionNoFocusedOption',
|
||||
|
||||
'ModalSubmitInteractionFieldNotFound',
|
||||
'ModalSubmitInteractionFieldType',
|
||||
'ModalSubmitInteractionComponentNotFound',
|
||||
'ModalSubmitInteractionComponentType',
|
||||
'ModalSubmitInteractionComponentEmpty',
|
||||
'ModalSubmitInteractionComponentInvalidChannelType',
|
||||
|
||||
'InvalidMissingScopes',
|
||||
'InvalidScopesWithPermissions',
|
||||
|
||||
@@ -127,10 +127,14 @@ const Messages = {
|
||||
`The type of channel of the option "${name}" is: ${type}; expected ${expected}.`,
|
||||
[ErrorCodes.AutocompleteInteractionOptionNoFocusedOption]: 'No focused option for autocomplete interaction.',
|
||||
|
||||
[ErrorCodes.ModalSubmitInteractionFieldNotFound]: customId =>
|
||||
`Required field with custom id "${customId}" not found.`,
|
||||
[ErrorCodes.ModalSubmitInteractionFieldType]: (customId, type, expected) =>
|
||||
`Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`,
|
||||
[ErrorCodes.ModalSubmitInteractionComponentNotFound]: customId =>
|
||||
`Required component with custom id "${customId}" not found.`,
|
||||
[ErrorCodes.ModalSubmitInteractionComponentType]: (customId, type, expected) =>
|
||||
`Component with custom id "${customId}" is of type: ${type}; expected ${expected}.`,
|
||||
[ErrorCodes.ModalSubmitInteractionComponentEmpty]: (customId, type) =>
|
||||
`Required component with custom id "${customId}" is of type: ${type}; expected a non-empty value.`,
|
||||
[ErrorCodes.ModalSubmitInteractionComponentInvalidChannelType]: (customId, type, expected) =>
|
||||
`The type of channel of the component with custom id "${customId}" is: ${type}; expected ${expected}.`,
|
||||
|
||||
[ErrorCodes.InvalidMissingScopes]: 'At least one valid scope must be provided for the invite',
|
||||
[ErrorCodes.InvalidScopesWithPermissions]: 'Permissions cannot be set without the bot scope.',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user