From e9e5a1617bb1a8d2d184488e3105d446a3cbcc94 Mon Sep 17 00:00:00 2001 From: itsMapleLeaf <19603573+itsMapleLeaf@users.noreply.github.com> Date: Wed, 16 Aug 2023 19:32:28 -0500 Subject: [PATCH] tooling overhaul --- .changeset/config.json | 18 +- .eslintrc.cjs | 38 - .eslintrc.json | 77 + .github/workflows/lint.yml | 35 + .github/workflows/main.yml | 53 - .github/workflows/release.yml | 19 +- .github/workflows/unit-test.yml | 30 + .gitignore | 5 +- .prettierignore | 7 +- .prettierrc | 18 + .prettierrc.cjs | 14 - package.json | 65 +- .../convert-object-property-case.test.ts | 58 +- .../helpers/convert-object-property-case.ts | 39 +- packages/helpers/get-environment-value.ts | 2 +- packages/helpers/is-instance-of.ts | 12 +- packages/helpers/is-object.ts | 8 +- packages/helpers/json.ts | 7 + packages/helpers/last.ts | 2 +- packages/helpers/log-pretty.ts | 14 +- packages/helpers/omit.test.ts | 7 + packages/helpers/omit.test.types.ts | 3 + packages/helpers/omit.ts | 19 +- packages/helpers/package.json | 18 +- packages/helpers/pick.ts | 22 +- packages/helpers/prune-nullish-values.test.ts | 51 +- packages/helpers/prune-nullish-values.ts | 54 +- packages/helpers/raise.ts | 2 +- packages/helpers/reject-after.ts | 10 +- packages/helpers/retry-with-timeout.ts | 26 +- packages/helpers/to-error.ts | 2 +- packages/helpers/to-upper.ts | 2 +- packages/helpers/tsconfig.json | 2 +- packages/helpers/types.test.types.ts | 4 + packages/helpers/types.ts | 26 +- packages/helpers/wait-for.ts | 25 +- packages/helpers/with-logged-method-calls.ts | 40 +- packages/reacord/env.d.ts | 1 + .../reacord/library/core/component-event.ts | 177 +- .../library/core/components/action-row.tsx | 34 +- .../core/components/button-shared-props.ts | 30 +- .../library/core/components/button.tsx | 98 +- .../library/core/components/embed-author.tsx | 49 +- .../library/core/components/embed-child.ts | 2 +- .../library/core/components/embed-field.tsx | 59 +- .../library/core/components/embed-footer.tsx | 55 +- .../library/core/components/embed-image.tsx | 31 +- .../library/core/components/embed-options.ts | 8 +- .../core/components/embed-thumbnail.tsx | 31 +- .../library/core/components/embed-title.tsx | 39 +- .../reacord/library/core/components/embed.tsx | 70 +- .../reacord/library/core/components/link.tsx | 51 +- .../library/core/components/option-node.ts | 22 +- .../library/core/components/option.tsx | 96 +- .../library/core/components/select.tsx | 215 +- .../reacord/library/core/instance-context.tsx | 12 +- packages/reacord/library/core/instance.ts | 21 +- .../library/core/reacord-discord-js.ts | 665 +- packages/reacord/library/core/reacord.tsx | 136 +- packages/reacord/library/internal/channel.ts | 4 +- .../reacord/library/internal/container.ts | 58 +- packages/reacord/library/internal/element.ts | 10 +- .../reacord/library/internal/interaction.ts | 30 +- .../library/internal/limited-collection.ts | 34 +- packages/reacord/library/internal/message.ts | 86 +- packages/reacord/library/internal/node.ts | 21 +- .../reacord/library/internal/reconciler.ts | 168 +- .../renderers/channel-message-renderer.ts | 12 +- .../renderers/interaction-reply-renderer.ts | 20 +- .../library/internal/renderers/renderer.ts | 184 +- .../reacord/library/internal/text-node.ts | 12 +- packages/reacord/library/internal/timeout.ts | 30 +- packages/reacord/package.json | 179 +- .../reacord/scripts/discordjs-manual-test.tsx | 188 +- packages/reacord/test/action-row.test.tsx | 65 +- .../reacord/test/commonjs-require.test.ts | 6 +- packages/reacord/test/embed.test.tsx | 501 +- packages/reacord/test/ephemeral-reply.test.ts | 1 + packages/reacord/test/link.test.tsx | 67 +- packages/reacord/test/reacord.test.tsx | 554 +- packages/reacord/test/select.test.tsx | 264 +- packages/reacord/test/test-adapter.ts | 405 +- packages/reacord/test/text-children.test.tsx | 159 +- packages/reacord/test/use-instance.test.tsx | 115 +- packages/reacord/tsconfig.json | 7 +- packages/website/astro.config.mjs | 16 +- packages/website/package.json | 72 +- .../src/components/landing-animation.tsx | 318 +- .../website/src/components/menu-item.astro | 12 +- .../website/src/components/nav-link.astro | 8 +- packages/website/src/content/config.ts | 12 +- .../src/content/guides/0-getting-started.md | 2 +- .../src/content/guides/1-sending-messages.md | 122 +- .../website/src/content/guides/2-embeds.md | 48 +- .../website/src/content/guides/3-buttons.md | 30 +- .../website/src/content/guides/4-links.md | 12 +- .../src/content/guides/5-select-menu.md | 80 +- .../src/content/guides/6-use-instance.md | 16 +- packages/website/src/env.d.ts | 3 +- .../website/src/pages/guides/[slug].astro | 2 +- packages/website/src/styles/prism-theme.css | 86 +- packages/website/src/styles/tailwind.css | 68 +- packages/website/tailwind.config.cjs | 18 - packages/website/tsconfig.json | 20 +- packages/website/typedoc.json | 40 +- pnpm-lock.yaml | 5957 +++++++++-------- tailwind.config.ts | 20 + tsconfig.base.json | 16 + tsconfig.json | 8 +- vercel.json | 6 +- .../vitest.config.ts => vitest.config.ts | 6 +- 111 files changed, 6758 insertions(+), 6156 deletions(-) delete mode 100644 .eslintrc.cjs create mode 100644 .eslintrc.json create mode 100644 .github/workflows/lint.yml delete mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/unit-test.yml create mode 100644 .prettierrc delete mode 100644 .prettierrc.cjs create mode 100644 packages/helpers/json.ts create mode 100644 packages/helpers/omit.test.ts create mode 100644 packages/helpers/omit.test.types.ts create mode 100644 packages/helpers/types.test.types.ts create mode 100644 packages/reacord/env.d.ts delete mode 100644 packages/website/tailwind.config.cjs create mode 100644 tailwind.config.ts create mode 100644 tsconfig.base.json rename packages/reacord/vitest.config.ts => vitest.config.ts (75%) diff --git a/.changeset/config.json b/.changeset/config.json index e1e01fb..d8e7f85 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,11 +1,11 @@ { - "$schema": "https://unpkg.com/@changesets/config@2.1.0/schema.json", - "changelog": "@changesets/cli/changelog", - "commit": false, - "fixed": [], - "linked": [], - "access": "public", - "baseBranch": "main", - "updateInternalDependencies": "patch", - "ignore": [] + "$schema": "https://unpkg.com/@changesets/config@2.1.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] } diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 2814a8e..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,38 +0,0 @@ -require("@rushstack/eslint-patch/modern-module-resolution") - -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: [require.resolve("@itsmapleleaf/configs/eslint")], - ignorePatterns: [ - "**/node_modules/**", - "**/.cache/**", - "**/build/**", - "**/dist/**", - "**/coverage/**", - "**/public/**", - ], - parserOptions: { - project: require.resolve("./tsconfig.base.json"), - extraFileExtensions: [".astro"], - }, - overrides: [ - { - files: ["packages/website/cypress/**"], - parserOptions: { - project: require.resolve("./packages/website/cypress/tsconfig.json"), - }, - }, - { - files: ["*.astro"], - parser: "astro-eslint-parser", - parserOptions: { - parser: "@typescript-eslint/parser", - }, - rules: { - "react/no-unknown-property": "off", - "react/jsx-key": "off", - "react/jsx-no-undef": "off", - }, - }, - ], -} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..e99e8d9 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,77 @@ +{ + "root": true, + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:@typescript-eslint/stylistic", + "plugin:@typescript-eslint/stylistic-type-checked", + "plugin:astro/recommended", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module", + "project": true + }, + "plugins": ["@typescript-eslint", "react"], + "rules": { + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/require-await": "off", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": [ + "warn", + { "argsIgnorePattern": "^_", "ignoreRestSiblings": true } + ], + "@typescript-eslint/no-empty-function": "off" + }, + "ignorePatterns": [ + "node_modules", + "dist", + ".astro", + "packages/website/public/api" + ], + "settings": { + "react": { + "version": "detect" + } + }, + "overrides": [ + { + "files": ["*.tsx", "*.jsx"], + "extends": [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended" + ], + "rules": { + "react/prop-types": "off" + } + }, + { + "files": ["*.astro"], + "parser": "astro-eslint-parser", + "parserOptions": { + "parser": "@typescript-eslint/parser", + "extraFileExtensions": [".astro"] + }, + "globals": { + "astroHTML": "readonly" + }, + "rules": { + "react/no-unknown-property": "off", + "react/jsx-key": "off", + "react/jsx-no-undef": "off" + } + } + ] +} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..2b0f1c2 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,35 @@ +name: lint + +on: + push: + branches: [main] + pull_request: + +env: + TEST_BOT_TOKEN: ${{ secrets.TEST_BOT_TOKEN }} + TEST_CHANNEL_ID: ${{ secrets.TEST_CHANNEL_ID }} + TEST_GUILD_ID: ${{ secrets.TEST_GUILD_ID }} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + strategy: + fail-fast: false + matrix: + script: ["prettier", "eslint", "tsc", "tsc-root"] + name: lint (${{ matrix.script }}) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + with: + version: 8 + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm run lint:${{ matrix.script }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 79a3e52..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: main - -on: - push: - branches: [main] - pull_request: - -env: - TEST_BOT_TOKEN: ${{ secrets.TEST_BOT_TOKEN }} - TEST_CHANNEL_ID: ${{ secrets.TEST_CHANNEL_ID }} - TEST_GUILD_ID: ${{ secrets.TEST_GUILD_ID }} - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - run-commands: - strategy: - fail-fast: false - matrix: - command: - # if these run in the same process, it dies, - # so we test them separate - - name: test reacord - run: pnpm -C packages/reacord test - # - name: test website - # run: pnpm -C packages/website test - - name: build - run: pnpm --recursive run build - - name: lint - run: pnpm run lint - - name: typecheck - run: pnpm run typecheck - name: ${{ matrix.command.name }} - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v3 - - - name: setup pnpm - uses: pnpm/action-setup@v2 - with: - version: 7.13.4 - - - name: setup node - uses: actions/setup-node@v3 - with: - node-version: 16 - cache: pnpm - - - run: pnpm install --frozen-lockfile - - run: ${{ matrix.command.run }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 164ee1c..d73fcde 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,22 +13,15 @@ jobs: name: release runs-on: ubuntu-latest steps: - - name: checkout - uses: actions/checkout@v3 - - - name: setup pnpm - uses: pnpm/action-setup@v2 + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 with: - version: 7.13.4 - - - name: setup node - uses: actions/setup-node@v3 + version: 8 + - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 cache: pnpm - - - name: install deps - run: pnpm install --frozen-lockfile + - run: pnpm install --frozen-lockfile - name: changesets release id: changesets diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 0000000..96725e9 --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,30 @@ +name: unit test + +on: + push: + branches: [main] + pull_request: + +env: + TEST_BOT_TOKEN: ${{ secrets.TEST_BOT_TOKEN }} + TEST_CHANNEL_ID: ${{ secrets.TEST_CHANNEL_ID }} + TEST_GUILD_ID: ${{ secrets.TEST_GUILD_ID }} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + with: + version: 8 + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm test diff --git a/.gitignore b/.gitignore index 6c5c2c3..8c77702 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,7 @@ coverage .env *.code-workspace .pnpm-debug.log - build .cache - -.vercel +.vercel +*.tsbuildinfo diff --git a/.prettierignore b/.prettierignore index 6db67ba..bfbf315 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,2 @@ -node_modules -dist -coverage pnpm-lock.yaml -build -.cache -packages/website/public/api +/packages/website/public/api diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a504112 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,18 @@ +{ + "semi": false, + "useTabs": true, + "htmlWhitespaceSensitivity": "ignore", + "plugins": [ + "prettier-plugin-jsdoc", + "prettier-plugin-astro", + "prettier-plugin-tailwindcss" + ], + "overrides": [ + { + "files": "*.astro", + "options": { + "parser": "astro" + } + } + ] +} diff --git a/.prettierrc.cjs b/.prettierrc.cjs deleted file mode 100644 index b3c2157..0000000 --- a/.prettierrc.cjs +++ /dev/null @@ -1,14 +0,0 @@ -const base = require("@itsmapleleaf/configs/prettier") - -module.exports = { - ...base, - plugins: [require.resolve("prettier-plugin-astro")], - overrides: [ - { - files: "*.astro", - options: { - parser: "astro", - }, - }, - ], -} diff --git a/package.json b/package.json index f1f1b7b..b9b8c6b 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,42 @@ { - "name": "reacord-monorepo", - "private": true, - "scripts": { - "lint": "eslint --ext js,ts,tsx .", - "lint-fix": "pnpm lint -- --fix", - "format": "prettier --write .", - "typecheck": "tsc -b", - "build": "pnpm -r run build", - "start": "pnpm -C packages/website run start", - "release": "pnpm -r run build && changeset publish" - }, - "devDependencies": { - "@changesets/cli": "^2.25.0", - "@itsmapleleaf/configs": "^1.1.7", - "@rushstack/eslint-patch": "^1.2.0", - "@types/eslint": "^8.4.6", - "astro-eslint-parser": "^0.12.0", - "eslint": "^8.36.0", - "prettier": "^2.7.1", - "prettier-plugin-astro": "^0.8.0", - "typescript": "^4.8.4" - }, - "resolutions": { - "esbuild": "latest" - } + "name": "reacord-monorepo", + "private": true, + "scripts": { + "lint": "run-p --print-label --continue-on-error --silent lint:*", + "lint:prettier": "prettier --cache --check .", + "lint:eslint": "eslint . --report-unused-disable-directives", + "lint:tsc": "pnpm -r --parallel --no-bail exec tsc -b", + "lint:tsc-root": "tsc -b", + "format": "run-s --continue-on-error format:*", + "format:eslint": "eslint . --report-unused-disable-directives --fix", + "format:prettier": "prettier --cache --write .", + "test": "vitest", + "build": "pnpm -r run build", + "build:website": "pnpm --filter website... run build", + "start": "pnpm -C packages/website run start", + "start:website": "pnpm -C packages/website run start", + "release": "pnpm -r run build && changeset publish" + }, + "devDependencies": { + "@changesets/cli": "^2.26.2", + "@itsmapleleaf/configs": "^3.0.1", + "@tailwindcss/typography": "^0.5.9", + "@types/eslint": "^8.44.2", + "@typescript-eslint/eslint-plugin": "^6.4.0", + "@typescript-eslint/parser": "^6.4.0", + "eslint": "^8.47.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-astro": "^0.28.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "npm-run-all": "^4.1.5", + "prettier": "^3.0.2", + "prettier-plugin-astro": "^0.11.1", + "prettier-plugin-jsdoc": "^1.0.1", + "prettier-plugin-tailwindcss": "^0.5.3", + "react": "^18.2.0", + "tailwindcss": "^3.3.3", + "typescript": "^5.1.6", + "vitest": "^0.34.1" + } } diff --git a/packages/helpers/convert-object-property-case.test.ts b/packages/helpers/convert-object-property-case.test.ts index 431765e..a0cc18a 100644 --- a/packages/helpers/convert-object-property-case.test.ts +++ b/packages/helpers/convert-object-property-case.test.ts @@ -1,42 +1,42 @@ +import { camelCaseDeep, snakeCaseDeep } from "./convert-object-property-case" import type { - CamelCasedPropertiesDeep, - SnakeCasedPropertiesDeep, + CamelCasedPropertiesDeep, + SnakeCasedPropertiesDeep, } from "type-fest" import { expect, test } from "vitest" -import { camelCaseDeep, snakeCaseDeep } from "./convert-object-property-case" test("camelCaseDeep", () => { - const input = { - some_prop: { - some_deep_prop: "some_deep_value", - }, - someOtherProp: "someOtherValue", - } + const input = { + some_prop: { + some_deep_prop: "some_deep_value", + }, + someOtherProp: "someOtherValue", + } - const expected: CamelCasedPropertiesDeep = { - someProp: { - someDeepProp: "some_deep_value", - }, - someOtherProp: "someOtherValue", - } + const expected: CamelCasedPropertiesDeep = { + someProp: { + someDeepProp: "some_deep_value", + }, + someOtherProp: "someOtherValue", + } - expect(camelCaseDeep(input)).toEqual(expected) + expect(camelCaseDeep(input)).toEqual(expected) }) test("snakeCaseDeep", () => { - const input = { - someProp: { - someDeepProp: "someDeepValue", - }, - some_other_prop: "someOtherValue", - } + const input = { + someProp: { + someDeepProp: "someDeepValue", + }, + some_other_prop: "someOtherValue", + } - const expected: SnakeCasedPropertiesDeep = { - some_prop: { - some_deep_prop: "someDeepValue", - }, - some_other_prop: "someOtherValue", - } + const expected: SnakeCasedPropertiesDeep = { + some_prop: { + some_deep_prop: "someDeepValue", + }, + some_other_prop: "someOtherValue", + } - expect(snakeCaseDeep(input)).toEqual(expected) + expect(snakeCaseDeep(input)).toEqual(expected) }) diff --git a/packages/helpers/convert-object-property-case.ts b/packages/helpers/convert-object-property-case.ts index 0bb2e2a..be3f94d 100644 --- a/packages/helpers/convert-object-property-case.ts +++ b/packages/helpers/convert-object-property-case.ts @@ -1,34 +1,35 @@ import { camelCase, isObject, snakeCase } from "lodash-es" import type { - CamelCasedPropertiesDeep, - SnakeCasedPropertiesDeep, + CamelCasedPropertiesDeep, + SnakeCasedPropertiesDeep, + UnknownRecord, } from "type-fest" function convertKeyCaseDeep( - input: Input, - convertKey: (key: string) => string, + input: Input, + convertKey: (key: string) => string, ): Output { - if (!isObject(input)) { - return input as unknown as Output - } + if (!isObject(input)) { + return input as unknown as Output + } - if (Array.isArray(input)) { - return input.map((item) => - convertKeyCaseDeep(item, convertKey), - ) as unknown as Output - } + if (Array.isArray(input)) { + return input.map((item) => + convertKeyCaseDeep(item, convertKey), + ) as unknown as Output + } - const output: any = {} - for (const [key, value] of Object.entries(input)) { - output[convertKey(key)] = convertKeyCaseDeep(value, convertKey) - } - return output + const output = {} as UnknownRecord + for (const [key, value] of Object.entries(input)) { + output[convertKey(key)] = convertKeyCaseDeep(value, convertKey) + } + return output as Output } export function camelCaseDeep(input: T): CamelCasedPropertiesDeep { - return convertKeyCaseDeep(input, camelCase) + return convertKeyCaseDeep(input, camelCase) } export function snakeCaseDeep(input: T): SnakeCasedPropertiesDeep { - return convertKeyCaseDeep(input, snakeCase) + return convertKeyCaseDeep(input, snakeCase) } diff --git a/packages/helpers/get-environment-value.ts b/packages/helpers/get-environment-value.ts index 4f74305..2f3c93f 100644 --- a/packages/helpers/get-environment-value.ts +++ b/packages/helpers/get-environment-value.ts @@ -1,5 +1,5 @@ import { raise } from "./raise.js" export function getEnvironmentValue(name: string) { - return process.env[name] ?? raise(`Missing environment variable: ${name}`) + return process.env[name] ?? raise(`Missing environment variable: ${name}`) } diff --git a/packages/helpers/is-instance-of.ts b/packages/helpers/is-instance-of.ts index 1fd16c5..d5b6e6d 100644 --- a/packages/helpers/is-instance-of.ts +++ b/packages/helpers/is-instance-of.ts @@ -1,7 +1,7 @@ -/** - * for narrowing instance types with array.filter - */ +/** For narrowing instance types with array.filter */ export const isInstanceOf = - (Constructor: new (...args: any[]) => T) => - (value: unknown): value is T => - value instanceof Constructor + ( + constructor: new (...args: Args) => Instance, + ) => + (value: unknown): value is Instance => + value instanceof constructor diff --git a/packages/helpers/is-object.ts b/packages/helpers/is-object.ts index b6d975d..d94ef70 100644 --- a/packages/helpers/is-object.ts +++ b/packages/helpers/is-object.ts @@ -1,7 +1,3 @@ -export function isObject( - value: T, -): value is Exclude { - return typeof value === "object" && value !== null +export function isObject(value: unknown): value is object { + return typeof value === "object" && value !== null } -type Primitive = string | number | boolean | undefined | null -type AnyFunction = (...args: any[]) => any diff --git a/packages/helpers/json.ts b/packages/helpers/json.ts new file mode 100644 index 0000000..6ab932d --- /dev/null +++ b/packages/helpers/json.ts @@ -0,0 +1,7 @@ +export function safeJsonStringify(value: unknown): string { + try { + return JSON.stringify(value) + } catch { + return String(value) + } +} diff --git a/packages/helpers/last.ts b/packages/helpers/last.ts index d58fb25..5d978e6 100644 --- a/packages/helpers/last.ts +++ b/packages/helpers/last.ts @@ -1,3 +1,3 @@ export function last(array: T[]): T | undefined { - return array[array.length - 1] + return array[array.length - 1] } diff --git a/packages/helpers/log-pretty.ts b/packages/helpers/log-pretty.ts index fa469d8..737a2f4 100644 --- a/packages/helpers/log-pretty.ts +++ b/packages/helpers/log-pretty.ts @@ -1,11 +1,11 @@ import { inspect } from "node:util" export function logPretty(value: unknown) { - console.info( - inspect(value, { - // depth: Number.POSITIVE_INFINITY, - depth: 10, - colors: true, - }), - ) + console.info( + inspect(value, { + // depth: Number.POSITIVE_INFINITY, + depth: 10, + colors: true, + }), + ) } diff --git a/packages/helpers/omit.test.ts b/packages/helpers/omit.test.ts new file mode 100644 index 0000000..2a2f1b9 --- /dev/null +++ b/packages/helpers/omit.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from "vitest" +import { omit } from "./omit.ts" + +test("omit", () => { + const subject = { a: 1, b: true } + expect(omit(subject, ["a"])).toStrictEqual({ b: true }) +}) diff --git a/packages/helpers/omit.test.types.ts b/packages/helpers/omit.test.types.ts new file mode 100644 index 0000000..1bbd3ee --- /dev/null +++ b/packages/helpers/omit.test.types.ts @@ -0,0 +1,3 @@ +import { omit } from "./omit.ts" + +omit({ a: 1, b: true }, ["a"]) satisfies { b: boolean } diff --git a/packages/helpers/omit.ts b/packages/helpers/omit.ts index d511018..c7e8998 100644 --- a/packages/helpers/omit.ts +++ b/packages/helpers/omit.ts @@ -1,13 +1,10 @@ export function omit( - subject: Subject, - keys: Key[], - // hack: using a conditional type preserves union types -): Subject extends any ? Omit : never { - const result: any = {} - for (const key in subject) { - if (!keys.includes(key as unknown as Key)) { - result[key] = subject[key] - } - } - return result + subject: Subject, + keys: Key[], +) { + const keySet = new Set(keys) + return Object.fromEntries( + Object.entries(subject).filter(([key]) => !keySet.has(key)), + // hack: conditional type preserves unions + ) as Subject extends unknown ? Omit : never } diff --git a/packages/helpers/package.json b/packages/helpers/package.json index f1bcd24..34aa624 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,11 +1,11 @@ { - "name": "@reacord/helpers", - "version": "0.0.0", - "private": true, - "dependencies": { - "@types/lodash-es": "^4.17.6", - "lodash-es": "^4.17.21", - "type-fest": "^2.17.0", - "vitest": "^0.18.1" - } + "name": "@reacord/helpers", + "version": "0.0.0", + "private": true, + "dependencies": { + "@types/lodash-es": "^4.17.8", + "lodash-es": "^4.17.21", + "type-fest": "^4.2.0", + "vitest": "^0.34.1" + } } diff --git a/packages/helpers/pick.ts b/packages/helpers/pick.ts index b8013e7..bf0d1f9 100644 --- a/packages/helpers/pick.ts +++ b/packages/helpers/pick.ts @@ -1,15 +1,11 @@ -import type { LoosePick, UnknownRecord } from "./types" +import type { LoosePick } from "./types" -export function pick( - object: T, - keys: K[], -): LoosePick { - const result: any = {} - for (const key of keys) { - const value = (object as UnknownRecord)[key] - if (value !== undefined) { - result[key] = value - } - } - return result +export function pick( + object: T, + keys: K[], +) { + const keySet = new Set(keys) + return Object.fromEntries( + Object.entries(object).filter(([key]) => keySet.has(key)), + ) as LoosePick } diff --git a/packages/helpers/prune-nullish-values.test.ts b/packages/helpers/prune-nullish-values.test.ts index eecefbd..d9611a8 100644 --- a/packages/helpers/prune-nullish-values.test.ts +++ b/packages/helpers/prune-nullish-values.test.ts @@ -3,33 +3,32 @@ import type { PruneNullishValues } from "./prune-nullish-values" import { pruneNullishValues } from "./prune-nullish-values" test("pruneNullishValues", () => { - type InputType = { - a: string - b: string | null | undefined - c?: string - d: { - a: string - b: string | undefined - } - } + interface InputType { + a: string + b: string | null | undefined + c?: string + d: { + a: string + b: string | undefined + } + } - const input: InputType = { - a: "a", - // eslint-disable-next-line unicorn/no-null - b: null, - c: undefined, - d: { - a: "a", - b: undefined, - }, - } + const input: InputType = { + a: "a", + b: null, + c: undefined, + d: { + a: "a", + b: undefined, + }, + } - const output: PruneNullishValues = { - a: "a", - d: { - a: "a", - }, - } + const output: PruneNullishValues = { + a: "a", + d: { + a: "a", + }, + } - expect(pruneNullishValues(input)).toEqual(output) + expect(pruneNullishValues(input)).toEqual(output) }) diff --git a/packages/helpers/prune-nullish-values.ts b/packages/helpers/prune-nullish-values.ts index d824bb0..19f066a 100644 --- a/packages/helpers/prune-nullish-values.ts +++ b/packages/helpers/prune-nullish-values.ts @@ -1,42 +1,44 @@ import { isObject } from "./is-object" export function pruneNullishValues(input: T): PruneNullishValues { - if (Array.isArray(input)) { - return input.filter(Boolean).map((item) => pruneNullishValues(item)) as any - } + if (!isObject(input)) { + return input as PruneNullishValues + } - if (!isObject(input)) { - return input as any - } + if (Array.isArray(input)) { + return input + .filter(Boolean) + .map((item) => pruneNullishValues(item)) as PruneNullishValues + } - const result: any = {} - for (const [key, value] of Object.entries(input as any)) { - if (value != undefined) { - result[key] = pruneNullishValues(value) - } - } - return result + const result: Record = {} + for (const [key, value] of Object.entries(input)) { + if (value != undefined) { + result[key] = pruneNullishValues(value) + } + } + return result as PruneNullishValues } export type PruneNullishValues = Input extends object - ? OptionalKeys< - { [Key in keyof Input]: NonNullable> }, - KeysWithNullishValues - > - : Input + ? OptionalKeys< + { [Key in keyof Input]: NonNullable> }, + KeysWithNullishValues + > + : Input type OptionalKeys = Omit & { - [Key in Keys]?: Input[Key] + [Key in Keys]?: Input[Key] } type KeysWithNullishValues = NonNullable< - Values<{ - [Key in keyof Input]: null extends Input[Key] - ? Key - : undefined extends Input[Key] - ? Key - : never - }> + Values<{ + [Key in keyof Input]: null extends Input[Key] + ? Key + : undefined extends Input[Key] + ? Key + : never + }> > type Values = Input[keyof Input] diff --git a/packages/helpers/raise.ts b/packages/helpers/raise.ts index 6472f9b..b13b824 100644 --- a/packages/helpers/raise.ts +++ b/packages/helpers/raise.ts @@ -1,5 +1,5 @@ import { toError } from "./to-error.js" export function raise(error: unknown): never { - throw toError(error) + throw toError(error) } diff --git a/packages/helpers/reject-after.ts b/packages/helpers/reject-after.ts index 12b236c..dac49fe 100644 --- a/packages/helpers/reject-after.ts +++ b/packages/helpers/reject-after.ts @@ -1,10 +1,10 @@ -import { setTimeout } from "node:timers/promises" import { toError } from "./to-error.js" +import { setTimeout } from "node:timers/promises" export async function rejectAfter( - timeMs: number, - error: unknown = `rejected after ${timeMs}ms`, + timeMs: number, + error: unknown = `rejected after ${timeMs}ms`, ): Promise { - await setTimeout(timeMs) - throw toError(error) + await setTimeout(timeMs) + throw toError(error) } diff --git a/packages/helpers/retry-with-timeout.ts b/packages/helpers/retry-with-timeout.ts index 25aa015..a40d455 100644 --- a/packages/helpers/retry-with-timeout.ts +++ b/packages/helpers/retry-with-timeout.ts @@ -4,18 +4,18 @@ const maxTime = 500 const waitPeriod = 50 export async function retryWithTimeout( - callback: () => Promise | T, + callback: () => Promise | T, ): Promise { - const startTime = Date.now() - // eslint-disable-next-line no-constant-condition - while (true) { - try { - return await callback() - } catch (error) { - if (Date.now() - startTime > maxTime) { - throw error - } - await setTimeout(waitPeriod) - } - } + const startTime = Date.now() + // eslint-disable-next-line no-constant-condition + while (true) { + try { + return await callback() + } catch (error) { + if (Date.now() - startTime > maxTime) { + throw error + } + await setTimeout(waitPeriod) + } + } } diff --git a/packages/helpers/to-error.ts b/packages/helpers/to-error.ts index 0a18003..1743d7c 100644 --- a/packages/helpers/to-error.ts +++ b/packages/helpers/to-error.ts @@ -1,3 +1,3 @@ export function toError(value: unknown) { - return value instanceof Error ? value : new Error(String(value)) + return value instanceof Error ? value : new Error(String(value)) } diff --git a/packages/helpers/to-upper.ts b/packages/helpers/to-upper.ts index 36195cf..387b1fe 100644 --- a/packages/helpers/to-upper.ts +++ b/packages/helpers/to-upper.ts @@ -1,4 +1,4 @@ /** A typesafe version of toUpperCase */ export function toUpper(string: S) { - return string.toUpperCase() as Uppercase + return string.toUpperCase() as Uppercase } diff --git a/packages/helpers/tsconfig.json b/packages/helpers/tsconfig.json index a700c4c..b361441 100644 --- a/packages/helpers/tsconfig.json +++ b/packages/helpers/tsconfig.json @@ -1,3 +1,3 @@ { - "extends": "@itsmapleleaf/configs/tsconfig.base" + "extends": "../../tsconfig.base.json" } diff --git a/packages/helpers/types.test.types.ts b/packages/helpers/types.test.types.ts new file mode 100644 index 0000000..54398bb --- /dev/null +++ b/packages/helpers/types.test.types.ts @@ -0,0 +1,4 @@ +import { LooseOmit, LoosePick, typeEquals } from "./types.ts" + +typeEquals, { a: 1 }>(true) +typeEquals, { b: 2 }>(true) diff --git a/packages/helpers/types.ts b/packages/helpers/types.ts index 255d947..34cfc8a 100644 --- a/packages/helpers/types.ts +++ b/packages/helpers/types.ts @@ -1,11 +1,21 @@ -export type MaybePromise = T | Promise +import { raise } from "./raise.ts" -export type ValueOf = Type extends ReadonlyArray - ? Value - : Type[keyof Type] +export type MaybePromise = T | PromiseLike -export type UnknownRecord = Record +export type ValueOf = Type extends readonly (infer Value)[] + ? Value + : Type[keyof Type] -export type LoosePick = { - [Key in Keys]: Shape extends Record ? Value : never -} +export type LoosePick = Simplify<{ + [Key in Extract]: Shape[Key] +}> + +export type LooseOmit = Simplify<{ + [Key in Exclude]: Shape[Key] +}> + +export type Simplify = { [Key in keyof T]: T[Key] } & NonNullable + +export const typeEquals = ( + _result: A extends B ? (B extends A ? true : false) : false, +) => raise("typeEquals() should not be called at runtime") diff --git a/packages/helpers/wait-for.ts b/packages/helpers/wait-for.ts index a99db48..83d8d92 100644 --- a/packages/helpers/wait-for.ts +++ b/packages/helpers/wait-for.ts @@ -1,21 +1,22 @@ import { setTimeout } from "node:timers/promises" +import { MaybePromise } from "./types.ts" const maxTime = 1000 export async function waitFor( - predicate: () => Result, + predicate: () => MaybePromise, ): Promise> { - const startTime = Date.now() - let lastError: unknown + const startTime = Date.now() + let lastError: unknown - while (Date.now() - startTime < maxTime) { - try { - return await predicate() - } catch (error) { - lastError = error - await setTimeout(50) - } - } + while (Date.now() - startTime < maxTime) { + try { + return await predicate() + } catch (error) { + lastError = error + await setTimeout(50) + } + } - throw lastError ?? new Error("Timeout") + throw lastError ?? new Error("Timeout") } diff --git a/packages/helpers/with-logged-method-calls.ts b/packages/helpers/with-logged-method-calls.ts index fb378c9..2dc864a 100644 --- a/packages/helpers/with-logged-method-calls.ts +++ b/packages/helpers/with-logged-method-calls.ts @@ -1,24 +1,24 @@ import { inspect } from "node:util" export function withLoggedMethodCalls(value: T) { - return new Proxy(value as Record, { - get(target, property) { - const value = target[property] - if (typeof value !== "function") { - return value - } - return (...values: any[]) => { - console.info( - `${String(property)}(${values - .map((value) => - typeof value === "object" && value !== null - ? value.constructor.name - : inspect(value, { colors: true }), - ) - .join(", ")})`, - ) - return value.apply(target, values) - } - }, - }) as T + return new Proxy(value as Record, { + get(target, property) { + const value = target[property] + if (typeof value !== "function") { + return value + } + return (...values: unknown[]) => { + console.info( + `${String(property)}(${values + .map((value) => + typeof value === "object" && value !== null + ? value.constructor.name + : inspect(value, { colors: true }), + ) + .join(", ")})`, + ) + return value.apply(target, values) + } + }, + }) as T } diff --git a/packages/reacord/env.d.ts b/packages/reacord/env.d.ts new file mode 100644 index 0000000..e2814d6 --- /dev/null +++ b/packages/reacord/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/reacord/library/core/component-event.ts b/packages/reacord/library/core/component-event.ts index 713f659..c6d3429 100644 --- a/packages/reacord/library/core/component-event.ts +++ b/packages/reacord/library/core/component-event.ts @@ -1,113 +1,102 @@ import type { ReactNode } from "react" import type { ReacordInstance } from "./instance" -/** - * @category Component Event - */ -export type ComponentEvent = { - /** - * The message associated with this event. - * For example: with a button click, - * this is the message that the button is on. - * @see https://discord.com/developers/docs/resources/channel#message-object - */ - message: MessageInfo +/** @category Component Event */ +export interface ComponentEvent { + /** + * The message associated with this event. For example: with a button click, + * this is the message that the button is on. + * + * @see https://discord.com/developers/docs/resources/channel#message-object + */ + message: MessageInfo - /** - * The channel that this event occurred in. - * @see https://discord.com/developers/docs/resources/channel#channel-object - */ - channel: ChannelInfo + /** + * The channel that this event occurred in. + * + * @see https://discord.com/developers/docs/resources/channel#channel-object + */ + channel: ChannelInfo - /** - * The user that triggered this event. - * @see https://discord.com/developers/docs/resources/user#user-object - */ - user: UserInfo + /** + * The user that triggered this event. + * + * @see https://discord.com/developers/docs/resources/user#user-object + */ + user: UserInfo - /** - * The guild that this event occurred in. - * @see https://discord.com/developers/docs/resources/guild#guild-object - */ - guild?: GuildInfo + /** + * The guild that this event occurred in. + * + * @see https://discord.com/developers/docs/resources/guild#guild-object + */ + guild?: GuildInfo - /** - * Create a new reply to this event. - */ - reply(content?: ReactNode): ReacordInstance + /** Create a new reply to this event. */ + reply(content?: ReactNode): ReacordInstance - /** - * Create an ephemeral reply to this event, - * shown only to the user who triggered it. - */ - ephemeralReply(content?: ReactNode): ReacordInstance + /** + * Create an ephemeral reply to this event, shown only to the user who + * triggered it. + */ + ephemeralReply(content?: ReactNode): ReacordInstance } -/** - * @category Component Event - */ -export type ChannelInfo = { - id: string - name?: string - topic?: string - nsfw?: boolean - lastMessageId?: string - ownerId?: string - parentId?: string - rateLimitPerUser?: number +/** @category Component Event */ +export interface ChannelInfo { + id: string + name?: string + topic?: string + nsfw?: boolean + lastMessageId?: string + ownerId?: string + parentId?: string + rateLimitPerUser?: number } -/** - * @category Component Event - */ -export type MessageInfo = { - id: string - channelId: string - authorId: UserInfo - member?: GuildMemberInfo - content: string - timestamp: string - editedTimestamp?: string - tts: boolean - mentionEveryone: boolean - /** The IDs of mentioned users */ - mentions: string[] +/** @category Component Event */ +export interface MessageInfo { + id: string + channelId: string + authorId: string + member?: GuildMemberInfo + content: string + timestamp: string + editedTimestamp?: string + tts: boolean + mentionEveryone: boolean + /** The IDs of mentioned users */ + mentions: string[] } -/** - * @category Component Event - */ -export type GuildInfo = { - id: string - name: string - member: GuildMemberInfo +/** @category Component Event */ +export interface GuildInfo { + id: string + name: string + member: GuildMemberInfo } -/** - * @category Component Event - */ -export type GuildMemberInfo = { - id: string - nick?: string - displayName: string - avatarUrl?: string - displayAvatarUrl: string - roles: string[] - color: number - joinedAt?: string - premiumSince?: string - pending?: boolean - communicationDisabledUntil?: string +/** @category Component Event */ +export interface GuildMemberInfo { + id: string + nick?: string + displayName: string + avatarUrl?: string + displayAvatarUrl: string + roles: string[] + color: number + joinedAt?: string + premiumSince?: string + pending?: boolean + communicationDisabledUntil?: string } -/** - * @category Component Event - */ -export type UserInfo = { - id: string - username: string - discriminator: string - tag: string - avatarUrl: string - accentColor?: number +/** @category Component Event */ +export interface UserInfo { + id: string + username: string + discriminator: string + tag: string + avatarUrl: string + accentColor?: number } diff --git a/packages/reacord/library/core/components/action-row.tsx b/packages/reacord/library/core/components/action-row.tsx index 8b3e5cb..a30b6c2 100644 --- a/packages/reacord/library/core/components/action-row.tsx +++ b/packages/reacord/library/core/components/action-row.tsx @@ -1,22 +1,22 @@ import type { ReactNode } from "react" -import React from "react" import { ReacordElement } from "../../internal/element.js" import type { MessageOptions } from "../../internal/message" import { Node } from "../../internal/node.js" /** * Props for an action row + * * @category Action Row */ -export type ActionRowProps = { - children?: ReactNode +export interface ActionRowProps { + children?: ReactNode } /** * An action row is a top-level container for message components. * - * You don't need to use this; Reacord automatically creates action rows for you. - * But this can be useful if you want a specific layout. + * You don't need to use this; Reacord automatically creates action rows for + * you. But this can be useful if you want a specific layout. * * ```tsx * // put buttons on two separate rows @@ -30,18 +30,18 @@ export type ActionRowProps = { * @see https://discord.com/developers/docs/interactions/message-components#action-rows */ export function ActionRow(props: ActionRowProps) { - return ( - new ActionRowNode(props)}> - {props.children} - - ) + return ( + new ActionRowNode(props)}> + {props.children} + + ) } -class ActionRowNode extends Node<{}> { - override modifyMessageOptions(options: MessageOptions): void { - options.actionRows.push([]) - for (const child of this.children) { - child.modifyMessageOptions(options) - } - } +class ActionRowNode extends Node { + override modifyMessageOptions(options: MessageOptions): void { + options.actionRows.push([]) + for (const child of this.children) { + child.modifyMessageOptions(options) + } + } } diff --git a/packages/reacord/library/core/components/button-shared-props.ts b/packages/reacord/library/core/components/button-shared-props.ts index 4218af1..4f7119e 100644 --- a/packages/reacord/library/core/components/button-shared-props.ts +++ b/packages/reacord/library/core/components/button-shared-props.ts @@ -2,23 +2,23 @@ import type { ReactNode } from "react" /** * Common props between button-like components + * * @category Button */ -export type ButtonSharedProps = { - /** The text on the button. Rich formatting (markdown) is not supported here. */ - label?: ReactNode +export interface ButtonSharedProps { + /** The text on the button. Rich formatting (markdown) is not supported here. */ + label?: ReactNode - /** When true, the button will be slightly faded, and cannot be clicked. */ - disabled?: boolean + /** When true, the button will be slightly faded, and cannot be clicked. */ + disabled?: boolean - /** - * Renders an emoji to the left of the text. - * Has to be a literal emoji character (e.g. 🍍), - * or an emoji code, like `<:plus_one:778531744860602388>`. - * - * To get an emoji code, type your emoji in Discord chat - * with a backslash `\` in front. - * The bot has to be in the emoji's guild to use it. - */ - emoji?: string + /** + * Renders an emoji to the left of the text. Has to be a literal emoji + * character (e.g. 🍍), or an emoji code, like + * `<:plus_one:778531744860602388>`. + * + * To get an emoji code, type your emoji in Discord chat with a backslash `\` + * in front. The bot has to be in the emoji's guild to use it. + */ + emoji?: string } diff --git a/packages/reacord/library/core/components/button.tsx b/packages/reacord/library/core/components/button.tsx index aa3e67a..dd5cabe 100644 --- a/packages/reacord/library/core/components/button.tsx +++ b/packages/reacord/library/core/components/button.tsx @@ -1,5 +1,4 @@ import { randomUUID } from "node:crypto" -import React from "react" import { ReacordElement } from "../../internal/element.js" import type { ComponentInteraction } from "../../internal/interaction" import type { MessageOptions } from "../../internal/message" @@ -8,70 +7,63 @@ import { Node } from "../../internal/node.js" import type { ComponentEvent } from "../component-event" import type { ButtonSharedProps } from "./button-shared-props" -/** - * @category Button - */ +/** @category Button */ export type ButtonProps = ButtonSharedProps & { - /** - * The style determines the color of the button and signals intent. - * @see https://discord.com/developers/docs/interactions/message-components#button-object-button-styles - */ - style?: "primary" | "secondary" | "success" | "danger" + /** + * The style determines the color of the button and signals intent. + * + * @see https://discord.com/developers/docs/interactions/message-components#button-object-button-styles + */ + style?: "primary" | "secondary" | "success" | "danger" - /** - * Happens when a user clicks the button. - */ - onClick: (event: ButtonClickEvent) => void + /** Happens when a user clicks the button. */ + onClick: (event: ButtonClickEvent) => void } -/** - * @category Button - */ +/** @category Button */ export type ButtonClickEvent = ComponentEvent -/** - * @category Button - */ +/** @category Button */ export function Button(props: ButtonProps) { - return ( - new ButtonNode(props)}> - new ButtonLabelNode({})}> - {props.label} - - - ) + return ( + new ButtonNode(props)}> + new ButtonLabelNode({})}> + {props.label} + + + ) } class ButtonNode extends Node { - private customId = randomUUID() + private customId = randomUUID() - // this has text children, but buttons themselves shouldn't yield text - // eslint-disable-next-line class-methods-use-this - override get text() { - return "" - } + // this has text children, but buttons themselves shouldn't yield text + // eslint-disable-next-line @typescript-eslint/class-literal-property-style + override get text() { + return "" + } - override modifyMessageOptions(options: MessageOptions): void { - getNextActionRow(options).push({ - type: "button", - customId: this.customId, - style: this.props.style ?? "secondary", - disabled: this.props.disabled, - emoji: this.props.emoji, - label: this.children.findType(ButtonLabelNode)?.text, - }) - } + override modifyMessageOptions(options: MessageOptions): void { + getNextActionRow(options).push({ + type: "button", + customId: this.customId, + style: this.props.style ?? "secondary", + disabled: this.props.disabled, + emoji: this.props.emoji, + label: this.children.findType(ButtonLabelNode)?.text, + }) + } - override handleComponentInteraction(interaction: ComponentInteraction) { - if ( - interaction.type === "button" && - interaction.customId === this.customId - ) { - this.props.onClick(interaction.event) - return true - } - return false - } + override handleComponentInteraction(interaction: ComponentInteraction) { + if ( + interaction.type === "button" && + interaction.customId === this.customId + ) { + this.props.onClick(interaction.event) + return true + } + return false + } } -class ButtonLabelNode extends Node<{}> {} +class ButtonLabelNode extends Node> {} diff --git a/packages/reacord/library/core/components/embed-author.tsx b/packages/reacord/library/core/components/embed-author.tsx index 5009ca8..8b9f1b5 100644 --- a/packages/reacord/library/core/components/embed-author.tsx +++ b/packages/reacord/library/core/components/embed-author.tsx @@ -1,41 +1,36 @@ import type { ReactNode } from "react" -import React from "react" import { ReacordElement } from "../../internal/element.js" import { Node } from "../../internal/node.js" import { EmbedChildNode } from "./embed-child.js" import type { EmbedOptions } from "./embed-options" -/** - * @category Embed - */ -export type EmbedAuthorProps = { - name?: ReactNode - children?: ReactNode - url?: string - iconUrl?: string +/** @category Embed */ +export interface EmbedAuthorProps { + name?: ReactNode + children?: ReactNode + url?: string + iconUrl?: string } -/** - * @category Embed - */ +/** @category Embed */ export function EmbedAuthor(props: EmbedAuthorProps) { - return ( - new EmbedAuthorNode(props)}> - new AuthorTextNode({})}> - {props.name ?? props.children} - - - ) + return ( + new EmbedAuthorNode(props)}> + new AuthorTextNode({})}> + {props.name ?? props.children} + + + ) } class EmbedAuthorNode extends EmbedChildNode { - override modifyEmbedOptions(options: EmbedOptions): void { - options.author = { - name: this.children.findType(AuthorTextNode)?.text ?? "", - url: this.props.url, - icon_url: this.props.iconUrl, - } - } + override modifyEmbedOptions(options: EmbedOptions): void { + options.author = { + name: this.children.findType(AuthorTextNode)?.text ?? "", + url: this.props.url, + icon_url: this.props.iconUrl, + } + } } -class AuthorTextNode extends Node<{}> {} +class AuthorTextNode extends Node> {} diff --git a/packages/reacord/library/core/components/embed-child.ts b/packages/reacord/library/core/components/embed-child.ts index 851f059..88ef4ce 100644 --- a/packages/reacord/library/core/components/embed-child.ts +++ b/packages/reacord/library/core/components/embed-child.ts @@ -2,5 +2,5 @@ import { Node } from "../../internal/node.js" import type { EmbedOptions } from "./embed-options" export abstract class EmbedChildNode extends Node { - abstract modifyEmbedOptions(options: EmbedOptions): void + abstract modifyEmbedOptions(options: EmbedOptions): void } diff --git a/packages/reacord/library/core/components/embed-field.tsx b/packages/reacord/library/core/components/embed-field.tsx index 0ae0b23..0ae46a5 100644 --- a/packages/reacord/library/core/components/embed-field.tsx +++ b/packages/reacord/library/core/components/embed-field.tsx @@ -1,46 +1,41 @@ import type { ReactNode } from "react" -import React from "react" import { ReacordElement } from "../../internal/element.js" import { Node } from "../../internal/node.js" import { EmbedChildNode } from "./embed-child.js" import type { EmbedOptions } from "./embed-options" -/** - * @category Embed - */ -export type EmbedFieldProps = { - name: ReactNode - value?: ReactNode - inline?: boolean - children?: ReactNode +/** @category Embed */ +export interface EmbedFieldProps { + name: ReactNode + value?: ReactNode + inline?: boolean + children?: ReactNode } -/** - * @category Embed - */ +/** @category Embed */ export function EmbedField(props: EmbedFieldProps) { - return ( - new EmbedFieldNode(props)}> - new FieldNameNode({})}> - {props.name} - - new FieldValueNode({})}> - {props.value || props.children} - - - ) + return ( + new EmbedFieldNode(props)}> + new FieldNameNode({})}> + {props.name} + + new FieldValueNode({})}> + {props.value ?? props.children} + + + ) } class EmbedFieldNode extends EmbedChildNode { - override modifyEmbedOptions(options: EmbedOptions): void { - options.fields ??= [] - options.fields.push({ - name: this.children.findType(FieldNameNode)?.text ?? "", - value: this.children.findType(FieldValueNode)?.text ?? "", - inline: this.props.inline, - }) - } + override modifyEmbedOptions(options: EmbedOptions): void { + options.fields ??= [] + options.fields.push({ + name: this.children.findType(FieldNameNode)?.text ?? "", + value: this.children.findType(FieldValueNode)?.text ?? "", + inline: this.props.inline, + }) + } } -class FieldNameNode extends Node<{}> {} -class FieldValueNode extends Node<{}> {} +class FieldNameNode extends Node> {} +class FieldValueNode extends Node> {} diff --git a/packages/reacord/library/core/components/embed-footer.tsx b/packages/reacord/library/core/components/embed-footer.tsx index 9340592..b360185 100644 --- a/packages/reacord/library/core/components/embed-footer.tsx +++ b/packages/reacord/library/core/components/embed-footer.tsx @@ -1,45 +1,40 @@ import type { ReactNode } from "react" -import React from "react" import { ReacordElement } from "../../internal/element.js" import { Node } from "../../internal/node.js" import { EmbedChildNode } from "./embed-child.js" import type { EmbedOptions } from "./embed-options" -/** - * @category Embed - */ -export type EmbedFooterProps = { - text?: ReactNode - children?: ReactNode - iconUrl?: string - timestamp?: string | number | Date +/** @category Embed */ +export interface EmbedFooterProps { + text?: ReactNode + children?: ReactNode + iconUrl?: string + timestamp?: string | number | Date } -/** - * @category Embed - */ +/** @category Embed */ export function EmbedFooter({ text, children, ...props }: EmbedFooterProps) { - return ( - new EmbedFooterNode(props)}> - new FooterTextNode({})}> - {text ?? children} - - - ) + return ( + new EmbedFooterNode(props)}> + new FooterTextNode({})}> + {text ?? children} + + + ) } class EmbedFooterNode extends EmbedChildNode< - Omit + Omit > { - override modifyEmbedOptions(options: EmbedOptions): void { - options.footer = { - text: this.children.findType(FooterTextNode)?.text ?? "", - icon_url: this.props.iconUrl, - } - options.timestamp = this.props.timestamp - ? new Date(this.props.timestamp).toISOString() - : undefined - } + override modifyEmbedOptions(options: EmbedOptions): void { + options.footer = { + text: this.children.findType(FooterTextNode)?.text ?? "", + icon_url: this.props.iconUrl, + } + options.timestamp = this.props.timestamp + ? new Date(this.props.timestamp).toISOString() + : undefined + } } -class FooterTextNode extends Node<{}> {} +class FooterTextNode extends Node> {} diff --git a/packages/reacord/library/core/components/embed-image.tsx b/packages/reacord/library/core/components/embed-image.tsx index 3c62906..8fb628d 100644 --- a/packages/reacord/library/core/components/embed-image.tsx +++ b/packages/reacord/library/core/components/embed-image.tsx @@ -1,29 +1,24 @@ -import React from "react" import { ReacordElement } from "../../internal/element.js" import { EmbedChildNode } from "./embed-child.js" import type { EmbedOptions } from "./embed-options" -/** - * @category Embed - */ -export type EmbedImageProps = { - url: string +/** @category Embed */ +export interface EmbedImageProps { + url: string } -/** - * @category Embed - */ +/** @category Embed */ export function EmbedImage(props: EmbedImageProps) { - return ( - new EmbedImageNode(props)} - /> - ) + return ( + new EmbedImageNode(props)} + /> + ) } class EmbedImageNode extends EmbedChildNode { - override modifyEmbedOptions(options: EmbedOptions): void { - options.image = { url: this.props.url } - } + override modifyEmbedOptions(options: EmbedOptions): void { + options.image = { url: this.props.url } + } } diff --git a/packages/reacord/library/core/components/embed-options.ts b/packages/reacord/library/core/components/embed-options.ts index 91170fd..7afda57 100644 --- a/packages/reacord/library/core/components/embed-options.ts +++ b/packages/reacord/library/core/components/embed-options.ts @@ -1,8 +1,8 @@ -import type { Except, SnakeCasedPropertiesDeep } from "type-fest" import type { EmbedProps } from "./embed" +import type { Except, SnakeCasedPropertiesDeep } from "type-fest" export type EmbedOptions = SnakeCasedPropertiesDeep< - Except & { - timestamp?: string - } + Except & { + timestamp?: string + } > diff --git a/packages/reacord/library/core/components/embed-thumbnail.tsx b/packages/reacord/library/core/components/embed-thumbnail.tsx index 838ff72..483b6f9 100644 --- a/packages/reacord/library/core/components/embed-thumbnail.tsx +++ b/packages/reacord/library/core/components/embed-thumbnail.tsx @@ -1,29 +1,24 @@ -import React from "react" import { ReacordElement } from "../../internal/element.js" import { EmbedChildNode } from "./embed-child.js" import type { EmbedOptions } from "./embed-options" -/** - * @category Embed - */ -export type EmbedThumbnailProps = { - url: string +/** @category Embed */ +export interface EmbedThumbnailProps { + url: string } -/** - * @category Embed - */ +/** @category Embed */ export function EmbedThumbnail(props: EmbedThumbnailProps) { - return ( - new EmbedThumbnailNode(props)} - /> - ) + return ( + new EmbedThumbnailNode(props)} + /> + ) } class EmbedThumbnailNode extends EmbedChildNode { - override modifyEmbedOptions(options: EmbedOptions): void { - options.thumbnail = { url: this.props.url } - } + override modifyEmbedOptions(options: EmbedOptions): void { + options.thumbnail = { url: this.props.url } + } } diff --git a/packages/reacord/library/core/components/embed-title.tsx b/packages/reacord/library/core/components/embed-title.tsx index 10cb027..79e60fa 100644 --- a/packages/reacord/library/core/components/embed-title.tsx +++ b/packages/reacord/library/core/components/embed-title.tsx @@ -1,36 +1,31 @@ import type { ReactNode } from "react" -import React from "react" import { ReacordElement } from "../../internal/element.js" import { Node } from "../../internal/node.js" import { EmbedChildNode } from "./embed-child.js" import type { EmbedOptions } from "./embed-options" -/** - * @category Embed - */ -export type EmbedTitleProps = { - children: ReactNode - url?: string +/** @category Embed */ +export interface EmbedTitleProps { + children: ReactNode + url?: string } -/** - * @category Embed - */ +/** @category Embed */ export function EmbedTitle({ children, ...props }: EmbedTitleProps) { - return ( - new EmbedTitleNode(props)}> - new TitleTextNode({})}> - {children} - - - ) + return ( + new EmbedTitleNode(props)}> + new TitleTextNode({})}> + {children} + + + ) } class EmbedTitleNode extends EmbedChildNode> { - override modifyEmbedOptions(options: EmbedOptions): void { - options.title = this.children.findType(TitleTextNode)?.text ?? "" - options.url = this.props.url - } + override modifyEmbedOptions(options: EmbedOptions): void { + options.title = this.children.findType(TitleTextNode)?.text ?? "" + options.url = this.props.url + } } -class TitleTextNode extends Node<{}> {} +class TitleTextNode extends Node> {} diff --git a/packages/reacord/library/core/components/embed.tsx b/packages/reacord/library/core/components/embed.tsx index 4dabe53..78f52f9 100644 --- a/packages/reacord/library/core/components/embed.tsx +++ b/packages/reacord/library/core/components/embed.tsx @@ -12,19 +12,19 @@ import type { EmbedOptions } from "./embed-options" * @category Embed * @see https://discord.com/developers/docs/resources/channel#embed-object */ -export type EmbedProps = { - title?: string - description?: string - url?: string - color?: number - fields?: Array<{ name: string; value: string; inline?: boolean }> - author?: { name: string; url?: string; iconUrl?: string } - thumbnail?: { url: string } - image?: { url: string } - video?: { url: string } - footer?: { text: string; iconUrl?: string } - timestamp?: string | number | Date - children?: React.ReactNode +export interface EmbedProps { + title?: string + description?: string + url?: string + color?: number + fields?: { name: string; value: string; inline?: boolean }[] + author?: { name: string; url?: string; iconUrl?: string } + thumbnail?: { url: string } + image?: { url: string } + video?: { url: string } + footer?: { text: string; iconUrl?: string } + timestamp?: string | number | Date + children?: React.ReactNode } /** @@ -32,31 +32,31 @@ export type EmbedProps = { * @see https://discord.com/developers/docs/resources/channel#embed-object */ export function Embed(props: EmbedProps) { - return ( - new EmbedNode(props)}> - {props.children} - - ) + return ( + new EmbedNode(props)}> + {props.children} + + ) } class EmbedNode extends Node { - override modifyMessageOptions(options: MessageOptions): void { - const embed: EmbedOptions = { - ...snakeCaseDeep(omit(this.props, ["children", "timestamp"])), - timestamp: this.props.timestamp - ? new Date(this.props.timestamp).toISOString() - : undefined, - } + override modifyMessageOptions(options: MessageOptions): void { + const embed: EmbedOptions = { + ...snakeCaseDeep(omit(this.props, ["children", "timestamp"])), + timestamp: this.props.timestamp + ? new Date(this.props.timestamp).toISOString() + : undefined, + } - for (const child of this.children) { - if (child instanceof EmbedChildNode) { - child.modifyEmbedOptions(embed) - } - if (child instanceof TextNode) { - embed.description = (embed.description || "") + child.props - } - } + for (const child of this.children) { + if (child instanceof EmbedChildNode) { + child.modifyEmbedOptions(embed) + } + if (child instanceof TextNode) { + embed.description = (embed.description ?? "") + child.props + } + } - options.embeds.push(embed) - } + options.embeds.push(embed) + } } diff --git a/packages/reacord/library/core/components/link.tsx b/packages/reacord/library/core/components/link.tsx index 990f75f..d459ef1 100644 --- a/packages/reacord/library/core/components/link.tsx +++ b/packages/reacord/library/core/components/link.tsx @@ -1,43 +1,38 @@ -import React from "react" import { ReacordElement } from "../../internal/element.js" import type { MessageOptions } from "../../internal/message" import { getNextActionRow } from "../../internal/message" import { Node } from "../../internal/node.js" import type { ButtonSharedProps } from "./button-shared-props" -/** - * @category Link - */ +/** @category Link */ export type LinkProps = ButtonSharedProps & { - /** The URL the link should lead to */ - url: string - /** The link text */ - children?: string + /** The URL the link should lead to */ + url: string + /** The link text */ + children?: string } -/** - * @category Link - */ +/** @category Link */ export function Link({ label, children, ...props }: LinkProps) { - return ( - new LinkNode(props)}> - new LinkTextNode({})}> - {label || children} - - - ) + return ( + new LinkNode(props)}> + new LinkTextNode({})}> + {label ?? children} + + + ) } class LinkNode extends Node> { - override modifyMessageOptions(options: MessageOptions): void { - getNextActionRow(options).push({ - type: "link", - disabled: this.props.disabled, - emoji: this.props.emoji, - label: this.children.findType(LinkTextNode)?.text, - url: this.props.url, - }) - } + override modifyMessageOptions(options: MessageOptions): void { + getNextActionRow(options).push({ + type: "link", + disabled: this.props.disabled, + emoji: this.props.emoji, + label: this.children.findType(LinkTextNode)?.text, + url: this.props.url, + }) + } } -class LinkTextNode extends Node<{}> {} +class LinkTextNode extends Node> {} diff --git a/packages/reacord/library/core/components/option-node.ts b/packages/reacord/library/core/components/option-node.ts index 067aeec..38cf0b4 100644 --- a/packages/reacord/library/core/components/option-node.ts +++ b/packages/reacord/library/core/components/option-node.ts @@ -3,17 +3,17 @@ import { Node } from "../../internal/node" import type { OptionProps } from "./option" export class OptionNode extends Node< - Omit + Omit > { - get options(): MessageSelectOptionOptions { - return { - label: this.children.findType(OptionLabelNode)?.text ?? this.props.value, - value: this.props.value, - description: this.children.findType(OptionDescriptionNode)?.text, - emoji: this.props.emoji, - } - } + get options(): MessageSelectOptionOptions { + return { + label: this.children.findType(OptionLabelNode)?.text ?? this.props.value, + value: this.props.value, + description: this.children.findType(OptionDescriptionNode)?.text, + emoji: this.props.emoji, + } + } } -export class OptionLabelNode extends Node<{}> {} -export class OptionDescriptionNode extends Node<{}> {} +export class OptionLabelNode extends Node> {} +export class OptionDescriptionNode extends Node> {} diff --git a/packages/reacord/library/core/components/option.tsx b/packages/reacord/library/core/components/option.tsx index ad060ef..84ff257 100644 --- a/packages/reacord/library/core/components/option.tsx +++ b/packages/reacord/library/core/components/option.tsx @@ -1,62 +1,56 @@ import type { ReactNode } from "react" -import React from "react" import { ReacordElement } from "../../internal/element" import { - OptionDescriptionNode, - OptionLabelNode, - OptionNode, + OptionDescriptionNode, + OptionLabelNode, + OptionNode, } from "./option-node" -/** - * @category Select - */ -export type OptionProps = { - /** The internal value of this option */ - value: string - /** The text shown to the user. This takes priority over `children` */ - label?: ReactNode - /** The text shown to the user */ - children?: ReactNode - /** Description for the option, shown to the user */ - description?: ReactNode +/** @category Select */ +export interface OptionProps { + /** The internal value of this option */ + value: string + /** The text shown to the user. This takes priority over `children` */ + label?: ReactNode + /** The text shown to the user */ + children?: ReactNode + /** Description for the option, shown to the user */ + description?: ReactNode - /** - * Renders an emoji to the left of the text. - * - * Has to be a literal emoji character (e.g. 🍍), - * or an emoji code, like `<:plus_one:778531744860602388>`. - * - * To get an emoji code, type your emoji in Discord chat - * with a backslash `\` in front. - * The bot has to be in the emoji's guild to use it. - */ - emoji?: string + /** + * Renders an emoji to the left of the text. + * + * Has to be a literal emoji character (e.g. 🍍), or an emoji code, like + * `<:plus_one:778531744860602388>`. + * + * To get an emoji code, type your emoji in Discord chat with a backslash `\` + * in front. The bot has to be in the emoji's guild to use it. + */ + emoji?: string } -/** - * @category Select - */ +/** @category Select */ export function Option({ - label, - children, - description, - ...props + label, + children, + description, + ...props }: OptionProps) { - return ( - new OptionNode(props)}> - {(label !== undefined || children !== undefined) && ( - new OptionLabelNode({})}> - {label || children} - - )} - {description !== undefined && ( - new OptionDescriptionNode({})} - > - {description} - - )} - - ) + return ( + new OptionNode(props)}> + {(label !== undefined || children !== undefined) && ( + new OptionLabelNode({})}> + {label ?? children} + + )} + {description !== undefined && ( + new OptionDescriptionNode({})} + > + {description} + + )} + + ) } diff --git a/packages/reacord/library/core/components/select.tsx b/packages/reacord/library/core/components/select.tsx index e736476..c937444 100644 --- a/packages/reacord/library/core/components/select.tsx +++ b/packages/reacord/library/core/components/select.tsx @@ -1,153 +1,150 @@ import { isInstanceOf } from "@reacord/helpers/is-instance-of" import { randomUUID } from "node:crypto" import type { ReactNode } from "react" -import React from "react" import { ReacordElement } from "../../internal/element.js" import type { ComponentInteraction } from "../../internal/interaction" import type { - ActionRow, - ActionRowItem, - MessageOptions, + ActionRow, + ActionRowItem, + MessageOptions, } from "../../internal/message" import { Node } from "../../internal/node.js" import type { ComponentEvent } from "../component-event" import { OptionNode } from "./option-node" -/** - * @category Select - */ -export type SelectProps = { - children?: ReactNode - /** Sets the currently selected value */ - value?: string +/** @category Select */ +export interface SelectProps { + children?: ReactNode + /** Sets the currently selected value */ + value?: string - /** Sets the currently selected values, for use with `multiple` */ - values?: string[] + /** Sets the currently selected values, for use with `multiple` */ + values?: string[] - /** The text shown when no value is selected */ - placeholder?: string + /** The text shown when no value is selected */ + placeholder?: string - /** Set to true to allow multiple selected values */ - multiple?: boolean + /** Set to true to allow multiple selected values */ + multiple?: boolean - /** - * With `multiple`, the minimum number of values that can be selected. - * When `multiple` is false or not defined, this is always 1. - * - * This only limits the number of values that can be received by the user. - * This does not limit the number of values that can be displayed by you. - */ - minValues?: number + /** + * With `multiple`, the minimum number of values that can be selected. When + * `multiple` is false or not defined, this is always 1. + * + * This only limits the number of values that can be received by the user. + * This does not limit the number of values that can be displayed by you. + */ + minValues?: number - /** - * With `multiple`, the maximum number of values that can be selected. - * When `multiple` is false or not defined, this is always 1. - * - * This only limits the number of values that can be received by the user. - * This does not limit the number of values that can be displayed by you. - */ - maxValues?: number + /** + * With `multiple`, the maximum number of values that can be selected. When + * `multiple` is false or not defined, this is always 1. + * + * This only limits the number of values that can be received by the user. + * This does not limit the number of values that can be displayed by you. + */ + maxValues?: number - /** When true, the select will be slightly faded, and cannot be interacted with. */ - disabled?: boolean + /** + * When true, the select will be slightly faded, and cannot be interacted + * with. + */ + disabled?: boolean - /** - * Called when the user inputs a selection. - * Receives the entire select change event, - * which can be used to create new replies, etc. - */ - onChange?: (event: SelectChangeEvent) => void + /** + * Called when the user inputs a selection. Receives the entire select change + * event, which can be used to create new replies, etc. + */ + onChange?: (event: SelectChangeEvent) => void - /** - * Convenience shorthand for `onChange`, which receives the first selected value. - */ - onChangeValue?: (value: string, event: SelectChangeEvent) => void + /** + * Convenience shorthand for `onChange`, which receives the first selected + * value. + */ + onChangeValue?: (value: string, event: SelectChangeEvent) => void - /** - * Convenience shorthand for `onChange`, which receives all selected values. - */ - onChangeMultiple?: (values: string[], event: SelectChangeEvent) => void + /** Convenience shorthand for `onChange`, which receives all selected values. */ + onChangeMultiple?: (values: string[], event: SelectChangeEvent) => void } -/** - * @category Select - */ +/** @category Select */ export type SelectChangeEvent = ComponentEvent & { - values: string[] + values: string[] } /** * See [the select menu guide](/guides/select-menu) for a usage example. + * * @category Select */ export function Select(props: SelectProps) { - return ( - new SelectNode(props)}> - {props.children} - - ) + return ( + new SelectNode(props)}> + {props.children} + + ) } class SelectNode extends Node { - readonly customId = randomUUID() + readonly customId = randomUUID() - override modifyMessageOptions(message: MessageOptions): void { - const actionRow: ActionRow = [] - message.actionRows.push(actionRow) + override modifyMessageOptions(message: MessageOptions): void { + const actionRow: ActionRow = [] + message.actionRows.push(actionRow) - const options = [...this.children] - .filter(isInstanceOf(OptionNode)) - .map((node) => node.options) + const options = [...this.children] + .filter(isInstanceOf(OptionNode)) + .map((node) => node.options) - const { - multiple, - value, - values, - minValues = 0, - maxValues = 25, - children, - onChange, - onChangeValue, - onChangeMultiple, - ...props - } = this.props + const { + multiple, + value, + values, + minValues = 0, + maxValues = 25, + children, + onChange, + onChangeValue, + onChangeMultiple, + ...props + } = this.props - const item: ActionRowItem = { - ...props, - type: "select", - customId: this.customId, - options, - values: [], - } + const item: ActionRowItem = { + ...props, + type: "select", + customId: this.customId, + options, + values: [], + } - if (multiple) { - item.minValues = minValues - item.maxValues = maxValues - if (values) item.values = values - } + if (multiple) { + item.minValues = minValues + item.maxValues = maxValues + if (values) item.values = values + } - if (!multiple && value != undefined) { - item.values = [value] - } + if (!multiple && value != undefined) { + item.values = [value] + } - actionRow.push(item) - } + actionRow.push(item) + } - override handleComponentInteraction( - interaction: ComponentInteraction, - ): boolean { - const isSelectInteraction = - interaction.type === "select" && - interaction.customId === this.customId && - !this.props.disabled + override handleComponentInteraction( + interaction: ComponentInteraction, + ): boolean { + const isSelectInteraction = + interaction.type === "select" && + interaction.customId === this.customId && + !this.props.disabled - if (!isSelectInteraction) return false + if (!isSelectInteraction) return false - this.props.onChange?.(interaction.event) - this.props.onChangeMultiple?.(interaction.event.values, interaction.event) - if (interaction.event.values[0]) { - this.props.onChangeValue?.(interaction.event.values[0], interaction.event) - } - return true - } + this.props.onChange?.(interaction.event) + this.props.onChangeMultiple?.(interaction.event.values, interaction.event) + if (interaction.event.values[0]) { + this.props.onChangeValue?.(interaction.event.values[0], interaction.event) + } + return true + } } diff --git a/packages/reacord/library/core/instance-context.tsx b/packages/reacord/library/core/instance-context.tsx index 0e62a39..410d79e 100644 --- a/packages/reacord/library/core/instance-context.tsx +++ b/packages/reacord/library/core/instance-context.tsx @@ -1,6 +1,6 @@ -import { raise } from "@reacord/helpers/raise" +import type { ReacordInstance } from "./instance.js" +import { raise } from "@reacord/helpers/raise.js" import * as React from "react" -import type { ReacordInstance } from "./instance" const Context = React.createContext(undefined) @@ -13,8 +13,8 @@ export const InstanceProvider = Context.Provider * @see https://reacord.mapleleaf.dev/guides/use-instance */ export function useInstance(): ReacordInstance { - return ( - React.useContext(Context) ?? - raise("Could not find instance, was this component rendered via Reacord?") - ) + return ( + React.useContext(Context) ?? + raise("Could not find instance, was this component rendered via Reacord?") + ) } diff --git a/packages/reacord/library/core/instance.ts b/packages/reacord/library/core/instance.ts index d0aa740..4c6eca3 100644 --- a/packages/reacord/library/core/instance.ts +++ b/packages/reacord/library/core/instance.ts @@ -2,18 +2,19 @@ import type { ReactNode } from "react" /** * Represents an interactive message, which can later be replaced or deleted. + * * @category Core */ -export type ReacordInstance = { - /** Render some JSX to this instance (edits the message) */ - render: (content: ReactNode) => void +export interface ReacordInstance { + /** Render some JSX to this instance (edits the message) */ + render: (content: ReactNode) => void - /** Remove this message */ - destroy: () => void + /** Remove this message */ + destroy: () => void - /** - * Same as destroy, but keeps the message and disables the components on it. - * This prevents it from listening to user interactions. - */ - deactivate: () => void + /** + * Same as destroy, but keeps the message and disables the components on it. + * This prevents it from listening to user interactions. + */ + deactivate: () => void } diff --git a/packages/reacord/library/core/reacord-discord-js.ts b/packages/reacord/library/core/reacord-discord-js.ts index ad988c3..13269ea 100644 --- a/packages/reacord/library/core/reacord-discord-js.ts +++ b/packages/reacord/library/core/reacord-discord-js.ts @@ -1,4 +1,4 @@ -/* eslint-disable class-methods-use-this */ +import { safeJsonStringify } from "@reacord/helpers/json" import { pick } from "@reacord/helpers/pick" import { pruneNullishValues } from "@reacord/helpers/prune-nullish-values" import { raise } from "@reacord/helpers/raise" @@ -7,18 +7,18 @@ import type { ReactNode } from "react" import type { Except } from "type-fest" import type { ComponentInteraction } from "../internal/interaction" import type { - Message, - MessageButtonOptions, - MessageOptions, + Message, + MessageButtonOptions, + MessageOptions, } from "../internal/message" import { ChannelMessageRenderer } from "../internal/renderers/channel-message-renderer" import { InteractionReplyRenderer } from "../internal/renderers/interaction-reply-renderer" import type { - ChannelInfo, - GuildInfo, - GuildMemberInfo, - MessageInfo, - UserInfo, + ChannelInfo, + GuildInfo, + GuildMemberInfo, + MessageInfo, + UserInfo, } from "./component-event" import type { ReacordInstance } from "./instance" import type { ReacordConfig } from "./reacord" @@ -26,364 +26,377 @@ import { Reacord } from "./reacord" /** * The Reacord adapter for Discord.js. + * * @category Core */ export class ReacordDiscordJs extends Reacord { - constructor(private client: Discord.Client, config: ReacordConfig = {}) { - super(config) + constructor( + private client: Discord.Client, + config: ReacordConfig = {}, + ) { + super(config) - client.on("interactionCreate", (interaction) => { - if (interaction.isButton() || interaction.isSelectMenu()) { - this.handleComponentInteraction( - this.createReacordComponentInteraction(interaction), - ) - } - }) - } + client.on("interactionCreate", (interaction) => { + if (interaction.isButton() || interaction.isSelectMenu()) { + this.handleComponentInteraction( + this.createReacordComponentInteraction(interaction), + ) + } + }) + } - /** - * Sends a message to a channel. - * @see https://reacord.mapleleaf.dev/guides/sending-messages - */ - override send( - channelId: string, - initialContent?: React.ReactNode, - ): ReacordInstance { - return this.createInstance( - this.createChannelRenderer(channelId), - initialContent, - ) - } + /** + * Sends a message to a channel. + * + * @see https://reacord.mapleleaf.dev/guides/sending-messages + */ + override send( + channelId: string, + initialContent?: React.ReactNode, + ): ReacordInstance { + return this.createInstance( + this.createChannelRenderer(channelId), + initialContent, + ) + } - /** - * Sends a message as a reply to a command interaction. - * @see https://reacord.mapleleaf.dev/guides/sending-messages - */ - override reply( - interaction: Discord.CommandInteraction, - initialContent?: React.ReactNode, - ): ReacordInstance { - return this.createInstance( - this.createInteractionReplyRenderer(interaction), - initialContent, - ) - } + /** + * Sends a message as a reply to a command interaction. + * + * @see https://reacord.mapleleaf.dev/guides/sending-messages + */ + override reply( + interaction: Discord.CommandInteraction, + initialContent?: React.ReactNode, + ): ReacordInstance { + return this.createInstance( + this.createInteractionReplyRenderer(interaction), + initialContent, + ) + } - /** - * Sends an ephemeral message as a reply to a command interaction. - * @see https://reacord.mapleleaf.dev/guides/sending-messages - */ - override ephemeralReply( - interaction: Discord.CommandInteraction, - initialContent?: React.ReactNode, - ): ReacordInstance { - return this.createInstance( - this.createEphemeralInteractionReplyRenderer(interaction), - initialContent, - ) - } + /** + * Sends an ephemeral message as a reply to a command interaction. + * + * @see https://reacord.mapleleaf.dev/guides/sending-messages + */ + override ephemeralReply( + interaction: Discord.CommandInteraction, + initialContent?: React.ReactNode, + ): ReacordInstance { + return this.createInstance( + this.createEphemeralInteractionReplyRenderer(interaction), + initialContent, + ) + } - private createChannelRenderer(channelId: string) { - return new ChannelMessageRenderer({ - send: async (options) => { - const channel = - this.client.channels.cache.get(channelId) ?? - (await this.client.channels.fetch(channelId)) ?? - raise(`Channel ${channelId} not found`) + private createChannelRenderer(channelId: string) { + return new ChannelMessageRenderer({ + send: async (options) => { + const channel = + this.client.channels.cache.get(channelId) ?? + (await this.client.channels.fetch(channelId)) ?? + raise(`Channel ${channelId} not found`) - if (!channel.isTextBased()) { - raise(`Channel ${channelId} is not a text channel`) - } + if (!channel.isTextBased()) { + raise(`Channel ${channelId} is not a text channel`) + } - const message = await channel.send(getDiscordMessageOptions(options)) - return createReacordMessage(message) - }, - }) - } + const message = await channel.send(getDiscordMessageOptions(options)) + return createReacordMessage(message) + }, + }) + } - private createInteractionReplyRenderer( - interaction: - | Discord.CommandInteraction - | Discord.MessageComponentInteraction, - ) { - return new InteractionReplyRenderer({ - type: "command", - id: interaction.id, - reply: async (options) => { - const message = await interaction.reply({ - ...getDiscordMessageOptions(options), - fetchReply: true, - }) - return createReacordMessage(message as Discord.Message) - }, - followUp: async (options) => { - const message = await interaction.followUp({ - ...getDiscordMessageOptions(options), - fetchReply: true, - }) - return createReacordMessage(message as Discord.Message) - }, - }) - } + private createInteractionReplyRenderer( + interaction: + | Discord.CommandInteraction + | Discord.MessageComponentInteraction, + ) { + return new InteractionReplyRenderer({ + type: "command", + id: interaction.id, + reply: async (options) => { + const message = await interaction.reply({ + ...getDiscordMessageOptions(options), + fetchReply: true, + }) + return createReacordMessage(message) + }, + followUp: async (options) => { + const message = await interaction.followUp({ + ...getDiscordMessageOptions(options), + fetchReply: true, + }) + return createReacordMessage(message) + }, + }) + } - private createEphemeralInteractionReplyRenderer( - interaction: - | Discord.CommandInteraction - | Discord.MessageComponentInteraction, - ) { - return new InteractionReplyRenderer({ - type: "command", - id: interaction.id, - reply: async (options) => { - await interaction.reply({ - ...getDiscordMessageOptions(options), - ephemeral: true, - }) - return createEphemeralReacordMessage() - }, - followUp: async (options) => { - await interaction.followUp({ - ...getDiscordMessageOptions(options), - ephemeral: true, - }) - return createEphemeralReacordMessage() - }, - }) - } + private createEphemeralInteractionReplyRenderer( + interaction: + | Discord.CommandInteraction + | Discord.MessageComponentInteraction, + ) { + return new InteractionReplyRenderer({ + type: "command", + id: interaction.id, + reply: async (options) => { + await interaction.reply({ + ...getDiscordMessageOptions(options), + ephemeral: true, + }) + return createEphemeralReacordMessage() + }, + followUp: async (options) => { + await interaction.followUp({ + ...getDiscordMessageOptions(options), + ephemeral: true, + }) + return createEphemeralReacordMessage() + }, + }) + } - private createReacordComponentInteraction( - interaction: Discord.MessageComponentInteraction, - ): ComponentInteraction { - // todo please dear god clean this up - const channel: ChannelInfo = interaction.channel - ? { - ...pruneNullishValues( - pick(interaction.channel, [ - "topic", - "nsfw", - "lastMessageId", - "ownerId", - "parentId", - "rateLimitPerUser", - ]), - ), - id: interaction.channelId, - } - : raise("Non-channel interactions are not supported") + private createReacordComponentInteraction( + interaction: Discord.MessageComponentInteraction, + ): ComponentInteraction { + // todo please dear god clean this up + const channel: ChannelInfo = interaction.channel + ? { + ...pruneNullishValues( + pick(interaction.channel, [ + "topic", + "nsfw", + "lastMessageId", + "ownerId", + "parentId", + "rateLimitPerUser", + ]), + ), + id: interaction.channelId, + } + : raise("Non-channel interactions are not supported") - const message: MessageInfo = - interaction.message instanceof Discord.Message - ? { - ...pick(interaction.message, [ - "id", - "channelId", - "authorId", - "content", - "tts", - "mentionEveryone", - ]), - timestamp: new Date( - interaction.message.createdTimestamp, - ).toISOString(), - editedTimestamp: interaction.message.editedTimestamp - ? new Date(interaction.message.editedTimestamp).toISOString() - : undefined, - mentions: interaction.message.mentions.users.map((u) => u.id), - } - : raise("Message not found") + const message: MessageInfo = + interaction.message instanceof Discord.Message + ? { + ...pick(interaction.message, [ + "id", + "channelId", + "authorId", + "content", + "tts", + "mentionEveryone", + ]), + timestamp: new Date( + interaction.message.createdTimestamp, + ).toISOString(), + editedTimestamp: interaction.message.editedTimestamp + ? new Date(interaction.message.editedTimestamp).toISOString() + : undefined, + mentions: interaction.message.mentions.users.map((u) => u.id), + authorId: interaction.message.author.id, + mentionEveryone: interaction.message.mentions.everyone, + } + : raise("Message not found") - const member: GuildMemberInfo | undefined = - interaction.member instanceof Discord.GuildMember - ? { - ...pruneNullishValues( - pick(interaction.member, [ - "id", - "nick", - "displayName", - "avatarUrl", - "displayAvatarUrl", - "color", - "pending", - ]), - ), - displayName: interaction.member.displayName, - roles: interaction.member.roles.cache.map((role) => role.id), - joinedAt: interaction.member.joinedAt?.toISOString(), - premiumSince: interaction.member.premiumSince?.toISOString(), - communicationDisabledUntil: - interaction.member.communicationDisabledUntil?.toISOString(), - } - : undefined + const member: GuildMemberInfo | undefined = + interaction.member instanceof Discord.GuildMember + ? { + ...pruneNullishValues( + pick(interaction.member, [ + "id", + "nick", + "displayName", + "avatarUrl", + "displayAvatarUrl", + "color", + "pending", + ]), + ), + displayName: interaction.member.displayName, + roles: interaction.member.roles.cache.map((role) => role.id), + joinedAt: interaction.member.joinedAt?.toISOString(), + premiumSince: interaction.member.premiumSince?.toISOString(), + communicationDisabledUntil: + interaction.member.communicationDisabledUntil?.toISOString(), + color: interaction.member.displayColor, + displayAvatarUrl: interaction.member.displayAvatarURL(), + } + : undefined - const guild: GuildInfo | undefined = interaction.guild - ? { - ...pruneNullishValues(pick(interaction.guild, ["id", "name"])), - member: member ?? raise("unexpected: member is undefined"), - } - : undefined + const guild: GuildInfo | undefined = interaction.guild + ? { + ...pruneNullishValues(pick(interaction.guild, ["id", "name"])), + member: member ?? raise("unexpected: member is undefined"), + } + : undefined - const user: UserInfo = { - ...pruneNullishValues( - pick(interaction.user, ["id", "username", "discriminator", "tag"]), - ), - avatarUrl: interaction.user.avatarURL()!, - accentColor: interaction.user.accentColor ?? undefined, - } + const user: UserInfo = { + ...pruneNullishValues( + pick(interaction.user, ["id", "username", "discriminator", "tag"]), + ), + avatarUrl: interaction.user.avatarURL()!, + accentColor: interaction.user.accentColor ?? undefined, + } - const baseProps: Except = { - id: interaction.id, - customId: interaction.customId, - update: async (options: MessageOptions) => { - await interaction.update(getDiscordMessageOptions(options)) - }, - deferUpdate: async () => { - if (interaction.replied || interaction.deferred) return - await interaction.deferUpdate() - }, - reply: async (options) => { - const message = await interaction.reply({ - ...getDiscordMessageOptions(options), - fetchReply: true, - }) - return createReacordMessage(message as Discord.Message) - }, - followUp: async (options) => { - const message = await interaction.followUp({ - ...getDiscordMessageOptions(options), - fetchReply: true, - }) - return createReacordMessage(message as Discord.Message) - }, - event: { - channel, - message, - user, - guild, + const baseProps: Except = { + id: interaction.id, + customId: interaction.customId, + update: async (options: MessageOptions) => { + await interaction.update(getDiscordMessageOptions(options)) + }, + deferUpdate: async () => { + if (interaction.replied || interaction.deferred) return + await interaction.deferUpdate() + }, + reply: async (options) => { + const message = await interaction.reply({ + ...getDiscordMessageOptions(options), + fetchReply: true, + }) + return createReacordMessage(message) + }, + followUp: async (options) => { + const message = await interaction.followUp({ + ...getDiscordMessageOptions(options), + fetchReply: true, + }) + return createReacordMessage(message) + }, + event: { + channel, + message, + user, + guild, - reply: (content?: ReactNode) => - this.createInstance( - this.createInteractionReplyRenderer(interaction), - content, - ), + reply: (content?: ReactNode) => + this.createInstance( + this.createInteractionReplyRenderer(interaction), + content, + ), - ephemeralReply: (content: ReactNode) => - this.createInstance( - this.createEphemeralInteractionReplyRenderer(interaction), - content, - ), - }, - } + ephemeralReply: (content: ReactNode) => + this.createInstance( + this.createEphemeralInteractionReplyRenderer(interaction), + content, + ), + }, + } - if (interaction.isButton()) { - return { - ...baseProps, - type: "button", - } - } + if (interaction.isButton()) { + return { + ...baseProps, + type: "button", + } + } - if (interaction.isSelectMenu()) { - return { - ...baseProps, - type: "select", - event: { - ...baseProps.event, - values: interaction.values, - }, - } - } + if (interaction.isSelectMenu()) { + return { + ...baseProps, + type: "select", + event: { + ...baseProps.event, + values: interaction.values, + }, + } + } - raise(`Unsupported component interaction type: ${interaction.type}`) - } + raise(`Unsupported component interaction type: ${interaction.type}`) + } } function createReacordMessage(message: Discord.Message): Message { - return { - edit: async (options) => { - await message.edit(getDiscordMessageOptions(options)) - }, - delete: async () => { - await message.delete() - }, - } + return { + edit: async (options) => { + await message.edit(getDiscordMessageOptions(options)) + }, + delete: async () => { + await message.delete() + }, + } } function createEphemeralReacordMessage(): Message { - return { - edit: () => { - console.warn("Ephemeral messages can't be edited") - return Promise.resolve() - }, - delete: () => { - console.warn("Ephemeral messages can't be deleted") - return Promise.resolve() - }, - } + return { + edit: () => { + console.warn("Ephemeral messages can't be edited") + return Promise.resolve() + }, + delete: () => { + console.warn("Ephemeral messages can't be deleted") + return Promise.resolve() + }, + } } function convertButtonStyleToEnum(style: MessageButtonOptions["style"]) { - const styleMap = { - primary: Discord.ButtonStyle.Primary, - secondary: Discord.ButtonStyle.Secondary, - success: Discord.ButtonStyle.Success, - danger: Discord.ButtonStyle.Danger, - } as const + const styleMap = { + primary: Discord.ButtonStyle.Primary, + secondary: Discord.ButtonStyle.Secondary, + success: Discord.ButtonStyle.Success, + danger: Discord.ButtonStyle.Danger, + } as const - return styleMap[style ?? "secondary"] + return styleMap[style ?? "secondary"] } // TODO: this could be a part of the core library, // and also handle some edge cases, e.g. empty messages function getDiscordMessageOptions(reacordOptions: MessageOptions) { - const options = { - // eslint-disable-next-line unicorn/no-null - content: reacordOptions.content || null, - embeds: reacordOptions.embeds, - components: reacordOptions.actionRows.map((row) => ({ - type: Discord.ComponentType.ActionRow, - components: row.map( - (component): Discord.MessageActionRowComponentData => { - if (component.type === "button") { - return { - type: Discord.ComponentType.Button, - customId: component.customId, - label: component.label ?? "", - style: convertButtonStyleToEnum(component.style), - disabled: component.disabled, - emoji: component.emoji, - } - } + const options = { + content: reacordOptions.content || undefined, + embeds: reacordOptions.embeds, + components: reacordOptions.actionRows.map((row) => ({ + type: Discord.ComponentType.ActionRow, + components: row.map( + (component): Discord.MessageActionRowComponentData => { + if (component.type === "button") { + return { + type: Discord.ComponentType.Button, + customId: component.customId, + label: component.label ?? "", + style: convertButtonStyleToEnum(component.style), + disabled: component.disabled, + emoji: component.emoji, + } + } - if (component.type === "link") { - return { - type: Discord.ComponentType.Button, - url: component.url, - label: component.label ?? "", - style: Discord.ButtonStyle.Link, - disabled: component.disabled, - emoji: component.emoji, - } - } + if (component.type === "link") { + return { + type: Discord.ComponentType.Button, + url: component.url, + label: component.label ?? "", + style: Discord.ButtonStyle.Link, + disabled: component.disabled, + emoji: component.emoji, + } + } - if (component.type === "select") { - return { - ...component, - type: Discord.ComponentType.SelectMenu, - options: component.options.map((option) => ({ - ...option, - default: component.values?.includes(option.value), - })), - } - } + if (component.type === "select") { + return { + ...component, + type: Discord.ComponentType.SelectMenu, + options: component.options.map((option) => ({ + ...option, + default: component.values?.includes(option.value), + })), + } + } - raise(`Unsupported component type: ${(component as any).type}`) - }, - ), - })), - } + component satisfies never + throw new Error( + `Invalid component type ${safeJsonStringify(component)}}`, + ) + }, + ), + })), + } - if (!options.content && !options.embeds?.length) { - options.content = "_ _" - } + if (!options.content && !options.embeds?.length) { + options.content = "_ _" + } - return options + return options } diff --git a/packages/reacord/library/core/reacord.tsx b/packages/reacord/library/core/reacord.tsx index 1dfede6..5cb3bf0 100644 --- a/packages/reacord/library/core/reacord.tsx +++ b/packages/reacord/library/core/reacord.tsx @@ -1,91 +1,85 @@ import type { ReactNode } from "react" -import React from "react" -import type { ComponentInteraction } from "../internal/interaction" +import type { ComponentInteraction } from "../internal/interaction.js" import { reconciler } from "../internal/reconciler.js" -import type { Renderer } from "../internal/renderers/renderer" -import type { ReacordInstance } from "./instance" -import { InstanceProvider } from "./instance-context" +import type { Renderer } from "../internal/renderers/renderer.js" +import { InstanceProvider } from "./instance-context.js" +import type { ReacordInstance } from "./instance.js" -/** - * @category Core - */ -export type ReacordConfig = { - /** - * The max number of active instances. - * When this limit is exceeded, the oldest instances will be disabled. - */ - maxInstances?: number +/** @category Core */ +export interface ReacordConfig { + /** + * The max number of active instances. When this limit is exceeded, the oldest + * instances will be disabled. + */ + maxInstances?: number } /** - * The main Reacord class that other Reacord adapters should extend. - * Only use this directly if you're making [a custom adapter](/guides/custom-adapters). + * The main Reacord class that other Reacord adapters should extend. Only use + * this directly if you're making [a custom adapter](/guides/custom-adapters). */ export abstract class Reacord { - private renderers: Renderer[] = [] + private renderers: Renderer[] = [] - constructor(private readonly config: ReacordConfig = {}) {} + constructor(private readonly config: ReacordConfig = {}) {} - abstract send(...args: unknown[]): ReacordInstance - abstract reply(...args: unknown[]): ReacordInstance - abstract ephemeralReply(...args: unknown[]): ReacordInstance + abstract send(...args: unknown[]): ReacordInstance + abstract reply(...args: unknown[]): ReacordInstance + abstract ephemeralReply(...args: unknown[]): ReacordInstance - protected handleComponentInteraction(interaction: ComponentInteraction) { - for (const renderer of this.renderers) { - if (renderer.handleComponentInteraction(interaction)) return - } - } + protected handleComponentInteraction(interaction: ComponentInteraction) { + for (const renderer of this.renderers) { + if (renderer.handleComponentInteraction(interaction)) return + } + } - private get maxInstances() { - return this.config.maxInstances ?? 50 - } + private get maxInstances() { + return this.config.maxInstances ?? 50 + } - protected createInstance(renderer: Renderer, initialContent?: ReactNode) { - if (this.renderers.length > this.maxInstances) { - this.deactivate(this.renderers[0]!) - } + protected createInstance(renderer: Renderer, initialContent?: ReactNode) { + if (this.renderers.length > this.maxInstances) { + this.deactivate(this.renderers[0]!) + } - this.renderers.push(renderer) + this.renderers.push(renderer) - const container = reconciler.createContainer( - renderer, - 0, - // eslint-disable-next-line unicorn/no-null - null, - false, - // eslint-disable-next-line unicorn/no-null - null, - "reacord", - () => {}, - // eslint-disable-next-line unicorn/no-null - null, - ) + const container = reconciler.createContainer( + renderer, + 0, + null, + false, + null, + "reacord", + () => {}, + null, + ) - const instance: ReacordInstance = { - render: (content: ReactNode) => { - reconciler.updateContainer( - {content}, - container, - ) - }, - deactivate: () => { - this.deactivate(renderer) - }, - destroy: () => { - this.renderers = this.renderers.filter((it) => it !== renderer) - renderer.destroy() - }, - } + const instance: ReacordInstance = { + render: (content: ReactNode) => { + reconciler.updateContainer( + {content}, + container, + ) + }, + deactivate: () => { + this.deactivate(renderer) + }, + destroy: () => { + this.renderers = this.renderers.filter((it) => it !== renderer) + renderer.destroy() + }, + } - if (initialContent !== undefined) { - instance.render(initialContent) - } + if (initialContent !== undefined) { + instance.render(initialContent) + } - return instance - } + return instance + } - private deactivate(renderer: Renderer) { - this.renderers = this.renderers.filter((it) => it !== renderer) - renderer.deactivate() - } + private deactivate(renderer: Renderer) { + this.renderers = this.renderers.filter((it) => it !== renderer) + renderer.deactivate() + } } diff --git a/packages/reacord/library/internal/channel.ts b/packages/reacord/library/internal/channel.ts index b574496..b6d851b 100644 --- a/packages/reacord/library/internal/channel.ts +++ b/packages/reacord/library/internal/channel.ts @@ -1,5 +1,5 @@ import type { Message, MessageOptions } from "./message" -export type Channel = { - send(message: MessageOptions): Promise +export interface Channel { + send(message: MessageOptions): Promise } diff --git a/packages/reacord/library/internal/container.ts b/packages/reacord/library/internal/container.ts index 8941fcd..0a8fcc4 100644 --- a/packages/reacord/library/internal/container.ts +++ b/packages/reacord/library/internal/container.ts @@ -1,37 +1,39 @@ export class Container { - private items: T[] = [] + private items: T[] = [] - add(...items: T[]) { - this.items.push(...items) - } + add(...items: T[]) { + this.items.push(...items) + } - addBefore(item: T, before: T) { - let index = this.items.indexOf(before) - if (index === -1) { - index = this.items.length - } - this.items.splice(index, 0, item) - } + addBefore(item: T, before: T) { + let index = this.items.indexOf(before) + if (index === -1) { + index = this.items.length + } + this.items.splice(index, 0, item) + } - remove(toRemove: T) { - this.items = this.items.filter((item) => item !== toRemove) - } + remove(toRemove: T) { + this.items = this.items.filter((item) => item !== toRemove) + } - clear() { - this.items = [] - } + clear() { + this.items = [] + } - find(predicate: (item: T) => boolean): T | undefined { - return this.items.find(predicate) - } + find(predicate: (item: T) => boolean): T | undefined { + return this.items.find(predicate) + } - findType(type: new (...args: any[]) => U): U | undefined { - for (const item of this.items) { - if (item instanceof type) return item - } - } + findType( + type: new (...args: NonNullable[]) => U, + ): U | undefined { + for (const item of this.items) { + if (item instanceof type) return item + } + } - [Symbol.iterator]() { - return this.items[Symbol.iterator]() - } + [Symbol.iterator]() { + return this.items[Symbol.iterator]() + } } diff --git a/packages/reacord/library/internal/element.ts b/packages/reacord/library/internal/element.ts index ab5f9ac..bdf48a9 100644 --- a/packages/reacord/library/internal/element.ts +++ b/packages/reacord/library/internal/element.ts @@ -1,11 +1,11 @@ +import type { Node } from "./node" import type { ReactNode } from "react" import React from "react" -import type { Node } from "./node" export function ReacordElement(props: { - props: Props - createNode: () => Node - children?: ReactNode + props: Props + createNode: () => Node + children?: ReactNode }) { - return React.createElement("reacord-element", props) + return React.createElement("reacord-element", props) } diff --git a/packages/reacord/library/internal/interaction.ts b/packages/reacord/library/internal/interaction.ts index 06c2daf..1872dc6 100644 --- a/packages/reacord/library/internal/interaction.ts +++ b/packages/reacord/library/internal/interaction.ts @@ -8,28 +8,28 @@ export type ComponentInteraction = ButtonInteraction | SelectInteraction export type CommandInteraction = BaseInteraction<"command"> export type ButtonInteraction = BaseComponentInteraction< - "button", - ButtonClickEvent + "button", + ButtonClickEvent > export type SelectInteraction = BaseComponentInteraction< - "select", - SelectChangeEvent + "select", + SelectChangeEvent > -export type BaseInteraction = { - type: Type - id: string - reply(messageOptions: MessageOptions): Promise - followUp(messageOptions: MessageOptions): Promise +export interface BaseInteraction { + type: Type + id: string + reply(messageOptions: MessageOptions): Promise + followUp(messageOptions: MessageOptions): Promise } export type BaseComponentInteraction< - Type extends string, - Event extends ComponentEvent, + Type extends string, + Event extends ComponentEvent, > = BaseInteraction & { - event: Event - customId: string - update(options: MessageOptions): Promise - deferUpdate(): Promise + event: Event + customId: string + update(options: MessageOptions): Promise + deferUpdate(): Promise } diff --git a/packages/reacord/library/internal/limited-collection.ts b/packages/reacord/library/internal/limited-collection.ts index 2150d3f..a04b238 100644 --- a/packages/reacord/library/internal/limited-collection.ts +++ b/packages/reacord/library/internal/limited-collection.ts @@ -1,24 +1,24 @@ export class LimitedCollection { - private items: T[] = [] + private items: T[] = [] - constructor(private readonly size: number) {} + constructor(private readonly size: number) {} - add(item: T) { - if (this.items.length >= this.size) { - this.items.shift() - } - this.items.push(item) - } + add(item: T) { + if (this.items.length >= this.size) { + this.items.shift() + } + this.items.push(item) + } - has(item: T) { - return this.items.includes(item) - } + has(item: T) { + return this.items.includes(item) + } - values(): readonly T[] { - return this.items - } + values(): readonly T[] { + return this.items + } - [Symbol.iterator]() { - return this.items[Symbol.iterator]() - } + [Symbol.iterator]() { + return this.items[Symbol.iterator]() + } } diff --git a/packages/reacord/library/internal/message.ts b/packages/reacord/library/internal/message.ts index 89b5e70..1f3b4d1 100644 --- a/packages/reacord/library/internal/message.ts +++ b/packages/reacord/library/internal/message.ts @@ -1,65 +1,65 @@ -import { last } from "@reacord/helpers/last" -import type { Except } from "type-fest" import type { EmbedOptions } from "../core/components/embed-options" import type { SelectProps } from "../core/components/select" +import { last } from "@reacord/helpers/last" +import type { Except } from "type-fest" -export type MessageOptions = { - content: string - embeds: EmbedOptions[] - actionRows: ActionRow[] +export interface MessageOptions { + content: string + embeds: EmbedOptions[] + actionRows: ActionRow[] } export type ActionRow = ActionRowItem[] export type ActionRowItem = - | MessageButtonOptions - | MessageLinkOptions - | MessageSelectOptions + | MessageButtonOptions + | MessageLinkOptions + | MessageSelectOptions -export type MessageButtonOptions = { - type: "button" - customId: string - label?: string - style?: "primary" | "secondary" | "success" | "danger" - disabled?: boolean - emoji?: string +export interface MessageButtonOptions { + type: "button" + customId: string + label?: string + style?: "primary" | "secondary" | "success" | "danger" + disabled?: boolean + emoji?: string } -export type MessageLinkOptions = { - type: "link" - url: string - label?: string - emoji?: string - disabled?: boolean +export interface MessageLinkOptions { + type: "link" + url: string + label?: string + emoji?: string + disabled?: boolean } export type MessageSelectOptions = Except & { - type: "select" - customId: string - options: MessageSelectOptionOptions[] + type: "select" + customId: string + options: MessageSelectOptionOptions[] } -export type MessageSelectOptionOptions = { - label: string - value: string - description?: string - emoji?: string +export interface MessageSelectOptionOptions { + label: string + value: string + description?: string + emoji?: string } -export type Message = { - edit(options: MessageOptions): Promise - delete(): Promise +export interface Message { + edit(options: MessageOptions): Promise + delete(): Promise } export function getNextActionRow(options: MessageOptions): ActionRow { - let actionRow = last(options.actionRows) - if ( - actionRow == undefined || - actionRow.length >= 5 || - actionRow[0]?.type === "select" - ) { - actionRow = [] - options.actionRows.push(actionRow) - } - return actionRow + let actionRow = last(options.actionRows) + if ( + actionRow == undefined || + actionRow.length >= 5 || + actionRow[0]?.type === "select" + ) { + actionRow = [] + options.actionRows.push(actionRow) + } + return actionRow } diff --git a/packages/reacord/library/internal/node.ts b/packages/reacord/library/internal/node.ts index 8efe584..9048d52 100644 --- a/packages/reacord/library/internal/node.ts +++ b/packages/reacord/library/internal/node.ts @@ -1,20 +1,21 @@ -/* eslint-disable class-methods-use-this */ import { Container } from "./container.js" import type { ComponentInteraction } from "./interaction" import type { MessageOptions } from "./message" export abstract class Node { - readonly children = new Container>() + readonly children = new Container>() - constructor(public props: Props) {} + constructor(public props: Props) {} - modifyMessageOptions(options: MessageOptions) {} + modifyMessageOptions(_options: MessageOptions) { + // noop + } - handleComponentInteraction(interaction: ComponentInteraction): boolean { - return false - } + handleComponentInteraction(_interaction: ComponentInteraction): boolean { + return false + } - get text(): string { - return [...this.children].map((child) => child.text).join("") - } + get text(): string { + return [...this.children].map((child) => child.text).join("") + } } diff --git a/packages/reacord/library/internal/reconciler.ts b/packages/reacord/library/internal/reconciler.ts index cbe9337..062f202 100644 --- a/packages/reacord/library/internal/reconciler.ts +++ b/packages/reacord/library/internal/reconciler.ts @@ -7,105 +7,101 @@ import type { Renderer } from "./renderers/renderer" import { TextNode } from "./text-node.js" const config: HostConfig< - string, // Type, - Record, // Props, - Renderer, // Container, - Node, // Instance, - TextNode, // TextInstance, - never, // SuspenseInstance, - never, // HydratableInstance, - never, // PublicInstance, - never, // HostContext, - true, // UpdatePayload, - never, // ChildSet, - number, // TimeoutHandle, - number // NoTimeout, + string, // Type, + Record, // Props, + Renderer, // Container, + Node, // Instance, + TextNode, // TextInstance, + never, // SuspenseInstance, + never, // HydratableInstance, + never, // PublicInstance, + never, // HostContext, + true, // UpdatePayload, + never, // ChildSet, + number, // TimeoutHandle, + number // NoTimeout, > = { - supportsMutation: true, - supportsPersistence: false, - supportsHydration: false, - isPrimaryRenderer: true, - scheduleTimeout: global.setTimeout, - cancelTimeout: global.clearTimeout, - noTimeout: -1, + supportsMutation: true, + supportsPersistence: false, + supportsHydration: false, + isPrimaryRenderer: true, + scheduleTimeout: global.setTimeout, + cancelTimeout: global.clearTimeout, + noTimeout: -1, - // eslint-disable-next-line unicorn/no-null - getRootHostContext: () => null, - getChildHostContext: (parentContext) => parentContext, + getRootHostContext: () => null, + getChildHostContext: (parentContext) => parentContext, - createInstance: (type, props) => { - if (type !== "reacord-element") { - raise(`Unknown element type: ${type}`) - } + createInstance: (type, props) => { + if (type !== "reacord-element") { + raise(`Unknown element type: ${type}`) + } - if (typeof props.createNode !== "function") { - raise(`Missing createNode function`) - } + if (typeof props.createNode !== "function") { + raise(`Missing createNode function`) + } - const node = props.createNode(props.props) - if (!(node instanceof Node)) { - raise(`createNode function did not return a Node`) - } + const node = props.createNode(props.props) + if (!(node instanceof Node)) { + raise(`createNode function did not return a Node`) + } - return node - }, - createTextInstance: (text) => new TextNode(text), - shouldSetTextContent: () => false, - detachDeletedInstance: (instance) => {}, - beforeActiveInstanceBlur: () => {}, - afterActiveInstanceBlur: () => {}, - // eslint-disable-next-line unicorn/no-null - getInstanceFromNode: (node: any) => null, - // eslint-disable-next-line unicorn/no-null - getInstanceFromScope: (scopeInstance: any) => null, + return node + }, + createTextInstance: (text) => new TextNode(text), + shouldSetTextContent: () => false, + detachDeletedInstance: (_instance) => {}, + beforeActiveInstanceBlur: () => {}, + afterActiveInstanceBlur: () => {}, + getInstanceFromNode: (_node: unknown) => null, + getInstanceFromScope: (_scopeInstance: unknown) => null, - clearContainer: (renderer) => { - renderer.nodes.clear() - }, - appendChildToContainer: (renderer, child) => { - renderer.nodes.add(child) - }, - removeChildFromContainer: (renderer, child) => { - renderer.nodes.remove(child) - }, - insertInContainerBefore: (renderer, child, before) => { - renderer.nodes.addBefore(child, before) - }, + clearContainer: (renderer) => { + renderer.nodes.clear() + }, + appendChildToContainer: (renderer, child) => { + renderer.nodes.add(child) + }, + removeChildFromContainer: (renderer, child) => { + renderer.nodes.remove(child) + }, + insertInContainerBefore: (renderer, child, before) => { + renderer.nodes.addBefore(child, before) + }, - appendInitialChild: (parent, child) => { - parent.children.add(child) - }, - appendChild: (parent, child) => { - parent.children.add(child) - }, - removeChild: (parent, child) => { - parent.children.remove(child) - }, - insertBefore: (parent, child, before) => { - parent.children.addBefore(child, before) - }, + appendInitialChild: (parent, child) => { + parent.children.add(child) + }, + appendChild: (parent, child) => { + parent.children.add(child) + }, + removeChild: (parent, child) => { + parent.children.remove(child) + }, + insertBefore: (parent, child, before) => { + parent.children.addBefore(child, before) + }, - prepareUpdate: () => true, - commitUpdate: (node, payload, type, oldProps, newProps) => { - node.props = newProps.props - }, - commitTextUpdate: (node, oldText, newText) => { - node.props = newText - }, + prepareUpdate: () => true, + commitUpdate: (node, payload, type, oldProps, newProps) => { + node.props = newProps.props + }, + commitTextUpdate: (node, oldText, newText) => { + node.props = newText + }, - // eslint-disable-next-line unicorn/no-null - prepareForCommit: () => null, - resetAfterCommit: (renderer) => { - renderer.render() - }, - prepareScopeUpdate: (scopeInstance: any, instance: any) => {}, + prepareForCommit: () => null, + resetAfterCommit: (renderer) => { + renderer.render() + }, + prepareScopeUpdate: (_scopeInstance: unknown, _instance: unknown) => {}, - preparePortalMount: () => raise("Portals are not supported"), - getPublicInstance: () => raise("Refs are currently not supported"), + preparePortalMount: () => raise("Portals are not supported"), + getPublicInstance: () => raise("Refs are currently not supported"), - finalizeInitialChildren: () => false, + finalizeInitialChildren: () => false, - getCurrentEventPriority: () => DefaultEventPriority, + getCurrentEventPriority: () => DefaultEventPriority, } export const reconciler = ReactReconciler(config) diff --git a/packages/reacord/library/internal/renderers/channel-message-renderer.ts b/packages/reacord/library/internal/renderers/channel-message-renderer.ts index 32fafe1..fca038f 100644 --- a/packages/reacord/library/internal/renderers/channel-message-renderer.ts +++ b/packages/reacord/library/internal/renderers/channel-message-renderer.ts @@ -3,11 +3,11 @@ import type { Message, MessageOptions } from "../message" import { Renderer } from "./renderer" export class ChannelMessageRenderer extends Renderer { - constructor(private channel: Channel) { - super() - } + constructor(private channel: Channel) { + super() + } - protected createMessage(options: MessageOptions): Promise { - return this.channel.send(options) - } + protected createMessage(options: MessageOptions): Promise { + return this.channel.send(options) + } } diff --git a/packages/reacord/library/internal/renderers/interaction-reply-renderer.ts b/packages/reacord/library/internal/renderers/interaction-reply-renderer.ts index 163c78a..b1c986c 100644 --- a/packages/reacord/library/internal/renderers/interaction-reply-renderer.ts +++ b/packages/reacord/library/internal/renderers/interaction-reply-renderer.ts @@ -7,16 +7,16 @@ import { Renderer } from "./renderer" const repliedInteractionIds = new Set() export class InteractionReplyRenderer extends Renderer { - constructor(private interaction: Interaction) { - super() - } + constructor(private interaction: Interaction) { + super() + } - protected createMessage(options: MessageOptions): Promise { - if (repliedInteractionIds.has(this.interaction.id)) { - return this.interaction.followUp(options) - } + protected createMessage(options: MessageOptions): Promise { + if (repliedInteractionIds.has(this.interaction.id)) { + return this.interaction.followUp(options) + } - repliedInteractionIds.add(this.interaction.id) - return this.interaction.reply(options) - } + repliedInteractionIds.add(this.interaction.id) + return this.interaction.reply(options) + } } diff --git a/packages/reacord/library/internal/renderers/renderer.ts b/packages/reacord/library/internal/renderers/renderer.ts index fb9146d..f10c0bc 100644 --- a/packages/reacord/library/internal/renderers/renderer.ts +++ b/packages/reacord/library/internal/renderers/renderer.ts @@ -1,119 +1,119 @@ -import { Subject } from "rxjs" -import { concatMap } from "rxjs/operators" import { Container } from "../container.js" import type { ComponentInteraction } from "../interaction" import type { Message, MessageOptions } from "../message" import type { Node } from "../node.js" +import { Subject } from "rxjs" +import { concatMap } from "rxjs/operators" type UpdatePayload = - | { action: "update" | "deactivate"; options: MessageOptions } - | { action: "deferUpdate"; interaction: ComponentInteraction } - | { action: "destroy" } + | { action: "update" | "deactivate"; options: MessageOptions } + | { action: "deferUpdate"; interaction: ComponentInteraction } + | { action: "destroy" } export abstract class Renderer { - readonly nodes = new Container>() - private componentInteraction?: ComponentInteraction - private message?: Message - private active = true - private updates = new Subject() + readonly nodes = new Container>() + private componentInteraction?: ComponentInteraction + private message?: Message + private active = true + private updates = new Subject() - private updateSubscription = this.updates - .pipe(concatMap((payload) => this.updateMessage(payload))) - .subscribe({ error: console.error }) + private updateSubscription = this.updates + .pipe(concatMap((payload) => this.updateMessage(payload))) + .subscribe({ error: console.error }) - render() { - if (!this.active) { - console.warn("Attempted to update a deactivated message") - return - } + render() { + if (!this.active) { + console.warn("Attempted to update a deactivated message") + return + } - this.updates.next({ - options: this.getMessageOptions(), - action: "update", - }) - } + this.updates.next({ + options: this.getMessageOptions(), + action: "update", + }) + } - deactivate() { - this.active = false - this.updates.next({ - options: this.getMessageOptions(), - action: "deactivate", - }) - } + deactivate() { + this.active = false + this.updates.next({ + options: this.getMessageOptions(), + action: "deactivate", + }) + } - destroy() { - this.active = false - this.updates.next({ action: "destroy" }) - } + destroy() { + this.active = false + this.updates.next({ action: "destroy" }) + } - handleComponentInteraction(interaction: ComponentInteraction) { - this.componentInteraction = interaction + handleComponentInteraction(interaction: ComponentInteraction) { + this.componentInteraction = interaction - setTimeout(() => { - this.updates.next({ action: "deferUpdate", interaction }) - }, 500) + setTimeout(() => { + this.updates.next({ action: "deferUpdate", interaction }) + }, 500) - for (const node of this.nodes) { - if (node.handleComponentInteraction(interaction)) { - return true - } - } - } + for (const node of this.nodes) { + if (node.handleComponentInteraction(interaction)) { + return true + } + } + } - protected abstract createMessage(options: MessageOptions): Promise + protected abstract createMessage(options: MessageOptions): Promise - private getMessageOptions(): MessageOptions { - const options: MessageOptions = { - content: "", - embeds: [], - actionRows: [], - } - for (const node of this.nodes) { - node.modifyMessageOptions(options) - } - return options - } + private getMessageOptions(): MessageOptions { + const options: MessageOptions = { + content: "", + embeds: [], + actionRows: [], + } + for (const node of this.nodes) { + node.modifyMessageOptions(options) + } + return options + } - private async updateMessage(payload: UpdatePayload) { - if (payload.action === "destroy") { - this.updateSubscription.unsubscribe() - await this.message?.delete() - return - } + private async updateMessage(payload: UpdatePayload) { + if (payload.action === "destroy") { + this.updateSubscription.unsubscribe() + await this.message?.delete() + return + } - if (payload.action === "deactivate") { - this.updateSubscription.unsubscribe() + if (payload.action === "deactivate") { + this.updateSubscription.unsubscribe() - await this.message?.edit({ - ...payload.options, - actionRows: payload.options.actionRows.map((row) => - row.map((component) => ({ - ...component, - disabled: true, - })), - ), - }) + await this.message?.edit({ + ...payload.options, + actionRows: payload.options.actionRows.map((row) => + row.map((component) => ({ + ...component, + disabled: true, + })), + ), + }) - return - } + return + } - if (payload.action === "deferUpdate") { - await payload.interaction.deferUpdate() - return - } + if (payload.action === "deferUpdate") { + await payload.interaction.deferUpdate() + return + } - if (this.componentInteraction) { - const promise = this.componentInteraction.update(payload.options) - this.componentInteraction = undefined - await promise - return - } + if (this.componentInteraction) { + const promise = this.componentInteraction.update(payload.options) + this.componentInteraction = undefined + await promise + return + } - if (this.message) { - await this.message.edit(payload.options) - return - } + if (this.message) { + await this.message.edit(payload.options) + return + } - this.message = await this.createMessage(payload.options) - } + this.message = await this.createMessage(payload.options) + } } diff --git a/packages/reacord/library/internal/text-node.ts b/packages/reacord/library/internal/text-node.ts index ead02ad..6d702b3 100644 --- a/packages/reacord/library/internal/text-node.ts +++ b/packages/reacord/library/internal/text-node.ts @@ -2,11 +2,11 @@ import type { MessageOptions } from "./message" import { Node } from "./node.js" export class TextNode extends Node { - override modifyMessageOptions(options: MessageOptions) { - options.content = options.content + this.props - } + override modifyMessageOptions(options: MessageOptions) { + options.content = options.content + this.props + } - override get text() { - return this.props - } + override get text() { + return this.props + } } diff --git a/packages/reacord/library/internal/timeout.ts b/packages/reacord/library/internal/timeout.ts index c3b178c..985e2d2 100644 --- a/packages/reacord/library/internal/timeout.ts +++ b/packages/reacord/library/internal/timeout.ts @@ -1,20 +1,20 @@ export class Timeout { - private timeoutId?: NodeJS.Timeout + private timeoutId?: NodeJS.Timeout - constructor( - private readonly time: number, - private readonly callback: () => void, - ) {} + constructor( + private readonly time: number, + private readonly callback: () => void, + ) {} - run() { - this.cancel() - this.timeoutId = setTimeout(this.callback, this.time) - } + run() { + this.cancel() + this.timeoutId = setTimeout(this.callback, this.time) + } - cancel() { - if (this.timeoutId) { - clearTimeout(this.timeoutId) - this.timeoutId = undefined - } - } + cancel() { + if (this.timeoutId) { + clearTimeout(this.timeoutId) + this.timeoutId = undefined + } + } } diff --git a/packages/reacord/package.json b/packages/reacord/package.json index 71816ea..0231945 100644 --- a/packages/reacord/package.json +++ b/packages/reacord/package.json @@ -1,93 +1,90 @@ { - "name": "reacord", - "type": "module", - "description": "Create interactive Discord messages using React.", - "version": "0.5.2", - "types": "./dist/main.d.ts", - "homepage": "https://reacord.mapleleaf.dev", - "repository": "https://github.com/itsMapleLeaf/reacord.git", - "changelog": "https://github.com/itsMapleLeaf/reacord/releases", - "license": "MIT", - "keywords": [ - "discord", - "discord-js", - "react", - "react-js", - "react-renderer", - "interaction", - "message", - "embed", - "reacord" - ], - "files": [ - "dist", - "README.md", - "LICENSE" - ], - "exports": { - ".": { - "import": "./dist/main.js", - "require": "./dist/main.cjs", - "types": "./dist/main.d.ts" - }, - "./package.json": { - "import": "./package.json", - "require": "./package.json" - } - }, - "scripts": { - "build": "cp ../../README.md . && cp ../../LICENSE . && tsup library/main.ts --target node16 --format cjs,esm --dts --sourcemap", - "build-watch": "pnpm build -- --watch", - "test": "vitest --coverage --no-watch", - "test-dev": "vitest", - "test-manual": "nodemon --exec tsx --ext ts,tsx ./scripts/discordjs-manual-test.tsx" - }, - "dependencies": { - "@types/node": "*", - "@types/react": "*", - "@types/react-reconciler": "^0.28.0", - "react-reconciler": "^0.29.0", - "rxjs": "^7.5.6" - }, - "peerDependencies": { - "discord.js": "^14", - "react": ">=17" - }, - "peerDependenciesMeta": { - "discord.js": { - "optional": true - } - }, - "devDependencies": { - "@reacord/helpers": "workspace:*", - "@types/lodash-es": "^4.17.6", - "c8": "^7.12.0", - "discord.js": "^14.0.3", - "dotenv": "^16.0.1", - "lodash-es": "^4.17.21", - "nodemon": "^2.0.19", - "prettier": "^2.7.1", - "pretty-ms": "^8.0.0", - "react": "^18.2.0", - "tsup": "^6.1.3", - "tsx": "^3.8.0", - "type-fest": "^2.17.0", - "typescript": "^4.7.4", - "vitest": "^0.18.1" - }, - "resolutions": { - "esbuild": "latest" - }, - "release-it": { - "git": { - "commitMessage": "release v${version}" - }, - "github": { - "release": true, - "web": true - } - }, - "publishConfig": { - "access": "public" - } + "name": "reacord", + "type": "module", + "description": "Create interactive Discord messages using React.", + "version": "0.5.2", + "types": "./dist/main.d.ts", + "homepage": "https://reacord.mapleleaf.dev", + "repository": "https://github.com/itsMapleLeaf/reacord.git", + "changelog": "https://github.com/itsMapleLeaf/reacord/releases", + "license": "MIT", + "keywords": [ + "discord", + "discord-js", + "react", + "react-js", + "react-renderer", + "interaction", + "message", + "embed", + "reacord" + ], + "files": [ + "library", + "dist", + "README.md", + "LICENSE" + ], + "exports": { + ".": { + "import": "./dist/main.js", + "require": "./dist/main.cjs", + "types": "./library/main.ts" + }, + "./package.json": { + "import": "./package.json", + "require": "./package.json" + } + }, + "scripts": { + "build": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node16 --format cjs,esm --sourcemap", + "build-watch": "pnpm build -- --watch", + "test": "vitest --coverage --no-watch", + "test-dev": "vitest", + "test-manual": "nodemon --exec tsx --ext ts,tsx ./scripts/discordjs-manual-test.tsx" + }, + "dependencies": { + "@types/node": "^20.5.0", + "@types/react": "^18.2.20", + "@types/react-reconciler": "^0.28.2", + "react-reconciler": "^0.29.0", + "rxjs": "^7.8.1" + }, + "peerDependencies": { + "discord.js": "^14", + "react": ">=17" + }, + "peerDependenciesMeta": { + "discord.js": { + "optional": true + } + }, + "devDependencies": { + "@reacord/helpers": "workspace:*", + "@types/lodash-es": "^4.17.8", + "c8": "^8.0.1", + "cpy-cli": "^5.0.0", + "discord.js": "^14.12.1", + "dotenv": "^16.3.1", + "lodash-es": "^4.17.21", + "nodemon": "^3.0.1", + "prettier": "^3.0.2", + "pretty-ms": "^8.0.0", + "react": "^18.2.0", + "tsup": "^7.2.0", + "tsx": "^3.12.7", + "type-fest": "^4.2.0" + }, + "release-it": { + "git": { + "commitMessage": "release v${version}" + }, + "github": { + "release": true, + "web": true + } + }, + "publishConfig": { + "access": "public" + } } diff --git a/packages/reacord/scripts/discordjs-manual-test.tsx b/packages/reacord/scripts/discordjs-manual-test.tsx index ebf97db..cc248e0 100644 --- a/packages/reacord/scripts/discordjs-manual-test.tsx +++ b/packages/reacord/scripts/discordjs-manual-test.tsx @@ -1,17 +1,17 @@ +import { + Button, + Link, + Option, + ReacordDiscordJs, + Select, + useInstance, +} from "../library/main.js" import type { TextChannel } from "discord.js" import { ChannelType, Client, IntentsBitField } from "discord.js" import "dotenv/config" import { kebabCase } from "lodash-es" import * as React from "react" import { useState } from "react" -import { - Button, - Link, - Option, - ReacordDiscordJs, - Select, - useInstance, -} from "../library/main" const client = new Client({ intents: IntentsBitField.Flags.Guilds }) const reacord = new ReacordDiscordJs(client) @@ -22,118 +22,118 @@ const guild = await client.guilds.fetch(process.env.TEST_GUILD_ID!) const category = await guild.channels.fetch(process.env.TEST_CATEGORY_ID!) if (category?.type !== ChannelType.GuildCategory) { - throw new Error( - `channel ${process.env.TEST_CATEGORY_ID} is not a guild category. received ${category?.type}`, - ) + throw new Error( + `channel ${process.env.TEST_CATEGORY_ID} is not a guild category. received ${category?.type}`, + ) } for (const [, channel] of category.children.cache) { - await channel.delete() + await channel.delete() } let prefix = 0 const createTest = async ( - name: string, - block: (channel: TextChannel) => void | Promise, + name: string, + block: (channel: TextChannel) => void | Promise, ) => { - prefix += 1 - const channel = await category.children.create({ - type: ChannelType.GuildText, - name: `${String(prefix).padStart(3, "0")}-${kebabCase(name)}`, - }) - await block(channel) + prefix += 1 + const channel = await category.children.create({ + type: ChannelType.GuildText, + name: `${String(prefix).padStart(3, "0")}-${kebabCase(name)}`, + }) + await block(channel) } await createTest("basic", (channel) => { - reacord.send(channel.id, "Hello, world!") + reacord.send(channel.id, "Hello, world!") }) await createTest("counter", (channel) => { - const Counter = () => { - const [count, setCount] = React.useState(0) - return ( - <> - count: {count} -