├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitattributes
├── .github
├── CODEOWNERS
├── hooks
│ ├── commit-msg
│ └── pre-commit
├── labels.yml
├── renovate.json
└── workflows
│ ├── auto-deprecate.yml
│ ├── auto-updater.yml
│ ├── codeql-analysis.yml
│ ├── continuous-delivery.yml
│ ├── continuous-integration.yml
│ ├── deprecate-on-merge.yml
│ ├── documentation.yml
│ ├── labelsync.yml
│ └── publish.yml
├── .gitignore
├── .npm-deprecaterc.yml
├── .prettierignore
├── .vscode
├── extensions.json
└── settings.json
├── .yarn
├── plugins
│ └── @yarnpkg
│ │ └── plugin-git-hooks.cjs
└── releases
│ └── yarn-4.9.2.cjs
├── .yarnrc.yml
├── LICENSE.md
├── README.md
├── package.json
├── packages
├── api
│ ├── .cliff-jumperrc.yml
│ ├── .rollup-type-bundlerrc.yml
│ ├── .typedoc-json-parserrc.yml
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── cliff.toml
│ ├── package.json
│ ├── scripts
│ │ └── sync-mime-types.mts
│ ├── src
│ │ ├── index.ts
│ │ ├── lib
│ │ │ ├── structures
│ │ │ │ ├── Augmentations.d.ts
│ │ │ │ ├── Middleware.ts
│ │ │ │ ├── MiddlewareStore.ts
│ │ │ │ ├── Route.ts
│ │ │ │ ├── RouteLoaderStrategy.ts
│ │ │ │ ├── RouteStore.ts
│ │ │ │ ├── api
│ │ │ │ │ ├── ApiRequest.ts
│ │ │ │ │ ├── ApiResponse.ts
│ │ │ │ │ └── CookieStore.ts
│ │ │ │ ├── http
│ │ │ │ │ ├── Auth.ts
│ │ │ │ │ ├── HttpCodes.ts
│ │ │ │ │ ├── HttpMethods.ts
│ │ │ │ │ └── Server.ts
│ │ │ │ └── router
│ │ │ │ │ ├── RouterBranch.ts
│ │ │ │ │ ├── RouterNode.ts
│ │ │ │ │ └── RouterRoot.ts
│ │ │ └── utils
│ │ │ │ ├── MimeType.ts
│ │ │ │ ├── _body
│ │ │ │ ├── RequestHeadersProxy.ts
│ │ │ │ ├── RequestProxy.ts
│ │ │ │ └── RequestURLProxy.ts
│ │ │ │ └── constants.ts
│ │ ├── listeners
│ │ │ ├── PluginRouteError.ts
│ │ │ ├── PluginServerMiddlewareError.ts
│ │ │ ├── PluginServerMiddlewareSuccess.ts
│ │ │ ├── PluginServerRequest.ts
│ │ │ ├── PluginServerRouterBranchMethodNotAllowed.ts
│ │ │ ├── PluginServerRouterBranchNotFound.ts
│ │ │ ├── PluginServerRouterFound.ts
│ │ │ └── _load.ts
│ │ ├── middlewares
│ │ │ ├── _load.ts
│ │ │ ├── auth.ts
│ │ │ ├── body.ts
│ │ │ ├── cookies.ts
│ │ │ └── headers.ts
│ │ ├── register.ts
│ │ ├── routes
│ │ │ ├── _load.ts
│ │ │ └── oauth
│ │ │ │ ├── callback.post.ts
│ │ │ │ └── logout.post.ts
│ │ └── tsconfig.json
│ ├── tests
│ │ ├── index.test.ts
│ │ ├── lib
│ │ │ └── structures
│ │ │ │ ├── Route.test.ts
│ │ │ │ └── router
│ │ │ │ └── RouterRoot.test.ts
│ │ ├── shared.ts
│ │ └── tsconfig.json
│ ├── tsconfig.eslint.json
│ ├── tsup.config.ts
│ ├── typedoc.json
│ └── vitest.config.ts
├── editable-commands
│ ├── .cliff-jumperrc.yml
│ ├── .rollup-type-bundlerrc.yml
│ ├── .typedoc-json-parserrc.yml
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── cliff.toml
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── listeners
│ │ │ ├── PluginMessageUpdate.ts
│ │ │ └── _load.ts
│ │ ├── register.ts
│ │ └── tsconfig.json
│ ├── tsconfig.eslint.json
│ ├── tsup.config.ts
│ └── typedoc.json
├── hmr
│ ├── .cliff-jumperrc.yml
│ ├── .rollup-type-bundlerrc.yml
│ ├── .typedoc-json-parserrc.yml
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── cliff.toml
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── lib
│ │ │ └── hmr.ts
│ │ ├── register.ts
│ │ └── tsconfig.json
│ ├── tsconfig.eslint.json
│ ├── tsup.config.ts
│ └── typedoc.json
├── i18next
│ ├── .cliff-jumperrc.yml
│ ├── .rollup-type-bundlerrc.yml
│ ├── .typedoc-json-parserrc.yml
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── cliff.toml
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── lib
│ │ │ ├── Augmentations.d.ts
│ │ │ ├── InternationalizationHandler.ts
│ │ │ ├── functions.ts
│ │ │ └── types.ts
│ │ ├── register.ts
│ │ └── tsconfig.json
│ ├── tests
│ │ ├── I18nextHandler.test.ts
│ │ ├── augments.d.ts
│ │ └── tsconfig.json
│ ├── tsconfig.eslint.json
│ ├── tsup.config.ts
│ ├── typedoc.json
│ └── vitest.config.ts
├── logger
│ ├── .cliff-jumperrc.yml
│ ├── .rollup-type-bundlerrc.yml
│ ├── .typedoc-json-parserrc.yml
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── cliff.toml
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── lib
│ │ │ ├── Logger.ts
│ │ │ ├── LoggerLevel.ts
│ │ │ ├── LoggerStyle.ts
│ │ │ └── LoggerTimestamp.ts
│ │ ├── register.ts
│ │ └── tsconfig.json
│ ├── tests
│ │ ├── Logger.test.ts
│ │ ├── LoggerLevel.test.ts
│ │ ├── LoggerStyle.test.ts
│ │ ├── LoggerTimestamp.test.ts
│ │ └── tsconfig.json
│ ├── tsconfig.eslint.json
│ ├── tsup.config.ts
│ ├── typedoc.json
│ └── vitest.config.ts
├── pattern-commands
│ ├── .cliff-jumperrc.yml
│ ├── .rollup-type-bundlerrc.yml
│ ├── .typedoc-json-parserrc.yml
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── cliff.toml
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── lib
│ │ │ ├── structures
│ │ │ │ ├── PatternCommand.ts
│ │ │ │ └── PatternCommandStore.ts
│ │ │ └── utils
│ │ │ │ ├── PatternCommandEvents.ts
│ │ │ │ └── PatternCommandInterfaces.ts
│ │ ├── listeners
│ │ │ ├── PluginCommandAccepted.ts
│ │ │ ├── PluginMessageParse.ts
│ │ │ ├── PluginPreCommandRun.ts
│ │ │ └── _load.ts
│ │ ├── register.ts
│ │ └── tsconfig.json
│ ├── tsconfig.eslint.json
│ ├── tsup.config.ts
│ └── typedoc.json
├── scheduled-tasks
│ ├── .cliff-jumperrc.yml
│ ├── .rollup-type-bundlerrc.yml
│ ├── .typedoc-json-parserrc.yml
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── UPGRADING-v9-v10.md
│ ├── cliff.toml
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── lib
│ │ │ ├── ScheduledTaskHandler.ts
│ │ │ ├── structures
│ │ │ │ ├── ScheduledTask.ts
│ │ │ │ └── ScheduledTaskStore.ts
│ │ │ └── types
│ │ │ │ ├── ScheduledTaskEvents.ts
│ │ │ │ └── ScheduledTaskTypes.ts
│ │ ├── listeners
│ │ │ ├── PluginScheduledTaskError.ts
│ │ │ ├── PluginScheduledTaskNotFound.ts
│ │ │ ├── PluginScheduledTaskStrategyClientError.ts
│ │ │ ├── PluginScheduledTaskStrategyConnectError.ts
│ │ │ ├── PluginScheduledTaskStrategyWorkerError.ts
│ │ │ └── _load.ts
│ │ ├── register.ts
│ │ └── tsconfig.json
│ ├── tsconfig.eslint.json
│ ├── tsup.config.ts
│ └── typedoc.json
├── subcommands
│ ├── .cliff-jumperrc.yml
│ ├── .rollup-type-bundlerrc.yml
│ ├── .typedoc-json-parserrc.yml
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── cliff.toml
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── lib
│ │ │ ├── Subcommand.ts
│ │ │ ├── precondition-resolvers
│ │ │ │ └── subcommandCooldown.ts
│ │ │ └── types
│ │ │ │ ├── Enums.ts
│ │ │ │ ├── Events.ts
│ │ │ │ └── SubcommandMappings.ts
│ │ ├── listeners
│ │ │ ├── PluginChatInputSubcommandError.ts
│ │ │ ├── PluginChatInputSubcommandNoMatch.ts
│ │ │ ├── PluginMessageSubcommandError.ts
│ │ │ ├── PluginMessageSubcommandNoMatch.ts
│ │ │ ├── PluginSubcommandMappingIsMissingChatInputCommandHandler.ts
│ │ │ ├── PluginSubcommandMappingIsMissingMessageCommandHandler.ts
│ │ │ └── _load.ts
│ │ ├── preconditions
│ │ │ ├── PluginSubcommandCooldown.ts
│ │ │ └── _load.ts
│ │ ├── register.ts
│ │ └── tsconfig.json
│ ├── tsconfig.eslint.json
│ ├── tsup.config.ts
│ └── typedoc.json
└── utilities-store
│ ├── .cliff-jumperrc.yml
│ ├── .rollup-type-bundlerrc.yml
│ ├── .typedoc-json-parserrc.yml
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── cliff.toml
│ ├── package.json
│ ├── src
│ ├── index.ts
│ ├── lib
│ │ ├── Augmentations.d.ts
│ │ ├── Utilities.ts
│ │ ├── UtilitiesStore.ts
│ │ └── Utility.ts
│ ├── register.ts
│ └── tsconfig.json
│ ├── tsconfig.eslint.json
│ ├── tsup.config.ts
│ └── typedoc.json
├── scripts
├── clean-register-imports.mts
├── rename-cjs-register.mts
├── tsup.config.ts
└── vitest.config.ts
├── sonar-project.properties
├── tsconfig.base.json
├── tsconfig.eslint.json
├── turbo.json
├── vitest.config.ts
├── vitest.workspace.ts
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 |
9 | [*.{js,ts}]
10 | indent_size = 4
11 | indent_style = tab
12 | block_comment_start = /*
13 | block_comment = *
14 | block_comment_end = */
15 |
16 | [*.{yml,yaml}]
17 | indent_size = 2
18 | indent_style = space
19 |
20 | [*.{md,rmd,mkd,mkdn,mdwn,mdown,markdown,litcoffee}]
21 | tab_width = 4
22 | trim_trailing_whitespace = false
23 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | **/dist/**/*
3 | **/docs/**/*
4 | **/build/**/*
5 | **/*.d.ts
6 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@sapphire",
3 | "overrides": [
4 | {
5 | "files": ["*.mjs"],
6 | "rules": {
7 | "@typescript-eslint/naming-convention": "off"
8 | }
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | /packages/api/**/*.ts @favna @kyranet @vladfrangu
2 | /packages/editable-commands/**/*.ts @favna @kyranet
3 | /packages/i18next/**/*.ts @nytelife26 @favna @kyranet @vladfrangu
4 | /packages/logger/**/*.ts @favna @kyranet
5 | /packages/pattern-commands/**/*.ts @favna @vladfrangu @feralheart
6 | /packages/subcommands/**/*.ts @favna @vladfrangu
7 | /packages/utilities-store/**/*.ts @favna
8 |
--------------------------------------------------------------------------------
/.github/hooks/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | yarn commitlint --edit $1
--------------------------------------------------------------------------------
/.github/hooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | yarn lint-staged
--------------------------------------------------------------------------------
/.github/labels.yml:
--------------------------------------------------------------------------------
1 | - name: packages:api
2 | color: 'fbca04'
3 | - name: packages:editable-commands
4 | color: 'fbca04'
5 | - name: packages:hmr
6 | color: 'fbca04'
7 | - name: packages:i18next
8 | color: 'fbca04'
9 | - name: packages:logger
10 | color: 'fbca04'
11 | - name: packages:pattern-commands
12 | color: 'fbca04'
13 | - name: packages:scheduled-tasks
14 | color: 'fbca04'
15 | - name: packages:subcommands
16 | color: 'fbca04'
17 | - name: packages:utilities-store
18 | color: 'fbca04'
19 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["github>sapphiredev/.github:sapphire-renovate"],
4 | "npm": {
5 | "packageRules": [
6 | {
7 | "enabled": false,
8 | "matchPackageNames": ["/cookie-es/"]
9 | }
10 | ]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.github/workflows/auto-deprecate.yml:
--------------------------------------------------------------------------------
1 | name: NPM Auto Deprecate
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * *'
6 |
7 | jobs:
8 | auto-deprecate:
9 | name: NPM Auto Deprecate
10 | uses: sapphiredev/.github/.github/workflows/reusable-yarn-job.yml@main
11 | with:
12 | script-name: npm-deprecate
13 | secrets:
14 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
15 |
--------------------------------------------------------------------------------
/.github/workflows/auto-updater.yml:
--------------------------------------------------------------------------------
1 | name: Automatic Data Update
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '0 2 * * *'
7 |
8 | jobs:
9 | DataUpdater:
10 | name: Automatic Data Update
11 | runs-on: ubuntu-latest
12 | if: github.repository_owner == 'sapphiredev'
13 | steps:
14 | - name: Checkout Project
15 | uses: actions/checkout@v4
16 | - name: Install dependencies
17 | uses: sapphiredev/.github/actions/install-yarn-dependencies@main
18 | with:
19 | node-version: 20
20 | - name: Run plugin-api MIME type sync
21 | run: yarn workspace @sapphire/plugin-api sync-mime-types
22 | - name: Run prettier on the code
23 | run: yarn format
24 | - name: Commit any changes and create a pull request
25 | env:
26 | GITHUB_USER: github-actions[bot]
27 | GITHUB_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 | run: |
30 | git add .;
31 | if git diff-index --quiet HEAD --; then
32 | echo "No changes to commit, exiting with code 0"
33 | exit 0;
34 | else
35 | git remote set-url origin "https://${GITHUB_TOKEN}:x-oauth-basic@github.com/${GITHUB_REPOSITORY}.git";
36 | git config --local user.email "${GITHUB_EMAIL}";
37 | git config --local user.name "${GITHUB_USER}";
38 | git checkout -b update-iana-mime-type/$(date +%F-%H-%M);
39 | git commit -sam "feat: update mime types";
40 | git push --set-upstream origin $(git rev-parse --abbrev-ref HEAD)
41 | gh pr create -t "feat: update mime types" -b "*bleep bloop* I updated the IANA mime type list" -B main;
42 | fi
43 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: Code scanning
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 | schedule:
11 | - cron: '30 1 * * 0'
12 |
13 | jobs:
14 | codeql:
15 | name: Analysis
16 | uses: sapphiredev/.github/.github/workflows/reusable-codeql.yml@main
17 |
--------------------------------------------------------------------------------
/.github/workflows/continuous-delivery.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Delivery
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | prNumber:
7 | description: The number of the PR that is being deployed
8 | required: false
9 | type: string
10 | ref:
11 | description: The branch that is being deployed. Should be a branch on the given repository
12 | required: false
13 | default: main
14 | type: string
15 | repository:
16 | description: The {owner}/{repository} that is being deployed.
17 | required: false
18 | default: sapphiredev/plugins
19 | type: string
20 | push:
21 | branches:
22 | - main
23 |
24 | jobs:
25 | Publish:
26 | name: Publish Next to npm
27 | uses: sapphiredev/.github/.github/workflows/reusable-continuous-delivery.yml@main
28 | strategy:
29 | fail-fast: false
30 | matrix:
31 | package:
32 | - api
33 | - editable-commands
34 | - hmr
35 | - i18next
36 | - logger
37 | - pattern-commands
38 | - scheduled-tasks
39 | - subcommands
40 | - utilities-store
41 | with:
42 | pr-number: ${{ github.event.inputs.prNumber }}
43 | ref: ${{ github.event.inputs.ref }}
44 | repository: ${{ github.event.inputs.repository }}
45 | working-directory: packages/${{ matrix.package }}
46 | secrets:
47 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
48 |
--------------------------------------------------------------------------------
/.github/workflows/continuous-integration.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | linting:
11 | name: Linting
12 | uses: sapphiredev/.github/.github/workflows/reusable-lint.yml@main
13 |
14 | build:
15 | name: Building
16 | uses: sapphiredev/.github/.github/workflows/reusable-build.yml@main
17 |
18 | docs:
19 | name: Docgen
20 | if: github.event_name == 'pull_request'
21 | uses: sapphiredev/.github/.github/workflows/reusable-yarn-job.yml@main
22 | with:
23 | script-name: docs
24 |
25 | test:
26 | name: Tests
27 | uses: sapphiredev/.github/.github/workflows/reusable-tests.yml@main
28 | with:
29 | enable-sonar: true
30 | secrets:
31 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
32 |
33 | typecheck:
34 | name: Typecheck
35 | uses: sapphiredev/.github/.github/workflows/reusable-typecheck.yml@main
36 |
--------------------------------------------------------------------------------
/.github/workflows/deprecate-on-merge.yml:
--------------------------------------------------------------------------------
1 | name: NPM Deprecate PR versions On Merge
2 |
3 | on:
4 | pull_request:
5 | types:
6 | - closed
7 |
8 | jobs:
9 | deprecate-on-merge:
10 | name: NPM Deprecate PR versions On Merge
11 | uses: sapphiredev/.github/.github/workflows/reusable-yarn-job.yml@main
12 | with:
13 | script-name: npm-deprecate --name "*pr-${{ github.event.number }}*" -d -v
14 | secrets:
15 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
16 |
--------------------------------------------------------------------------------
/.github/workflows/documentation.yml:
--------------------------------------------------------------------------------
1 | name: Documentation
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | tags:
8 | - '**'
9 |
10 | concurrency:
11 | group: ${{ github.workspace }}|${{ github.head_ref || github.ref }}
12 | cancel-in-progress: true
13 |
14 | jobs:
15 | docgen:
16 | uses: sapphiredev/.github/.github/workflows/reusable-documentation-docgen.yml@main
17 | with:
18 | mono-repo: true
19 |
20 | upload:
21 | needs: docgen
22 | uses: sapphiredev/.github/.github/workflows/reusable-documentation-upload.yml@main
23 | strategy:
24 | max-parallel: 1
25 | fail-fast: false
26 | matrix:
27 | package:
28 | - api
29 | - editable-commands
30 | - hmr
31 | - i18next
32 | - logger
33 | - pattern-commands
34 | - scheduled-tasks
35 | - subcommands
36 | - utilities-store
37 | with:
38 | project-name: plugins
39 | mono-repo: true
40 | name: ${{ needs.docgen.outputs.NAME }}
41 | type: ${{ needs.docgen.outputs.TYPE }}
42 | sha: ${{ needs.docgen.outputs.SHA }}
43 | package: ${{ matrix.package }}
44 | artifact-id: ${{ needs.docgen.outputs.artifact-id }}
45 | secrets:
46 | SKYRA_TOKEN: ${{ secrets.SKYRA_TOKEN }}
47 |
--------------------------------------------------------------------------------
/.github/workflows/labelsync.yml:
--------------------------------------------------------------------------------
1 | name: Automatic Label Sync
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * *'
6 | workflow_dispatch:
7 |
8 | jobs:
9 | label_sync:
10 | uses: sapphiredev/.github/.github/workflows/reusable-labelsync.yml@main
11 | with:
12 | merge-labels: true
13 | repository-overwrite-labels: sapphiredev/plugins
14 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 | run-name: Publish @sapphire/${{ inputs.package }}
3 |
4 | on:
5 | workflow_dispatch:
6 | inputs:
7 | package:
8 | description: The package to release
9 | required: true
10 | type: choice
11 | options:
12 | - api
13 | - editable-commands
14 | - hmr
15 | - i18next
16 | - logger
17 | - pattern-commands
18 | - scheduled-tasks
19 | - subcommands
20 | - utilities-store
21 | skip-automatic-bump:
22 | description: Whether to skip the automatic bumping of the packageversion
23 | required: false
24 | default: false
25 | type: boolean
26 |
27 | jobs:
28 | PublishPackage:
29 | name: Publish @sapphire/${{ inputs.package}}
30 | uses: sapphiredev/.github/.github/workflows/reusable-publish.yml@main
31 | with:
32 | project-name: '@sapphire/${{ inputs.package}}'
33 | working-directory: packages/${{ inputs.package }}
34 | skip-automatic-bump: ${{ inputs.skip-automatic-bump }}
35 | secrets:
36 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
37 | SKYRA_TOKEN: ${{ secrets.SKYRA_TOKEN }}
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore a blackhole and the folder for development
2 | node_modules/
3 | .vs/
4 | .idea/
5 | *.iml
6 |
7 | # misc
8 | .DS_Store
9 | .env.local
10 | .env.development.local
11 | .env.test.local
12 | .env.production.local
13 |
14 | # Yarn files
15 | .yarn/install-state.gz
16 | .yarn/build-state.yml
17 |
18 | # Build Artifacts
19 | dist/
20 | build/
21 | docs/
22 | test_out/
23 | *.tsbuildinfo
24 | *.zip
25 | *.tar
26 | *.tar.xz
27 | *.tar.gz
28 | *.7z
29 | *.rar
30 | *.tgz
31 |
32 | # Ignore heapsnapshot and log files
33 | *.heapsnapshot
34 | *.log
35 | coverage/
36 | docs/
37 |
38 | # Ignore package locks
39 | package-lock.json
40 |
41 | # Ignore Turbo caching
42 | .turbo
43 |
--------------------------------------------------------------------------------
/.npm-deprecaterc.yml:
--------------------------------------------------------------------------------
1 | name: '*next*'
2 | package:
3 | - '@sapphire/plugin-api'
4 | - '@sapphire/plugin-editable-commands'
5 | - '@sapphire/plugin-i18next'
6 | - '@sapphire/plugin-logger'
7 | - '@sapphire/plugin-pattern-commands'
8 | - '@sapphire/plugin-scheduled-tasks'
9 | - '@sapphire/plugin-subcommands'
10 | - '@sapphire/plugin-utilities-store'
11 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | CHANGELOG.md
2 | .turbo
3 | dist/
4 | coverage/
5 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["bierner.github-markdown-preview", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": ["Dashless"],
3 | "sonarlint.connectedMode.project": {
4 | "connectionId": "sapphiredev",
5 | "projectKey": "sapphiredev_plugins"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | compressionLevel: mixed
2 |
3 | enableGlobalCache: true
4 |
5 | gitHooksPath: .github/hooks
6 |
7 | nodeLinker: node-modules
8 |
9 | plugins:
10 | - path: .yarn/plugins/@yarnpkg/plugin-git-hooks.cjs
11 | spec: 'https://raw.githubusercontent.com/trufflehq/yarn-plugin-git-hooks/main/bundles/%40yarnpkg/plugin-git-hooks.js'
12 |
13 | yarnPath: .yarn/releases/yarn-4.9.2.cjs
14 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | Copyright © `2020` `The Sapphire Community and its contributors`
4 |
5 | Permission is hereby granted, free of charge, to any person
6 | obtaining a copy of this software and associated documentation
7 | files (the “Software”), to deal in the Software without
8 | restriction, including without limitation the rights to use,
9 | copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the
11 | Software is furnished to do so, subject to the following
12 | conditions:
13 |
14 | The above copyright notice and this permission notice shall be
15 | included in all copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24 | OTHER DEALINGS IN THE SOFTWARE.
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
4 |
5 | # Sapphire Plugins
6 |
7 | **Plugins for Sapphire Framework.**
8 |
9 | [](https://github.com/sapphiredev/plugins/blob/main/LICENSE.md)
10 |
11 | **Packages**
12 |
13 | [](https://www.npmjs.com/package/@sapphire/plugin-api)
14 | [](https://www.npmjs.com/package/@sapphire/plugin-editable-commands)
15 | [](https://www.npmjs.com/package/@sapphire/plugin-logger)
16 | [](https://www.npmjs.com/package/@sapphire/plugin-i18next)
17 | [](https://www.npmjs.com/package/@sapphire/plugin-subcommands)
18 | [](https://www.npmjs.com/package/@sapphire/plugin-scheduled-tasks)
19 | [](https://www.npmjs.com/package/@sapphire/plugin-pattern-commands)
20 | [](https://www.npmjs.com/package/@sapphire/plugin-hmr)
21 |
22 |
23 |
24 | ## Buy us some doughnuts
25 |
26 | Sapphire Community is and always will be open source, even if we don't get donations. That being said, we know there are amazing people who may still want to donate just to show their appreciation. Thank you very much in advance!
27 |
28 | We accept donations through Open Collective, Ko-fi, Paypal, Patreon and GitHub Sponsorships. You can use the buttons below to donate through your method of choice.
29 |
30 | | Donate With | Address |
31 | | :-------------: | :-------------------------------------------------: |
32 | | Open Collective | [Click Here](https://sapphirejs.dev/opencollective) |
33 | | Ko-fi | [Click Here](https://sapphirejs.dev/kofi) |
34 | | Patreon | [Click Here](https://sapphirejs.dev/patreon) |
35 | | PayPal | [Click Here](https://sapphirejs.dev/paypal) |
36 |
37 | ## Contributors
38 |
39 | Please make sure to read the [Contributing Guide][contributing] before making a pull request.
40 |
41 | Thank you to all the people who already contributed to Sapphire!
42 |
43 |
44 |
45 |
46 |
47 | [contributing]: https://github.com/sapphiredev/.github/blob/main/.github/CONTRIBUTING.md
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "root-plugins",
3 | "private": true,
4 | "workspaces": [
5 | "packages/*"
6 | ],
7 | "scripts": {
8 | "clean": "rimraf \"packages/**/dist\" \"packages/**/.turbo\"",
9 | "lint": "eslint packages --ext mjs,js,ts,tsx --fix",
10 | "format": "prettier --write \"packages/**/{src,tests,scripts}/**/*.{mjs,ts,js}\"",
11 | "test": "vitest run",
12 | "build": "turbo run build",
13 | "docs": "turbo run docs",
14 | "typecheck": "turbo run typecheck",
15 | "update": "yarn upgrade-interactive",
16 | "check-update": "turbo run check-update"
17 | },
18 | "devDependencies": {
19 | "@commitlint/cli": "^19.8.1",
20 | "@commitlint/config-conventional": "^19.8.1",
21 | "@favware/cliff-jumper": "^6.0.0",
22 | "@favware/npm-deprecate": "^2.0.0",
23 | "@favware/rollup-type-bundler": "^4.0.0",
24 | "@sapphire/eslint-config": "^5.0.6",
25 | "@sapphire/framework": "^5.3.6",
26 | "@sapphire/pieces": "^4.4.1",
27 | "@sapphire/prettier-config": "^2.0.0",
28 | "@sapphire/stopwatch": "^1.5.4",
29 | "@sapphire/ts-config": "^5.0.1",
30 | "@sapphire/utilities": "^3.18.2",
31 | "@types/node": "^22.15.30",
32 | "@types/ws": "^8.18.1",
33 | "@typescript-eslint/eslint-plugin": "^7.18.0",
34 | "@typescript-eslint/parser": "^7.18.0",
35 | "@vitest/coverage-v8": "^3.2.2",
36 | "concurrently": "^9.1.2",
37 | "cz-conventional-changelog": "^3.3.0",
38 | "discord-api-types": "^0.38.4",
39 | "discord.js": "^14.19.3",
40 | "esbuild-plugin-file-path-extensions": "^2.1.4",
41 | "esbuild-plugin-version-injector": "^1.2.1",
42 | "eslint": "^8.57.1",
43 | "eslint-config-prettier": "^10.1.5",
44 | "eslint-plugin-prettier": "^5.4.1",
45 | "lint-staged": "^16.1.0",
46 | "prettier": "^3.5.3",
47 | "rimraf": "^6.0.1",
48 | "tsup": "^8.5.0",
49 | "tsx": "^4.19.4",
50 | "turbo": "^2.5.4",
51 | "typescript": "~5.4.5",
52 | "vite": "^6.3.5",
53 | "vitest": "^3.2.2"
54 | },
55 | "repository": {
56 | "type": "git",
57 | "url": "git+https://github.com/sapphiredev/plugins.git"
58 | },
59 | "engines": {
60 | "node": ">=v18"
61 | },
62 | "commitlint": {
63 | "extends": [
64 | "@commitlint/config-conventional"
65 | ]
66 | },
67 | "lint-staged": {
68 | "*": "prettier --ignore-unknown --write",
69 | "*.{mjs,js,ts}": "eslint --fix --ext mjs,js,ts"
70 | },
71 | "config": {
72 | "commitizen": {
73 | "path": "./node_modules/cz-conventional-changelog"
74 | }
75 | },
76 | "resolutions": {
77 | "acorn": "^8.14.1",
78 | "ansi-regex": "^5.0.1",
79 | "minimist": "^1.2.8"
80 | },
81 | "prettier": "@sapphire/prettier-config",
82 | "packageManager": "yarn@4.9.2"
83 | }
84 |
--------------------------------------------------------------------------------
/packages/api/.cliff-jumperrc.yml:
--------------------------------------------------------------------------------
1 | name: plugin-api
2 | org: sapphire
3 | install: true
4 | packagePath: packages/api
5 | identifierBase: false
6 | pushTag: true
7 | githubRelease: true
8 | githubReleaseLatest: true
9 | gitRepo: sapphiredev/plugins
10 | gitHostVariant: github
11 |
--------------------------------------------------------------------------------
/packages/api/.rollup-type-bundlerrc.yml:
--------------------------------------------------------------------------------
1 | external:
2 | - node:http
3 | - node:net
4 | - node:events
5 | - node:stream
6 | - node:buffer
7 | - zlib
8 | onlyBundle: true
9 | excludeFromClean:
10 | - dist/esm/register.d.mts
11 | - dist/cjs/register.d.ts
12 |
--------------------------------------------------------------------------------
/packages/api/.typedoc-json-parserrc.yml:
--------------------------------------------------------------------------------
1 | json: 'docs/api.json'
2 |
--------------------------------------------------------------------------------
/packages/api/cliff.toml:
--------------------------------------------------------------------------------
1 | [changelog]
2 | header = """
3 | # Changelog
4 |
5 | All notable changes to this project will be documented in this file.\n
6 | """
7 | body = """
8 | {%- macro remote_url() -%}
9 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
10 | {%- endmacro -%}
11 | {% if version %}\
12 | # [{{ version | trim_start_matches(pat="v") }}]\
13 | {% if previous %}\
14 | {% if previous.version %}\
15 | ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
16 | {% else %}\
17 | ({{ self::remote_url() }}/tree/{{ version }})\
18 | {% endif %}\
19 | {% endif %} \
20 | - ({{ timestamp | date(format="%Y-%m-%d") }})
21 | {% else %}\
22 | # [unreleased]
23 | {% endif %}\
24 | {% for group, commits in commits | group_by(attribute="group") %}
25 | ## {{ group | upper_first }}
26 | {% for commit in commits %}
27 | - {% if commit.scope %}\
28 | **{{commit.scope}}:** \
29 | {% endif %}\
30 | {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
31 | {% if commit.github.pr_number %} (\
32 | [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) by @{{ commit.github.username }}) \
33 | {%- endif %}\
34 | {% if commit.breaking %}\
35 | {% for breakingChange in commit.footers %}\
36 | \n{% raw %} {% endraw %}- 💥 **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
37 | {% endfor %}\
38 | {% endif %}\
39 | {% endfor %}
40 | {% endfor %}\n
41 | """
42 | trim = true
43 | footer = ""
44 |
45 | [git]
46 | conventional_commits = true
47 | filter_unconventional = true
48 | commit_parsers = [
49 | { message = "^feat", group = "🚀 Features" },
50 | { message = "^fix", group = "🐛 Bug Fixes" },
51 | { message = "^docs", group = "📝 Documentation" },
52 | { message = "^perf", group = "🏃 Performance" },
53 | { message = "^refactor", group = "🏠 Refactor" },
54 | { message = "^typings", group = "⌨️ Typings" },
55 | { message = "^types", group = "⌨️ Typings" },
56 | { message = ".*deprecated", body = ".*deprecated", group = "🚨 Deprecation" },
57 | { message = "^revert", skip = true },
58 | { message = "^style", group = "🪞 Styling" },
59 | { message = "^test", group = "🧪 Testing" },
60 | { message = "^chore", skip = true },
61 | { message = "^ci", skip = true },
62 | { message = "^build", skip = true },
63 | { body = ".*security", group = "🛡️ Security" },
64 | ]
65 | commit_preprocessors = [
66 | # remove issue numbers from commits
67 | { pattern = '\s\((\w+\s)?#([0-9]+)\)', replace = "" },
68 | ]
69 | filter_commits = true
70 | tag_pattern = "@sapphire/plugin-api@[0-9]*"
71 | ignore_tags = ""
72 | topo_order = false
73 | sort_commits = "newest"
74 |
75 | [remote.github]
76 | owner = "sapphiredev"
77 | repo = "plugins"
78 |
--------------------------------------------------------------------------------
/packages/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sapphire/plugin-api",
3 | "version": "8.2.1",
4 | "description": "Plugin for @sapphire/framework to expose a REST API",
5 | "author": "@sapphire",
6 | "license": "MIT",
7 | "main": "dist/cjs/index.cjs",
8 | "module": "dist/esm/index.mjs",
9 | "types": "dist/cjs/index.d.cts",
10 | "exports": {
11 | ".": {
12 | "import": {
13 | "types": "./dist/esm/index.d.mts",
14 | "default": "./dist/esm/index.mjs"
15 | },
16 | "require": {
17 | "types": "./dist/cjs/index.d.cts",
18 | "default": "./dist/cjs/index.cjs"
19 | }
20 | },
21 | "./register": {
22 | "import": {
23 | "types": "./dist/esm/register.d.mts",
24 | "default": "./dist/esm/register.mjs"
25 | },
26 | "require": {
27 | "types": "./dist/cjs/register.d.cts",
28 | "default": "./dist/cjs/register.cjs"
29 | }
30 | }
31 | },
32 | "sideEffects": [
33 | "./dist/cjs/register.cjs",
34 | "./dist/esm/register.mjs"
35 | ],
36 | "homepage": "https://github.com/sapphiredev/plugins/tree/main/packages/api",
37 | "scripts": {
38 | "test": "vitest run",
39 | "lint": "eslint src tests --ext ts --fix",
40 | "build": "tsup && yarn build:types && yarn build:rename-cjs-register",
41 | "build:types": "concurrently \"yarn:build:types:*\"",
42 | "build:types:cjs": "rollup-type-bundler -d dist/cjs --output-typings-file-extension .cts",
43 | "build:types:esm": "rollup-type-bundler -d dist/esm -t .mts",
44 | "build:types:cleanup": "tsx ../../scripts/clean-register-imports.mts",
45 | "build:rename-cjs-register": "tsx ../../scripts/rename-cjs-register.mts",
46 | "sync-mime-types": "tsx scripts/sync-mime-types.mts",
47 | "typecheck": "tsc -b src",
48 | "docs": "typedoc-json-parser",
49 | "prepack": "yarn build",
50 | "bump": "cliff-jumper",
51 | "check-update": "cliff-jumper --dry-run"
52 | },
53 | "dependencies": {
54 | "@types/ws": "^8.18.1",
55 | "@vladfrangu/async_event_emitter": "2.4.6",
56 | "cookie-es": "^1.2.2",
57 | "tldts": "^7.0.8",
58 | "undici": "^7.10.0"
59 | },
60 | "repository": {
61 | "type": "git",
62 | "url": "git+https://github.com/sapphiredev/plugins.git",
63 | "directory": "packages/api"
64 | },
65 | "files": [
66 | "dist/"
67 | ],
68 | "engines": {
69 | "node": ">=v18",
70 | "npm": ">=7"
71 | },
72 | "keywords": [
73 | "sapphiredev",
74 | "plugin",
75 | "bot",
76 | "typescript",
77 | "ts",
78 | "yarn",
79 | "discord",
80 | "sapphire"
81 | ],
82 | "bugs": {
83 | "url": "https://github.com/sapphiredev/plugins/issues"
84 | },
85 | "publishConfig": {
86 | "access": "public"
87 | },
88 | "devDependencies": {
89 | "@favware/cliff-jumper": "^6.0.0",
90 | "@favware/rollup-type-bundler": "^4.0.0",
91 | "concurrently": "^9.1.2",
92 | "tsup": "^8.5.0",
93 | "tsx": "^4.19.4",
94 | "typedoc": "^0.26.11",
95 | "typedoc-json-parser": "^10.2.0",
96 | "typescript": "~5.4.5"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/packages/api/scripts/sync-mime-types.mts:
--------------------------------------------------------------------------------
1 | import { writeFile } from 'node:fs/promises';
2 |
3 | const output = new URL('../src/lib/utils/MimeType.ts', import.meta.url);
4 |
5 | const response = await fetch('https://www.iana.org/assignments/media-types/media-types.xml');
6 | const text = await response.text();
7 |
8 | const fileTypeTemplateRegex = /(\w+\/.+?)<\/file>/g;
9 | const outputLines = ['export type MimeType ='];
10 |
11 | let result: RegExpExecArray | null;
12 | while ((result = fileTypeTemplateRegex.exec(text))) {
13 | outputLines.push(`\t| '${result[1]}'`);
14 | }
15 |
16 | outputLines.push('\t| `X-${string}/${string}`;');
17 | outputLines.push('');
18 |
19 | await writeFile(output, outputLines.join('\n'), 'utf8');
20 | console.log(`Successfully written to ${output.href}: ${outputLines.length - 3} mime types total`);
21 |
--------------------------------------------------------------------------------
/packages/api/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { Server, ServerOptions } from './lib/structures/http/Server';
2 | import type { MiddlewareStore } from './lib/structures/MiddlewareStore';
3 | import type { RouteStore } from './lib/structures/RouteStore';
4 |
5 | export * from './lib/structures/api/ApiRequest';
6 | export * from './lib/structures/api/ApiResponse';
7 | export * from './lib/structures/api/CookieStore';
8 | export * from './lib/structures/http/Auth';
9 | export * from './lib/structures/http/HttpCodes';
10 | export * from './lib/structures/http/HttpMethods';
11 | export * from './lib/structures/http/Server';
12 | export * from './lib/structures/Middleware';
13 | export * from './lib/structures/MiddlewareStore';
14 | export * from './lib/structures/Route';
15 | export * from './lib/structures/router/RouterBranch';
16 | export * from './lib/structures/router/RouterNode';
17 | export * from './lib/structures/router/RouterRoot';
18 | export * from './lib/structures/RouteStore';
19 | export type * from './lib/utils/MimeType';
20 |
21 | export { loadListeners } from './listeners/_load';
22 | export { loadMiddlewares } from './middlewares/_load';
23 | export { loadRoutes } from './routes/_load';
24 |
25 | declare module 'discord.js' {
26 | interface Client {
27 | server: Server;
28 | }
29 |
30 | interface ClientOptions {
31 | api?: ServerOptions;
32 | }
33 | }
34 |
35 | declare module '@sapphire/pieces' {
36 | interface StoreRegistryEntries {
37 | routes: RouteStore;
38 | middlewares: MiddlewareStore;
39 | }
40 |
41 | interface Container {
42 | server: Server;
43 | }
44 | }
45 |
46 | /**
47 | * The [@sapphire/plugin-api](https://github.com/sapphiredev/plugins/blob/main/packages/api) version that you are currently using.
48 | * An example use of this is showing it of in a bot information command.
49 | *
50 | * Note to Sapphire developers: This needs to explicitly be `string` so it is not typed as the string that gets replaced by esbuild
51 | */
52 | // eslint-disable-next-line @typescript-eslint/no-inferrable-types
53 | export const version: string = '[VI]{{inject}}[/VI]';
54 |
--------------------------------------------------------------------------------
/packages/api/src/lib/structures/Augmentations.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ================================================
3 | * | THIS IS FOR TYPEDOC. DO NOT REMOVE THIS FILE |
4 | * ================================================
5 | */
6 |
7 | import type { MiddlewareStore, RouteStore } from '../..';
8 | import type { Server, ServerOptions } from './http/Server';
9 |
10 | declare module 'discord.js' {
11 | export interface Client {
12 | server: Server;
13 | }
14 |
15 | export interface ClientOptions {
16 | api?: ServerOptions;
17 | }
18 | }
19 |
20 | declare module '@sapphire/framework' {
21 | interface StoreRegistryEntries {
22 | routes: RouteStore;
23 | middlewares: MiddlewareStore;
24 | }
25 | }
26 |
27 | declare module '@sapphire/pieces' {
28 | interface Container {
29 | server: Server;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/api/src/lib/structures/Middleware.ts:
--------------------------------------------------------------------------------
1 | import { Piece } from '@sapphire/pieces';
2 | import type { Awaitable } from '@sapphire/utilities';
3 | import type { ApiRequest } from './api/ApiRequest';
4 | import type { ApiResponse } from './api/ApiResponse';
5 |
6 | /**
7 | * @since 1.0.0
8 | */
9 | export abstract class Middleware extends Piece {
10 | /**
11 | * The position the middleware has. The {@link MiddlewareStore} will run all middlewares with lower position than
12 | * this one.
13 | *
14 | * The built-in middlewares follow the following positions:
15 | * - headers: 10
16 | * - body: 20
17 | * - cookies: 30
18 | * - auth: 40
19 | */
20 | public readonly position: number;
21 |
22 | public constructor(context: Middleware.LoaderContext, options: Options = {} as Options) {
23 | super(context, options);
24 | this.position = options.position ?? 1000;
25 | }
26 |
27 | /**
28 | * The method to be overridden by other middlewares.
29 | * @param request The client's request.
30 | * @param response The server's response.
31 | * @param route The route that matched this request, will be `null` if none matched.
32 | */
33 | public abstract run(request: Middleware.Request, response: Middleware.Response): Awaitable;
34 | }
35 |
36 | /**
37 | * The options for all middlewares.
38 | */
39 | export interface MiddlewareOptions extends Piece.Options {
40 | /**
41 | * The position to insert the middleware at.
42 | * @see Middleware#position
43 | * @default 1000
44 | */
45 | position?: number;
46 | }
47 |
48 | export namespace Middleware {
49 | /** @deprecated Use {@linkcode LoaderContext} instead. */
50 | export type Context = LoaderContext;
51 | export type LoaderContext = Piece.LoaderContext<'middlewares'>;
52 | export type Options = MiddlewareOptions;
53 | export type JSON = Piece.JSON;
54 | export type LocationJSON = Piece.LocationJSON;
55 |
56 | export type Request = ApiRequest;
57 | export type Response = ApiResponse;
58 | }
59 |
--------------------------------------------------------------------------------
/packages/api/src/lib/structures/MiddlewareStore.ts:
--------------------------------------------------------------------------------
1 | import { Store } from '@sapphire/pieces';
2 | import { Middleware } from './Middleware';
3 |
4 | /**
5 | * @since 1.0.0
6 | */
7 | export class MiddlewareStore extends Store {
8 | /**
9 | * The sorted middlewares, in ascending order of see {@link Middleware.position}.
10 | */
11 | public readonly sortedMiddlewares: Middleware[] = [];
12 |
13 | public constructor() {
14 | super(Middleware, { name: 'middlewares' });
15 | }
16 |
17 | public async run(request: Middleware.Request, response: Middleware.Response): Promise {
18 | for (const middleware of this.sortedMiddlewares) {
19 | if (response.writableEnded) return;
20 | if (middleware.enabled) await middleware.run(request, response);
21 | }
22 | }
23 |
24 | public override set(key: string, value: Middleware): this {
25 | const index = this.sortedMiddlewares.findIndex((middleware) => middleware.position >= value.position);
26 |
27 | // If a middleware with lower priority wasn't found, push to the end of the array
28 | if (index === -1) this.sortedMiddlewares.push(value);
29 | else this.sortedMiddlewares.splice(index, 0, value);
30 |
31 | return super.set(key, value);
32 | }
33 |
34 | public override delete(key: string): boolean {
35 | const index = this.sortedMiddlewares.findIndex((middleware) => middleware.name === key);
36 |
37 | // If the middleware was found, remove it
38 | if (index !== -1) this.sortedMiddlewares.splice(index, 1);
39 |
40 | return super.delete(key);
41 | }
42 |
43 | public override clear(): void {
44 | this.sortedMiddlewares.length = 0;
45 | return super.clear();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/packages/api/src/lib/structures/RouteLoaderStrategy.ts:
--------------------------------------------------------------------------------
1 | import { LoaderStrategy } from '@sapphire/pieces';
2 | import type { Route } from './Route';
3 | import type { RouteStore } from './RouteStore';
4 |
5 | export class RouteLoaderStrategy extends LoaderStrategy {
6 | public override onLoad(store: RouteStore, piece: Route): void {
7 | store.router.add(piece);
8 | }
9 |
10 | public override onUnload(store: RouteStore, piece: Route): void {
11 | store.router.remove(piece);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/api/src/lib/structures/RouteStore.ts:
--------------------------------------------------------------------------------
1 | import { Store } from '@sapphire/pieces';
2 | import { Route } from './Route';
3 | import { RouteLoaderStrategy } from './RouteLoaderStrategy';
4 | import { RouterRoot } from './router/RouterRoot';
5 |
6 | /**
7 | * @since 1.0.0
8 | */
9 | export class RouteStore extends Store {
10 | public readonly router = new RouterRoot();
11 |
12 | public constructor() {
13 | super(Route, { name: 'routes', strategy: new RouteLoaderStrategy() });
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/api/src/lib/structures/http/HttpMethods.ts:
--------------------------------------------------------------------------------
1 | export type MethodName = (typeof MethodNames)[number];
2 |
3 | export const MethodNames = [
4 | 'ACL',
5 | 'BIND',
6 | 'CHECKOUT',
7 | 'CONNECT',
8 | 'COPY',
9 | 'DELETE',
10 | 'GET',
11 | 'HEAD',
12 | 'LINK',
13 | 'LOCK',
14 | 'M-SEARCH',
15 | 'MERGE',
16 | 'MKACTIVITY',
17 | 'MKCALENDAR',
18 | 'MKCOL',
19 | 'MOVE',
20 | 'NOTIFY',
21 | 'OPTIONS',
22 | 'PATCH',
23 | 'POST',
24 | 'PROPFIND',
25 | 'PROPPATCH',
26 | 'PURGE',
27 | 'PUT',
28 | 'QUERY',
29 | 'REBIND',
30 | 'REPORT',
31 | 'SEARCH',
32 | 'SOURCE',
33 | 'SUBSCRIBE',
34 | 'TRACE',
35 | 'UNBIND',
36 | 'UNLINK',
37 | 'UNLOCK',
38 | 'UNSUBSCRIBE'
39 | ] as const;
40 |
--------------------------------------------------------------------------------
/packages/api/src/lib/structures/router/RouterNode.ts:
--------------------------------------------------------------------------------
1 | import { Collection } from 'discord.js';
2 | import type { Route } from '../Route';
3 | import type { MethodName } from '../http/HttpMethods';
4 | import type { RouterBranch } from './RouterBranch';
5 |
6 | export class RouterNode {
7 | /**
8 | * The branch containing this node.
9 | */
10 | public readonly parent: RouterBranch;
11 |
12 | /**
13 | * The methods this node supports.
14 | */
15 | readonly #methods = new Collection();
16 |
17 | public constructor(parent: RouterBranch) {
18 | this.parent = parent;
19 | }
20 |
21 | public get path() {
22 | return this.parent.path;
23 | }
24 |
25 | public extractParameters(parts: readonly string[]): Record {
26 | const parameters: Record = {};
27 |
28 | let branch: RouterBranch | null = this.parent;
29 | let index = parts.length - 1;
30 | do {
31 | if (branch.dynamic) parameters[branch.name] = parts[index];
32 |
33 | branch = branch.parent;
34 | --index;
35 | } while (branch);
36 |
37 | return parameters;
38 | }
39 |
40 | public get(method: MethodName): Route | null {
41 | return this.#methods.get(method) ?? null;
42 | }
43 |
44 | public set(method: MethodName, route: Route): this {
45 | this.#methods.set(method, route);
46 | return this;
47 | }
48 |
49 | public delete(method: MethodName, route: Route): boolean {
50 | const existing = this.#methods.get(method);
51 | if (existing === route) {
52 | this.#methods.delete(method);
53 | return true;
54 | }
55 |
56 | return false;
57 | }
58 |
59 | public methods(): IterableIterator {
60 | return this.#methods.keys();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/packages/api/src/lib/structures/router/RouterRoot.ts:
--------------------------------------------------------------------------------
1 | import { isNullishOrEmpty } from '@sapphire/utilities';
2 | import type { Route } from '../Route';
3 | import type { MethodName } from '../http/HttpMethods';
4 | import { RouterBranch } from './RouterBranch';
5 | import type { RouterNode } from './RouterNode';
6 |
7 | export class RouterRoot extends RouterBranch {
8 | public constructor() {
9 | super('::ROOT::', false, null);
10 | }
11 |
12 | /**
13 | * Adds a route to the branch
14 | *
15 | * @param route The route to add
16 | * @returns The node the route was added to
17 | */
18 | public add(route: Route): RouterNode {
19 | return this._add(route.path, 0, route);
20 | }
21 |
22 | /**
23 | * Removes a route from the branch
24 | *
25 | * @param route The route to remove
26 | * @returns Whether or not the route was removed
27 | */
28 | public remove(route: Route): boolean {
29 | return this._remove(route.path, 0, route);
30 | }
31 |
32 | // eslint-disable-next-line @typescript-eslint/class-literal-property-style
33 | public override get path(): string {
34 | return '';
35 | }
36 |
37 | public override toString(): string {
38 | return '';
39 | }
40 |
41 | public static makeRoutePathForPiece(directories: readonly string[], name: string): string {
42 | const parts: string[] = [];
43 | for (const directory of directories) {
44 | const trimmed = directory.trim();
45 |
46 | // If empty, skip:
47 | if (isNullishOrEmpty(trimmed)) continue;
48 | // If it's a group, skip:
49 | if (trimmed.startsWith('(') && trimmed.endsWith(')')) continue;
50 |
51 | parts.push(trimmed);
52 | }
53 |
54 | if (name !== 'index') {
55 | parts.push(name.trim());
56 | }
57 |
58 | return parts.join('/');
59 | }
60 |
61 | public static normalize(path: string | null | undefined): string[] {
62 | const parts = [] as string[];
63 | if (isNullishOrEmpty(path)) return parts;
64 |
65 | let part = '';
66 | for (const char of path) {
67 | if (char === '/') {
68 | if (part.length) {
69 | parts.push(part);
70 | part = '';
71 | }
72 | } else {
73 | part += char;
74 | }
75 | }
76 |
77 | if (part.length) {
78 | parts.push(part);
79 | }
80 |
81 | return parts;
82 | }
83 |
84 | public static extractMethod(path: string | readonly string[]): MethodName | null {
85 | if (path.length === 0) return null;
86 | if (typeof path === 'string') {
87 | const methodSeparatorPositionIndex = path.lastIndexOf('.');
88 | if (methodSeparatorPositionIndex === -1 || methodSeparatorPositionIndex === path.length - 1) return null;
89 |
90 | return path.slice(methodSeparatorPositionIndex + 1).toUpperCase() as MethodName;
91 | }
92 |
93 | const lastIndex = path.length - 1;
94 | return RouterRoot.extractMethod(path[lastIndex]);
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/packages/api/src/lib/utils/_body/RequestHeadersProxy.ts:
--------------------------------------------------------------------------------
1 | import { isNullishOrEmpty } from '@sapphire/utilities';
2 | import { splitSetCookieString } from 'cookie-es';
3 | import type { Headers, SpecIterableIterator } from 'undici';
4 | import type { ApiRequest } from '../../structures/api/ApiRequest';
5 | import { NodeUtilInspectSymbol } from '../constants';
6 |
7 | export class RequestHeadersProxy implements Headers {
8 | private readonly request: ApiRequest;
9 |
10 | public constructor(request: ApiRequest) {
11 | this.request = request;
12 | }
13 |
14 | public append(name: string, value: string): void {
15 | const { headers } = this.request;
16 | const current = headers[name];
17 | if (current) {
18 | if (Array.isArray(current)) {
19 | current.push(value);
20 | } else {
21 | headers[name] = [current, value];
22 | }
23 | } else {
24 | headers[name] = value;
25 | }
26 | }
27 |
28 | public delete(name: string): void {
29 | this.request.headers[name] = undefined;
30 | }
31 |
32 | public get(name: string): string | null {
33 | return normalizeValue(this.request.headers[name]);
34 | }
35 |
36 | public has(name: string): boolean {
37 | return !isNullishOrEmpty(this.request.headers[name]);
38 | }
39 |
40 | public set(name: string, value: string): void {
41 | this.request.headers[name] = value;
42 | }
43 |
44 | public getSetCookie(): string[] {
45 | const setCookie = this.get('set-cookie');
46 | return setCookie === null ? [] : splitSetCookieString(setCookie);
47 | }
48 |
49 | public forEach(callbackfn: (value: string, key: string, iterable: Headers) => void, thisArg?: unknown): void {
50 | for (const [key, value] of this.entries()) {
51 | callbackfn.call(thisArg, value, key, this);
52 | }
53 | }
54 |
55 | public *keys(): SpecIterableIterator {
56 | const { headers } = this.request;
57 | for (const key of Object.keys(headers)) {
58 | const value = headers[key];
59 |
60 | if (!isNullishOrEmpty(value)) {
61 | yield key;
62 | }
63 | }
64 | }
65 |
66 | public *values(): SpecIterableIterator {
67 | const { headers } = this.request;
68 | for (const key of Object.keys(headers)) {
69 | const value = headers[key];
70 |
71 | if (!isNullishOrEmpty(value)) {
72 | yield normalizeValue(value);
73 | }
74 | }
75 | }
76 |
77 | public *entries(): SpecIterableIterator<[string, string]> {
78 | const { headers } = this.request;
79 | for (const key of Object.keys(headers)) {
80 | const value = headers[key];
81 |
82 | if (!isNullishOrEmpty(value)) {
83 | yield [key, normalizeValue(value)];
84 | }
85 | }
86 | }
87 |
88 | public [Symbol.iterator](): SpecIterableIterator<[string, string]> {
89 | return this.entries();
90 | }
91 |
92 | // eslint-disable-next-line @typescript-eslint/class-literal-property-style
93 | public get [Symbol.toStringTag]() {
94 | return 'Headers';
95 | }
96 |
97 | public [NodeUtilInspectSymbol]() {
98 | return Object.fromEntries(this.entries());
99 | }
100 | }
101 |
102 | function normalizeValue(value: string | string[] | undefined): string {
103 | if (Array.isArray(value)) {
104 | return value.join(', ');
105 | }
106 |
107 | return String(value ?? '');
108 | }
109 |
--------------------------------------------------------------------------------
/packages/api/src/lib/utils/constants.ts:
--------------------------------------------------------------------------------
1 | export const NodeUtilInspectSymbol = Symbol.for('nodejs.util.inspect.custom');
2 |
--------------------------------------------------------------------------------
/packages/api/src/listeners/PluginRouteError.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import type { ApiRequest } from '../lib/structures/api/ApiRequest';
3 | import type { ApiResponse } from '../lib/structures/api/ApiResponse';
4 | import { HttpCodes } from '../lib/structures/http/HttpCodes';
5 | import { ServerEvent } from '../lib/structures/http/Server';
6 |
7 | export class PluginListener extends Listener {
8 | public constructor(context: Listener.LoaderContext) {
9 | super(context, { emitter: 'server', event: ServerEvent.RouteError });
10 | }
11 |
12 | public override run(error: Error, _request: ApiRequest, response: ApiResponse) {
13 | // Log the error to console:
14 | this.container.logger.fatal(error);
15 |
16 | // Send a response to the client if none was sent:
17 | if (!response.writableEnded) response.status(HttpCodes.InternalServerError).json({ error: error.message ?? error });
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/api/src/listeners/PluginServerMiddlewareError.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import type { ApiRequest } from '../lib/structures/api/ApiRequest';
3 | import type { ApiResponse } from '../lib/structures/api/ApiResponse';
4 | import { HttpCodes } from '../lib/structures/http/HttpCodes';
5 | import { ServerEvent } from '../lib/structures/http/Server';
6 |
7 | export class PluginListener extends Listener {
8 | public constructor(context: Listener.LoaderContext) {
9 | super(context, { emitter: 'server', event: ServerEvent.MiddlewareError });
10 | }
11 |
12 | public override run(error: Error, _request: ApiRequest, response: ApiResponse) {
13 | // Log the error to console:
14 | this.container.logger.fatal(error);
15 |
16 | // Send a response to the client if none was sent:
17 | if (!response.writableEnded) response.status(HttpCodes.InternalServerError).json({ error: error.message ?? error });
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/api/src/listeners/PluginServerMiddlewareSuccess.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import type { ApiRequest } from '../lib/structures/api/ApiRequest';
3 | import type { ApiResponse } from '../lib/structures/api/ApiResponse';
4 | import { ServerEvent } from '../lib/structures/http/Server';
5 |
6 | export class PluginListener extends Listener {
7 | public constructor(context: Listener.LoaderContext) {
8 | super(context, { emitter: 'server', event: ServerEvent.MiddlewareSuccess });
9 | }
10 |
11 | public override async run(request: ApiRequest, response: ApiResponse) {
12 | try {
13 | await request.route!.run(request, response);
14 | } catch (error) {
15 | this.container.server.emit(ServerEvent.RouteError, error as Error, request, response);
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/api/src/listeners/PluginServerRequest.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import type { ApiRequest } from '../lib/structures/api/ApiRequest';
3 | import type { ApiResponse } from '../lib/structures/api/ApiResponse';
4 | import type { MethodName } from '../lib/structures/http/HttpMethods';
5 | import { ServerEvent } from '../lib/structures/http/Server';
6 | import { RouterRoot } from '../lib/structures/router/RouterRoot';
7 |
8 | export class PluginListener extends Listener {
9 | public constructor(context: Listener.LoaderContext) {
10 | super(context, { emitter: 'server', event: ServerEvent.Request });
11 | }
12 |
13 | public override async run(request: ApiRequest, response: ApiResponse) {
14 | const { parts, querystring } = this.#parseURL(request.url);
15 | request.query = Object.fromEntries(new URLSearchParams(querystring).entries());
16 |
17 | const branch = this.container.server.routes.router.find(parts);
18 | const node = branch ? branch.node : null;
19 | const route = node ? node.get((request.method ?? 'GET') as MethodName) : null;
20 |
21 | if (node !== null) {
22 | request.params = node!.extractParameters(parts);
23 | }
24 |
25 | request.routerNode = node;
26 | request.route = route;
27 |
28 | try {
29 | // Middlewares need to be run regardless of the match, specially since browsers do an OPTIONS request first.
30 | await this.container.server.middlewares.run(request, response);
31 | } catch (error) {
32 | this.container.server.emit(ServerEvent.MiddlewareError, error as Error, request, response);
33 |
34 | // If a middleware errored, it might cause undefined behavior in the routes, so we will return early.
35 | return;
36 | }
37 |
38 | if (branch === null) {
39 | this.container.server.emit(ServerEvent.RouterBranchNotFound, request, response);
40 | } else if (route === null) {
41 | this.container.server.emit(ServerEvent.RouterBranchMethodNotAllowed, request, response, branch);
42 | } else {
43 | this.container.server.emit(ServerEvent.RouterFound, request, response);
44 | }
45 | }
46 |
47 | #parseURL(url = '') {
48 | const index = url.indexOf('?');
49 |
50 | let pathname: string;
51 | let querystring: string;
52 | if (index === -1) {
53 | pathname = url;
54 | querystring = '';
55 | } else {
56 | pathname = url.substring(0, index);
57 | querystring = url.substring(index + 1);
58 | }
59 |
60 | return { parts: RouterRoot.normalize(pathname), querystring };
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/packages/api/src/listeners/PluginServerRouterBranchMethodNotAllowed.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import type { ApiRequest } from '../lib/structures/api/ApiRequest';
3 | import type { ApiResponse } from '../lib/structures/api/ApiResponse';
4 | import { ServerEvent } from '../lib/structures/http/Server';
5 |
6 | export class PluginListener extends Listener {
7 | public constructor(context: Listener.LoaderContext) {
8 | super(context, { emitter: 'server', event: ServerEvent.RouterBranchMethodNotAllowed });
9 | }
10 |
11 | public override run(_: ApiRequest, response: ApiResponse) {
12 | if (!response.writableEnded) response.methodNotAllowed();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/api/src/listeners/PluginServerRouterBranchNotFound.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import type { ApiRequest } from '../lib/structures/api/ApiRequest';
3 | import type { ApiResponse } from '../lib/structures/api/ApiResponse';
4 | import { ServerEvent } from '../lib/structures/http/Server';
5 |
6 | export class PluginListener extends Listener {
7 | public constructor(context: Listener.LoaderContext) {
8 | super(context, { emitter: 'server', event: ServerEvent.RouterBranchNotFound });
9 | }
10 |
11 | public override run(_: ApiRequest, response: ApiResponse) {
12 | if (!response.writableEnded) response.notFound();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/api/src/listeners/PluginServerRouterFound.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import type { ApiRequest } from '../lib/structures/api/ApiRequest';
3 | import type { ApiResponse } from '../lib/structures/api/ApiResponse';
4 | import { ServerEvent } from '../lib/structures/http/Server';
5 |
6 | export class PluginListener extends Listener {
7 | public constructor(context: Listener.LoaderContext) {
8 | super(context, { emitter: 'server', event: ServerEvent.RouterFound });
9 | }
10 |
11 | public override run(request: ApiRequest, response: ApiResponse) {
12 | const event = response.writableEnded ? ServerEvent.MiddlewareFailure : ServerEvent.MiddlewareSuccess;
13 | this.container.server.emit(event, request, response);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/api/src/listeners/_load.ts:
--------------------------------------------------------------------------------
1 | import { container } from '@sapphire/pieces';
2 | import { PluginListener as PluginRouteError } from './PluginRouteError';
3 | import { PluginListener as PluginServerMiddlewareError } from './PluginServerMiddlewareError';
4 | import { PluginListener as PluginServerMiddlewareSuccess } from './PluginServerMiddlewareSuccess';
5 | import { PluginListener as PluginServerRequest } from './PluginServerRequest';
6 | import { PluginListener as PluginServerRouterBranchMethodNotAllowed } from './PluginServerRouterBranchMethodNotAllowed';
7 | import { PluginListener as PluginServerRouterBranchNotFound } from './PluginServerRouterBranchNotFound';
8 | import { PluginListener as PluginServerRouterFound } from './PluginServerRouterFound';
9 |
10 | export function loadListeners() {
11 | const store = 'listeners';
12 | void container.stores.loadPiece({ name: 'PluginRouteError', piece: PluginRouteError, store });
13 | void container.stores.loadPiece({ name: 'PluginServerMiddlewareError', piece: PluginServerMiddlewareError, store });
14 | void container.stores.loadPiece({ name: 'PluginServerMiddlewareSuccess', piece: PluginServerMiddlewareSuccess, store });
15 | void container.stores.loadPiece({ name: 'PluginServerRequest', piece: PluginServerRequest, store });
16 | void container.stores.loadPiece({ name: 'PluginServerRouterBranchMethodNotAllowed', piece: PluginServerRouterBranchMethodNotAllowed, store });
17 | void container.stores.loadPiece({ name: 'PluginServerRouterBranchNotFound', piece: PluginServerRouterBranchNotFound, store });
18 | void container.stores.loadPiece({ name: 'PluginServerRouterFound', piece: PluginServerRouterFound, store });
19 | }
20 |
--------------------------------------------------------------------------------
/packages/api/src/middlewares/_load.ts:
--------------------------------------------------------------------------------
1 | import { container } from '@sapphire/pieces';
2 | import { PluginMiddleware as PluginAuth } from './auth';
3 | import { PluginMiddleware as PluginBody } from './body';
4 | import { PluginMiddleware as PluginCookies } from './cookies';
5 | import { PluginMiddleware as PluginHeaders } from './headers';
6 |
7 | export function loadMiddlewares() {
8 | const store = 'middlewares';
9 | void container.stores.loadPiece({ name: 'auth', piece: PluginAuth, store });
10 | void container.stores.loadPiece({ name: 'body', piece: PluginBody, store });
11 | void container.stores.loadPiece({ name: 'cookies', piece: PluginCookies, store });
12 | void container.stores.loadPiece({ name: 'headers', piece: PluginHeaders, store });
13 | }
14 |
--------------------------------------------------------------------------------
/packages/api/src/middlewares/auth.ts:
--------------------------------------------------------------------------------
1 | import { Middleware } from '../lib/structures/Middleware';
2 |
3 | export class PluginMiddleware extends Middleware {
4 | private readonly cookieName: string;
5 |
6 | public constructor(context: Middleware.LoaderContext) {
7 | super(context, { position: 40 });
8 |
9 | const { server } = this.container;
10 | this.cookieName = server.auth?.cookie ?? 'SAPPHIRE_AUTH';
11 | this.enabled = server.auth !== null;
12 | }
13 |
14 | public override run(request: Middleware.Request, response: Middleware.Response) {
15 | // If there are no cookies, set auth as null:
16 | const authorization = response.cookies.get(this.cookieName);
17 | if (!authorization) {
18 | request.auth = null;
19 | return;
20 | }
21 |
22 | // Decrypt the cookie, and if the token is invalid, remove the cookie:
23 | request.auth = this.container.server.auth!.decrypt(authorization);
24 | if (request.auth === null) response.cookies.remove(this.cookieName);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/api/src/middlewares/body.ts:
--------------------------------------------------------------------------------
1 | import { HttpCodes } from '../lib/structures/http/HttpCodes';
2 | import { Middleware } from '../lib/structures/Middleware';
3 |
4 | export class PluginMiddleware extends Middleware {
5 | public constructor(context: Middleware.LoaderContext) {
6 | super(context, { position: 20 });
7 | }
8 |
9 | public override run(request: Middleware.Request, response: Middleware.Response) {
10 | if (!request.route) return;
11 |
12 | // RFC 1341 4.
13 | const contentType = request.headers['content-type'];
14 | if (typeof contentType !== 'string') return;
15 |
16 | // RFC 7230 3.3.2.
17 | const lengthString = request.headers['content-length'];
18 | if (typeof lengthString !== 'string') return;
19 |
20 | // Verify if the content length is lower than accepted:
21 | const length = Number(lengthString);
22 | const maximumLength = request.route.maximumBodyLength;
23 | if (length > maximumLength) {
24 | response.status(HttpCodes.PayloadTooLarge).json({ error: 'Exceeded maximum content length.' });
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/api/src/middlewares/cookies.ts:
--------------------------------------------------------------------------------
1 | import { Middleware } from '../lib/structures/Middleware';
2 | import { CookieStore } from '../lib/structures/api/CookieStore';
3 |
4 | export class PluginMiddleware extends Middleware {
5 | private readonly production: boolean = process.env.NODE_ENV === 'production';
6 | private readonly domainOverwrite: string | null;
7 |
8 | public constructor(context: Middleware.LoaderContext) {
9 | super(context, { position: 30 });
10 |
11 | const { server } = this.container;
12 | this.domainOverwrite = server.auth?.domainOverwrite ?? null;
13 | }
14 |
15 | public override run(request: Middleware.Request, response: Middleware.Response) {
16 | response.cookies = new CookieStore(request, response, this.production, this.domainOverwrite);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/api/src/middlewares/headers.ts:
--------------------------------------------------------------------------------
1 | import { isNullish } from '@sapphire/utilities';
2 | import { Middleware } from '../lib/structures/Middleware';
3 | import type { RouteStore } from '../lib/structures/RouteStore';
4 | import { HttpCodes } from '../lib/structures/http/HttpCodes';
5 | import type { RouterNode } from '../lib/structures/router/RouterNode';
6 |
7 | export class PluginMiddleware extends Middleware {
8 | private readonly origin: string;
9 | private readonly routes: RouteStore;
10 |
11 | public constructor(context: Middleware.LoaderContext) {
12 | super(context, { position: 10 });
13 | this.origin = this.container.server.options.origin ?? '*';
14 | this.routes = this.container.stores.get('routes');
15 | }
16 |
17 | public override run(request: Middleware.Request, response: Middleware.Response) {
18 | response.setHeader('Date', new Date().toUTCString());
19 | response.setHeader('Access-Control-Allow-Credentials', 'true');
20 | response.setHeader('Access-Control-Allow-Origin', this.origin);
21 | response.setHeader('Access-Control-Allow-Headers', 'Authorization, User-Agent, Content-Type');
22 | response.setHeader('Access-Control-Allow-Methods', this.getMethods(request.routerNode));
23 |
24 | this.ensurePotentialEarlyExit(request, response);
25 | }
26 |
27 | private getMethods(routerNode: RouterNode | null | undefined): string {
28 | if (isNullish(routerNode)) {
29 | return this.routes.router.supportedMethods.join(', ');
30 | }
31 |
32 | return [...routerNode.methods()].join(', ');
33 | }
34 |
35 | /**
36 | * **RFC 7231 4.3.7.**
37 | * > This method allows a client to determine the options and/or requirements associated with a
38 | * > resource, or the capabilities of a server, without implying a resource action.
39 | *
40 | * This method ensures that the request is exited early in case required
41 | * The conditions in which an early exit is required are:
42 | * 1. If the request method is 'OPTIONS'. In this case the request is returned with status code 200
43 | * 2. If the requested route isn't matched with any existing route in the RouteStore.
44 | * In this case the request is returned with a status code 404.
45 | *
46 | * @param request The API Request coming in
47 | * @param response The API response that will go out
48 | * @param route The route being requested by the request
49 | */
50 | private ensurePotentialEarlyExit({ method, route, routerNode }: Middleware.Request, response: Middleware.Response) {
51 | if (method === 'OPTIONS') {
52 | if (!route?.methods.has('OPTIONS')) {
53 | response.end();
54 | }
55 | } else if (routerNode === null) {
56 | response.status(HttpCodes.NotFound).end();
57 | } else if (route === null) {
58 | response.status(HttpCodes.MethodNotAllowed).end();
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/packages/api/src/register.ts:
--------------------------------------------------------------------------------
1 | import './index';
2 |
3 | import { Plugin, postInitialization, preLogin, SapphireClient } from '@sapphire/framework';
4 | import type { ClientOptions } from 'discord.js';
5 | import { loadListeners, loadMiddlewares, loadRoutes, Server } from './index';
6 |
7 | /**
8 | * @since 1.0.0
9 | */
10 | export class Api extends Plugin {
11 | /**
12 | * @since 1.0.0
13 | */
14 | public static override [postInitialization](this: SapphireClient, options: ClientOptions): void {
15 | this.server = new Server(options.api);
16 | this.stores
17 | .register(this.server.routes) //
18 | .register(this.server.middlewares);
19 |
20 | loadListeners();
21 | loadMiddlewares();
22 | loadRoutes();
23 | }
24 |
25 | /**
26 | * @since 1.0.0
27 | */
28 | public static override async [preLogin](this: SapphireClient): Promise {
29 | if (!(this.server.options.automaticallyConnect ?? true)) {
30 | return;
31 | }
32 |
33 | await this.server.connect();
34 | }
35 | }
36 |
37 | SapphireClient.plugins.registerPostInitializationHook(Api[postInitialization], 'API-PostInitialization');
38 | SapphireClient.plugins.registerPreLoginHook(Api[preLogin], 'API-PreLogin');
39 |
--------------------------------------------------------------------------------
/packages/api/src/routes/_load.ts:
--------------------------------------------------------------------------------
1 | import { container } from '@sapphire/pieces';
2 | import { PluginRoute as PluginOAuthCallback } from './oauth/callback.post';
3 | import { PluginRoute as PluginOAuthLogout } from './oauth/logout.post';
4 |
5 | export function loadRoutes() {
6 | const store = 'routes';
7 | void container.stores.loadPiece({ name: 'callback', piece: PluginOAuthCallback, store });
8 | void container.stores.loadPiece({ name: 'logout', piece: PluginOAuthLogout, store });
9 | }
10 |
--------------------------------------------------------------------------------
/packages/api/src/routes/oauth/callback.post.ts:
--------------------------------------------------------------------------------
1 | import { OAuth2Routes, type RESTPostOAuth2AccessTokenResult, type RESTPostOAuth2AccessTokenURLEncodedData } from 'discord.js';
2 | import { stringify } from 'querystring';
3 | import { fetch } from 'undici';
4 | import { Route } from '../../lib/structures/Route';
5 | import { HttpCodes } from '../../lib/structures/http/HttpCodes';
6 |
7 | export class PluginRoute extends Route {
8 | private readonly redirectUri: string | undefined;
9 |
10 | public constructor(context: Route.LoaderContext) {
11 | super(context, { route: 'oauth/callback', methods: ['POST'] });
12 |
13 | const { server } = this.container;
14 | this.enabled = server.auth !== null;
15 | this.redirectUri = server.auth?.redirect;
16 | }
17 |
18 | public override async run(request: Route.Request, response: Route.Response) {
19 | const body = (await request.readBodyJson()) as OAuth2BodyData;
20 | if (typeof body?.code !== 'string') {
21 | return response.badRequest();
22 | }
23 |
24 | const value = await this.fetchAuth(body);
25 | if (value === null) {
26 | return response.status(HttpCodes.InternalServerError).json({ error: 'Failed to fetch the token.' });
27 | }
28 |
29 | const now = Date.now();
30 | const auth = this.container.server.auth!;
31 | const data = await auth.fetchData(value.access_token);
32 | if (!data.user) {
33 | return response.status(HttpCodes.InternalServerError).json({ error: 'Failed to fetch the user.' });
34 | }
35 |
36 | const token = auth.encrypt({
37 | id: data.user.id,
38 | expires: now + value.expires_in * 1000,
39 | refresh: value.refresh_token,
40 | token: value.access_token
41 | });
42 |
43 | response.cookies.add(auth.cookie, token, { maxAge: value.expires_in });
44 | return response.json(data);
45 | }
46 |
47 | private async fetchAuth(body: OAuth2BodyData) {
48 | const { id, secret } = this.container.server.auth!;
49 |
50 | const data: RESTPostOAuth2AccessTokenURLEncodedData = {
51 | /* eslint-disable @typescript-eslint/naming-convention */
52 | client_id: id,
53 | client_secret: secret,
54 | code: body.code,
55 | grant_type: 'authorization_code',
56 | redirect_uri: this.redirectUri ?? body.redirectUri
57 | /* eslint-enable @typescript-eslint/naming-convention */
58 | };
59 |
60 | const result = await fetch(OAuth2Routes.tokenURL, {
61 | method: 'POST',
62 | body: stringify(data as any),
63 | headers: {
64 | 'content-type': 'application/x-www-form-urlencoded'
65 | }
66 | });
67 |
68 | const json = await result.json();
69 | if (result.ok) return json as RESTPostOAuth2AccessTokenResult;
70 |
71 | this.container.logger.error(json);
72 | return null;
73 | }
74 | }
75 |
76 | /**
77 | * The OAuth2 body data sent to the callback.
78 | * @since 1.2.0
79 | */
80 | export interface OAuth2BodyData {
81 | /**
82 | * The code sent by the client.
83 | * @since 1.2.0
84 | */
85 | code: string;
86 |
87 | /**
88 | * The client's ID.
89 | * @since 1.2.0
90 | */
91 | clientId: string;
92 |
93 | /**
94 | * The redirect URI.
95 | * @since 1.2.0
96 | */
97 | redirectUri: string;
98 | }
99 |
--------------------------------------------------------------------------------
/packages/api/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "rootDir": "./"
5 | },
6 | "include": ["."]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/api/tests/index.test.ts:
--------------------------------------------------------------------------------
1 | import { isClass } from '@sapphire/utilities';
2 | import { ApiRequest, ApiResponse, CookieStore, Server } from '../src';
3 |
4 | describe('Integration', () => {
5 | test('ApiRequest should be a class', () => {
6 | expect(isClass(ApiRequest)).toBe(true);
7 | });
8 |
9 | test('ApiResponse should be a class', () => {
10 | expect(isClass(ApiResponse)).toBe(true);
11 | });
12 |
13 | test('CookieStore should be a class', () => {
14 | expect(isClass(CookieStore)).toBe(true);
15 | });
16 |
17 | test('Server should be a class', () => {
18 | expect(isClass(Server)).toBe(true);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/packages/api/tests/shared.ts:
--------------------------------------------------------------------------------
1 | import { VirtualPath, container } from '@sapphire/pieces';
2 | import { Route, type MethodName } from '../src';
3 |
4 | export function makeRoute(route: string, methods: readonly MethodName[] = ['GET']) {
5 | // @ts-expect-error Stub
6 | container.server ??= { options: {} };
7 |
8 | class UserRoute extends Route {
9 | public constructor(context: Route.LoaderContext) {
10 | super(context, { route, methods });
11 | }
12 |
13 | public override run() {
14 | // noop
15 | }
16 | }
17 |
18 | return new UserRoute({
19 | name: VirtualPath,
20 | path: VirtualPath,
21 | root: VirtualPath,
22 | store: container.stores.get('routes')
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/packages/api/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "types": ["vitest/globals"]
5 | },
6 | "include": ["."]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/api/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true
6 | },
7 | "include": ["scripts", "src", "tests"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/api/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { createTsupConfig } from '../../scripts/tsup.config';
2 |
3 | export default createTsupConfig();
4 |
--------------------------------------------------------------------------------
/packages/api/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://typedoc.org/schema.json",
3 | "entryPoints": ["src/index.ts"],
4 | "json": "docs/api.json",
5 | "tsconfig": "src/tsconfig.json",
6 | "excludeExternals": true,
7 | "excludePrivate": false
8 | }
9 |
--------------------------------------------------------------------------------
/packages/api/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { createVitestConfig } from '../../scripts/vitest.config';
2 |
3 | export default createVitestConfig();
4 |
--------------------------------------------------------------------------------
/packages/editable-commands/.cliff-jumperrc.yml:
--------------------------------------------------------------------------------
1 | name: plugin-editable-commands
2 | org: sapphire
3 | install: true
4 | packagePath: packages/editable-commands
5 | identifierBase: false
6 | pushTag: true
7 | githubRelease: true
8 | githubReleaseLatest: true
9 | gitRepo: sapphiredev/plugins
10 | gitHostVariant: github
11 |
--------------------------------------------------------------------------------
/packages/editable-commands/.rollup-type-bundlerrc.yml:
--------------------------------------------------------------------------------
1 | onlyBundle: true
2 | excludeFromClean:
3 | - dist/esm/register.d.mts
4 | - dist/cjs/register.d.ts
5 |
--------------------------------------------------------------------------------
/packages/editable-commands/.typedoc-json-parserrc.yml:
--------------------------------------------------------------------------------
1 | json: 'docs/api.json'
2 |
--------------------------------------------------------------------------------
/packages/editable-commands/cliff.toml:
--------------------------------------------------------------------------------
1 | [changelog]
2 | header = """
3 | # Changelog
4 |
5 | All notable changes to this project will be documented in this file.\n
6 | """
7 | body = """
8 | {%- macro remote_url() -%}
9 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
10 | {%- endmacro -%}
11 | {% if version %}\
12 | # [{{ version | trim_start_matches(pat="v") }}]\
13 | {% if previous %}\
14 | {% if previous.version %}\
15 | ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
16 | {% else %}\
17 | ({{ self::remote_url() }}/tree/{{ version }})\
18 | {% endif %}\
19 | {% endif %} \
20 | - ({{ timestamp | date(format="%Y-%m-%d") }})
21 | {% else %}\
22 | # [unreleased]
23 | {% endif %}\
24 | {% for group, commits in commits | group_by(attribute="group") %}
25 | ## {{ group | upper_first }}
26 | {% for commit in commits %}
27 | - {% if commit.scope %}\
28 | **{{commit.scope}}:** \
29 | {% endif %}\
30 | {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
31 | {% if commit.github.pr_number %} (\
32 | [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) by @{{ commit.github.username }}) \
33 | {%- endif %}\
34 | {% if commit.breaking %}\
35 | {% for breakingChange in commit.footers %}\
36 | \n{% raw %} {% endraw %}- 💥 **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
37 | {% endfor %}\
38 | {% endif %}\
39 | {% endfor %}
40 | {% endfor %}\n
41 | """
42 | trim = true
43 | footer = ""
44 |
45 | [git]
46 | conventional_commits = true
47 | filter_unconventional = true
48 | commit_parsers = [
49 | { message = "^feat", group = "🚀 Features" },
50 | { message = "^fix", group = "🐛 Bug Fixes" },
51 | { message = "^docs", group = "📝 Documentation" },
52 | { message = "^perf", group = "🏃 Performance" },
53 | { message = "^refactor", group = "🏠 Refactor" },
54 | { message = "^typings", group = "⌨️ Typings" },
55 | { message = "^types", group = "⌨️ Typings" },
56 | { message = ".*deprecated", body = ".*deprecated", group = "🚨 Deprecation" },
57 | { message = "^revert", skip = true },
58 | { message = "^style", group = "🪞 Styling" },
59 | { message = "^test", group = "🧪 Testing" },
60 | { message = "^chore", skip = true },
61 | { message = "^ci", skip = true },
62 | { message = "^build", skip = true },
63 | { body = ".*security", group = "🛡️ Security" },
64 | ]
65 | commit_preprocessors = [
66 | # remove issue numbers from commits
67 | { pattern = '\s\((\w+\s)?#([0-9]+)\)', replace = "" },
68 | ]
69 | filter_commits = true
70 | tag_pattern = "@sapphire/plugin-editable-commands@[0-9]*"
71 | ignore_tags = ""
72 | topo_order = false
73 | sort_commits = "newest"
74 |
75 | [remote.github]
76 | owner = "sapphiredev"
77 | repo = "plugins"
78 |
--------------------------------------------------------------------------------
/packages/editable-commands/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sapphire/plugin-editable-commands",
3 | "version": "4.0.4",
4 | "description": "Plugin for @sapphire/framework to have editable commands",
5 | "author": "@sapphire",
6 | "license": "MIT",
7 | "main": "dist/cjs/index.cjs",
8 | "module": "dist/esm/index.mjs",
9 | "types": "dist/cjs/index.d.cts",
10 | "exports": {
11 | ".": {
12 | "import": {
13 | "types": "./dist/esm/index.d.mts",
14 | "default": "./dist/esm/index.mjs"
15 | },
16 | "require": {
17 | "types": "./dist/cjs/index.d.cts",
18 | "default": "./dist/cjs/index.cjs"
19 | }
20 | },
21 | "./register": {
22 | "import": {
23 | "types": "./dist/esm/register.d.mts",
24 | "default": "./dist/esm/register.mjs"
25 | },
26 | "require": {
27 | "types": "./dist/cjs/register.d.cts",
28 | "default": "./dist/cjs/register.cjs"
29 | }
30 | }
31 | },
32 | "sideEffects": [
33 | "./dist/cjs/register.cjs",
34 | "./dist/esm/register.mjs"
35 | ],
36 | "homepage": "https://github.com/sapphiredev/plugins/tree/main/packages/editable-commands",
37 | "scripts": {
38 | "lint": "eslint src --ext ts --fix",
39 | "build": "tsup && yarn build:types && yarn build:rename-cjs-register",
40 | "build:types": "concurrently \"yarn:build:types:*\"",
41 | "build:types:cjs": "rollup-type-bundler -d dist/cjs --output-typings-file-extension .cts",
42 | "build:types:esm": "rollup-type-bundler -d dist/esm -t .mts",
43 | "build:types:cleanup": "tsx ../../scripts/clean-register-imports.mts",
44 | "build:rename-cjs-register": "tsx ../../scripts/rename-cjs-register.mts",
45 | "typecheck": "tsc -b src",
46 | "docs": "typedoc-json-parser",
47 | "prepack": "yarn build",
48 | "bump": "cliff-jumper",
49 | "check-update": "cliff-jumper --dry-run"
50 | },
51 | "dependencies": {
52 | "@skyra/editable-commands": "^3.0.4"
53 | },
54 | "repository": {
55 | "type": "git",
56 | "url": "git+https://github.com/sapphiredev/plugins.git",
57 | "directory": "packages/editable-commands"
58 | },
59 | "files": [
60 | "dist/"
61 | ],
62 | "engines": {
63 | "node": ">=v18",
64 | "npm": ">=7"
65 | },
66 | "keywords": [
67 | "sapphiredev",
68 | "plugin",
69 | "bot",
70 | "typescript",
71 | "ts",
72 | "yarn",
73 | "discord",
74 | "sapphire"
75 | ],
76 | "bugs": {
77 | "url": "https://github.com/sapphiredev/plugins/issues"
78 | },
79 | "publishConfig": {
80 | "access": "public"
81 | },
82 | "devDependencies": {
83 | "@favware/cliff-jumper": "^6.0.0",
84 | "@favware/rollup-type-bundler": "^4.0.0",
85 | "concurrently": "^9.1.2",
86 | "tsup": "^8.5.0",
87 | "tsx": "^4.19.4",
88 | "typedoc": "^0.26.11",
89 | "typedoc-json-parser": "^10.2.0",
90 | "typescript": "~5.4.5"
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/packages/editable-commands/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from '@skyra/editable-commands';
2 |
3 | export { loadListeners } from './listeners/_load';
4 |
5 | /**
6 | * The [@sapphire/plugin-editable-commands](https://github.com/sapphiredev/plugins/blob/main/packages/editable-commands)
7 | * version that you are currently using.
8 | * An example use of this is showing it of in a bot information command.
9 | *
10 | * Note to Sapphire developers: This needs to explicitly be `string` so it is not typed as the string that gets replaced by esbuild
11 | */
12 | // eslint-disable-next-line @typescript-eslint/no-inferrable-types
13 | export const version: string = '[VI]{{inject}}[/VI]';
14 |
--------------------------------------------------------------------------------
/packages/editable-commands/src/listeners/PluginMessageUpdate.ts:
--------------------------------------------------------------------------------
1 | import { Events, Listener } from '@sapphire/framework';
2 | import type { Message, OmitPartialGroupDMChannel, PartialMessage } from 'discord.js';
3 |
4 | export class PluginListener extends Listener {
5 | public constructor(context: Listener.LoaderContext) {
6 | super(context, { event: Events.MessageUpdate });
7 | }
8 |
9 | public override run(old: OmitPartialGroupDMChannel | PartialMessage>, message: Message) {
10 | // If the contents of both messages are the same, return:
11 | if (old.content === message.content) return;
12 |
13 | // If the message was sent by a webhook, return:
14 | if (message.webhookId !== null) return;
15 |
16 | // If the message was sent by the Discord system, return:
17 | if (message.system) return;
18 |
19 | // If the message was sent by a bot, return:
20 | if (message.author.bot) return;
21 |
22 | // Run the message parser.
23 | this.container.client.emit(Events.PreMessageParsed, message);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/editable-commands/src/listeners/_load.ts:
--------------------------------------------------------------------------------
1 | import { container } from '@sapphire/pieces';
2 | import { PluginListener as PluginMessageUpdate } from './PluginMessageUpdate';
3 |
4 | export function loadListeners() {
5 | const store = 'listeners' as const;
6 | void container.stores.loadPiece({ name: 'PluginMessageUpdate', piece: PluginMessageUpdate, store });
7 | }
8 |
--------------------------------------------------------------------------------
/packages/editable-commands/src/register.ts:
--------------------------------------------------------------------------------
1 | import { Plugin, postInitialization, SapphireClient } from '@sapphire/framework';
2 | import { loadListeners } from './index';
3 |
4 | /**
5 | * @since 1.0.0
6 | */
7 | export class EditableCommandsPlugin extends Plugin {
8 | /**
9 | * @since 1.0.0
10 | */
11 | public static [postInitialization](this: SapphireClient): void {
12 | loadListeners();
13 | }
14 | }
15 |
16 | SapphireClient.plugins.registerPostInitializationHook(EditableCommandsPlugin[postInitialization], 'EditableCommands-PostInitialization');
17 |
--------------------------------------------------------------------------------
/packages/editable-commands/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "rootDir": "./"
5 | },
6 | "include": ["."]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/editable-commands/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src", "tests"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/editable-commands/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { createTsupConfig } from '../../scripts/tsup.config';
2 |
3 | export default createTsupConfig();
4 |
--------------------------------------------------------------------------------
/packages/editable-commands/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://typedoc.org/schema.json",
3 | "entryPoints": ["src/index.ts"],
4 | "json": "docs/api.json",
5 | "tsconfig": "src/tsconfig.json",
6 | "excludePrivate": false
7 | }
8 |
--------------------------------------------------------------------------------
/packages/hmr/.cliff-jumperrc.yml:
--------------------------------------------------------------------------------
1 | name: plugin-hmr
2 | org: sapphire
3 | install: true
4 | packagePath: packages/hmr
5 | identifierBase: false
6 | pushTag: true
7 | githubRelease: true
8 | githubReleaseLatest: true
9 | gitRepo: sapphiredev/plugins
10 | gitHostVariant: github
11 |
--------------------------------------------------------------------------------
/packages/hmr/.rollup-type-bundlerrc.yml:
--------------------------------------------------------------------------------
1 | onlyBundle: true
2 | excludeFromClean:
3 | - dist/esm/register.d.mts
4 | - dist/cjs/register.d.ts
5 |
--------------------------------------------------------------------------------
/packages/hmr/.typedoc-json-parserrc.yml:
--------------------------------------------------------------------------------
1 | json: 'docs/api.json'
2 |
--------------------------------------------------------------------------------
/packages/hmr/cliff.toml:
--------------------------------------------------------------------------------
1 | [changelog]
2 | header = """
3 | # Changelog
4 |
5 | All notable changes to this project will be documented in this file.\n
6 | """
7 | body = """
8 | {%- macro remote_url() -%}
9 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
10 | {%- endmacro -%}
11 | {% if version %}\
12 | # [{{ version | trim_start_matches(pat="v") }}]\
13 | {% if previous %}\
14 | {% if previous.version %}\
15 | ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
16 | {% else %}\
17 | ({{ self::remote_url() }}/tree/{{ version }})\
18 | {% endif %}\
19 | {% endif %} \
20 | - ({{ timestamp | date(format="%Y-%m-%d") }})
21 | {% else %}\
22 | # [unreleased]
23 | {% endif %}\
24 | {% for group, commits in commits | group_by(attribute="group") %}
25 | ## {{ group | upper_first }}
26 | {% for commit in commits %}
27 | - {% if commit.scope %}\
28 | **{{commit.scope}}:** \
29 | {% endif %}\
30 | {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
31 | {% if commit.github.pr_number %} (\
32 | [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) by @{{ commit.github.username }}) \
33 | {%- endif %}\
34 | {% if commit.breaking %}\
35 | {% for breakingChange in commit.footers %}\
36 | \n{% raw %} {% endraw %}- 💥 **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
37 | {% endfor %}\
38 | {% endif %}\
39 | {% endfor %}
40 | {% endfor %}\n
41 | """
42 | trim = true
43 | footer = ""
44 |
45 | [git]
46 | conventional_commits = true
47 | filter_unconventional = true
48 | commit_parsers = [
49 | { message = "^feat", group = "🚀 Features" },
50 | { message = "^fix", group = "🐛 Bug Fixes" },
51 | { message = "^docs", group = "📝 Documentation" },
52 | { message = "^perf", group = "🏃 Performance" },
53 | { message = "^refactor", group = "🏠 Refactor" },
54 | { message = "^typings", group = "⌨️ Typings" },
55 | { message = "^types", group = "⌨️ Typings" },
56 | { message = ".*deprecated", body = ".*deprecated", group = "🚨 Deprecation" },
57 | { message = "^revert", skip = true },
58 | { message = "^style", group = "🪞 Styling" },
59 | { message = "^test", group = "🧪 Testing" },
60 | { message = "^chore", skip = true },
61 | { message = "^ci", skip = true },
62 | { message = "^build", skip = true },
63 | { body = ".*security", group = "🛡️ Security" },
64 | ]
65 | commit_preprocessors = [
66 | # remove issue numbers from commits
67 | { pattern = '\s\((\w+\s)?#([0-9]+)\)', replace = "" },
68 | ]
69 | filter_commits = true
70 | tag_pattern = "@sapphire/plugin-hmr@[0-9]*"
71 | ignore_tags = ""
72 | topo_order = false
73 | sort_commits = "newest"
74 |
75 | [remote.github]
76 | owner = "sapphiredev"
77 | repo = "plugins"
78 |
--------------------------------------------------------------------------------
/packages/hmr/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sapphire/plugin-hmr",
3 | "version": "3.0.2",
4 | "description": "Plugin for @sapphire/framework for hot module reloading for pieces",
5 | "author": "@sapphire",
6 | "license": "MIT",
7 | "main": "dist/cjs/index.cjs",
8 | "module": "dist/esm/index.mjs",
9 | "types": "dist/cjs/index.d.cts",
10 | "exports": {
11 | ".": {
12 | "import": {
13 | "types": "./dist/esm/index.d.mts",
14 | "default": "./dist/esm/index.mjs"
15 | },
16 | "require": {
17 | "types": "./dist/cjs/index.d.cts",
18 | "default": "./dist/cjs/index.cjs"
19 | }
20 | },
21 | "./register": {
22 | "import": {
23 | "types": "./dist/esm/register.d.mts",
24 | "default": "./dist/esm/register.mjs"
25 | },
26 | "require": {
27 | "types": "./dist/cjs/register.d.cts",
28 | "default": "./dist/cjs/register.cjs"
29 | }
30 | }
31 | },
32 | "sideEffects": [
33 | "./dist/cjs/register.cjs",
34 | "./dist/esm/register.mjs"
35 | ],
36 | "homepage": "https://sapphirejs.dev",
37 | "scripts": {
38 | "lint": "eslint src --ext ts --fix",
39 | "build": "tsup && yarn build:types && yarn build:rename-cjs-register",
40 | "build:types": "concurrently \"yarn:build:types:*\"",
41 | "build:types:cjs": "rollup-type-bundler -d dist/cjs --output-typings-file-extension .cts",
42 | "build:types:esm": "rollup-type-bundler -d dist/esm -t .mts",
43 | "build:types:cleanup": "tsx ../../scripts/clean-register-imports.mts",
44 | "build:rename-cjs-register": "tsx ../../scripts/rename-cjs-register.mts",
45 | "typecheck": "tsc -b src",
46 | "docs": "typedoc-json-parser",
47 | "prepack": "yarn build",
48 | "bump": "cliff-jumper",
49 | "check-update": "cliff-jumper --dry-run"
50 | },
51 | "dependencies": {
52 | "chokidar": "^4.0.3"
53 | },
54 | "repository": {
55 | "type": "git",
56 | "url": "git+https://github.com/sapphiredev/plugins.git",
57 | "directory": "packages/hmr"
58 | },
59 | "files": [
60 | "dist/"
61 | ],
62 | "engines": {
63 | "node": ">=v18",
64 | "npm": ">=7"
65 | },
66 | "keywords": [
67 | "sapphiredev",
68 | "plugin",
69 | "bot",
70 | "typescript",
71 | "ts",
72 | "yarn",
73 | "discord",
74 | "sapphire"
75 | ],
76 | "bugs": {
77 | "url": "https://github.com/sapphiredev/plugins/issues"
78 | },
79 | "publishConfig": {
80 | "access": "public"
81 | },
82 | "devDependencies": {
83 | "@favware/cliff-jumper": "^6.0.0",
84 | "@favware/rollup-type-bundler": "^4.0.0",
85 | "concurrently": "^9.1.2",
86 | "tsup": "^8.5.0",
87 | "tsx": "^4.19.4",
88 | "typedoc": "^0.26.11",
89 | "typedoc-json-parser": "^10.2.0",
90 | "typescript": "~5.4.5"
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/packages/hmr/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lib/hmr';
2 |
3 | import type { HMROptions } from './lib/hmr';
4 |
5 | declare module 'discord.js' {
6 | export interface ClientOptions {
7 | hmr?: HMROptions;
8 | }
9 | }
10 |
11 | /**
12 | * The [@sapphire/plugin-hmr](https://github.com/sapphiredev/plugins/blob/main/packages/hmr) version that you are currently using.
13 | * An example use of this is showing it of in a bot information command.
14 | *
15 | * Note to Sapphire developers: This needs to explicitly be `string` so it is not typed as the string that gets replaced by esbuild
16 | */
17 | // eslint-disable-next-line @typescript-eslint/no-inferrable-types
18 | export const version: string = '[VI]{{inject}}[/VI]';
19 |
--------------------------------------------------------------------------------
/packages/hmr/src/lib/hmr.ts:
--------------------------------------------------------------------------------
1 | import { Piece, Result, Store, container } from '@sapphire/framework';
2 | import { watch, type ChokidarOptions } from 'chokidar';
3 | import { relative } from 'node:path';
4 |
5 | export interface HMROptions extends ChokidarOptions {
6 | enabled?: boolean;
7 | silent?: boolean;
8 | }
9 |
10 | /**
11 | * Starts HMR for all registered {@link Store Stores} in {@link container.stores the main container}.
12 | *
13 | * @param __namedParameter The {@link HMROptions}.
14 | * This includes [all options from chokidar](https://github.com/paulmillr/chokidar#persistence),
15 | * as well as whether the HMR should be enabled.
16 | * The default options are `{ enabled: true }`,
17 | * and if not provided in the object then `enabled` is also set to true.
18 | *
19 | */
20 | export function start({ enabled = true, silent = false, ...options }: HMROptions = { enabled: true }) {
21 | // Do not enable plugin when enabled is false
22 | if (!enabled) return;
23 |
24 | if (!silent) container.logger.info('[HMR-Plugin]: Enabled. Watching for piece changes.');
25 |
26 | for (const store of container.stores.values()) {
27 | watch([...store.paths], options)
28 | .on('change', (path) => handlePiecePathUpdate(store, path, silent))
29 | .on('unlink', (path) => handlePiecePathDelete(store, path, silent));
30 | }
31 | }
32 |
33 | async function handlePiecePathDelete(store: Store, path: string, silent: boolean) {
34 | if (!store.strategy.filter(path)) return;
35 |
36 | const pieceToDelete = store.find((piece) => piece.location.full === path);
37 | if (!pieceToDelete) return;
38 |
39 | const result = await Result.fromAsync(async () => {
40 | await pieceToDelete.unload();
41 | if (!silent) container.logger.info(`[HMR-Plugin]: Unloaded ${pieceToDelete.name} piece from ${pieceToDelete.store.name} store.`);
42 | });
43 |
44 | result.inspectErr((error) =>
45 | container.logger.error(`[HMR-Plugin]: Failed to unload ${pieceToDelete.name} piece from ${pieceToDelete.store.name} store.`, error)
46 | );
47 | }
48 |
49 | async function handlePiecePathUpdate(store: Store, path: string, silent: boolean) {
50 | if (!store.strategy.filter(path)) return;
51 |
52 | const pieceToUpdate = store.find((piece) => piece.location.full === path);
53 |
54 | const result = await Result.fromAsync(async () => {
55 | if (pieceToUpdate) {
56 | await pieceToUpdate.reload();
57 | if (!silent) container.logger.info(`[HMR-Plugin]: reloaded ${pieceToUpdate.name} piece from ${pieceToUpdate.store.name} store.`);
58 | } else {
59 | const rootPath = [...store.paths].find((storePath) => path.startsWith(storePath));
60 | if (!rootPath) throw new Error(`[HMR-Plugin]: Could not find root path for ${path}.`);
61 |
62 | const piecesLoaded = await store.load(rootPath, relative(rootPath, path));
63 | const piecesLoadedNames = piecesLoaded.map((piece) => piece.name);
64 | const piecesLoadedStoreNames = piecesLoaded.map((piece) => piece.store.name);
65 | if (!silent)
66 | container.logger.info(
67 | `[HMR-Plugin]: Loaded ${piecesLoadedNames.join(', ')} piece(s) from ${[...new Set(piecesLoadedStoreNames)].join(', ')} store(s).`
68 | );
69 | }
70 | });
71 |
72 | result.inspectErr((error) => container.logger.error(`[HMR-Plugin]: Failed to load pieces from ${path}.`, error));
73 | }
74 |
--------------------------------------------------------------------------------
/packages/hmr/src/register.ts:
--------------------------------------------------------------------------------
1 | import './index';
2 |
3 | import { Plugin, postLogin, SapphireClient } from '@sapphire/framework';
4 | import { start } from './index';
5 |
6 | /**
7 | * @since 1.0.0
8 | */
9 | export class HmrPlugin extends Plugin {
10 | /**
11 | * @since 1.0.0
12 | */
13 | public static [postLogin](this: SapphireClient): void {
14 | start(this.options.hmr);
15 | }
16 | }
17 |
18 | SapphireClient.plugins.registerPostLoginHook(HmrPlugin[postLogin], 'Hmr-PostLogin');
19 |
--------------------------------------------------------------------------------
/packages/hmr/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "rootDir": "./"
5 | },
6 | "include": ["."]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/hmr/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/hmr/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { createTsupConfig } from '../../scripts/tsup.config';
2 |
3 | export default createTsupConfig();
4 |
--------------------------------------------------------------------------------
/packages/hmr/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://typedoc.org/schema.json",
3 | "entryPoints": ["src/index.ts"],
4 | "json": "docs/api.json",
5 | "tsconfig": "src/tsconfig.json",
6 | "excludePrivate": false
7 | }
8 |
--------------------------------------------------------------------------------
/packages/i18next/.cliff-jumperrc.yml:
--------------------------------------------------------------------------------
1 | name: plugin-i18next
2 | org: sapphire
3 | install: true
4 | packagePath: packages/i18next
5 | identifierBase: false
6 | pushTag: true
7 | githubRelease: true
8 | githubReleaseLatest: true
9 | gitRepo: sapphiredev/plugins
10 | gitHostVariant: github
11 |
--------------------------------------------------------------------------------
/packages/i18next/.rollup-type-bundlerrc.yml:
--------------------------------------------------------------------------------
1 | external:
2 | - node:fs
3 | onlyBundle: true
4 | excludeFromClean:
5 | - dist/esm/register.d.mts
6 | - dist/cjs/register.d.ts
7 |
--------------------------------------------------------------------------------
/packages/i18next/.typedoc-json-parserrc.yml:
--------------------------------------------------------------------------------
1 | json: 'docs/api.json'
2 |
--------------------------------------------------------------------------------
/packages/i18next/cliff.toml:
--------------------------------------------------------------------------------
1 | [changelog]
2 | header = """
3 | # Changelog
4 |
5 | All notable changes to this project will be documented in this file.\n
6 | """
7 | body = """
8 | {%- macro remote_url() -%}
9 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
10 | {%- endmacro -%}
11 | {% if version %}\
12 | # [{{ version | trim_start_matches(pat="v") }}]\
13 | {% if previous %}\
14 | {% if previous.version %}\
15 | ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
16 | {% else %}\
17 | ({{ self::remote_url() }}/tree/{{ version }})\
18 | {% endif %}\
19 | {% endif %} \
20 | - ({{ timestamp | date(format="%Y-%m-%d") }})
21 | {% else %}\
22 | # [unreleased]
23 | {% endif %}\
24 | {% for group, commits in commits | group_by(attribute="group") %}
25 | ## {{ group | upper_first }}
26 | {% for commit in commits %}
27 | - {% if commit.scope %}\
28 | **{{commit.scope}}:** \
29 | {% endif %}\
30 | {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
31 | {% if commit.github.pr_number %} (\
32 | [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) by @{{ commit.github.username }}) \
33 | {%- endif %}\
34 | {% if commit.breaking %}\
35 | {% for breakingChange in commit.footers %}\
36 | \n{% raw %} {% endraw %}- 💥 **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
37 | {% endfor %}\
38 | {% endif %}\
39 | {% endfor %}
40 | {% endfor %}\n
41 | """
42 | trim = true
43 | footer = ""
44 |
45 | [git]
46 | conventional_commits = true
47 | filter_unconventional = true
48 | commit_parsers = [
49 | { message = "^feat", group = "🚀 Features" },
50 | { message = "^fix", group = "🐛 Bug Fixes" },
51 | { message = "^docs", group = "📝 Documentation" },
52 | { message = "^perf", group = "🏃 Performance" },
53 | { message = "^refactor", group = "🏠 Refactor" },
54 | { message = "^typings", group = "⌨️ Typings" },
55 | { message = "^types", group = "⌨️ Typings" },
56 | { message = ".*deprecated", body = ".*deprecated", group = "🚨 Deprecation" },
57 | { message = "^revert", skip = true },
58 | { message = "^style", group = "🪞 Styling" },
59 | { message = "^test", group = "🧪 Testing" },
60 | { message = "^chore", skip = true },
61 | { message = "^ci", skip = true },
62 | { message = "^build", skip = true },
63 | { body = ".*security", group = "🛡️ Security" },
64 | ]
65 | commit_preprocessors = [
66 | # remove issue numbers from commits
67 | { pattern = '\s\((\w+\s)?#([0-9]+)\)', replace = "" },
68 | ]
69 | filter_commits = true
70 | tag_pattern = "@sapphire/plugin-i18next@[0-9]*"
71 | ignore_tags = ""
72 | topo_order = false
73 | sort_commits = "newest"
74 |
75 | [remote.github]
76 | owner = "sapphiredev"
77 | repo = "plugins"
78 |
--------------------------------------------------------------------------------
/packages/i18next/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sapphire/plugin-i18next",
3 | "version": "8.0.0",
4 | "description": "Plugin for @sapphire/framework to support i18next.",
5 | "author": "@sapphire",
6 | "license": "MIT",
7 | "main": "dist/cjs/index.cjs",
8 | "module": "dist/esm/index.mjs",
9 | "types": "dist/cjs/index.d.cts",
10 | "exports": {
11 | ".": {
12 | "import": {
13 | "types": "./dist/esm/index.d.mts",
14 | "default": "./dist/esm/index.mjs"
15 | },
16 | "require": {
17 | "types": "./dist/cjs/index.d.cts",
18 | "default": "./dist/cjs/index.cjs"
19 | }
20 | },
21 | "./register": {
22 | "import": {
23 | "types": "./dist/esm/register.d.mts",
24 | "default": "./dist/esm/register.mjs"
25 | },
26 | "require": {
27 | "types": "./dist/cjs/register.d.cts",
28 | "default": "./dist/cjs/register.cjs"
29 | }
30 | }
31 | },
32 | "sideEffects": [
33 | "./dist/cjs/register.cjs",
34 | "./dist/esm/register.mjs"
35 | ],
36 | "homepage": "https://github.com/sapphiredev/plugins/tree/main/packages/i18next",
37 | "scripts": {
38 | "test": "vitest run",
39 | "lint": "eslint src tests --ext ts --fix",
40 | "build": "tsup && yarn build:types && yarn build:rename-cjs-register",
41 | "build:types": "concurrently \"yarn:build:types:*\"",
42 | "build:types:cjs": "rollup-type-bundler -d dist/cjs --output-typings-file-extension .cts",
43 | "build:types:esm": "rollup-type-bundler -d dist/esm -t .mts",
44 | "build:types:cleanup": "tsx ../../scripts/clean-register-imports.mts",
45 | "build:rename-cjs-register": "tsx ../../scripts/rename-cjs-register.mts",
46 | "typecheck": "tsc -b src",
47 | "docs": "typedoc-json-parser",
48 | "prepack": "yarn build",
49 | "bump": "cliff-jumper",
50 | "check-update": "cliff-jumper --dry-run"
51 | },
52 | "dependencies": {
53 | "@sapphire/utilities": "^3.18.2",
54 | "@skyra/i18next-backend": "^2.0.6",
55 | "chokidar": "^4.0.3",
56 | "i18next": "^25.2.1"
57 | },
58 | "repository": {
59 | "type": "git",
60 | "url": "git+https://github.com/sapphiredev/plugins.git",
61 | "directory": "packages/i18next"
62 | },
63 | "files": [
64 | "dist/"
65 | ],
66 | "engines": {
67 | "node": ">=v18",
68 | "npm": ">=7"
69 | },
70 | "keywords": [
71 | "sapphiredev",
72 | "plugin",
73 | "bot",
74 | "typescript",
75 | "ts",
76 | "yarn",
77 | "discord",
78 | "sapphire",
79 | "i18next",
80 | "i18n"
81 | ],
82 | "bugs": {
83 | "url": "https://github.com/sapphiredev/plugins/issues"
84 | },
85 | "publishConfig": {
86 | "access": "public"
87 | },
88 | "devDependencies": {
89 | "@favware/cliff-jumper": "^6.0.0",
90 | "@favware/rollup-type-bundler": "^4.0.0",
91 | "concurrently": "^9.1.2",
92 | "tsup": "^8.5.0",
93 | "tsx": "^4.19.4",
94 | "typedoc": "^0.26.11",
95 | "typedoc-json-parser": "^10.2.0",
96 | "typescript": "~5.4.5"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/packages/i18next/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { InternationalizationHandler } from './lib/InternationalizationHandler';
2 | import type { InternationalizationClientOptions } from './lib/types';
3 |
4 | export { default as i18next, type TFunction, type TOptions } from 'i18next';
5 | export * from './lib/InternationalizationHandler';
6 | export * from './lib/functions';
7 | export * from './lib/types';
8 |
9 | declare module '@sapphire/pieces' {
10 | interface Container {
11 | i18n: InternationalizationHandler;
12 | }
13 | }
14 |
15 | declare module 'discord.js' {
16 | export interface ClientOptions extends InternationalizationClientOptions {}
17 | }
18 |
19 | /**
20 | * The [@sapphire/plugin-i18next](https://github.com/sapphiredev/plugins/blob/main/packages/i18next) version that you are currently using.
21 | * An example use of this is showing it of in a bot information command.
22 | *
23 | * Note to Sapphire developers: This needs to explicitly be `string` so it is not typed as the string that gets replaced by esbuild
24 | */
25 | // eslint-disable-next-line @typescript-eslint/no-inferrable-types
26 | export const version: string = '[VI]{{inject}}[/VI]';
27 |
--------------------------------------------------------------------------------
/packages/i18next/src/lib/Augmentations.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ================================================
3 | * | THIS IS FOR TYPEDOC. DO NOT REMOVE THIS FILE |
4 | * ================================================
5 | */
6 |
7 | import type { InternationalizationHandler } from './InternationalizationHandler';
8 |
9 | declare module '@sapphire/pieces' {
10 | interface Container {
11 | i18n: InternationalizationHandler;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/i18next/src/register.ts:
--------------------------------------------------------------------------------
1 | import './index';
2 |
3 | import { Plugin, SapphireClient, container, postLogin, preGenericsInitialization, preLogin } from '@sapphire/framework';
4 | import { watch } from 'chokidar';
5 | import type { ClientOptions } from 'discord.js';
6 | import { InternationalizationHandler } from './index';
7 |
8 | export class I18nextPlugin extends Plugin {
9 | public static [preGenericsInitialization](this: SapphireClient, options: ClientOptions): void {
10 | container.i18n = new InternationalizationHandler(options.i18n);
11 | }
12 |
13 | public static async [preLogin](this: SapphireClient): Promise {
14 | await container.i18n.init();
15 | }
16 |
17 | public static [postLogin](this: SapphireClient): void {
18 | if (this.options.i18n?.hmr?.enabled) {
19 | container.logger.info('[i18next-Plugin]: HMR enabled. Watching for languages changes.');
20 |
21 | watch(container.i18n.languagesDirectory, this.options.i18n.hmr.options ?? {})
22 | .on('change', () => container.i18n.reloadResources())
23 | .on('unlink', () => container.i18n.reloadResources());
24 | }
25 | }
26 | }
27 |
28 | SapphireClient.plugins.registerPostInitializationHook(I18nextPlugin[preGenericsInitialization], 'I18next-PreGenericsInitialization');
29 | SapphireClient.plugins.registerPreLoginHook(I18nextPlugin[preLogin], 'I18next-PreLogin');
30 | SapphireClient.plugins.registerPostLoginHook(I18nextPlugin[postLogin], 'I18next-PostLogin');
31 |
--------------------------------------------------------------------------------
/packages/i18next/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "rootDir": "./"
5 | },
6 | "include": ["."]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/i18next/tests/I18nextHandler.test.ts:
--------------------------------------------------------------------------------
1 | import { isClass } from '@sapphire/utilities';
2 | import { InternationalizationHandler } from '../src';
3 |
4 | function structureTest(i18n: InternationalizationHandler) {
5 | expect(i18n.languagesLoaded).toBe(false);
6 | expect(i18n.languages).toBeDefined();
7 | }
8 |
9 | describe('InternationalizationHandler', () => {
10 | test('InternationalizationHandler should be a class', () => {
11 | expect(isClass(InternationalizationHandler)).toBe(true);
12 | });
13 |
14 | test('Empty Constructor', () => {
15 | const i18n = new InternationalizationHandler();
16 | structureTest(i18n);
17 | });
18 |
19 | test('Empty Object Constructor', () => {
20 | const i18n = new InternationalizationHandler({});
21 | structureTest(i18n);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/packages/i18next/tests/augments.d.ts:
--------------------------------------------------------------------------------
1 | import type { InternationalizationClientOptions } from '../src';
2 | import type { InternationalizationHandler } from '../src/lib/InternationalizationHandler';
3 |
4 | declare module '@sapphire/pieces' {
5 | interface Container {
6 | client: any; // Client type doesn't really matter for tests but needs to be there for the type checker
7 | i18n: InternationalizationHandler;
8 | }
9 | }
10 |
11 | declare module 'discord.js' {
12 | export interface ClientOptions extends InternationalizationClientOptions {}
13 | }
14 |
--------------------------------------------------------------------------------
/packages/i18next/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "types": ["vitest/globals"]
5 | },
6 | "include": ["."]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/i18next/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src", "tests"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/i18next/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { createTsupConfig } from '../../scripts/tsup.config';
2 |
3 | export default createTsupConfig();
4 |
--------------------------------------------------------------------------------
/packages/i18next/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://typedoc.org/schema.json",
3 | "entryPoints": ["src/index.ts"],
4 | "json": "docs/api.json",
5 | "tsconfig": "src/tsconfig.json",
6 | "excludePrivate": false
7 | }
8 |
--------------------------------------------------------------------------------
/packages/i18next/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { createVitestConfig } from '../../scripts/vitest.config';
2 |
3 | export default createVitestConfig();
4 |
--------------------------------------------------------------------------------
/packages/logger/.cliff-jumperrc.yml:
--------------------------------------------------------------------------------
1 | name: plugin-logger
2 | org: sapphire
3 | install: true
4 | packagePath: packages/logger
5 | identifierBase: false
6 | pushTag: true
7 | githubRelease: true
8 | githubReleaseLatest: true
9 | gitRepo: sapphiredev/plugins
10 | gitHostVariant: github
11 |
--------------------------------------------------------------------------------
/packages/logger/.rollup-type-bundlerrc.yml:
--------------------------------------------------------------------------------
1 | onlyBundle: true
2 | excludeFromClean:
3 | - dist/esm/register.d.mts
4 | - dist/cjs/register.d.ts
5 |
--------------------------------------------------------------------------------
/packages/logger/.typedoc-json-parserrc.yml:
--------------------------------------------------------------------------------
1 | json: 'docs/api.json'
2 |
--------------------------------------------------------------------------------
/packages/logger/cliff.toml:
--------------------------------------------------------------------------------
1 | [changelog]
2 | header = """
3 | # Changelog
4 |
5 | All notable changes to this project will be documented in this file.\n
6 | """
7 | body = """
8 | {%- macro remote_url() -%}
9 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
10 | {%- endmacro -%}
11 | {% if version %}\
12 | # [{{ version | trim_start_matches(pat="v") }}]\
13 | {% if previous %}\
14 | {% if previous.version %}\
15 | ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
16 | {% else %}\
17 | ({{ self::remote_url() }}/tree/{{ version }})\
18 | {% endif %}\
19 | {% endif %} \
20 | - ({{ timestamp | date(format="%Y-%m-%d") }})
21 | {% else %}\
22 | # [unreleased]
23 | {% endif %}\
24 | {% for group, commits in commits | group_by(attribute="group") %}
25 | ## {{ group | upper_first }}
26 | {% for commit in commits %}
27 | - {% if commit.scope %}\
28 | **{{commit.scope}}:** \
29 | {% endif %}\
30 | {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
31 | {% if commit.github.pr_number %} (\
32 | [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) by @{{ commit.github.username }}) \
33 | {%- endif %}\
34 | {% if commit.breaking %}\
35 | {% for breakingChange in commit.footers %}\
36 | \n{% raw %} {% endraw %}- 💥 **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
37 | {% endfor %}\
38 | {% endif %}\
39 | {% endfor %}
40 | {% endfor %}\n
41 | """
42 | trim = true
43 | footer = ""
44 |
45 | [git]
46 | conventional_commits = true
47 | filter_unconventional = true
48 | commit_parsers = [
49 | { message = "^feat", group = "🚀 Features" },
50 | { message = "^fix", group = "🐛 Bug Fixes" },
51 | { message = "^docs", group = "📝 Documentation" },
52 | { message = "^perf", group = "🏃 Performance" },
53 | { message = "^refactor", group = "🏠 Refactor" },
54 | { message = "^typings", group = "⌨️ Typings" },
55 | { message = "^types", group = "⌨️ Typings" },
56 | { message = ".*deprecated", body = ".*deprecated", group = "🚨 Deprecation" },
57 | { message = "^revert", skip = true },
58 | { message = "^style", group = "🪞 Styling" },
59 | { message = "^test", group = "🧪 Testing" },
60 | { message = "^chore", skip = true },
61 | { message = "^ci", skip = true },
62 | { message = "^build", skip = true },
63 | { body = ".*security", group = "🛡️ Security" },
64 | ]
65 | commit_preprocessors = [
66 | # remove issue numbers from commits
67 | { pattern = '\s\((\w+\s)?#([0-9]+)\)', replace = "" },
68 | ]
69 | filter_commits = true
70 | tag_pattern = "@sapphire/plugin-logger@[0-9]*"
71 | ignore_tags = ""
72 | topo_order = false
73 | sort_commits = "newest"
74 |
75 | [remote.github]
76 | owner = "sapphiredev"
77 | repo = "plugins"
78 |
--------------------------------------------------------------------------------
/packages/logger/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sapphire/plugin-logger",
3 | "version": "4.0.2",
4 | "description": "Plugin for @sapphire/framework to have pretty console output",
5 | "author": "@sapphire",
6 | "license": "MIT",
7 | "main": "dist/cjs/index.cjs",
8 | "module": "dist/esm/index.mjs",
9 | "types": "dist/cjs/index.d.cts",
10 | "exports": {
11 | ".": {
12 | "import": {
13 | "types": "./dist/esm/index.d.mts",
14 | "default": "./dist/esm/index.mjs"
15 | },
16 | "require": {
17 | "types": "./dist/cjs/index.d.cts",
18 | "default": "./dist/cjs/index.cjs"
19 | }
20 | },
21 | "./register": {
22 | "import": {
23 | "types": "./dist/esm/register.d.mts",
24 | "default": "./dist/esm/register.mjs"
25 | },
26 | "require": {
27 | "types": "./dist/cjs/register.d.cts",
28 | "default": "./dist/cjs/register.cjs"
29 | }
30 | }
31 | },
32 | "sideEffects": [
33 | "./dist/cjs/register.cjs",
34 | "./dist/esm/register.mjs"
35 | ],
36 | "homepage": "https://github.com/sapphiredev/plugins/tree/main/packages/logger",
37 | "scripts": {
38 | "test": "vitest run",
39 | "lint": "eslint src tests --ext ts --fix",
40 | "build": "tsup && yarn build:types && yarn build:rename-cjs-register",
41 | "build:types": "concurrently \"yarn:build:types:*\"",
42 | "build:types:cjs": "rollup-type-bundler -d dist/cjs --output-typings-file-extension .cts",
43 | "build:types:esm": "rollup-type-bundler -d dist/esm -t .mts",
44 | "build:types:cleanup": "tsx ../../scripts/clean-register-imports.mts",
45 | "build:rename-cjs-register": "tsx ../../scripts/rename-cjs-register.mts",
46 | "typecheck": "tsc -b src",
47 | "docs": "typedoc-json-parser",
48 | "prepack": "yarn build",
49 | "bump": "cliff-jumper",
50 | "check-update": "cliff-jumper --dry-run"
51 | },
52 | "dependencies": {
53 | "@sapphire/timestamp": "^1.0.5",
54 | "colorette": "^2.0.20"
55 | },
56 | "repository": {
57 | "type": "git",
58 | "url": "git+https://github.com/sapphiredev/plugins.git",
59 | "directory": "packages/logger"
60 | },
61 | "files": [
62 | "dist/"
63 | ],
64 | "engines": {
65 | "node": ">=v18",
66 | "npm": ">=7"
67 | },
68 | "keywords": [
69 | "sapphiredev",
70 | "plugin",
71 | "bot",
72 | "typescript",
73 | "ts",
74 | "yarn",
75 | "discord",
76 | "sapphire"
77 | ],
78 | "bugs": {
79 | "url": "https://github.com/sapphiredev/plugins/issues"
80 | },
81 | "publishConfig": {
82 | "access": "public"
83 | },
84 | "devDependencies": {
85 | "@favware/cliff-jumper": "^6.0.0",
86 | "@favware/rollup-type-bundler": "^4.0.0",
87 | "concurrently": "^9.1.2",
88 | "tsup": "^8.5.0",
89 | "tsx": "^4.19.4",
90 | "typedoc": "^0.26.11",
91 | "typedoc-json-parser": "^10.2.0",
92 | "typescript": "~5.4.5"
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/packages/logger/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { LoggerOptions } from './lib/Logger';
2 |
3 | export * from './lib/Logger';
4 | export * from './lib/LoggerLevel';
5 | export * from './lib/LoggerStyle';
6 | export * from './lib/LoggerTimestamp';
7 |
8 | declare module '@sapphire/framework' {
9 | export interface ClientLoggerOptions extends LoggerOptions {}
10 | }
11 |
12 | /**
13 | * The [@sapphire/plugin-logger](https://github.com/sapphiredev/plugins/blob/main/packages/logger) version that you are currently using.
14 | * An example use of this is showing it of in a bot information command.
15 | *
16 | * Note to Sapphire developers: This needs to explicitly be `string` so it is not typed as the string that gets replaced by esbuild
17 | */
18 | // eslint-disable-next-line @typescript-eslint/no-inferrable-types
19 | export const version: string = '[VI]{{inject}}[/VI]';
20 |
--------------------------------------------------------------------------------
/packages/logger/src/lib/LoggerLevel.ts:
--------------------------------------------------------------------------------
1 | import { LoggerStyle, type LoggerStyleResolvable } from './LoggerStyle';
2 | import { LoggerTimestamp, type LoggerTimestampOptions } from './LoggerTimestamp';
3 |
4 | /**
5 | * Logger utility that stores and applies a full style into the message.
6 | * @since 1.0.0
7 | */
8 | export class LoggerLevel {
9 | /**
10 | * The timestamp formatter.
11 | * @since 1.0.0
12 | */
13 | public timestamp: LoggerTimestamp | null;
14 |
15 | /**
16 | * The infix, added between the timestamp and the message.
17 | * @since 1.0.0
18 | */
19 | public infix: string;
20 |
21 | /**
22 | * The style formatter for the message.
23 | * @since 1.0.0
24 | */
25 | public message: LoggerStyle | null;
26 |
27 | public constructor(options: LoggerLevelOptions = {}) {
28 | this.timestamp = options.timestamp === null ? null : new LoggerTimestamp(options.timestamp);
29 | this.infix = options.infix ?? '';
30 | this.message = options.message === null ? null : new LoggerStyle(options.message);
31 | }
32 |
33 | public run(content: string) {
34 | const prefix = (this.timestamp?.run() ?? '') + this.infix;
35 |
36 | if (prefix.length) {
37 | const formatter = this.message //
38 | ? (line: string) => prefix + this.message!.run(line)
39 | : (line: string) => prefix + line;
40 | return content.split('\n').map(formatter).join('\n');
41 | }
42 |
43 | return this.message ? this.message.run(content) : content;
44 | }
45 | }
46 |
47 | /**
48 | * The options for {@link LoggerLevel}.
49 | * @since 1.0.0
50 | */
51 | export interface LoggerLevelOptions {
52 | /**
53 | * The timestamp options. Set to `null` to disable timestamp parsing.
54 | * @since 1.0.0
55 | * @default {}
56 | */
57 | timestamp?: LoggerTimestampOptions | null;
58 |
59 | /**
60 | * The infix to be included between the timestamp and the message.
61 | * @since 1.0.0
62 | * @default ''
63 | */
64 | infix?: string;
65 |
66 | /**
67 | * The style options for the message.
68 | * @since 1.0.0
69 | * @default colorette.clear
70 | */
71 | message?: LoggerStyleResolvable | null;
72 | }
73 |
--------------------------------------------------------------------------------
/packages/logger/src/lib/LoggerTimestamp.ts:
--------------------------------------------------------------------------------
1 | import { Timestamp } from '@sapphire/timestamp';
2 | import { LoggerStyle, type LoggerStyleResolvable } from './LoggerStyle';
3 |
4 | /**
5 | * Logger utility that formats a timestamp.
6 | * @since 1.0.0
7 | */
8 | export class LoggerTimestamp {
9 | /**
10 | * The timestamp used to format the current date.
11 | * @since 1.0.0
12 | */
13 | public timestamp: Timestamp;
14 |
15 | /**
16 | * Whether or not the logger will show a timestamp in UTC.
17 | * @since 1.0.0
18 | */
19 | public utc: boolean;
20 |
21 | /**
22 | * The logger style to apply the color to the timestamp.
23 | * @since 1.0.0
24 | */
25 | public color: LoggerStyle | null;
26 |
27 | /**
28 | * The final formatter.
29 | * @since 1.0.0
30 | */
31 | public formatter: LoggerTimestampFormatter;
32 |
33 | public constructor(options: LoggerTimestampOptions = {}) {
34 | this.timestamp = new Timestamp(options.pattern ?? 'YYYY-MM-DD HH:mm:ss');
35 | this.utc = options.utc ?? false;
36 | this.color = options.color === null ? null : new LoggerStyle(options.color);
37 | this.formatter = options.formatter ?? ((timestamp) => `${timestamp} - `);
38 | }
39 |
40 | /**
41 | * Formats the current time.
42 | * @since 1.0.0
43 | */
44 | public run() {
45 | const date = new Date();
46 | const result = this.utc ? this.timestamp.displayUTC(date) : this.timestamp.display(date);
47 | return this.formatter(this.color ? this.color.run(result) : result);
48 | }
49 | }
50 |
51 | /**
52 | * The options for {@link LoggerTimestamp}.
53 | * @since 1.0.0
54 | */
55 | export interface LoggerTimestampOptions {
56 | /**
57 | * The {@link Timestamp} pattern.
58 | * @since 1.0.0
59 | * @default 'YYYY-MM-DD HH:mm:ss'
60 | * @example
61 | * ```typescript
62 | * 'YYYY-MM-DD HH:mm:ss'
63 | * // 2020-12-23 22:01:10
64 | * ```
65 | */
66 | pattern?: string;
67 |
68 | /**
69 | * Whether or not the date should be UTC.
70 | * @since 1.0.0
71 | * @default false
72 | */
73 | utc?: boolean;
74 |
75 | /**
76 | * The color to use.
77 | * @since 1.0.0
78 | * @default colorette.reset
79 | */
80 | color?: LoggerStyleResolvable | null;
81 |
82 | /**
83 | * The formatter. See {@link LoggerTimestampFormatter} for more information.
84 | * @since 1.0.0
85 | * @default (value) => `${value} - `
86 | */
87 | formatter?: LoggerTimestampFormatter;
88 | }
89 |
90 | /**
91 | * The formatter used for {@link LoggerTimestampOptions}. This will be run **after** applying the color to the formatter.
92 | * @since 1.0.0
93 | */
94 | export interface LoggerTimestampFormatter {
95 | /**
96 | * @param timestamp The output of {@link LoggerStyle.run} on {@link Timestamp.display}/{@link Timestamp.displayUTC}.
97 | * @since 1.0.0
98 | */
99 | (timestamp: string): string;
100 | }
101 |
--------------------------------------------------------------------------------
/packages/logger/src/register.ts:
--------------------------------------------------------------------------------
1 | import './index';
2 |
3 | import { Plugin, preGenericsInitialization, SapphireClient } from '@sapphire/framework';
4 | import type { ClientOptions } from 'discord.js';
5 | import { Logger } from './index';
6 |
7 | /**
8 | * @since 1.0.0
9 | */
10 | export class LoggerPlugin extends Plugin {
11 | /**
12 | * @since 1.0.0
13 | */
14 | public static [preGenericsInitialization](this: SapphireClient, options: ClientOptions): void {
15 | options.logger ??= {};
16 | options.logger.instance = new Logger(options.logger);
17 | }
18 | }
19 |
20 | SapphireClient.plugins.registerPreGenericsInitializationHook(LoggerPlugin[preGenericsInitialization], 'Logger-PreGenericsInitialization');
21 |
--------------------------------------------------------------------------------
/packages/logger/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "rootDir": "./"
5 | },
6 | "include": ["."]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/logger/tests/Logger.test.ts:
--------------------------------------------------------------------------------
1 | import { LogLevel } from '@sapphire/framework';
2 | import { isClass } from '@sapphire/utilities';
3 | import { Logger } from '../src';
4 |
5 | const levels = [LogLevel.Trace, LogLevel.Debug, LogLevel.Info, LogLevel.Warn, LogLevel.Error, LogLevel.Fatal, LogLevel.Trace] as const;
6 |
7 | describe('Logger', () => {
8 | test('Logger should be a class', () => {
9 | expect(isClass(Logger)).toBe(true);
10 | });
11 |
12 | test('Empty Constructor', () => {
13 | const logger = new Logger();
14 | expect(logger.console).toBeDefined();
15 | expect(logger.depth).toBe(0);
16 | expect(logger.formats.size).toBe(7);
17 | levels.forEach((level) => expect(logger.formats.has(level)).toBe(true));
18 | expect(logger.join).toBe(' ');
19 | expect(logger.level).toBe(LogLevel.Info);
20 | });
21 |
22 | test('Empty Object Constructor', () => {
23 | const logger = new Logger({});
24 | expect(logger.console).toBeDefined();
25 | expect(logger.depth).toBe(0);
26 | expect(logger.formats.size).toBe(7);
27 | levels.forEach((level) => expect(logger.formats.has(level)).toBe(true));
28 | expect(logger.join).toBe(' ');
29 | expect(logger.level).toBe(LogLevel.Info);
30 | });
31 |
32 | test('Depth', () => {
33 | const logger = new Logger({ depth: 2 });
34 | expect(logger.depth).toBe(2);
35 | });
36 |
37 | test('Join', () => {
38 | const logger = new Logger({ join: '\n' });
39 | expect(logger.join).toBe('\n');
40 | });
41 |
42 | test('Level', () => {
43 | const logger = new Logger({ level: LogLevel.Debug });
44 | expect(logger.level).toBe(LogLevel.Debug);
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/packages/logger/tests/LoggerLevel.test.ts:
--------------------------------------------------------------------------------
1 | import { isClass } from '@sapphire/utilities';
2 | import * as Colorette from 'colorette';
3 | import { LoggerLevel, LoggerStyleText } from '../src';
4 |
5 | describe('LoggerLevel', () => {
6 | test('LoggerLevel should be a class', () => {
7 | expect(isClass(LoggerLevel)).toBe(true);
8 | });
9 |
10 | test('Empty Constructor', () => {
11 | const level = new LoggerLevel();
12 | expect(level.infix).toBe('');
13 | expect(level.message).not.toBeNull();
14 | expect(level.message!.style).toBe(Colorette.reset);
15 | expect(level.timestamp).not.toBeNull();
16 | expect(level.timestamp!.color).not.toBeNull();
17 | expect(level.timestamp!.color!.style).toBe(Colorette.reset);
18 | expect(level.timestamp!.formatter('SHARD 0')).toBe('SHARD 0 - ');
19 | expect(level.timestamp!.timestamp.pattern).toBe('YYYY-MM-DD HH:mm:ss');
20 | expect(level.timestamp!.utc).toBe(false);
21 | });
22 |
23 | test('Empty Object Constructor', () => {
24 | const level = new LoggerLevel({});
25 | expect(level.infix).toBe('');
26 | expect(level.message).not.toBeNull();
27 | expect(level.message!.style).toBe(Colorette.reset);
28 | expect(level.timestamp).not.toBeNull();
29 | expect(level.timestamp!.color).not.toBeNull();
30 | expect(level.timestamp!.color!.style).toBe(Colorette.reset);
31 | expect(level.timestamp!.formatter('SHARD 0')).toBe('SHARD 0 - ');
32 | expect(level.timestamp!.timestamp.pattern).toBe('YYYY-MM-DD HH:mm:ss');
33 | expect(level.timestamp!.utc).toBe(false);
34 | });
35 |
36 | test('Infix', () => {
37 | const level = new LoggerLevel({ infix: 'WARN' });
38 | expect(level.infix).toBe('WARN');
39 | });
40 |
41 | test('Message (Colorette)', () => {
42 | const level = new LoggerLevel({ message: Colorette.yellow });
43 | expect(level.message).not.toBeNull();
44 | expect(level.message!.style).toBe(Colorette.yellow);
45 | });
46 |
47 | test('Message (Enum)', () => {
48 | const level = new LoggerLevel({ message: { text: LoggerStyleText.Yellow } });
49 | expect(level.message).not.toBeNull();
50 | expect(level.message!.style).toBe(Colorette.yellow);
51 | });
52 |
53 | test('Message (None)', () => {
54 | const level = new LoggerLevel({ message: null });
55 | expect(level.message).toBeNull();
56 | });
57 |
58 | test('Timestamp (Colorette)', () => {
59 | const level = new LoggerLevel({ timestamp: { color: Colorette.yellow } });
60 | expect(level.timestamp).not.toBeNull();
61 | expect(level.timestamp!.color).not.toBeNull();
62 | expect(level.timestamp!.color!.style).toBe(Colorette.yellow);
63 | });
64 |
65 | test('Timestamp (Enum)', () => {
66 | const level = new LoggerLevel({ timestamp: { color: { text: LoggerStyleText.Yellow } } });
67 | expect(level.timestamp).not.toBeNull();
68 | expect(level.timestamp!.color).not.toBeNull();
69 | expect(level.timestamp!.color!.style).toBe(Colorette.yellow);
70 | });
71 |
72 | test('Timestamp (None)', () => {
73 | const level = new LoggerLevel({ timestamp: null });
74 | expect(level.timestamp).toBeNull();
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/packages/logger/tests/LoggerStyle.test.ts:
--------------------------------------------------------------------------------
1 | import { isClass } from '@sapphire/utilities';
2 | import { bgCyan, bold, dim, green, inverse, reset } from 'colorette';
3 | import { LoggerStyle, LoggerStyleBackground, LoggerStyleEffect, LoggerStyleText } from '../src';
4 |
5 | describe('LoggerStyle', () => {
6 | test('LoggerStyle should be a class', () => {
7 | expect(isClass(LoggerStyle)).toBe(true);
8 | });
9 |
10 | test('Empty Constructor', () => {
11 | const style = new LoggerStyle();
12 | expect(style.style).toBe(reset);
13 | });
14 |
15 | test('Empty Object Constructor', () => {
16 | const style = new LoggerStyle({});
17 | expect(style.style).toBe(reset);
18 | });
19 |
20 | test('Background', () => {
21 | const style = new LoggerStyle({ background: LoggerStyleBackground.Cyan });
22 | expect(style.run('World')).toBe(bgCyan('World'));
23 | });
24 |
25 | test('Text', () => {
26 | const style = new LoggerStyle({ text: LoggerStyleText.Green });
27 | expect(style.run('World')).toBe(green('World'));
28 | });
29 |
30 | test('Effect', () => {
31 | const style = new LoggerStyle({ effects: [LoggerStyleEffect.Inverse] });
32 | expect(style.run('World')).toBe(inverse('World'));
33 | });
34 |
35 | test('Effects', () => {
36 | const style = new LoggerStyle({ effects: [LoggerStyleEffect.Dim, LoggerStyleEffect.Bold] });
37 | expect(style.run('World')).toBe(bold(dim('World')));
38 | });
39 |
40 | test('Multiple', () => {
41 | const style = new LoggerStyle({
42 | background: LoggerStyleBackground.Cyan,
43 | text: LoggerStyleText.Green,
44 | effects: [LoggerStyleEffect.Dim, LoggerStyleEffect.Bold]
45 | });
46 | expect(style.run('World')).toBe(bgCyan(green(bold(dim('World')))));
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/packages/logger/tests/LoggerTimestamp.test.ts:
--------------------------------------------------------------------------------
1 | import { isClass } from '@sapphire/utilities';
2 | import { bgCyan, reset } from 'colorette';
3 | import { LoggerStyleBackground, LoggerTimestamp } from '../src';
4 |
5 | describe('LoggerTimestamp', () => {
6 | test('LoggerTimestamp should be a class', () => {
7 | expect(isClass(LoggerTimestamp)).toBe(true);
8 | });
9 |
10 | test('Empty Constructor', () => {
11 | const timestamp = new LoggerTimestamp();
12 | expect(timestamp.color!.style).toBe(reset);
13 | expect(timestamp.formatter('SHARD 0')).toBe('SHARD 0 - ');
14 | expect(timestamp.timestamp.pattern).toBe('YYYY-MM-DD HH:mm:ss');
15 | expect(timestamp.utc).toBe(false);
16 | });
17 |
18 | test('Empty Object Constructor', () => {
19 | const timestamp = new LoggerTimestamp({});
20 | expect(timestamp.color!.style).toBe(reset);
21 | expect(timestamp.formatter('SHARD 0')).toBe('SHARD 0 - ');
22 | expect(timestamp.timestamp.pattern).toBe('YYYY-MM-DD HH:mm:ss');
23 | expect(timestamp.utc).toBe(false);
24 | });
25 |
26 | test('Color (Defined)', () => {
27 | const timestamp = new LoggerTimestamp({ color: { background: LoggerStyleBackground.Cyan } });
28 | expect(timestamp.color!.style).toBe(bgCyan);
29 | });
30 |
31 | test('Color (None)', () => {
32 | const timestamp = new LoggerTimestamp({ color: null });
33 | expect(timestamp.color).toBeNull();
34 | });
35 |
36 | test('UTC', () => {
37 | const timestamp = new LoggerTimestamp({ utc: true });
38 | expect(timestamp.utc).toBe(true);
39 | });
40 |
41 | test('Pattern', () => {
42 | const timestamp = new LoggerTimestamp({ pattern: 'HH:mm:ss' });
43 | expect(timestamp.timestamp.pattern).toBe('HH:mm:ss');
44 | });
45 |
46 | test('Formatter', () => {
47 | const timestamp = new LoggerTimestamp({ formatter: (string) => `[${string}]` });
48 | expect(timestamp.formatter('10:33:34')).toBe('[10:33:34]');
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/packages/logger/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "types": ["vitest/globals"]
5 | },
6 | "include": ["."]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/logger/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src", "tests"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/logger/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { createTsupConfig } from '../../scripts/tsup.config';
2 |
3 | export default createTsupConfig();
4 |
--------------------------------------------------------------------------------
/packages/logger/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://typedoc.org/schema.json",
3 | "entryPoints": ["src/index.ts"],
4 | "json": "docs/api.json",
5 | "tsconfig": "src/tsconfig.json",
6 | "excludePrivate": false
7 | }
8 |
--------------------------------------------------------------------------------
/packages/logger/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { createVitestConfig } from '../../scripts/vitest.config';
2 |
3 | export default createVitestConfig();
4 |
--------------------------------------------------------------------------------
/packages/pattern-commands/.cliff-jumperrc.yml:
--------------------------------------------------------------------------------
1 | name: plugin-pattern-commands
2 | org: sapphire
3 | install: true
4 | packagePath: packages/pattern-commands
5 | identifierBase: false
6 | pushTag: true
7 | githubRelease: true
8 | githubReleaseLatest: true
9 | gitRepo: sapphiredev/plugins
10 | gitHostVariant: github
11 |
--------------------------------------------------------------------------------
/packages/pattern-commands/.rollup-type-bundlerrc.yml:
--------------------------------------------------------------------------------
1 | onlyBundle: true
2 | excludeFromClean:
3 | - dist/esm/register.d.mts
4 | - dist/cjs/register.d.ts
5 |
--------------------------------------------------------------------------------
/packages/pattern-commands/.typedoc-json-parserrc.yml:
--------------------------------------------------------------------------------
1 | json: 'docs/api.json'
2 |
--------------------------------------------------------------------------------
/packages/pattern-commands/cliff.toml:
--------------------------------------------------------------------------------
1 | [changelog]
2 | header = """
3 | # Changelog
4 |
5 | All notable changes to this project will be documented in this file.\n
6 | """
7 | body = """
8 | {%- macro remote_url() -%}
9 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
10 | {%- endmacro -%}
11 | {% if version %}\
12 | # [{{ version | trim_start_matches(pat="v") }}]\
13 | {% if previous %}\
14 | {% if previous.version %}\
15 | ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
16 | {% else %}\
17 | ({{ self::remote_url() }}/tree/{{ version }})\
18 | {% endif %}\
19 | {% endif %} \
20 | - ({{ timestamp | date(format="%Y-%m-%d") }})
21 | {% else %}\
22 | # [unreleased]
23 | {% endif %}\
24 | {% for group, commits in commits | group_by(attribute="group") %}
25 | ## {{ group | upper_first }}
26 | {% for commit in commits %}
27 | - {% if commit.scope %}\
28 | **{{commit.scope}}:** \
29 | {% endif %}\
30 | {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
31 | {% if commit.github.pr_number %} (\
32 | [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) by @{{ commit.github.username }}) \
33 | {%- endif %}\
34 | {% if commit.breaking %}\
35 | {% for breakingChange in commit.footers %}\
36 | \n{% raw %} {% endraw %}- 💥 **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
37 | {% endfor %}\
38 | {% endif %}\
39 | {% endfor %}
40 | {% endfor %}\n
41 | """
42 | trim = true
43 | footer = ""
44 |
45 | [git]
46 | conventional_commits = true
47 | filter_unconventional = true
48 | commit_parsers = [
49 | { message = "^feat", group = "🚀 Features" },
50 | { message = "^fix", group = "🐛 Bug Fixes" },
51 | { message = "^docs", group = "📝 Documentation" },
52 | { message = "^perf", group = "🏃 Performance" },
53 | { message = "^refactor", group = "🏠 Refactor" },
54 | { message = "^typings", group = "⌨️ Typings" },
55 | { message = "^types", group = "⌨️ Typings" },
56 | { message = ".*deprecated", body = ".*deprecated", group = "🚨 Deprecation" },
57 | { message = "^revert", skip = true },
58 | { message = "^style", group = "🪞 Styling" },
59 | { message = "^test", group = "🧪 Testing" },
60 | { message = "^chore", skip = true },
61 | { message = "^ci", skip = true },
62 | { message = "^build", skip = true },
63 | { body = ".*security", group = "🛡️ Security" },
64 | ]
65 | commit_preprocessors = [
66 | # remove issue numbers from commits
67 | { pattern = '\s\((\w+\s)?#([0-9]+)\)', replace = "" },
68 | ]
69 | filter_commits = true
70 | tag_pattern = "@sapphire/plugin-pattern-commands@[0-9]*"
71 | ignore_tags = ""
72 | topo_order = false
73 | sort_commits = "newest"
74 |
75 | [remote.github]
76 | owner = "sapphiredev"
77 | repo = "plugins"
78 |
--------------------------------------------------------------------------------
/packages/pattern-commands/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sapphire/plugin-pattern-commands",
3 | "version": "6.0.3",
4 | "description": "Plugin for @sapphire/framework that adds support for pattern commands.",
5 | "author": "@sapphire",
6 | "license": "MIT",
7 | "main": "dist/cjs/index.cjs",
8 | "module": "dist/esm/index.mjs",
9 | "types": "dist/cjs/index.d.cts",
10 | "exports": {
11 | ".": {
12 | "import": {
13 | "types": "./dist/esm/index.d.mts",
14 | "default": "./dist/esm/index.mjs"
15 | },
16 | "require": {
17 | "types": "./dist/cjs/index.d.cts",
18 | "default": "./dist/cjs/index.cjs"
19 | }
20 | },
21 | "./register": {
22 | "import": {
23 | "types": "./dist/esm/register.d.mts",
24 | "default": "./dist/esm/register.mjs"
25 | },
26 | "require": {
27 | "types": "./dist/cjs/register.d.cts",
28 | "default": "./dist/cjs/register.cjs"
29 | }
30 | }
31 | },
32 | "sideEffects": [
33 | "./dist/cjs/register.cjs",
34 | "./dist/esm/register.mjs"
35 | ],
36 | "homepage": "https://github.com/sapphiredev/plugins/tree/main/packages/pattern-commands",
37 | "scripts": {
38 | "lint": "eslint src --ext ts --fix",
39 | "build": "tsup && yarn build:types && yarn build:rename-cjs-register",
40 | "build:types": "concurrently \"yarn:build:types:*\"",
41 | "build:types:cjs": "rollup-type-bundler -d dist/cjs --output-typings-file-extension .cts",
42 | "build:types:esm": "rollup-type-bundler -d dist/esm -t .mts",
43 | "build:types:cleanup": "tsx ../../scripts/clean-register-imports.mts",
44 | "build:rename-cjs-register": "tsx ../../scripts/rename-cjs-register.mts",
45 | "typecheck": "tsc -b src",
46 | "docs": "typedoc-json-parser",
47 | "prepack": "yarn build",
48 | "bump": "cliff-jumper",
49 | "check-update": "cliff-jumper --dry-run"
50 | },
51 | "repository": {
52 | "type": "git",
53 | "url": "git+https://github.com/sapphiredev/plugins.git",
54 | "directory": "packages/pattern-commands"
55 | },
56 | "files": [
57 | "dist/"
58 | ],
59 | "engines": {
60 | "node": ">=v18",
61 | "npm": ">=7"
62 | },
63 | "keywords": [
64 | "sapphiredev",
65 | "plugin",
66 | "bot",
67 | "typescript",
68 | "ts",
69 | "yarn",
70 | "discord",
71 | "sapphire",
72 | "pattern-commands"
73 | ],
74 | "bugs": {
75 | "url": "https://github.com/sapphiredev/plugins/issues"
76 | },
77 | "publishConfig": {
78 | "access": "public"
79 | },
80 | "devDependencies": {
81 | "@favware/cliff-jumper": "^6.0.0",
82 | "@favware/rollup-type-bundler": "^4.0.0",
83 | "concurrently": "^9.1.2",
84 | "tsup": "^8.5.0",
85 | "tsx": "^4.19.4",
86 | "typedoc": "^0.26.11",
87 | "typedoc-json-parser": "^10.2.0",
88 | "typescript": "~5.4.5"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/packages/pattern-commands/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { PatternCommandStore } from './lib/structures/PatternCommandStore';
2 |
3 | export * from './lib/structures/PatternCommandStore';
4 | export * from './lib/structures/PatternCommand';
5 | export * from './lib/utils/PatternCommandEvents';
6 | export * from './lib/utils/PatternCommandInterfaces';
7 | export { PluginListener as PluginPatternCommandsCommandAcceptedListener } from './listeners/PluginCommandAccepted';
8 | export { PluginListener as PluginPatternCommandsMessageParseListener } from './listeners/PluginMessageParse';
9 | export { PluginListener as PluginPatternCommandsPreCommandRunListener } from './listeners/PluginPreCommandRun';
10 |
11 | export { loadListeners } from './listeners/_load';
12 |
13 | declare module '@sapphire/pieces' {
14 | interface StoreRegistryEntries {
15 | 'pattern-commands': PatternCommandStore;
16 | }
17 | }
18 |
19 | /**
20 | * The [@sapphire/plugin-pattern-commands](https://github.com/sapphiredev/plugins/blob/main/packages/pattern-commands) version that you are currently using.
21 | * An example use of this is showing it of in a bot information command.
22 | *
23 | * Note to Sapphire developers: This needs to explicitly be `string` so it is not typed as the string that gets replaced by esbuild
24 | */
25 | // eslint-disable-next-line @typescript-eslint/no-inferrable-types
26 | export const version: string = '[VI]{{inject}}[/VI]';
27 |
--------------------------------------------------------------------------------
/packages/pattern-commands/src/lib/structures/PatternCommand.ts:
--------------------------------------------------------------------------------
1 | import { Args, Command, type MessageCommand } from '@sapphire/framework';
2 | import type { Awaitable, Message } from 'discord.js';
3 |
4 | export abstract class PatternCommand extends Command {
5 | public readonly chance: number;
6 | public readonly weight: number;
7 | public readonly matchFullName: boolean;
8 | public constructor(context: PatternCommand.LoaderContext, options: PatternCommand.Options) {
9 | super(context, options);
10 | this.chance = options.chance ?? 100;
11 | if (options.weight) {
12 | if (options.weight < 0) {
13 | this.weight = 0;
14 | } else if (options.weight > 10) {
15 | this.weight = 10;
16 | } else {
17 | this.weight = options.weight;
18 | }
19 | } else {
20 | this.weight = 5;
21 | }
22 | this.matchFullName = options.matchFullName ?? false;
23 | }
24 |
25 | /**
26 | * Executes the pattern command's logic.
27 | * @param message The message that triggered the pattern command.
28 | */
29 | public abstract override messageRun(message: Message): Awaitable;
30 | }
31 |
32 | export interface PatternCommandOptions extends MessageCommand.Options {
33 | /**
34 | * The chance that the pattern command is triggered.
35 | * @default 100
36 | */
37 | chance?: number;
38 | /**
39 | * The matching weight of the command.
40 | * @default 5
41 | */
42 | weight?: number;
43 | /**
44 | * If true it will only trigger on full matches (for example, explore won't trigger lore)
45 | * Note: It will only change the behavior of the command's name and not for the command's aliasses
46 | * @default false
47 | */
48 | matchFullName?: boolean;
49 | }
50 |
51 | export namespace PatternCommand {
52 | /**
53 | * Re-export of {@link MessageCommand.LoaderContext}
54 | * @deprecated Use {@linkcode LoaderContext} instead.
55 | */
56 | export type Context = LoaderContext;
57 | export type LoaderContext = MessageCommand.LoaderContext;
58 |
59 | /** Re-export of {@link MessageCommand.RunContext} */
60 | export type RunContext = MessageCommand.RunContext;
61 |
62 | /** Re-export of {@link MessageCommand.JSON} */
63 | export type JSON = MessageCommand.JSON;
64 |
65 | /** Re-export of {@link MessageCommand.RunInTypes} */
66 | export type RunInTypes = MessageCommand.RunInTypes;
67 |
68 | /**
69 | * The PatternCommand Options
70 | */
71 | export type Options = PatternCommandOptions;
72 | }
73 |
--------------------------------------------------------------------------------
/packages/pattern-commands/src/lib/structures/PatternCommandStore.ts:
--------------------------------------------------------------------------------
1 | import { AliasStore } from '@sapphire/pieces';
2 | import { PatternCommand } from './PatternCommand';
3 |
4 | export class PatternCommandStore extends AliasStore {
5 | public constructor() {
6 | super(PatternCommand, { name: 'pattern-commands' });
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/pattern-commands/src/lib/utils/PatternCommandEvents.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Events emitted during the parsing and command run.
3 | * You can use these events for debugging and logging purposes.
4 | */
5 | export enum PatternCommandEvents {
6 | /**
7 | * Event that is emitted when the RNG doesn't love the command
8 | * @param message The message where the command was triggered
9 | * @param command The command's piece
10 | * @param alias The alias that triggered the command
11 | */
12 | CommandNoLuck = 'patternCommandNoLuck',
13 | /**
14 | * Event that is emitted when an alias triggered the command but before parsing the preconditions
15 | * @param payload PatternCommandRunPayload which contains message, command and alias
16 | */
17 | PreCommandRun = 'patternCommandPreRun',
18 | /**
19 | * Event that is emitted after the preconditions if none of them denied the command
20 | * @param payload PatternCommandAcceptedPayload which contains parameters, context, message, command and alias
21 | */
22 | CommandAccepted = 'patternCommandAccepted',
23 | /**
24 | * Event that is emitted after the preconditions if at least one of them denied the command
25 | * @param error The error of the precondition which denied the command
26 | * @param payload PatternCommandDeniedPayload which contains parameters, context, message, command and alias
27 | */
28 | CommandDenied = 'patternCommandDenied',
29 | /**
30 | * Event that is emitted just before the command is ran
31 | * @param message The message where the command was triggered
32 | * @param command The command's piece
33 | * @param alias The alias that triggered the command
34 | */
35 | CommandRun = 'patternCommandRun',
36 | /**
37 | * Event that is emitted if there's no error while running the command
38 | * @param result The result of command's run
39 | * @param command The command's piece
40 | * @param alias The alias that triggered the command
41 | * @param duration The duration which indicates how long it took the command to run
42 | */
43 | CommandSuccess = 'patternCommandSuccess',
44 | /**
45 | * Event that is emitted if there's an error while running the command
46 | * @param error The error message which happened while the command was running
47 | * @param command The command's piece
48 | * @param payload PatternCommandAcceptedPayload which contains parameters, context, message, command and alias
49 | */
50 | CommandError = 'patternCommandError',
51 | /**
52 | * Event that is emitted if the command has finished, regardless of whether an error occurred or not
53 | * @param command The command's piece
54 | * @param duration The duration which indicates how long it took the command to run
55 | * @param payload PatternCommandAcceptedPayload which contains parameters, context, message, command and alias
56 | */
57 | CommandFinished = 'patternCommandFinished'
58 | }
59 |
--------------------------------------------------------------------------------
/packages/pattern-commands/src/lib/utils/PatternCommandInterfaces.ts:
--------------------------------------------------------------------------------
1 | import type { Message } from 'discord.js';
2 | import type { PatternCommand } from '../structures/PatternCommand';
3 |
4 | export interface PatternCommandPrePayload {
5 | message: Message;
6 | possibleCommands: PossiblePatternCommand[];
7 | }
8 |
9 | export interface PatternCommandPayload {
10 | /** The message that triggered this PatternCommand */
11 | message: Message;
12 | /** The command that is triggered by this PatternCommand */
13 | command: PatternCommand;
14 | /** The alias of this pattern command */
15 | alias: string;
16 | }
17 |
18 | export interface PatternPreCommandRunPayload extends PatternCommandDeniedPayload {}
19 |
20 | export interface PatternCommandDeniedPayload extends PatternCommandPayload {
21 | parameters: string;
22 | context: PatternCommand.RunContext;
23 | }
24 |
25 | export interface PatternCommandAcceptedPayload extends PatternCommandPayload {
26 | parameters: string;
27 | context: PatternCommand.RunContext;
28 | }
29 |
30 | export interface PatternCommandSuccessPayload extends PatternCommandFinishedPayload {
31 | result: unknown;
32 | }
33 |
34 | export interface PatternCommandFinishedPayload extends PatternCommandAcceptedPayload {
35 | duration: number;
36 | success: boolean;
37 | }
38 |
39 | export interface PatternCommandErrorPayload extends PatternCommandFinishedPayload {}
40 |
41 | export interface PatternCommandNoLuckPayload extends PatternCommandAcceptedPayload {}
42 |
43 | export interface PossiblePatternCommand {
44 | command: PatternCommand;
45 | alias: string;
46 | weight: number;
47 | }
48 |
--------------------------------------------------------------------------------
/packages/pattern-commands/src/listeners/PluginCommandAccepted.ts:
--------------------------------------------------------------------------------
1 | import { Listener, Result } from '@sapphire/framework';
2 | import { Stopwatch } from '@sapphire/stopwatch';
3 | import { PatternCommandEvents } from '../lib/utils/PatternCommandEvents';
4 | import type { PatternCommandAcceptedPayload } from '../lib/utils/PatternCommandInterfaces';
5 |
6 | export class PluginListener extends Listener {
7 | public constructor(context: Listener.LoaderContext) {
8 | super(context, { event: PatternCommandEvents.CommandAccepted });
9 | }
10 |
11 | public override async run(payload: PatternCommandAcceptedPayload) {
12 | const { message, command } = payload;
13 |
14 | const result = await Result.fromAsync(async () => {
15 | message.client.emit(PatternCommandEvents.CommandRun, message, command, payload);
16 |
17 | const stopwatch = new Stopwatch();
18 | const result = await command.messageRun(message);
19 | const { duration } = stopwatch.stop();
20 |
21 | message.client.emit(PatternCommandEvents.CommandSuccess, { ...payload, result, duration });
22 |
23 | return duration;
24 | });
25 |
26 | result.inspectErr((error) => message.client.emit(PatternCommandEvents.CommandError, error, { ...payload, duration: -1 }));
27 |
28 | message.client.emit(PatternCommandEvents.CommandFinished, message, command, {
29 | ...payload,
30 | success: result.isOk(),
31 | duration: result.unwrapOr(-1)
32 | });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/pattern-commands/src/listeners/PluginPreCommandRun.ts:
--------------------------------------------------------------------------------
1 | import { Listener, type PreconditionStore } from '@sapphire/framework';
2 | import { PatternCommandEvents } from '../lib/utils/PatternCommandEvents';
3 | import type { PatternCommandPayload, PatternCommandPrePayload } from '../lib/utils/PatternCommandInterfaces';
4 |
5 | export class PluginListener extends Listener {
6 | public constructor(context: Listener.LoaderContext) {
7 | super(context, { event: PatternCommandEvents.PreCommandRun });
8 | }
9 |
10 | public override async run(payload: PatternCommandPrePayload) {
11 | const { message, possibleCommands } = payload;
12 |
13 | for (const possibleCommand of possibleCommands) {
14 | const { command } = possibleCommand;
15 | const commandPayload: PatternCommandPayload = {
16 | message,
17 | command,
18 | alias: possibleCommand.alias
19 | };
20 |
21 | // Run global preconditions:
22 | const globalResult = await (this.container.stores.get('preconditions') as unknown as PreconditionStore).messageRun(
23 | message,
24 | command,
25 | commandPayload as any
26 | );
27 |
28 | if (globalResult.isErr()) {
29 | message.client.emit(PatternCommandEvents.CommandDenied, globalResult.unwrapErr(), commandPayload);
30 | continue;
31 | }
32 |
33 | // Run command-specific preconditions:
34 | const localResult = await command.preconditions.messageRun(message, command, payload as any);
35 | if (localResult.isErr()) {
36 | message.client.emit(PatternCommandEvents.CommandDenied, localResult.unwrapErr(), commandPayload);
37 | continue;
38 | }
39 |
40 | if (command.chance >= Math.round(Math.random() * 99) + 1) {
41 | message.client.emit(PatternCommandEvents.CommandAccepted, commandPayload);
42 | break;
43 | } else {
44 | message.client.emit(PatternCommandEvents.CommandNoLuck, commandPayload);
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/packages/pattern-commands/src/listeners/_load.ts:
--------------------------------------------------------------------------------
1 | import { container } from '@sapphire/pieces';
2 | import { PluginListener as PluginCommandAccepted } from './PluginCommandAccepted';
3 | import { PluginListener as PluginMessageParse } from './PluginMessageParse';
4 | import { PluginListener as PluginPreCommandRun } from './PluginPreCommandRun';
5 |
6 | export function loadListeners() {
7 | const store = 'listeners' as const;
8 | void container.stores.loadPiece({ name: 'PluginCommandAccepted', piece: PluginCommandAccepted, store });
9 | void container.stores.loadPiece({ name: 'PluginMessageParse', piece: PluginMessageParse, store });
10 | void container.stores.loadPiece({ name: 'PluginPreCommandRun', piece: PluginPreCommandRun, store });
11 | }
12 |
--------------------------------------------------------------------------------
/packages/pattern-commands/src/register.ts:
--------------------------------------------------------------------------------
1 | import './index';
2 |
3 | import { Plugin, postInitialization, SapphireClient } from '@sapphire/framework';
4 | import type { ClientOptions } from 'discord.js';
5 | import { loadListeners, PatternCommandStore } from './index';
6 |
7 | /**
8 | * @since 1.0.0
9 | */
10 | export class PatternCommandPlugin extends Plugin {
11 | /**
12 | * @since 1.0.0
13 | */
14 | public static [postInitialization](this: SapphireClient, _options: ClientOptions): void {
15 | this.stores.register(new PatternCommandStore());
16 | loadListeners();
17 | }
18 | }
19 |
20 | SapphireClient.plugins.registerPostInitializationHook(PatternCommandPlugin[postInitialization], 'Pattern-Command-PostInitialization');
21 |
--------------------------------------------------------------------------------
/packages/pattern-commands/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "rootDir": "./"
5 | },
6 | "include": ["."]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/pattern-commands/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true
6 | },
7 | "include": ["src", "tests"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/pattern-commands/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { createTsupConfig } from '../../scripts/tsup.config';
2 |
3 | export default createTsupConfig();
4 |
--------------------------------------------------------------------------------
/packages/pattern-commands/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://typedoc.org/schema.json",
3 | "entryPoints": ["src/index.ts"],
4 | "json": "docs/api.json",
5 | "tsconfig": "src/tsconfig.json",
6 | "excludeExternals": true,
7 | "excludePrivate": false
8 | }
9 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/.cliff-jumperrc.yml:
--------------------------------------------------------------------------------
1 | name: plugin-scheduled-tasks
2 | org: sapphire
3 | install: true
4 | packagePath: packages/scheduled-tasks
5 | identifierBase: false
6 | pushTag: true
7 | githubRelease: true
8 | githubReleaseLatest: true
9 | gitRepo: sapphiredev/plugins
10 | gitHostVariant: github
11 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/.rollup-type-bundlerrc.yml:
--------------------------------------------------------------------------------
1 | onlyBundle: true
2 | excludeFromClean:
3 | - dist/esm/register.d.mts
4 | - dist/cjs/register.d.ts
5 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/.typedoc-json-parserrc.yml:
--------------------------------------------------------------------------------
1 | json: 'docs/api.json'
2 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/UPGRADING-v9-v10.md:
--------------------------------------------------------------------------------
1 | # Migration guide for @sapphire/plugin-scheduled-tasks v9.x to v10.x
2 |
3 | ## Task payloads
4 |
5 | ### Enforcing types
6 |
7 | The `ScheduledTaskJob` interface has been removed in favor of defining types on `ScheduledTasks`.
8 |
9 | You can define a payload type for a task by using module augmentation on `ScheduledTasks`. If the value of the entry is set to `never` or `undefined` in the interface, then there is no payload required for the task. If the type union contains an `undefined`, then the payload will be optional. Otherise, it will enforce the provided type for that task. Below are some examples.
10 |
11 | ```ts
12 | declare module '@sapphire/plugin-scheduled-tasks' {
13 | interface ScheduledTasks {
14 | [ExampleTasks.One]: { data: string } | null;
15 | [ExampleTasks.Two]: never;
16 | [ExampleTasks.Three]: boolean | undefined;
17 | }
18 | }
19 |
20 | /** ExampleTasks.One */
21 |
22 | // Good
23 | await container.tasks.create({ name: ExampleTasks.One, payload: { data: 'value' } });
24 |
25 | await container.tasks.create({ name: ExampleTasks.One, payload: null });
26 |
27 | // Type error
28 | await container.tasks.create({ name: ExampleTasks.One, payload: { data: true } });
29 |
30 | await container.tasks.create({ name: ExampleTasks.One, payload: false });
31 |
32 | /** ExampleTasks.Two */
33 |
34 | // Good
35 | await container.tasks.create(ExampleTasks.Two);
36 |
37 | await container.tasks.create({ name: ExampleTasks.Two });
38 |
39 | // Type error
40 | await container.tasks.create({ name: ExampleTasks.Two, payload: null });
41 |
42 | /** ExampleTasks.Three */
43 |
44 | // Good
45 | await container.tasks.create(ExampleTasks.Three);
46 |
47 | await container.tasks.create({ name: ExampleTasks.Three });
48 |
49 | await container.tasks.create({ name: ExampleTasks.Three, payload: true });
50 | ```
51 |
52 | ## Task handler
53 |
54 | ### Internal client
55 |
56 | Due to the removal of `ScheduledTaskJob`, the `BullClient` will now be typed as `unknown` since the Job types in the Queue can not _really_ be known. So you will need to do validation when interacting directly with the client.
57 |
58 | ## Error listeners
59 |
60 | The included error listeners are now enabled by default. If you want them to be disabled, just set `loadScheduledTaskErrorListeners` to false in the `SapphireClient` options.
61 |
62 | ### Error handling
63 |
64 | The internal BullMQ client does not actually throw any errors, it just emits them from the client. As such, those error events will now be sent to the corresponding error listener registered by the plugin.
65 |
66 | ### Error payload types
67 |
68 | The error listeners previously only returned the name of the task when an error was emitted, but now the event will provide the associated Piece.
69 |
70 | ## BullMQ v5
71 |
72 | The BullMQ dependency is being updated to v5 from v3. If you depend on the internal BullMQ client in any way, check out their releases for breaking changes.
73 |
74 | ### Required `tasks` client property
75 |
76 | The `tasks` property for options relating to this plugin are now required. They were previously optional which was an oversight, as a connection string is always needed.
77 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/cliff.toml:
--------------------------------------------------------------------------------
1 | [changelog]
2 | header = """
3 | # Changelog
4 |
5 | All notable changes to this project will be documented in this file.\n
6 | """
7 | body = """
8 | {%- macro remote_url() -%}
9 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
10 | {%- endmacro -%}
11 | {% if version %}\
12 | # [{{ version | trim_start_matches(pat="v") }}]\
13 | {% if previous %}\
14 | {% if previous.version %}\
15 | ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
16 | {% else %}\
17 | ({{ self::remote_url() }}/tree/{{ version }})\
18 | {% endif %}\
19 | {% endif %} \
20 | - ({{ timestamp | date(format="%Y-%m-%d") }})
21 | {% else %}\
22 | # [unreleased]
23 | {% endif %}\
24 | {% for group, commits in commits | group_by(attribute="group") %}
25 | ## {{ group | upper_first }}
26 | {% for commit in commits %}
27 | - {% if commit.scope %}\
28 | **{{commit.scope}}:** \
29 | {% endif %}\
30 | {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
31 | {% if commit.github.pr_number %} (\
32 | [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) by @{{ commit.github.username }}) \
33 | {%- endif %}\
34 | {% if commit.breaking %}\
35 | {% for breakingChange in commit.footers %}\
36 | \n{% raw %} {% endraw %}- 💥 **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
37 | {% endfor %}\
38 | {% endif %}\
39 | {% endfor %}
40 | {% endfor %}\n
41 | """
42 | trim = true
43 | footer = ""
44 |
45 | [git]
46 | conventional_commits = true
47 | filter_unconventional = true
48 | commit_parsers = [
49 | { message = "^feat", group = "🚀 Features" },
50 | { message = "^fix", group = "🐛 Bug Fixes" },
51 | { message = "^docs", group = "📝 Documentation" },
52 | { message = "^perf", group = "🏃 Performance" },
53 | { message = "^refactor", group = "🏠 Refactor" },
54 | { message = "^typings", group = "⌨️ Typings" },
55 | { message = "^types", group = "⌨️ Typings" },
56 | { message = ".*deprecated", body = ".*deprecated", group = "🚨 Deprecation" },
57 | { message = "^revert", skip = true },
58 | { message = "^style", group = "🪞 Styling" },
59 | { message = "^test", group = "🧪 Testing" },
60 | { message = "^chore", skip = true },
61 | { message = "^ci", skip = true },
62 | { message = "^build", skip = true },
63 | { body = ".*security", group = "🛡️ Security" },
64 | ]
65 | commit_preprocessors = [
66 | # remove issue numbers from commits
67 | { pattern = '\s\((\w+\s)?#([0-9]+)\)', replace = "" },
68 | ]
69 | filter_commits = true
70 | tag_pattern = "@sapphire/plugin-scheduled-tasks@[0-9]*"
71 | ignore_tags = ""
72 | topo_order = false
73 | sort_commits = "newest"
74 |
75 | [remote.github]
76 | owner = "sapphiredev"
77 | repo = "plugins"
78 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sapphire/plugin-scheduled-tasks",
3 | "version": "10.0.3",
4 | "description": "Plugin for @sapphire/framework to have scheduled tasks",
5 | "author": "@sapphire",
6 | "license": "MIT",
7 | "main": "dist/cjs/index.cjs",
8 | "module": "dist/esm/index.mjs",
9 | "types": "dist/cjs/index.d.cts",
10 | "exports": {
11 | ".": {
12 | "import": {
13 | "types": "./dist/esm/index.d.mts",
14 | "default": "./dist/esm/index.mjs"
15 | },
16 | "require": {
17 | "types": "./dist/cjs/index.d.cts",
18 | "default": "./dist/cjs/index.cjs"
19 | }
20 | },
21 | "./register": {
22 | "import": {
23 | "types": "./dist/esm/register.d.mts",
24 | "default": "./dist/esm/register.mjs"
25 | },
26 | "require": {
27 | "types": "./dist/cjs/register.d.cts",
28 | "default": "./dist/cjs/register.cjs"
29 | }
30 | }
31 | },
32 | "sideEffects": [
33 | "./dist/cjs/register.cjs",
34 | "./dist/esm/register.mjs"
35 | ],
36 | "homepage": "https://github.com/sapphiredev/plugins/tree/main/packages/scheduled-tasks",
37 | "scripts": {
38 | "lint": "eslint src --ext ts --fix",
39 | "build": "tsup && yarn build:types && yarn build:rename-cjs-register",
40 | "build:types": "concurrently \"yarn:build:types:*\"",
41 | "build:types:cjs": "rollup-type-bundler -d dist/cjs --output-typings-file-extension .cts",
42 | "build:types:esm": "rollup-type-bundler -d dist/esm -t .mts",
43 | "build:types:cleanup": "tsx ../../scripts/clean-register-imports.mts",
44 | "build:rename-cjs-register": "tsx ../../scripts/rename-cjs-register.mts",
45 | "typecheck": "tsc -b src",
46 | "docs": "typedoc-json-parser",
47 | "prepack": "yarn build",
48 | "bump": "cliff-jumper",
49 | "check-update": "cliff-jumper --dry-run"
50 | },
51 | "dependencies": {
52 | "@sapphire/stopwatch": "^1.5.4",
53 | "@sapphire/utilities": "^3.18.2",
54 | "bullmq": "5.53.2"
55 | },
56 | "devDependencies": {
57 | "@favware/cliff-jumper": "^6.0.0",
58 | "@favware/rollup-type-bundler": "^4.0.0",
59 | "concurrently": "^9.1.2",
60 | "tsup": "^8.5.0",
61 | "tsx": "^4.19.4",
62 | "typedoc": "^0.26.11",
63 | "typedoc-json-parser": "^10.2.0",
64 | "typescript": "~5.4.5"
65 | },
66 | "repository": {
67 | "type": "git",
68 | "url": "git+https://github.com/sapphiredev/plugins.git",
69 | "directory": "packages/scheduled-tasks"
70 | },
71 | "files": [
72 | "dist/",
73 | "UPGRADING-*.md"
74 | ],
75 | "engines": {
76 | "node": ">=v18",
77 | "npm": ">=7"
78 | },
79 | "keywords": [
80 | "sapphiredev",
81 | "plugin",
82 | "bot",
83 | "typescript",
84 | "ts",
85 | "yarn",
86 | "discord",
87 | "sapphire"
88 | ],
89 | "bugs": {
90 | "url": "https://github.com/sapphiredev/plugins/issues"
91 | },
92 | "publishConfig": {
93 | "access": "public"
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { ScheduledTaskHandler } from './lib/ScheduledTaskHandler';
2 | import type { ScheduledTaskStore } from './lib/structures/ScheduledTaskStore';
3 | import type { ScheduledTaskHandlerOptions } from './lib/types/ScheduledTaskTypes';
4 |
5 | export * from './lib/ScheduledTaskHandler';
6 | export * from './lib/structures/ScheduledTask';
7 | export * from './lib/structures/ScheduledTaskStore';
8 | export * from './lib/types/ScheduledTaskEvents';
9 | export type * from './lib/types/ScheduledTaskTypes';
10 |
11 | export { loadListeners } from './listeners/_load';
12 |
13 | declare module '@sapphire/pieces' {
14 | interface Container {
15 | tasks: ScheduledTaskHandler;
16 | }
17 |
18 | interface StoreRegistryEntries {
19 | 'scheduled-tasks': ScheduledTaskStore;
20 | }
21 | }
22 |
23 | declare module 'discord.js' {
24 | export interface ClientOptions {
25 | tasks: ScheduledTaskHandlerOptions;
26 | /**
27 | * If the the pre-included scheduled task error listeners should be loaded
28 | * @default true
29 | */
30 | loadScheduledTaskErrorListeners?: boolean;
31 | }
32 | }
33 |
34 | /**
35 | * The [@sapphire/plugin-scheduled-tasks](https://github.com/sapphiredev/plugins/blob/main/packages/scheduled-tasks) version that you are currently using.
36 | * An example use of this is showing it of in a bot information command.
37 | *
38 | * Note to Sapphire developers: This needs to explicitly be `string` so it is not typed as the string that gets replaced by esbuild
39 | */
40 | // eslint-disable-next-line @typescript-eslint/no-inferrable-types
41 | export const version: string = '[VI]{{inject}}[/VI]';
42 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/src/lib/structures/ScheduledTask.ts:
--------------------------------------------------------------------------------
1 | import { Piece } from '@sapphire/pieces';
2 | import type { Awaitable } from '@sapphire/utilities';
3 | import type { JobsOptions } from 'bullmq';
4 | import type { ScheduledTasksKeys, ScheduledTasksPayload } from '../types/ScheduledTaskTypes';
5 |
6 | /**
7 | * Represents a scheduled task that can be run at a specified interval or pattern.
8 | * @abstract
9 | */
10 | export abstract class ScheduledTask<
11 | Task extends ScheduledTasksKeys = ScheduledTasksKeys, //
12 | Options extends ScheduledTask.Options = ScheduledTask.Options
13 | > extends Piece {
14 | public readonly interval: number | null;
15 | public readonly pattern: string | null;
16 | public readonly timezone: string;
17 | public readonly customJobOptions?: ScheduledTaskCustomJobOptions;
18 |
19 | public constructor(context: ScheduledTask.LoaderContext, options: ScheduledTaskOptions) {
20 | super(context, options);
21 | this.interval = options.interval ?? null;
22 | this.pattern = options.pattern ?? null;
23 | this.customJobOptions = options.customJobOptions;
24 | this.timezone = options.timezone ?? 'UTC';
25 | }
26 |
27 | public abstract run(payload: ScheduledTasksPayload): Awaitable;
28 | }
29 |
30 | /**
31 | * Options for configuring a scheduled task.
32 | */
33 | export interface ScheduledTaskOptions extends Piece.Options {
34 | /**
35 | * The interval (in milliseconds) at which the task should run.
36 | */
37 | interval?: number | null;
38 | /**
39 | * A cron pattern specifying when the task should run.
40 | */
41 | pattern?: string | null;
42 | /**
43 | * Custom options to pass to the job scheduler.
44 | */
45 | customJobOptions?: ScheduledTaskCustomJobOptions;
46 |
47 | /**
48 | * The timezone to use for the task.
49 | * @default 'UTC'
50 | */
51 | timezone?: string | null;
52 | }
53 |
54 | /**
55 | * Custom options for a job in a scheduled task.
56 | */
57 | export type ScheduledTaskCustomJobOptions = Omit;
58 |
59 | /**
60 | * The namespace for {@link ScheduledTask}.
61 | */
62 | export namespace ScheduledTask {
63 | /**
64 | * The options for a {@link ScheduledTask}.
65 | */
66 | export type Options = ScheduledTaskOptions;
67 |
68 | /**
69 | * The context for a {@link ScheduledTask}.
70 | * @deprecated Use {@linkcode LoaderContext} instead.
71 | */
72 | export type Context = LoaderContext;
73 | export type LoaderContext = Piece.LoaderContext<'scheduled-tasks'>;
74 | export type JSON = Piece.JSON;
75 | export type LocationJSON = Piece.LocationJSON;
76 | }
77 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/src/lib/structures/ScheduledTaskStore.ts:
--------------------------------------------------------------------------------
1 | import { Store } from '@sapphire/pieces';
2 | import { ScheduledTask } from './ScheduledTask';
3 |
4 | /**
5 | * A store for managing scheduled tasks.
6 | */
7 | export class ScheduledTaskStore extends Store {
8 | public readonly repeatedTasks: ScheduledTask[] = [];
9 |
10 | public constructor() {
11 | super(ScheduledTask, { name: 'scheduled-tasks' });
12 | }
13 |
14 | public override set(key: string, value: ScheduledTask): this {
15 | if (value.interval !== null || value.pattern !== null) {
16 | this.repeatedTasks.push(value);
17 | }
18 |
19 | return super.set(key, value);
20 | }
21 |
22 | public override delete(key: string): boolean {
23 | const index = this.repeatedTasks.findIndex((task) => task.name === key);
24 |
25 | // If the scheduled task was found, remove it
26 | if (index !== -1) this.repeatedTasks.splice(index, 1);
27 |
28 | return super.delete(key);
29 | }
30 |
31 | public override clear(): void {
32 | this.repeatedTasks.length = 0;
33 | return super.clear();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/src/lib/types/ScheduledTaskEvents.ts:
--------------------------------------------------------------------------------
1 | import { ScheduledTask } from '../structures/ScheduledTask';
2 |
3 | /**
4 | * Events emitted during the process setting up the scheduler and running a task.
5 | * You can use these events to trace the progress for debugging purposes.
6 | */
7 | export const ScheduledTaskEvents = {
8 | /**
9 | * Event that is emitted if a task piece is not found in the store
10 | */
11 | ScheduledTaskNotFound: 'scheduledTaskNotFound' as const,
12 | /**
13 | * Event that is emitted before a task's "run" method is called
14 | */
15 | ScheduledTaskRun: 'scheduledTaskRun' as const,
16 | /**
17 | * Event that is emitted when a task's "run" method throws an error
18 | */
19 | ScheduledTaskError: 'scheduledTaskError' as const,
20 | /**
21 | * Event that is emitted when a tasks's "run" method is successful
22 | */
23 | ScheduledTaskSuccess: 'scheduledTaskSuccess' as const,
24 | /**
25 | * Event that is emitted when a task's "run" method finishes, regardless of whether an error occurred or not
26 | */
27 | ScheduledTaskFinished: 'scheduledTaskFinished' as const,
28 | /**
29 | * Event that is emitted when the scheduler fails to connect to the server (i.e. redis)
30 | */
31 | ScheduledTaskStrategyConnectError: 'scheduledTaskStrategyConnectError' as const,
32 | /**
33 | * Event that is emitted when the scheduled task client encounters an error.
34 | */
35 | ScheduledTaskStrategyClientError: 'scheduledTaskStrategyClientError' as const,
36 | /**
37 | * Event that is emitted when the scheduled task worker encounters an error.
38 | */
39 | ScheduledTaskStrategyWorkerError: 'scheduledTaskStrategyWorkerError' as const
40 | };
41 |
42 | declare module 'discord.js' {
43 | interface ClientEvents {
44 | [ScheduledTaskEvents.ScheduledTaskNotFound]: [
45 | task: string, //
46 | payload: unknown
47 | ];
48 | [ScheduledTaskEvents.ScheduledTaskRun]: [
49 | task: ScheduledTask, //
50 | payload: unknown
51 | ];
52 | [ScheduledTaskEvents.ScheduledTaskError]: [
53 | error: unknown, //
54 | task: ScheduledTask,
55 | payload: unknown
56 | ];
57 | [ScheduledTaskEvents.ScheduledTaskSuccess]: [
58 | task: ScheduledTask, //
59 | payload: unknown,
60 | result: unknown,
61 | duration: number
62 | ];
63 | [ScheduledTaskEvents.ScheduledTaskFinished]: [
64 | task: ScheduledTask, //
65 | duration: number | null,
66 | payload: unknown
67 | ];
68 | [ScheduledTaskEvents.ScheduledTaskStrategyConnectError]: [error: unknown];
69 | [ScheduledTaskEvents.ScheduledTaskStrategyClientError]: [error: unknown];
70 | [ScheduledTaskEvents.ScheduledTaskStrategyWorkerError]: [error: unknown];
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/src/listeners/PluginScheduledTaskError.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import { ScheduledTaskEvents } from '../lib/types/ScheduledTaskEvents';
3 | import { ScheduledTask } from '../lib/structures/ScheduledTask';
4 |
5 | export class PluginListener extends Listener {
6 | public constructor(context: Listener.LoaderContext) {
7 | super(context, {
8 | name: ScheduledTaskEvents.ScheduledTaskError,
9 | event: ScheduledTaskEvents.ScheduledTaskError
10 | });
11 | }
12 |
13 | public override run(error: unknown, task: ScheduledTask) {
14 | const { name, location } = task;
15 | this.container.logger.error(`Encountered error on scheduled task "${name}" at path "${location.full}"`, error);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/src/listeners/PluginScheduledTaskNotFound.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import { ScheduledTaskEvents } from '../lib/types/ScheduledTaskEvents';
3 |
4 | export class PluginListener extends Listener {
5 | public constructor(context: Listener.LoaderContext) {
6 | super(context, {
7 | name: ScheduledTaskEvents.ScheduledTaskNotFound,
8 | event: ScheduledTaskEvents.ScheduledTaskNotFound
9 | });
10 | }
11 |
12 | public override run(task: string) {
13 | this.container.logger.error(`[ScheduledTaskPlugin] There was no task found for "${task}"`);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/src/listeners/PluginScheduledTaskStrategyClientError.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import { ScheduledTaskEvents } from '../lib/types/ScheduledTaskEvents';
3 |
4 | export class PluginListener extends Listener {
5 | public constructor(context: Listener.LoaderContext) {
6 | super(context, {
7 | name: ScheduledTaskEvents.ScheduledTaskStrategyClientError,
8 | event: ScheduledTaskEvents.ScheduledTaskStrategyClientError
9 | });
10 | }
11 |
12 | public override run(error: unknown) {
13 | this.container.logger.error(`[ScheduledTaskPlugin] Scheduled Task handler encountered an error`, error);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/src/listeners/PluginScheduledTaskStrategyConnectError.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import { ScheduledTaskEvents } from '../lib/types/ScheduledTaskEvents';
3 |
4 | export class PluginListener extends Listener {
5 | public constructor(context: Listener.LoaderContext) {
6 | super(context, {
7 | name: ScheduledTaskEvents.ScheduledTaskStrategyConnectError,
8 | event: ScheduledTaskEvents.ScheduledTaskStrategyConnectError
9 | });
10 | }
11 |
12 | public override run(error: unknown) {
13 | this.container.logger.error(`[ScheduledTaskPlugin] Encountered an error when trying to connect to the Redis instance`, error);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/src/listeners/PluginScheduledTaskStrategyWorkerError.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import { ScheduledTaskEvents } from '../lib/types/ScheduledTaskEvents';
3 |
4 | export class PluginListener extends Listener {
5 | public constructor(context: Listener.LoaderContext) {
6 | super(context, {
7 | name: ScheduledTaskEvents.ScheduledTaskStrategyWorkerError,
8 | event: ScheduledTaskEvents.ScheduledTaskStrategyWorkerError
9 | });
10 | }
11 |
12 | public override run(error: unknown) {
13 | this.container.logger.error(`[ScheduledTaskPlugin] The BullMQ worker encountered an error`, error);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/src/listeners/_load.ts:
--------------------------------------------------------------------------------
1 | import { container } from '@sapphire/pieces';
2 | import { PluginListener as PluginScheduledTaskError } from './PluginScheduledTaskError';
3 | import { PluginListener as PluginScheduledTaskNotFound } from './PluginScheduledTaskNotFound';
4 | import { PluginListener as PluginScheduledTaskStrategyConnectError } from './PluginScheduledTaskStrategyConnectError';
5 | import { PluginListener as PluginScheduledTaskStrategyClientError } from './PluginScheduledTaskStrategyClientError';
6 | import { PluginListener as PluginScheduledTaskStrategyWorkerError } from './PluginScheduledTaskStrategyWorkerError';
7 |
8 | export function loadListeners() {
9 | const store = 'listeners' as const;
10 | void container.stores.loadPiece({ name: 'PluginScheduledTaskError', piece: PluginScheduledTaskError, store });
11 | void container.stores.loadPiece({ name: 'PluginScheduledTaskNotFound', piece: PluginScheduledTaskNotFound, store });
12 | void container.stores.loadPiece({ name: 'PluginScheduledTaskStrategyConnectError', piece: PluginScheduledTaskStrategyConnectError, store });
13 | void container.stores.loadPiece({ name: 'PluginScheduledTaskStrategyClientError', piece: PluginScheduledTaskStrategyClientError, store });
14 | void container.stores.loadPiece({ name: 'PluginScheduledTaskStrategyWorkerError', piece: PluginScheduledTaskStrategyWorkerError, store });
15 | }
16 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/src/register.ts:
--------------------------------------------------------------------------------
1 | import './index';
2 |
3 | import { container, Plugin, postInitialization, postLogin, preGenericsInitialization, SapphireClient } from '@sapphire/framework';
4 | import type { ClientOptions } from 'discord.js';
5 | import { loadListeners, ScheduledTaskHandler, ScheduledTaskStore } from './index';
6 |
7 | /**
8 | * A plugin for scheduling tasks in a SapphireClient.
9 | * @since 1.0.0
10 | */
11 | export class ScheduledTasksPlugin extends Plugin {
12 | public service: string | undefined;
13 | /**
14 | * @since 1.0.0
15 | */
16 | public static [preGenericsInitialization](this: SapphireClient, options: ClientOptions): void {
17 | container.tasks = new ScheduledTaskHandler(options.tasks);
18 | }
19 |
20 | /**
21 | * @since 1.0.0
22 | */
23 | public static [postInitialization](this: SapphireClient, options: ClientOptions): void {
24 | this.stores.register(new ScheduledTaskStore());
25 |
26 | if (options.loadScheduledTaskErrorListeners !== false) {
27 | loadListeners();
28 | }
29 | }
30 |
31 | /**
32 | * @since 1.0.0
33 | */
34 | public static [postLogin](this: SapphireClient): void {
35 | void container.tasks.createRepeated();
36 | }
37 | }
38 |
39 | SapphireClient.plugins.registerPreGenericsInitializationHook(
40 | ScheduledTasksPlugin[preGenericsInitialization],
41 | 'Scheduled-Task-PreGenericsInitialization'
42 | );
43 |
44 | SapphireClient.plugins.registerPostInitializationHook(ScheduledTasksPlugin[postInitialization], 'Scheduled-Task-PostInitialization');
45 |
46 | SapphireClient.plugins.registerPostLoginHook(ScheduledTasksPlugin[postLogin], 'Scheduled-Task-PostLogin');
47 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "rootDir": "./"
5 | },
6 | "include": ["."]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src", "tests"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { createTsupConfig } from '../../scripts/tsup.config';
2 |
3 | export default createTsupConfig();
4 |
--------------------------------------------------------------------------------
/packages/scheduled-tasks/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://typedoc.org/schema.json",
3 | "entryPoints": ["src/index.ts"],
4 | "json": "docs/api.json",
5 | "tsconfig": "src/tsconfig.json",
6 | "excludeExternals": true,
7 | "excludePrivate": false
8 | }
9 |
--------------------------------------------------------------------------------
/packages/subcommands/.cliff-jumperrc.yml:
--------------------------------------------------------------------------------
1 | name: plugin-subcommands
2 | org: sapphire
3 | install: true
4 | packagePath: packages/subcommands
5 | identifierBase: false
6 | pushTag: true
7 | githubRelease: true
8 | githubReleaseLatest: true
9 | gitRepo: sapphiredev/plugins
10 | gitHostVariant: github
11 |
--------------------------------------------------------------------------------
/packages/subcommands/.rollup-type-bundlerrc.yml:
--------------------------------------------------------------------------------
1 | onlyBundle: true
2 | excludeFromClean:
3 | - dist/esm/register.d.mts
4 | - dist/cjs/register.d.ts
5 |
--------------------------------------------------------------------------------
/packages/subcommands/.typedoc-json-parserrc.yml:
--------------------------------------------------------------------------------
1 | json: 'docs/api.json'
2 |
--------------------------------------------------------------------------------
/packages/subcommands/cliff.toml:
--------------------------------------------------------------------------------
1 | [changelog]
2 | header = """
3 | # Changelog
4 |
5 | All notable changes to this project will be documented in this file.\n
6 | """
7 | body = """
8 | {%- macro remote_url() -%}
9 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
10 | {%- endmacro -%}
11 | {% if version %}\
12 | # [{{ version | trim_start_matches(pat="v") }}]\
13 | {% if previous %}\
14 | {% if previous.version %}\
15 | ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
16 | {% else %}\
17 | ({{ self::remote_url() }}/tree/{{ version }})\
18 | {% endif %}\
19 | {% endif %} \
20 | - ({{ timestamp | date(format="%Y-%m-%d") }})
21 | {% else %}\
22 | # [unreleased]
23 | {% endif %}\
24 | {% for group, commits in commits | group_by(attribute="group") %}
25 | ## {{ group | upper_first }}
26 | {% for commit in commits %}
27 | - {% if commit.scope %}\
28 | **{{commit.scope}}:** \
29 | {% endif %}\
30 | {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
31 | {% if commit.github.pr_number %} (\
32 | [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) by @{{ commit.github.username }}) \
33 | {%- endif %}\
34 | {% if commit.breaking %}\
35 | {% for breakingChange in commit.footers %}\
36 | \n{% raw %} {% endraw %}- 💥 **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
37 | {% endfor %}\
38 | {% endif %}\
39 | {% endfor %}
40 | {% endfor %}\n
41 | """
42 | trim = true
43 | footer = ""
44 |
45 | [git]
46 | conventional_commits = true
47 | filter_unconventional = true
48 | commit_parsers = [
49 | { message = "^feat", group = "🚀 Features" },
50 | { message = "^fix", group = "🐛 Bug Fixes" },
51 | { message = "^docs", group = "📝 Documentation" },
52 | { message = "^perf", group = "🏃 Performance" },
53 | { message = "^refactor", group = "🏠 Refactor" },
54 | { message = "^typings", group = "⌨️ Typings" },
55 | { message = "^types", group = "⌨️ Typings" },
56 | { message = ".*deprecated", body = ".*deprecated", group = "🚨 Deprecation" },
57 | { message = "^revert", skip = true },
58 | { message = "^style", group = "🪞 Styling" },
59 | { message = "^test", group = "🧪 Testing" },
60 | { message = "^chore", skip = true },
61 | { message = "^ci", skip = true },
62 | { message = "^build", skip = true },
63 | { body = ".*security", group = "🛡️ Security" },
64 | ]
65 | commit_preprocessors = [
66 | # remove issue numbers from commits
67 | { pattern = '\s\((\w+\s)?#([0-9]+)\)', replace = "" },
68 | ]
69 | filter_commits = true
70 | tag_pattern = "@sapphire/plugin-subcommands@[0-9]*"
71 | ignore_tags = ""
72 | topo_order = false
73 | sort_commits = "newest"
74 |
75 | [remote.github]
76 | owner = "sapphiredev"
77 | repo = "plugins"
78 |
--------------------------------------------------------------------------------
/packages/subcommands/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sapphire/plugin-subcommands",
3 | "version": "7.0.1",
4 | "description": "Plugin for @sapphire/framework that adds support for subcommands.",
5 | "author": "@sapphire",
6 | "license": "MIT",
7 | "main": "dist/cjs/index.cjs",
8 | "module": "dist/esm/index.mjs",
9 | "types": "dist/cjs/index.d.cts",
10 | "exports": {
11 | ".": {
12 | "import": {
13 | "types": "./dist/esm/index.d.mts",
14 | "default": "./dist/esm/index.mjs"
15 | },
16 | "require": {
17 | "types": "./dist/cjs/index.d.cts",
18 | "default": "./dist/cjs/index.cjs"
19 | }
20 | },
21 | "./register": {
22 | "import": {
23 | "types": "./dist/esm/register.d.mts",
24 | "default": "./dist/esm/register.mjs"
25 | },
26 | "require": {
27 | "types": "./dist/cjs/register.d.cts",
28 | "default": "./dist/cjs/register.cjs"
29 | }
30 | }
31 | },
32 | "sideEffects": [
33 | "./dist/cjs/register.cjs",
34 | "./dist/esm/register.mjs"
35 | ],
36 | "homepage": "https://github.com/sapphiredev/plugins/tree/main/packages/subcommands",
37 | "scripts": {
38 | "lint": "eslint src --ext ts --fix",
39 | "build": "tsup && yarn build:types && yarn build:rename-cjs-register",
40 | "build:types": "concurrently \"yarn:build:types:*\"",
41 | "build:types:cjs": "rollup-type-bundler -d dist/cjs --output-typings-file-extension .cts",
42 | "build:types:esm": "rollup-type-bundler -d dist/esm -t .mts",
43 | "build:types:cleanup": "tsx ../../scripts/clean-register-imports.mts",
44 | "build:rename-cjs-register": "tsx ../../scripts/rename-cjs-register.mts",
45 | "typecheck": "tsc -b src",
46 | "docs": "typedoc-json-parser",
47 | "prepack": "yarn build",
48 | "bump": "cliff-jumper",
49 | "check-update": "cliff-jumper --dry-run"
50 | },
51 | "dependencies": {
52 | "@sapphire/utilities": "^3.18.2"
53 | },
54 | "repository": {
55 | "type": "git",
56 | "url": "git+https://github.com/sapphiredev/plugins.git",
57 | "directory": "packages/subcommands"
58 | },
59 | "files": [
60 | "dist/"
61 | ],
62 | "engines": {
63 | "node": ">=v18",
64 | "npm": ">=7"
65 | },
66 | "keywords": [
67 | "sapphiredev",
68 | "plugin",
69 | "bot",
70 | "typescript",
71 | "ts",
72 | "yarn",
73 | "discord",
74 | "sapphire",
75 | "subcommands"
76 | ],
77 | "bugs": {
78 | "url": "https://github.com/sapphiredev/plugins/issues"
79 | },
80 | "publishConfig": {
81 | "access": "public"
82 | },
83 | "devDependencies": {
84 | "@favware/cliff-jumper": "^6.0.0",
85 | "@favware/rollup-type-bundler": "^4.0.0",
86 | "concurrently": "^9.1.2",
87 | "tsup": "^8.5.0",
88 | "tsx": "^4.19.4",
89 | "typedoc": "^0.26.11",
90 | "typedoc-json-parser": "^10.2.0",
91 | "typescript": "~5.4.5"
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/packages/subcommands/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { CooldownOptions } from '@sapphire/framework';
2 | import {
3 | PluginPrecondition as PluginSubcommandCooldown,
4 | type PluginSubcommandCooldownPreconditionContext
5 | } from './preconditions/PluginSubcommandCooldown';
6 |
7 | export * from './lib/Subcommand';
8 | export * as SubcommandPreconditionResolvers from './lib/precondition-resolvers/subcommandCooldown';
9 | export * from './lib/types/Enums';
10 | export * from './lib/types/Events';
11 | export * from './lib/types/SubcommandMappings';
12 | export {
13 | PluginPrecondition as PluginSubcommandCooldownPrecondition,
14 | type PluginSubcommandCooldownPreconditionContext
15 | } from './preconditions/PluginSubcommandCooldown';
16 |
17 | export { loadListeners } from './listeners/_load';
18 | export { loadPreconditions } from './preconditions/_load';
19 |
20 | declare module 'discord.js' {
21 | interface ClientOptions {
22 | /**
23 | * If Plugin-subcommand to load pre-included subcommand error event listeners that log any encountered errors to the {@link SapphireClient.logger} instance
24 | * @since 3.1.2
25 | * @default true
26 | */
27 | loadSubcommandErrorListeners?: boolean;
28 | /**
29 | * Sets the default cooldown time for all subcommands.
30 | * @remark This is separate from {@link ClientOptions.defaultCooldown} as it is only used for subcommands
31 | * @remark Note that for the `filteredCommands` option you have to provide it as
32 | * - For a subcommand without a group: `commandName.subcommandName` (e.g. `config.show`).
33 | * - For a subcommand with a group: `commandName.groupName.subcommandName` (e.g. `config.set.prefix`).
34 | * @since 5.1.0
35 | * @default "No cooldown options"
36 | */
37 | subcommandDefaultCooldown?: CooldownOptions;
38 | }
39 | }
40 |
41 | declare module '@sapphire/framework' {
42 | interface Preconditions {
43 | PluginSubcommandCooldown: SubcommandPreconditions.PluginSubcommandCooldownContext;
44 | }
45 | }
46 |
47 | /**
48 | * The preconditions specific to subcommands
49 | * @since 5.1.0
50 | * @deprecated - This will be replaced with a regular top level export of {@link PluginSubcommandCooldown}
51 | * in the next major version as opposed to a namespaced export.
52 | */
53 | export const SubcommandPreconditions = {
54 | PluginSubcommandCooldown
55 | };
56 |
57 | /**
58 | * The preconditions specific to subcommands
59 | * @since 5.1.0
60 | * @deprecated - This will be replaced with a regular top level export of {@link PluginSubcommandCooldownPreconditionContext}
61 | * in the next major version as opposed to a namespaced export.
62 | */
63 | export namespace SubcommandPreconditions {
64 | /** The context for the subcommand cooldown precondition */
65 | export type PluginSubcommandCooldownContext = PluginSubcommandCooldownPreconditionContext;
66 | }
67 |
68 | /**
69 | * The [@sapphire/plugin-subcommands](https://github.com/sapphiredev/plugins/blob/main/packages/subcommands) version that you are currently using.
70 | * An example use of this is showing it of in a bot information command.
71 | *
72 | * Note to Sapphire developers: This needs to explicitly be `string` so it is not typed as the string that gets replaced by esbuild
73 | */
74 | // eslint-disable-next-line @typescript-eslint/no-inferrable-types
75 | export const version: string = '[VI]{{inject}}[/VI]';
76 |
--------------------------------------------------------------------------------
/packages/subcommands/src/lib/precondition-resolvers/subcommandCooldown.ts:
--------------------------------------------------------------------------------
1 | import { Args, BucketScope, PreconditionContainerArray } from '@sapphire/framework';
2 | import { container } from '@sapphire/pieces';
3 | import { Subcommand } from '../Subcommand';
4 | import { SubcommandCommandPreConditions } from '../types/Enums';
5 |
6 | /** The options for adding this subcommand cooldown precondition */
7 | export interface ParseSubcommandConstructorPreConditionsCooldownParameters<
8 | PreParseReturn extends Args = Args,
9 | Options extends Subcommand.Options = Subcommand.Options
10 | > {
11 | /** The command to parse cooldowns for. */
12 | subcommand: Subcommand;
13 | /** The cooldown limit to use. */
14 | cooldownLimit: number | undefined;
15 | /** The cooldown delay to use. */
16 | cooldownDelay: number | undefined;
17 | /** The cooldown scope to use. */
18 | cooldownScope: BucketScope | undefined;
19 | /** The cooldown filtered users to use. */
20 | cooldownFilteredUsers: string[] | undefined;
21 | /** The name this precondition is for. */
22 | subcommandMethodName: string;
23 | /** The group this precondition is for, if any. */
24 | subcommandGroupName?: string;
25 | /** The precondition container array to append the precondition to. */
26 | preconditionContainerArray: PreconditionContainerArray;
27 | }
28 |
29 | /**
30 | * Appends the `PluginSubcommandCooldown` precondition when {@link Subcommand.Options.cooldownLimit} and
31 | * {@link Subcommand.Options.cooldownDelay} are both non-zero.
32 | *
33 | * @param options The {@link ParseSubcommandConstructorPreConditionsCooldownParameters} for adding this subcommand cooldown precondition
34 | */
35 | export function parseSubcommandConstructorPreConditionsCooldown<
36 | PreParseReturn extends Args = Args,
37 | Options extends Subcommand.Options = Subcommand.Options
38 | >({
39 | subcommand: command,
40 | cooldownLimit,
41 | cooldownDelay,
42 | cooldownScope,
43 | cooldownFilteredUsers,
44 | subcommandMethodName,
45 | subcommandGroupName,
46 | preconditionContainerArray
47 | }: ParseSubcommandConstructorPreConditionsCooldownParameters) {
48 | const { subcommandDefaultCooldown } = container.client.options;
49 |
50 | // We will check for whether the subcommand is filtered from the defaults, but we will allow overridden values to
51 | // be set. If an overridden value is passed, it will have priority. Otherwise, it will default to 0 if filtered
52 | // (causing the precondition to not be registered) or the default value with a fallback to a single-use cooldown.
53 | const filtered =
54 | subcommandDefaultCooldown?.filteredCommands?.includes(
55 | subcommandGroupName ? `${command.name}.${subcommandGroupName}.${subcommandMethodName}` : `${command.name}.${subcommandMethodName}`
56 | ) ?? false;
57 | const limit = cooldownLimit ?? (filtered ? 0 : (subcommandDefaultCooldown?.limit ?? 1));
58 | const delay = cooldownDelay ?? (filtered ? 0 : (subcommandDefaultCooldown?.delay ?? 0));
59 |
60 | if (limit && delay) {
61 | const scope = cooldownScope ?? subcommandDefaultCooldown?.scope ?? BucketScope.User;
62 | const filteredUsers = cooldownFilteredUsers ?? subcommandDefaultCooldown?.filteredUsers;
63 |
64 | preconditionContainerArray.append({
65 | name: SubcommandCommandPreConditions.PluginSubcommandCooldown,
66 | context: { scope, limit, delay, filteredUsers, subcommandGroupName, subcommandMethodName }
67 | });
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/packages/subcommands/src/lib/types/Enums.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The available command pre-conditions.
3 | * @since 2.0.0
4 | */
5 | export enum SubcommandCommandPreConditions {
6 | PluginSubcommandCooldown = 'PluginSubcommandCooldown'
7 | }
8 |
9 | /**
10 | * The available subcommand pre-conditions.
11 | * @since 5.1.0
12 | */
13 | export enum SubcommandIdentifiers {
14 | /** The identifier for the subcommand cooldown precondition */
15 | SubcommandPreconditionCooldown = 'subcommandPreconditionCooldown'
16 | }
17 |
--------------------------------------------------------------------------------
/packages/subcommands/src/listeners/PluginChatInputSubcommandError.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import { SubcommandPluginEvents, type ChatInputSubcommandErrorPayload } from '../lib/types/Events';
3 |
4 | export class PluginListener extends Listener {
5 | public constructor(context: Listener.LoaderContext) {
6 | super(context, { event: SubcommandPluginEvents.ChatInputSubcommandError });
7 | }
8 |
9 | public override run(error: unknown, context: ChatInputSubcommandErrorPayload) {
10 | const { name, location } = context.command;
11 | this.container.logger.error(`Encountered error on chat input subcommand "${name}" at path "${location.full}"`, error);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/subcommands/src/listeners/PluginChatInputSubcommandNoMatch.ts:
--------------------------------------------------------------------------------
1 | import { Listener, type ChatInputCommand } from '@sapphire/framework';
2 | import { SubcommandPluginEvents, type ChatInputSubcommandNoMatchContext } from '../lib/types/Events';
3 |
4 | export class PluginListener extends Listener {
5 | public constructor(context: Listener.LoaderContext) {
6 | super(context, { event: SubcommandPluginEvents.ChatInputSubcommandNoMatch });
7 | }
8 |
9 | public override run(_interaction: ChatInputCommand.Interaction, context: ChatInputSubcommandNoMatchContext) {
10 | this.container.logger.error(context.message);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/subcommands/src/listeners/PluginMessageSubcommandError.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import { SubcommandPluginEvents, type MessageSubcommandErrorPayload } from '../lib/types/Events';
3 |
4 | export class PluginListener extends Listener {
5 | public constructor(context: Listener.LoaderContext) {
6 | super(context, { event: SubcommandPluginEvents.MessageSubcommandError });
7 | }
8 |
9 | public override run(error: unknown, context: MessageSubcommandErrorPayload) {
10 | const { name, location } = context.command;
11 | this.container.logger.error(`Encountered error on message subcommand "${name}" at path "${location.full}"`, error);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/subcommands/src/listeners/PluginMessageSubcommandNoMatch.ts:
--------------------------------------------------------------------------------
1 | import { Args, Listener } from '@sapphire/framework';
2 | import type { Message } from 'discord.js';
3 | import { SubcommandPluginEvents, type MessageSubcommandNoMatchContext } from '../lib/types/Events';
4 |
5 | export class PluginListener extends Listener {
6 | public constructor(context: Listener.LoaderContext) {
7 | super(context, { event: SubcommandPluginEvents.MessageSubcommandNoMatch });
8 | }
9 |
10 | public override run(_message: Message, _args: Args, context: MessageSubcommandNoMatchContext) {
11 | this.container.logger.error(context.message);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/subcommands/src/listeners/PluginSubcommandMappingIsMissingChatInputCommandHandler.ts:
--------------------------------------------------------------------------------
1 | import { Listener, type ChatInputCommand } from '@sapphire/framework';
2 | import { SubcommandPluginEvents, type ChatInputSubcommandErrorPayload } from '../lib/types/Events';
3 | import type { SubcommandMappingMethod } from '../lib/types/SubcommandMappings';
4 |
5 | export class PluginListener extends Listener {
6 | public constructor(context: Listener.LoaderContext) {
7 | super(context, { event: SubcommandPluginEvents.SubcommandMappingIsMissingChatInputCommandHandler });
8 | }
9 |
10 | public override run(_: ChatInputCommand.Interaction, subcommand: SubcommandMappingMethod, context: ChatInputSubcommandErrorPayload) {
11 | const { name, location } = context.command;
12 | this.container.logger.error(`Encountered a missing mapping on chat input subcommand "${name}" at "${location.full}"`, subcommand);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/subcommands/src/listeners/PluginSubcommandMappingIsMissingMessageCommandHandler.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '@sapphire/framework';
2 | import type { Message } from 'discord.js';
3 | import { SubcommandPluginEvents, type MessageSubcommandErrorPayload } from '../lib/types/Events';
4 | import type { SubcommandMappingMethod } from '../lib/types/SubcommandMappings';
5 |
6 | export class PluginListener extends Listener {
7 | public constructor(context: Listener.LoaderContext) {
8 | super(context, { event: SubcommandPluginEvents.SubcommandMappingIsMissingMessageCommandHandler });
9 | }
10 |
11 | public override run(_: Message, subcommand: SubcommandMappingMethod, context: MessageSubcommandErrorPayload) {
12 | const { name, location } = context.command;
13 | this.container.logger.error(`Encountered a missing mapping on message subcommand "${name}" at "${location.full}"`, subcommand);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/subcommands/src/listeners/_load.ts:
--------------------------------------------------------------------------------
1 | import { container } from '@sapphire/pieces';
2 | import { PluginListener as PluginChatInputSubcommandError } from './PluginChatInputSubcommandError';
3 | import { PluginListener as PluginChatInputSubcommandNoMatch } from './PluginChatInputSubcommandNoMatch';
4 | import { PluginListener as PluginMessageSubcommandError } from './PluginMessageSubcommandError';
5 | import { PluginListener as PluginMessageSubcommandNoMatch } from './PluginMessageSubcommandNoMatch';
6 | import { PluginListener as PluginSubcommandMappingIsMissingChatInputCommandHandler } from './PluginSubcommandMappingIsMissingChatInputCommandHandler';
7 | import { PluginListener as PluginSubcommandMappingIsMissingMessageCommandHandler } from './PluginSubcommandMappingIsMissingMessageCommandHandler';
8 |
9 | export function loadListeners() {
10 | const store = 'listeners' as const;
11 | void container.stores.loadPiece({ name: 'PluginChatInputSubcommandError', piece: PluginChatInputSubcommandError, store });
12 | void container.stores.loadPiece({ name: 'PluginMessageSubcommandError', piece: PluginMessageSubcommandError, store });
13 | void container.stores.loadPiece({
14 | name: 'PluginSubcommandMappingIsMissingChatInputCommandHandler',
15 | piece: PluginSubcommandMappingIsMissingChatInputCommandHandler,
16 | store
17 | });
18 | void container.stores.loadPiece({
19 | name: 'PluginSubcommandMappingIsMissingMessageCommandHandler',
20 | piece: PluginSubcommandMappingIsMissingMessageCommandHandler,
21 | store
22 | });
23 | void container.stores.loadPiece({
24 | name: 'PluginMessageSubcommandNoMatch',
25 | piece: PluginMessageSubcommandNoMatch,
26 | store
27 | });
28 | void container.stores.loadPiece({
29 | name: 'PluginChatInputSubcommandNoMatch',
30 | piece: PluginChatInputSubcommandNoMatch,
31 | store
32 | });
33 | }
34 |
--------------------------------------------------------------------------------
/packages/subcommands/src/preconditions/_load.ts:
--------------------------------------------------------------------------------
1 | import { container } from '@sapphire/pieces';
2 | import { PluginPrecondition as PluginSubcommandCooldown } from './PluginSubcommandCooldown';
3 |
4 | export function loadPreconditions() {
5 | const store = 'preconditions' as const;
6 | void container.stores.loadPiece({ name: 'PluginSubcommandCooldown', piece: PluginSubcommandCooldown, store });
7 | }
8 |
--------------------------------------------------------------------------------
/packages/subcommands/src/register.ts:
--------------------------------------------------------------------------------
1 | import './index';
2 |
3 | import { Plugin, postInitialization, SapphireClient } from '@sapphire/framework';
4 | import type { ClientOptions } from 'discord.js';
5 | import { loadListeners, loadPreconditions } from './index';
6 |
7 | /**
8 | * @since 3.1.2
9 | */
10 | export class SubcommandsPlugin extends Plugin {
11 | /**
12 | * @since 3.1.2
13 | */
14 | public static [postInitialization](this: SapphireClient, options: ClientOptions): void {
15 | loadPreconditions();
16 |
17 | if (options.loadSubcommandErrorListeners !== false) {
18 | loadListeners();
19 | }
20 | }
21 | }
22 |
23 | SapphireClient.plugins.registerPostInitializationHook(SubcommandsPlugin[postInitialization], 'Subcommand-PostInitialization');
24 |
--------------------------------------------------------------------------------
/packages/subcommands/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../../tsconfig.base.json"],
3 | "compilerOptions": {
4 | "rootDir": "./"
5 | },
6 | "include": ["."]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/subcommands/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true
6 | },
7 | "include": ["src", "tests"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/subcommands/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { createTsupConfig } from '../../scripts/tsup.config';
2 |
3 | export default createTsupConfig();
4 |
--------------------------------------------------------------------------------
/packages/subcommands/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://typedoc.org/schema.json",
3 | "entryPoints": ["src/index.ts"],
4 | "json": "docs/api.json",
5 | "tsconfig": "src/tsconfig.json",
6 | "excludePrivate": false
7 | }
8 |
--------------------------------------------------------------------------------
/packages/utilities-store/.cliff-jumperrc.yml:
--------------------------------------------------------------------------------
1 | name: plugin-utilities-store
2 | org: sapphire
3 | install: true
4 | packagePath: packages/utilities-store
5 | identifierBase: false
6 | pushTag: true
7 | githubRelease: true
8 | githubReleaseLatest: true
9 | gitRepo: sapphiredev/plugins
10 | gitHostVariant: github
11 |
--------------------------------------------------------------------------------
/packages/utilities-store/.rollup-type-bundlerrc.yml:
--------------------------------------------------------------------------------
1 | onlyBundle: true
2 | excludeFromClean:
3 | - dist/esm/register.d.mts
4 | - dist/cjs/register.d.ts
5 |
--------------------------------------------------------------------------------
/packages/utilities-store/.typedoc-json-parserrc.yml:
--------------------------------------------------------------------------------
1 | json: 'docs/api.json'
2 |
--------------------------------------------------------------------------------
/packages/utilities-store/cliff.toml:
--------------------------------------------------------------------------------
1 | [changelog]
2 | header = """
3 | # Changelog
4 |
5 | All notable changes to this project will be documented in this file.\n
6 | """
7 | body = """
8 | {%- macro remote_url() -%}
9 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
10 | {%- endmacro -%}
11 | {% if version %}\
12 | # [{{ version | trim_start_matches(pat="v") }}]\
13 | {% if previous %}\
14 | {% if previous.version %}\
15 | ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
16 | {% else %}\
17 | ({{ self::remote_url() }}/tree/{{ version }})\
18 | {% endif %}\
19 | {% endif %} \
20 | - ({{ timestamp | date(format="%Y-%m-%d") }})
21 | {% else %}\
22 | # [unreleased]
23 | {% endif %}\
24 | {% for group, commits in commits | group_by(attribute="group") %}
25 | ## {{ group | upper_first }}
26 | {% for commit in commits %}
27 | - {% if commit.scope %}\
28 | **{{commit.scope}}:** \
29 | {% endif %}\
30 | {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
31 | {% if commit.github.pr_number %} (\
32 | [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) by @{{ commit.github.username }}) \
33 | {%- endif %}\
34 | {% if commit.breaking %}\
35 | {% for breakingChange in commit.footers %}\
36 | \n{% raw %} {% endraw %}- 💥 **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
37 | {% endfor %}\
38 | {% endif %}\
39 | {% endfor %}
40 | {% endfor %}\n
41 | """
42 | trim = true
43 | footer = ""
44 |
45 | [git]
46 | conventional_commits = true
47 | filter_unconventional = true
48 | commit_parsers = [
49 | { message = "^feat", group = "🚀 Features" },
50 | { message = "^fix", group = "🐛 Bug Fixes" },
51 | { message = "^docs", group = "📝 Documentation" },
52 | { message = "^perf", group = "🏃 Performance" },
53 | { message = "^refactor", group = "🏠 Refactor" },
54 | { message = "^typings", group = "⌨️ Typings" },
55 | { message = "^types", group = "⌨️ Typings" },
56 | { message = ".*deprecated", body = ".*deprecated", group = "🚨 Deprecation" },
57 | { message = "^revert", skip = true },
58 | { message = "^style", group = "🪞 Styling" },
59 | { message = "^test", group = "🧪 Testing" },
60 | { message = "^chore", skip = true },
61 | { message = "^ci", skip = true },
62 | { message = "^build", skip = true },
63 | { body = ".*security", group = "🛡️ Security" },
64 | ]
65 | commit_preprocessors = [
66 | # remove issue numbers from commits
67 | { pattern = '\s\((\w+\s)?#([0-9]+)\)', replace = "" },
68 | ]
69 | filter_commits = true
70 | tag_pattern = "@sapphire/plugin-utilities-store@[0-9]*"
71 | ignore_tags = ""
72 | topo_order = false
73 | sort_commits = "newest"
74 |
75 | [remote.github]
76 | owner = "sapphiredev"
77 | repo = "plugins"
78 |
--------------------------------------------------------------------------------
/packages/utilities-store/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sapphire/plugin-utilities-store",
3 | "version": "2.0.3",
4 | "description": "Plugin for @sapphire/framework to have a Sapphire store which you can fill with utility functions available through the container",
5 | "author": "@sapphire",
6 | "license": "MIT",
7 | "main": "dist/cjs/index.cjs",
8 | "module": "dist/esm/index.mjs",
9 | "types": "dist/cjs/index.d.cts",
10 | "exports": {
11 | ".": {
12 | "import": {
13 | "types": "./dist/esm/index.d.mts",
14 | "default": "./dist/esm/index.mjs"
15 | },
16 | "require": {
17 | "types": "./dist/cjs/index.d.cts",
18 | "default": "./dist/cjs/index.cjs"
19 | }
20 | },
21 | "./register": {
22 | "import": {
23 | "types": "./dist/esm/register.d.mts",
24 | "default": "./dist/esm/register.mjs"
25 | },
26 | "require": {
27 | "types": "./dist/cjs/register.d.cts",
28 | "default": "./dist/cjs/register.cjs"
29 | }
30 | }
31 | },
32 | "sideEffects": [
33 | "./dist/cjs/register.cjs",
34 | "./dist/esm/register.mjs"
35 | ],
36 | "homepage": "https://github.com/sapphiredev/plugins/tree/main/packages/api",
37 | "scripts": {
38 | "lint": "eslint src --ext ts --fix",
39 | "build": "tsup && yarn build:types && yarn build:rename-cjs-register",
40 | "build:types": "concurrently \"yarn:build:types:*\"",
41 | "build:types:cjs": "rollup-type-bundler -d dist/cjs --output-typings-file-extension .cts",
42 | "build:types:esm": "rollup-type-bundler -d dist/esm -t .mts",
43 | "build:types:cleanup": "tsx ../../scripts/clean-register-imports.mts",
44 | "build:rename-cjs-register": "tsx ../../scripts/rename-cjs-register.mts",
45 | "typecheck": "tsc -b src",
46 | "docs": "typedoc-json-parser",
47 | "prepack": "yarn build",
48 | "bump": "cliff-jumper",
49 | "check-update": "cliff-jumper --dry-run"
50 | },
51 | "repository": {
52 | "type": "git",
53 | "url": "git+https://github.com/sapphiredev/plugins.git",
54 | "directory": "packages/utilities-store"
55 | },
56 | "files": [
57 | "dist/"
58 | ],
59 | "engines": {
60 | "node": ">=v18",
61 | "npm": ">=7"
62 | },
63 | "keywords": [
64 | "sapphiredev",
65 | "plugin",
66 | "bot",
67 | "typescript",
68 | "ts",
69 | "yarn",
70 | "discord",
71 | "sapphire"
72 | ],
73 | "bugs": {
74 | "url": "https://github.com/sapphiredev/plugins/issues"
75 | },
76 | "publishConfig": {
77 | "access": "public"
78 | },
79 | "devDependencies": {
80 | "@favware/cliff-jumper": "^6.0.0",
81 | "@favware/rollup-type-bundler": "^4.0.0",
82 | "concurrently": "^9.1.2",
83 | "tsup": "^8.5.0",
84 | "tsx": "^4.19.4",
85 | "typedoc": "^0.26.11",
86 | "typedoc-json-parser": "^10.2.0",
87 | "typescript": "~5.4.5"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/packages/utilities-store/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { Utilities } from './lib/Utilities';
2 | import type { UtilitiesStore } from './lib/UtilitiesStore';
3 |
4 | export * from './lib/Utilities';
5 | export * from './lib/UtilitiesStore';
6 | export * from './lib/Utility';
7 |
8 | declare module 'discord.js' {
9 | export interface Client {
10 | utilities: Utilities;
11 | }
12 | }
13 |
14 | declare module '@sapphire/pieces' {
15 | interface StoreRegistryEntries {
16 | utilities: UtilitiesStore;
17 | }
18 |
19 | interface Container {
20 | utilities: Utilities;
21 | }
22 | }
23 |
24 | /**
25 | * The [@sapphire/plugin-utilities-store](https://github.com/sapphiredev/plugins/blob/main/packages/utilities-store) version that you are currently using.
26 | * An example use of this is showing it of in a bot information command.
27 | *
28 | * Note to Sapphire developers: This needs to explicitly be `string` so it is not typed as the string that gets replaced by esbuild
29 | */
30 | // eslint-disable-next-line @typescript-eslint/no-inferrable-types
31 | export const version: string = '[VI]{{inject}}[/VI]';
32 |
--------------------------------------------------------------------------------
/packages/utilities-store/src/lib/Augmentations.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * ================================================
3 | * | THIS IS FOR TYPEDOC. DO NOT REMOVE THIS FILE |
4 | * ================================================
5 | */
6 |
7 | import type { Utilities } from '../index';
8 | import type { UtilitiesStore } from './UtilitiesStore';
9 |
10 | declare module '@sapphire/framework' {
11 | interface StoreRegistryEntries {
12 | utilities: UtilitiesStore;
13 | }
14 | }
15 |
16 | declare module '@sapphire/pieces' {
17 | interface Container {
18 | utilities: Utilities;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/utilities-store/src/lib/Utilities.ts:
--------------------------------------------------------------------------------
1 | import { container } from '@sapphire/pieces';
2 | import { UtilitiesStore } from './UtilitiesStore';
3 | import type { Utility } from './Utility';
4 |
5 | /**
6 | * @since 1.0.0
7 | */
8 | export class Utilities {
9 | /**
10 | * The utilities this store holds.
11 | * @since 1.0.0
12 | */
13 | public readonly store: UtilitiesStore;
14 |
15 | /**
16 | * @since 1.0.0
17 | */
18 | public constructor() {
19 | container.utilities = this;
20 | this.store = new UtilitiesStore();
21 | }
22 |
23 | /**
24 | * Registers a piece on this class.
25 | * @param name The name of the piece to register on this class
26 | * @param piece The piece to register on this class
27 | */
28 | public exposePiece(name: string, piece: Utility) {
29 | // @ts-ignore Bypass TypeScript check for dynamic property assignment
30 | this[name] = piece;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/utilities-store/src/lib/UtilitiesStore.ts:
--------------------------------------------------------------------------------
1 | import { Store } from '@sapphire/pieces';
2 | import { Utility } from './Utility';
3 |
4 | /**
5 | * @since 1.0.0
6 | */
7 | export class UtilitiesStore extends Store {
8 | public constructor() {
9 | super(Utility, { name: 'utilities' });
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/utilities-store/src/lib/Utility.ts:
--------------------------------------------------------------------------------
1 | import { Piece } from '@sapphire/pieces';
2 |
3 | /**
4 | * @since 1.0.0
5 | */
6 | export abstract class Utility extends Piece {}
7 |
8 | export namespace Utility {
9 | /** @deprecated Use {@linkcode LoaderContext} instead. */
10 | export type Context = LoaderContext;
11 | export type LoaderContext = Piece.LoaderContext<'utilities'>;
12 | export type Options = Piece.Options;
13 | export type JSON = Piece.JSON;
14 | export type LocationJSON = Piece.LocationJSON;
15 | }
16 |
--------------------------------------------------------------------------------
/packages/utilities-store/src/register.ts:
--------------------------------------------------------------------------------
1 | import './index';
2 |
3 | import { Plugin, postLogin, preInitialization, SapphireClient } from '@sapphire/framework';
4 | import { Utilities } from './index';
5 |
6 | /**
7 | * @since 1.0.0
8 | */
9 | export class UtilitiesPlugin extends Plugin {
10 | /**
11 | * @since 1.0.0
12 | */
13 | public static [preInitialization](this: SapphireClient): void {
14 | this.utilities = new Utilities();
15 | this.stores.register(this.utilities.store);
16 | }
17 |
18 | /**
19 | * @since 1.0.0
20 | */
21 | public static [postLogin](this: SapphireClient): void {
22 | const pieces = this.utilities.store;
23 |
24 | for (const [name, piece] of pieces.entries()) {
25 | this.utilities.exposePiece(name, piece);
26 | }
27 | }
28 | }
29 |
30 | SapphireClient.plugins.registerPreInitializationHook(UtilitiesPlugin[preInitialization], 'UtilitiesStore-PreInitialization');
31 | SapphireClient.plugins.registerPostLoginHook(UtilitiesPlugin[postLogin], 'UtilitiesStore-PostLogin');
32 |
--------------------------------------------------------------------------------
/packages/utilities-store/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "rootDir": "./"
5 | },
6 | "include": ["."]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/utilities-store/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true
6 | },
7 | "include": ["src"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/utilities-store/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { createTsupConfig } from '../../scripts/tsup.config';
2 |
3 | export default createTsupConfig();
4 |
--------------------------------------------------------------------------------
/packages/utilities-store/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://typedoc.org/schema.json",
3 | "entryPoints": ["src/index.ts"],
4 | "json": "docs/api.json",
5 | "tsconfig": "src/tsconfig.json",
6 | "excludeExternals": true,
7 | "excludePrivate": false
8 | }
9 |
--------------------------------------------------------------------------------
/scripts/clean-register-imports.mts:
--------------------------------------------------------------------------------
1 | import { bold, green } from 'colorette';
2 | import { readFile, writeFile } from 'node:fs/promises';
3 | import { join } from 'node:path';
4 |
5 | const paths = ['dist/cjs/register.d.ts', 'dist/esm/register.d.mts'];
6 | const cleanupImportsRegex = /^import '(?!.*index\.[cm]?js).*';$/gm;
7 | const cleanupNewlineRegex = /\n{2,}/g;
8 |
9 | for (const path of paths) {
10 | const fullPathUrl = join(process.cwd(), path);
11 | const fileContent = await readFile(fullPathUrl, { encoding: 'utf8' });
12 | const cleanedUpContent = fileContent
13 | .replace(cleanupImportsRegex, '')
14 | .replace(cleanupNewlineRegex, '\n\n')
15 | .replace("import './index.js';", "import './index.cjs';");
16 |
17 | await writeFile(fullPathUrl, cleanedUpContent, { encoding: 'utf8' });
18 | console.log(green(`✅ Cleaned up ${bold(path)}`));
19 | }
20 |
--------------------------------------------------------------------------------
/scripts/rename-cjs-register.mts:
--------------------------------------------------------------------------------
1 | import { bold, green } from 'colorette';
2 | import { rename } from 'node:fs/promises';
3 | import { join } from 'node:path';
4 |
5 | const inputPath = 'dist/cjs/register.d.ts';
6 | const outputPath = 'dist/cjs/register.d.cts';
7 |
8 | const fullInputPathUrl = join(process.cwd(), inputPath);
9 | const fullOutputPathUrl = join(process.cwd(), outputPath);
10 |
11 | await rename(fullInputPathUrl, fullOutputPathUrl);
12 | console.log(green(`✅ Renamed ${bold(inputPath)} to ${bold(outputPath)}`));
13 |
--------------------------------------------------------------------------------
/scripts/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions';
2 | import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector';
3 | import { defineConfig, type Options } from 'tsup';
4 |
5 | const baseOptions: Options = {
6 | clean: true,
7 | entry: ['src/**/*.ts'],
8 | dts: true,
9 | minify: false,
10 | skipNodeModulesBundle: true,
11 | sourcemap: true,
12 | target: 'es2021',
13 | tsconfig: 'src/tsconfig.json',
14 | keepNames: true,
15 | esbuildPlugins: [esbuildPluginVersionInjector(), esbuildPluginFilePathExtensions()],
16 | treeshake: true
17 | };
18 |
19 | export function createTsupConfig() {
20 | return [
21 | defineConfig({
22 | ...baseOptions,
23 | outDir: 'dist/cjs',
24 | format: 'cjs',
25 | outExtension: () => ({ js: '.cjs' })
26 | }),
27 | defineConfig({
28 | ...baseOptions,
29 | outDir: 'dist/esm',
30 | format: 'esm'
31 | })
32 | ];
33 | }
34 |
--------------------------------------------------------------------------------
/scripts/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import type { ESBuildOptions } from 'vite';
2 | import { defineConfig, type UserConfig } from 'vitest/config';
3 |
4 | export const createVitestConfig = (options: UserConfig = {}) =>
5 | defineConfig({
6 | ...options,
7 | test: {
8 | ...options?.test,
9 | globals: true,
10 | coverage: {
11 | ...options.test?.coverage,
12 | provider: 'v8',
13 | enabled: true,
14 | reporter: ['text', 'lcov'],
15 | exclude: [...(options.test?.coverage?.exclude ?? []), '**/node_modules/**', '**/dist/**', '**/tests/**']
16 | }
17 | },
18 | esbuild: {
19 | ...options?.esbuild,
20 | target: (options?.esbuild as ESBuildOptions | undefined)?.target ?? 'es2020'
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=sapphiredev_plugins
2 | sonar.organization=sapphiredev
3 | sonar.pullrequest.github.summary_comment=false
4 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@sapphire/ts-config", "@sapphire/ts-config/extra-strict", "@sapphire/ts-config/verbatim", "@sapphire/ts-config/bundler"],
3 | "compilerOptions": {
4 | "noEmit": true,
5 | "incremental": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "include": ["scripts", "packages/**/*.ts", "./vitest.config.ts", "./vitest.workspace.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "tasks": {
3 | "build": {
4 | "dependsOn": ["^build"],
5 | "outputs": ["dist/**"]
6 | },
7 | "typecheck": {
8 | "dependsOn": [],
9 | "outputs": []
10 | },
11 | "lint": {
12 | "dependsOn": [],
13 | "outputs": []
14 | },
15 | "bump": {
16 | "dependsOn": [],
17 | "outputs": ["CHANGELOG.md"]
18 | },
19 | "check-update": {
20 | "dependsOn": [],
21 | "outputs": []
22 | },
23 | "docs": {
24 | "dependsOn": [],
25 | "outputs": ["docs/**"]
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { createVitestConfig } from './scripts/vitest.config';
2 |
3 | export default createVitestConfig();
4 |
--------------------------------------------------------------------------------
/vitest.workspace.ts:
--------------------------------------------------------------------------------
1 | import { defineWorkspace } from 'vitest/config';
2 |
3 | export default defineWorkspace([
4 | './vitest.config.ts',
5 | './packages/api/vitest.config.ts',
6 | './packages/logger/vitest.config.ts',
7 | './packages/i18next/vitest.config.ts'
8 | ]);
9 |
--------------------------------------------------------------------------------