tooling overhaul

This commit is contained in:
itsMapleLeaf
2023-08-16 19:32:28 -05:00
parent 7ac1a9cdce
commit e9e5a1617b
111 changed files with 6758 additions and 6156 deletions

View File

@@ -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",
},
},
],
}

77
.eslintrc.json Normal file
View File

@@ -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"
}
}
]
}

35
.github/workflows/lint.yml vendored Normal file
View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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

30
.github/workflows/unit-test.yml vendored Normal file
View File

@@ -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

3
.gitignore vendored
View File

@@ -5,8 +5,7 @@ coverage
.env
*.code-workspace
.pnpm-debug.log
build
.cache
.vercel
*.tsbuildinfo

View File

@@ -1,7 +1,2 @@
node_modules
dist
coverage
pnpm-lock.yaml
build
.cache
packages/website/public/api
/packages/website/public/api

18
.prettierrc Normal file
View File

@@ -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"
}
}
]
}

View File

@@ -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",
},
},
],
}

View File

@@ -2,26 +2,41 @@
"name": "reacord-monorepo",
"private": true,
"scripts": {
"lint": "eslint --ext js,ts,tsx .",
"lint-fix": "pnpm lint -- --fix",
"format": "prettier --write .",
"typecheck": "tsc -b",
"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.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"
"@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"
}
}

View File

@@ -1,9 +1,9 @@
import { camelCaseDeep, snakeCaseDeep } from "./convert-object-property-case"
import type {
CamelCasedPropertiesDeep,
SnakeCasedPropertiesDeep,
} from "type-fest"
import { expect, test } from "vitest"
import { camelCaseDeep, snakeCaseDeep } from "./convert-object-property-case"
test("camelCaseDeep", () => {
const input = {

View File

@@ -2,6 +2,7 @@ import { camelCase, isObject, snakeCase } from "lodash-es"
import type {
CamelCasedPropertiesDeep,
SnakeCasedPropertiesDeep,
UnknownRecord,
} from "type-fest"
function convertKeyCaseDeep<Input, Output>(
@@ -18,11 +19,11 @@ function convertKeyCaseDeep<Input, Output>(
) as unknown as Output
}
const output: any = {}
const output = {} as UnknownRecord
for (const [key, value] of Object.entries(input)) {
output[convertKey(key)] = convertKeyCaseDeep(value, convertKey)
}
return output
return output as Output
}
export function camelCaseDeep<T>(input: T): CamelCasedPropertiesDeep<T> {

View File

@@ -1,7 +1,7 @@
/**
* for narrowing instance types with array.filter
*/
/** For narrowing instance types with array.filter */
export const isInstanceOf =
<T>(Constructor: new (...args: any[]) => T) =>
(value: unknown): value is T =>
value instanceof Constructor
<Instance, Args extends unknown[]>(
constructor: new (...args: Args) => Instance,
) =>
(value: unknown): value is Instance =>
value instanceof constructor

View File

@@ -1,7 +1,3 @@
export function isObject<T>(
value: T,
): value is Exclude<T, Primitive | AnyFunction> {
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

7
packages/helpers/json.ts Normal file
View File

@@ -0,0 +1,7 @@
export function safeJsonStringify(value: unknown): string {
try {
return JSON.stringify(value)
} catch {
return String(value)
}
}

View File

@@ -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 })
})

View File

@@ -0,0 +1,3 @@
import { omit } from "./omit.ts"
omit({ a: 1, b: true }, ["a"]) satisfies { b: boolean }

View File

@@ -1,13 +1,10 @@
export function omit<Subject extends object, Key extends PropertyKey>(
subject: Subject,
keys: Key[],
// hack: using a conditional type preserves union types
): Subject extends any ? Omit<Subject, Key> : never {
const result: any = {}
for (const key in subject) {
if (!keys.includes(key as unknown as Key)) {
result[key] = subject[key]
}
}
return result
) {
const keySet = new Set<PropertyKey>(keys)
return Object.fromEntries(
Object.entries(subject).filter(([key]) => !keySet.has(key)),
// hack: conditional type preserves unions
) as Subject extends unknown ? Omit<Subject, Key> : never
}

View File

@@ -3,9 +3,9 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@types/lodash-es": "^4.17.6",
"@types/lodash-es": "^4.17.8",
"lodash-es": "^4.17.21",
"type-fest": "^2.17.0",
"vitest": "^0.18.1"
"type-fest": "^4.2.0",
"vitest": "^0.34.1"
}
}

View File

@@ -1,15 +1,11 @@
import type { LoosePick, UnknownRecord } from "./types"
import type { LoosePick } from "./types"
export function pick<T, K extends keyof T | PropertyKey>(
export function pick<T extends object, K extends keyof T | PropertyKey>(
object: T,
keys: K[],
): LoosePick<T, K> {
const result: any = {}
for (const key of keys) {
const value = (object as UnknownRecord)[key]
if (value !== undefined) {
result[key] = value
}
}
return result
) {
const keySet = new Set<PropertyKey>(keys)
return Object.fromEntries(
Object.entries(object).filter(([key]) => keySet.has(key)),
) as LoosePick<T, K>
}

View File

@@ -3,7 +3,7 @@ import type { PruneNullishValues } from "./prune-nullish-values"
import { pruneNullishValues } from "./prune-nullish-values"
test("pruneNullishValues", () => {
type InputType = {
interface InputType {
a: string
b: string | null | undefined
c?: string
@@ -15,7 +15,6 @@ test("pruneNullishValues", () => {
const input: InputType = {
a: "a",
// eslint-disable-next-line unicorn/no-null
b: null,
c: undefined,
d: {

View File

@@ -1,21 +1,23 @@
import { isObject } from "./is-object"
export function pruneNullishValues<T>(input: T): PruneNullishValues<T> {
if (Array.isArray(input)) {
return input.filter(Boolean).map((item) => pruneNullishValues(item)) as any
}
if (!isObject(input)) {
return input as any
return input as PruneNullishValues<T>
}
const result: any = {}
for (const [key, value] of Object.entries(input as any)) {
if (Array.isArray(input)) {
return input
.filter(Boolean)
.map((item) => pruneNullishValues(item)) as PruneNullishValues<T>
}
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(input)) {
if (value != undefined) {
result[key] = pruneNullishValues(value)
}
}
return result
return result as PruneNullishValues<T>
}
export type PruneNullishValues<Input> = Input extends object

View File

@@ -1,5 +1,5 @@
import { setTimeout } from "node:timers/promises"
import { toError } from "./to-error.js"
import { setTimeout } from "node:timers/promises"
export async function rejectAfter(
timeMs: number,

View File

@@ -1,3 +1,3 @@
{
"extends": "@itsmapleleaf/configs/tsconfig.base"
"extends": "../../tsconfig.base.json"
}

View File

@@ -0,0 +1,4 @@
import { LooseOmit, LoosePick, typeEquals } from "./types.ts"
typeEquals<LoosePick<{ a: 1; b: 2 }, "a">, { a: 1 }>(true)
typeEquals<LooseOmit<{ a: 1; b: 2 }, "a">, { b: 2 }>(true)

View File

@@ -1,11 +1,21 @@
export type MaybePromise<T> = T | Promise<T>
import { raise } from "./raise.ts"
export type ValueOf<Type> = Type extends ReadonlyArray<infer Value>
export type MaybePromise<T> = T | PromiseLike<T>
export type ValueOf<Type> = Type extends readonly (infer Value)[]
? Value
: Type[keyof Type]
export type UnknownRecord = Record<PropertyKey, unknown>
export type LoosePick<Shape, Keys extends PropertyKey> = Simplify<{
[Key in Extract<Keys, keyof Shape>]: Shape[Key]
}>
export type LoosePick<Shape, Keys extends PropertyKey> = {
[Key in Keys]: Shape extends Record<Key, infer Value> ? Value : never
}
export type LooseOmit<Shape, Keys extends PropertyKey> = Simplify<{
[Key in Exclude<keyof Shape, Keys>]: Shape[Key]
}>
export type Simplify<T> = { [Key in keyof T]: T[Key] } & NonNullable<unknown>
export const typeEquals = <A, B>(
_result: A extends B ? (B extends A ? true : false) : false,
) => raise("typeEquals() should not be called at runtime")

View File

@@ -1,9 +1,10 @@
import { setTimeout } from "node:timers/promises"
import { MaybePromise } from "./types.ts"
const maxTime = 1000
export async function waitFor<Result>(
predicate: () => Result,
predicate: () => MaybePromise<Result>,
): Promise<Awaited<Result>> {
const startTime = Date.now()
let lastError: unknown

View File

@@ -7,7 +7,7 @@ export function withLoggedMethodCalls<T extends object>(value: T) {
if (typeof value !== "function") {
return value
}
return (...values: any[]) => {
return (...values: unknown[]) => {
console.info(
`${String(property)}(${values
.map((value) =>

1
packages/reacord/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="@total-typescript/ts-reset" />

View File

@@ -1,52 +1,49 @@
import type { ReactNode } from "react"
import type { ReacordInstance } from "./instance"
/** @category Component Event */
export interface ComponentEvent {
/**
* @category Component Event
*/
export type ComponentEvent = {
/**
* The message associated with this event.
* For example: with a button click,
* 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 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
/**
* Create a new reply to this event.
*/
/** 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.
* 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 = {
/** @category Component Event */
export interface ChannelInfo {
id: string
name?: string
topic?: string
@@ -57,13 +54,11 @@ export type ChannelInfo = {
rateLimitPerUser?: number
}
/**
* @category Component Event
*/
export type MessageInfo = {
/** @category Component Event */
export interface MessageInfo {
id: string
channelId: string
authorId: UserInfo
authorId: string
member?: GuildMemberInfo
content: string
timestamp: string
@@ -74,19 +69,15 @@ export type MessageInfo = {
mentions: string[]
}
/**
* @category Component Event
*/
export type GuildInfo = {
/** @category Component Event */
export interface GuildInfo {
id: string
name: string
member: GuildMemberInfo
}
/**
* @category Component Event
*/
export type GuildMemberInfo = {
/** @category Component Event */
export interface GuildMemberInfo {
id: string
nick?: string
displayName: string
@@ -100,10 +91,8 @@ export type GuildMemberInfo = {
communicationDisabledUntil?: string
}
/**
* @category Component Event
*/
export type UserInfo = {
/** @category Component Event */
export interface UserInfo {
id: string
username: string
discriminator: string

View File

@@ -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 = {
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
@@ -37,7 +37,7 @@ export function ActionRow(props: ActionRowProps) {
)
}
class ActionRowNode extends Node<{}> {
class ActionRowNode extends Node<ActionRowProps> {
override modifyMessageOptions(options: MessageOptions): void {
options.actionRows.push([])
for (const child of this.children) {

View File

@@ -2,9 +2,10 @@ import type { ReactNode } from "react"
/**
* Common props between button-like components
*
* @category Button
*/
export type ButtonSharedProps = {
export interface ButtonSharedProps {
/** The text on the button. Rich formatting (markdown) is not supported here. */
label?: ReactNode
@@ -12,13 +13,12 @@ export type ButtonSharedProps = {
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>`.
* 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.
* 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
}

View File

@@ -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,30 +7,23 @@ 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"
/**
* Happens when a user clicks the button.
*/
/** 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 (
<ReacordElement props={props} createNode={() => new ButtonNode(props)}>
@@ -46,7 +38,7 @@ class ButtonNode extends Node<ButtonProps> {
private customId = randomUUID()
// this has text children, but buttons themselves shouldn't yield text
// eslint-disable-next-line class-methods-use-this
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
override get text() {
return ""
}
@@ -74,4 +66,4 @@ class ButtonNode extends Node<ButtonProps> {
}
}
class ButtonLabelNode extends Node<{}> {}
class ButtonLabelNode extends Node<Record<string, never>> {}

View File

@@ -1,23 +1,18 @@
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 = {
/** @category Embed */
export interface EmbedAuthorProps {
name?: ReactNode
children?: ReactNode
url?: string
iconUrl?: string
}
/**
* @category Embed
*/
/** @category Embed */
export function EmbedAuthor(props: EmbedAuthorProps) {
return (
<ReacordElement props={props} createNode={() => new EmbedAuthorNode(props)}>
@@ -38,4 +33,4 @@ class EmbedAuthorNode extends EmbedChildNode<EmbedAuthorProps> {
}
}
class AuthorTextNode extends Node<{}> {}
class AuthorTextNode extends Node<Record<string, never>> {}

View File

@@ -1,23 +1,18 @@
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 = {
/** @category Embed */
export interface EmbedFieldProps {
name: ReactNode
value?: ReactNode
inline?: boolean
children?: ReactNode
}
/**
* @category Embed
*/
/** @category Embed */
export function EmbedField(props: EmbedFieldProps) {
return (
<ReacordElement props={props} createNode={() => new EmbedFieldNode(props)}>
@@ -25,7 +20,7 @@ export function EmbedField(props: EmbedFieldProps) {
{props.name}
</ReacordElement>
<ReacordElement props={{}} createNode={() => new FieldValueNode({})}>
{props.value || props.children}
{props.value ?? props.children}
</ReacordElement>
</ReacordElement>
)
@@ -42,5 +37,5 @@ class EmbedFieldNode extends EmbedChildNode<EmbedFieldProps> {
}
}
class FieldNameNode extends Node<{}> {}
class FieldValueNode extends Node<{}> {}
class FieldNameNode extends Node<Record<string, never>> {}
class FieldValueNode extends Node<Record<string, never>> {}

View File

@@ -1,23 +1,18 @@
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 = {
/** @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 (
<ReacordElement props={props} createNode={() => new EmbedFooterNode(props)}>
@@ -42,4 +37,4 @@ class EmbedFooterNode extends EmbedChildNode<
}
}
class FooterTextNode extends Node<{}> {}
class FooterTextNode extends Node<Record<string, never>> {}

View File

@@ -1,18 +1,13 @@
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 = {
/** @category Embed */
export interface EmbedImageProps {
url: string
}
/**
* @category Embed
*/
/** @category Embed */
export function EmbedImage(props: EmbedImageProps) {
return (
<ReacordElement

View File

@@ -1,5 +1,5 @@
import type { Except, SnakeCasedPropertiesDeep } from "type-fest"
import type { EmbedProps } from "./embed"
import type { Except, SnakeCasedPropertiesDeep } from "type-fest"
export type EmbedOptions = SnakeCasedPropertiesDeep<
Except<EmbedProps, "timestamp" | "children"> & {

View File

@@ -1,18 +1,13 @@
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 = {
/** @category Embed */
export interface EmbedThumbnailProps {
url: string
}
/**
* @category Embed
*/
/** @category Embed */
export function EmbedThumbnail(props: EmbedThumbnailProps) {
return (
<ReacordElement

View File

@@ -1,21 +1,16 @@
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 = {
/** @category Embed */
export interface EmbedTitleProps {
children: ReactNode
url?: string
}
/**
* @category Embed
*/
/** @category Embed */
export function EmbedTitle({ children, ...props }: EmbedTitleProps) {
return (
<ReacordElement props={props} createNode={() => new EmbedTitleNode(props)}>
@@ -33,4 +28,4 @@ class EmbedTitleNode extends EmbedChildNode<Omit<EmbedTitleProps, "children">> {
}
}
class TitleTextNode extends Node<{}> {}
class TitleTextNode extends Node<Record<string, never>> {}

View File

@@ -12,12 +12,12 @@ import type { EmbedOptions } from "./embed-options"
* @category Embed
* @see https://discord.com/developers/docs/resources/channel#embed-object
*/
export type EmbedProps = {
export interface EmbedProps {
title?: string
description?: string
url?: string
color?: number
fields?: Array<{ name: string; value: string; inline?: boolean }>
fields?: { name: string; value: string; inline?: boolean }[]
author?: { name: string; url?: string; iconUrl?: string }
thumbnail?: { url: string }
image?: { url: string }
@@ -53,7 +53,7 @@ class EmbedNode extends Node<EmbedProps> {
child.modifyEmbedOptions(embed)
}
if (child instanceof TextNode) {
embed.description = (embed.description || "") + child.props
embed.description = (embed.description ?? "") + child.props
}
}

View File

@@ -1,13 +1,10 @@
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
@@ -15,14 +12,12 @@ export type LinkProps = ButtonSharedProps & {
children?: string
}
/**
* @category Link
*/
/** @category Link */
export function Link({ label, children, ...props }: LinkProps) {
return (
<ReacordElement props={props} createNode={() => new LinkNode(props)}>
<ReacordElement props={{}} createNode={() => new LinkTextNode({})}>
{label || children}
{label ?? children}
</ReacordElement>
</ReacordElement>
)
@@ -40,4 +35,4 @@ class LinkNode extends Node<Omit<LinkProps, "label" | "children">> {
}
}
class LinkTextNode extends Node<{}> {}
class LinkTextNode extends Node<Record<string, never>> {}

View File

@@ -15,5 +15,5 @@ export class OptionNode extends Node<
}
}
export class OptionLabelNode extends Node<{}> {}
export class OptionDescriptionNode extends Node<{}> {}
export class OptionLabelNode extends Node<Record<string, never>> {}
export class OptionDescriptionNode extends Node<Record<string, never>> {}

View File

@@ -1,5 +1,4 @@
import type { ReactNode } from "react"
import React from "react"
import { ReacordElement } from "../../internal/element"
import {
OptionDescriptionNode,
@@ -7,10 +6,8 @@ import {
OptionNode,
} from "./option-node"
/**
* @category Select
*/
export type OptionProps = {
/** @category Select */
export interface OptionProps {
/** The internal value of this option */
value: string
/** The text shown to the user. This takes priority over `children` */
@@ -23,19 +20,16 @@ export type OptionProps = {
/**
* 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>`.
* 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.
* 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,
@@ -46,7 +40,7 @@ export function Option({
<ReacordElement props={props} createNode={() => new OptionNode(props)}>
{(label !== undefined || children !== undefined) && (
<ReacordElement props={{}} createNode={() => new OptionLabelNode({})}>
{label || children}
{label ?? children}
</ReacordElement>
)}
{description !== undefined && (

View File

@@ -1,7 +1,6 @@
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 {
@@ -13,10 +12,8 @@ import { Node } from "../../internal/node.js"
import type { ComponentEvent } from "../component-event"
import { OptionNode } from "./option-node"
/**
* @category Select
*/
export type SelectProps = {
/** @category Select */
export interface SelectProps {
children?: ReactNode
/** Sets the currently selected value */
value?: string
@@ -31,8 +28,8 @@ export type SelectProps = {
multiple?: boolean
/**
* With `multiple`, the minimum number of values that can be selected.
* When `multiple` is false or not defined, this is always 1.
* 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.
@@ -40,44 +37,44 @@ export type SelectProps = {
minValues?: number
/**
* With `multiple`, the maximum number of values that can be selected.
* When `multiple` is false or not defined, this is always 1.
* 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. */
/**
* 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.
* 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.
* 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.
*/
/** 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[]
}
/**
* See [the select menu guide](/guides/select-menu) for a usage example.
*
* @category Select
*/
export function Select(props: SelectProps) {

View File

@@ -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<ReacordInstance | undefined>(undefined)

View File

@@ -2,9 +2,10 @@ import type { ReactNode } from "react"
/**
* Represents an interactive message, which can later be replaced or deleted.
*
* @category Core
*/
export type ReacordInstance = {
export interface ReacordInstance {
/** Render some JSX to this instance (edits the message) */
render: (content: ReactNode) => void

View File

@@ -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"
@@ -26,10 +26,14 @@ import { Reacord } from "./reacord"
/**
* The Reacord adapter for Discord.js.
*
* @category Core
*/
export class ReacordDiscordJs extends Reacord {
constructor(private client: Discord.Client, config: ReacordConfig = {}) {
constructor(
private client: Discord.Client,
config: ReacordConfig = {},
) {
super(config)
client.on("interactionCreate", (interaction) => {
@@ -43,6 +47,7 @@ export class ReacordDiscordJs extends Reacord {
/**
* Sends a message to a channel.
*
* @see https://reacord.mapleleaf.dev/guides/sending-messages
*/
override send(
@@ -57,6 +62,7 @@ export class ReacordDiscordJs extends Reacord {
/**
* Sends a message as a reply to a command interaction.
*
* @see https://reacord.mapleleaf.dev/guides/sending-messages
*/
override reply(
@@ -71,6 +77,7 @@ export class ReacordDiscordJs extends Reacord {
/**
* Sends an ephemeral message as a reply to a command interaction.
*
* @see https://reacord.mapleleaf.dev/guides/sending-messages
*/
override ephemeralReply(
@@ -114,14 +121,14 @@ export class ReacordDiscordJs extends Reacord {
...getDiscordMessageOptions(options),
fetchReply: true,
})
return createReacordMessage(message as Discord.Message)
return createReacordMessage(message)
},
followUp: async (options) => {
const message = await interaction.followUp({
...getDiscordMessageOptions(options),
fetchReply: true,
})
return createReacordMessage(message as Discord.Message)
return createReacordMessage(message)
},
})
}
@@ -189,6 +196,8 @@ export class ReacordDiscordJs extends Reacord {
? 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")
@@ -212,6 +221,8 @@ export class ReacordDiscordJs extends Reacord {
premiumSince: interaction.member.premiumSince?.toISOString(),
communicationDisabledUntil:
interaction.member.communicationDisabledUntil?.toISOString(),
color: interaction.member.displayColor,
displayAvatarUrl: interaction.member.displayAvatarURL(),
}
: undefined
@@ -245,14 +256,14 @@ export class ReacordDiscordJs extends Reacord {
...getDiscordMessageOptions(options),
fetchReply: true,
})
return createReacordMessage(message as Discord.Message)
return createReacordMessage(message)
},
followUp: async (options) => {
const message = await interaction.followUp({
...getDiscordMessageOptions(options),
fetchReply: true,
})
return createReacordMessage(message as Discord.Message)
return createReacordMessage(message)
},
event: {
channel,
@@ -335,8 +346,7 @@ function convertButtonStyleToEnum(style: MessageButtonOptions["style"]) {
// 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,
content: reacordOptions.content || undefined,
embeds: reacordOptions.embeds,
components: reacordOptions.actionRows.map((row) => ({
type: Discord.ComponentType.ActionRow,
@@ -375,7 +385,10 @@ function getDiscordMessageOptions(reacordOptions: MessageOptions) {
}
}
raise(`Unsupported component type: ${(component as any).type}`)
component satisfies never
throw new Error(
`Invalid component type ${safeJsonStringify(component)}}`,
)
},
),
})),

View File

@@ -1,25 +1,22 @@
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 interface ReacordConfig {
/**
* @category Core
*/
export type ReacordConfig = {
/**
* The max number of active instances.
* When this limit is exceeded, the oldest instances will be disabled.
* 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[] = []
@@ -50,14 +47,11 @@ export abstract class Reacord {
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,
)

View File

@@ -1,5 +1,5 @@
import type { Message, MessageOptions } from "./message"
export type Channel = {
export interface Channel {
send(message: MessageOptions): Promise<Message>
}

View File

@@ -25,7 +25,9 @@ export class Container<T> {
return this.items.find(predicate)
}
findType<U extends T>(type: new (...args: any[]) => U): U | undefined {
findType<U extends T>(
type: new (...args: NonNullable<unknown>[]) => U,
): U | undefined {
for (const item of this.items) {
if (item instanceof type) return item
}

View File

@@ -1,6 +1,6 @@
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: Props

View File

@@ -17,7 +17,7 @@ export type SelectInteraction = BaseComponentInteraction<
SelectChangeEvent
>
export type BaseInteraction<Type extends string> = {
export interface BaseInteraction<Type extends string> {
type: Type
id: string
reply(messageOptions: MessageOptions): Promise<Message>

View File

@@ -1,9 +1,9 @@
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 = {
export interface MessageOptions {
content: string
embeds: EmbedOptions[]
actionRows: ActionRow[]
@@ -16,7 +16,7 @@ export type ActionRowItem =
| MessageLinkOptions
| MessageSelectOptions
export type MessageButtonOptions = {
export interface MessageButtonOptions {
type: "button"
customId: string
label?: string
@@ -25,7 +25,7 @@ export type MessageButtonOptions = {
emoji?: string
}
export type MessageLinkOptions = {
export interface MessageLinkOptions {
type: "link"
url: string
label?: string
@@ -39,14 +39,14 @@ export type MessageSelectOptions = Except<SelectProps, "children" | "value"> & {
options: MessageSelectOptionOptions[]
}
export type MessageSelectOptionOptions = {
export interface MessageSelectOptionOptions {
label: string
value: string
description?: string
emoji?: string
}
export type Message = {
export interface Message {
edit(options: MessageOptions): Promise<void>
delete(): Promise<void>
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable class-methods-use-this */
import { Container } from "./container.js"
import type { ComponentInteraction } from "./interaction"
import type { MessageOptions } from "./message"
@@ -8,9 +7,11 @@ export abstract class Node<Props> {
constructor(public props: Props) {}
modifyMessageOptions(options: MessageOptions) {}
modifyMessageOptions(_options: MessageOptions) {
// noop
}
handleComponentInteraction(interaction: ComponentInteraction): boolean {
handleComponentInteraction(_interaction: ComponentInteraction): boolean {
return false
}

View File

@@ -29,7 +29,6 @@ const config: HostConfig<
cancelTimeout: global.clearTimeout,
noTimeout: -1,
// eslint-disable-next-line unicorn/no-null
getRootHostContext: () => null,
getChildHostContext: (parentContext) => parentContext,
@@ -51,13 +50,11 @@ const config: HostConfig<
},
createTextInstance: (text) => new TextNode(text),
shouldSetTextContent: () => false,
detachDeletedInstance: (instance) => {},
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,
getInstanceFromNode: (_node: unknown) => null,
getInstanceFromScope: (_scopeInstance: unknown) => null,
clearContainer: (renderer) => {
renderer.nodes.clear()
@@ -93,12 +90,11 @@ const config: HostConfig<
node.props = newText
},
// eslint-disable-next-line unicorn/no-null
prepareForCommit: () => null,
resetAfterCommit: (renderer) => {
renderer.render()
},
prepareScopeUpdate: (scopeInstance: any, instance: any) => {},
prepareScopeUpdate: (_scopeInstance: unknown, _instance: unknown) => {},
preparePortalMount: () => raise("Portals are not supported"),
getPublicInstance: () => raise("Refs are currently not supported"),

View File

@@ -1,9 +1,9 @@
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 }

View File

@@ -20,6 +20,7 @@
"reacord"
],
"files": [
"library",
"dist",
"README.md",
"LICENSE"
@@ -28,7 +29,7 @@
".": {
"import": "./dist/main.js",
"require": "./dist/main.cjs",
"types": "./dist/main.d.ts"
"types": "./library/main.ts"
},
"./package.json": {
"import": "./package.json",
@@ -36,18 +37,18 @@
}
},
"scripts": {
"build": "cp ../../README.md . && cp ../../LICENSE . && tsup library/main.ts --target node16 --format cjs,esm --dts --sourcemap",
"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": "*",
"@types/react": "*",
"@types/react-reconciler": "^0.28.0",
"@types/node": "^20.5.0",
"@types/react": "^18.2.20",
"@types/react-reconciler": "^0.28.2",
"react-reconciler": "^0.29.0",
"rxjs": "^7.5.6"
"rxjs": "^7.8.1"
},
"peerDependencies": {
"discord.js": "^14",
@@ -60,23 +61,19 @@
},
"devDependencies": {
"@reacord/helpers": "workspace:*",
"@types/lodash-es": "^4.17.6",
"c8": "^7.12.0",
"discord.js": "^14.0.3",
"dotenv": "^16.0.1",
"@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": "^2.0.19",
"prettier": "^2.7.1",
"nodemon": "^3.0.1",
"prettier": "^3.0.2",
"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"
"tsup": "^7.2.0",
"tsx": "^3.12.7",
"type-fest": "^4.2.0"
},
"release-it": {
"git": {

View File

@@ -1,9 +1,3 @@
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,
@@ -11,7 +5,13 @@ import {
ReacordDiscordJs,
Select,
useInstance,
} from "../library/main"
} 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"
const client = new Client({ intents: IntentsBitField.Flags.Guilds })
const reacord = new ReacordDiscordJs(client)

View File

@@ -1,4 +1,3 @@
import React from "react"
import { test } from "vitest"
import { ActionRow, Button, Select } from "../library/main"
import { ReacordTester } from "./test-adapter"

View File

@@ -1,4 +1,3 @@
import React from "react"
import { test } from "vitest"
import {
Embed,

View File

@@ -1,2 +1,3 @@
import { test } from "vitest"
test.todo("ephemeral reply")

View File

@@ -1,4 +1,3 @@
import React from "react"
import { test } from "vitest"
import { Link } from "../library/main"
import { ReacordTester } from "./test-adapter"

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import { test } from "vitest"
import { Button, Embed, EmbedField, EmbedTitle } from "../library/main"
import { ReacordTester } from "./test-adapter"
import * as React from "react"
import { test } from "vitest"
test("rendering behavior", async () => {
const tester = new ReacordTester()

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react"
import { useState } from "react"
import { expect, test, vi } from "vitest"
import { Button, Option, Select } from "../library/main"
import { ReacordTester } from "./test-adapter"

View File

@@ -1,5 +1,3 @@
/* eslint-disable class-methods-use-this */
/* eslint-disable require-await */
import { logPretty } from "@reacord/helpers/log-pretty"
import { omit } from "@reacord/helpers/omit"
import { pruneNullishValues } from "@reacord/helpers/prune-nullish-values"
@@ -32,9 +30,7 @@ import { InteractionReplyRenderer } from "../library/internal/renderers/interact
export type MessageSample = ReturnType<ReacordTester["sampleMessages"]>[0]
/**
* A Record adapter for automated tests. WIP
*/
/** A Record adapter for automated tests. WIP */
export class ReacordTester extends Reacord {
private messageContainer = new Container<TestMessage>()
@@ -252,10 +248,10 @@ class TestSelectInteraction
class TestComponentEvent {
constructor(private tester: ReacordTester) {}
message: MessageInfo = {} as any // todo
channel: ChannelInfo = {} as any // todo
user: UserInfo = {} as any // todo
guild: GuildInfo = {} as any // todo
message: MessageInfo = {} as MessageInfo // todo
channel: ChannelInfo = {} as ChannelInfo // todo
user: UserInfo = {} as UserInfo // todo
guild: GuildInfo = {} as GuildInfo // todo
reply(content?: ReactNode): ReacordInstance {
return this.tester.reply(content)
@@ -274,7 +270,10 @@ class TestSelectChangeEvent
extends TestComponentEvent
implements SelectChangeEvent
{
constructor(readonly values: string[], tester: ReacordTester) {
constructor(
readonly values: string[],
tester: ReacordTester,
) {
super(tester)
}
}

View File

@@ -1,4 +1,3 @@
import * as React from "react"
import { test } from "vitest"
import {
Button,

View File

@@ -1,9 +1,8 @@
import React from "react"
import { describe, expect, it } from "vitest"
import type { ReacordInstance } from "../library/main"
import { Button, useInstance } from "../library/main"
import type { MessageSample } from "./test-adapter"
import { ReacordTester } from "./test-adapter"
import { describe, expect, it } from "vitest"
describe("useInstance", () => {
it("returns the instance of itself", async () => {

View File

@@ -1,4 +1,7 @@
{
"extends": "@itsmapleleaf/configs/tsconfig.base",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx"
},
"exclude": ["node_modules", "dist"]
}

View File

@@ -7,9 +7,7 @@ import { defineConfig } from "astro/config"
export default defineConfig({
integrations: [
tailwind({
config: {
applyBaseStyles: false,
},
}),
react(),
prefetch(),

View File

@@ -12,27 +12,27 @@
"build": "typedoc && astro build"
},
"dependencies": {
"@astrojs/prefetch": "^0.2.0",
"@astrojs/react": "^2.1.0",
"@astrojs/prefetch": "^0.3.0",
"@astrojs/react": "^2.2.2",
"@fontsource/jetbrains-mono": "^4.5.12",
"@fontsource/rubik": "^4.5.14",
"@heroicons/react": "^2.0.16",
"@heroicons/react": "^2.0.18",
"@tailwindcss/typography": "^0.5.9",
"astro": "^2.1.2",
"clsx": "^1.2.1",
"astro": "^2.10.9",
"clsx": "^2.0.0",
"reacord": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@astrojs/tailwind": "^3.1.0",
"@types/node": "*",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@astrojs/tailwind": "^4.0.0",
"@total-typescript/ts-reset": "^0.4.2",
"@types/node": "^20.5.0",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"npm-run-all": "^4.1.5",
"tailwindcss": "^3.2.7",
"typedoc": "^0.23.26",
"typescript": "^4.9.5",
"tailwindcss": "^3.3.3",
"typedoc": "^0.24.8",
"wait-on": "^7.0.1"
}
}

View File

@@ -125,21 +125,21 @@ export function LandingAnimation() {
return (
<div
className="grid gap-2 relative pointer-events-none select-none"
className="pointer-events-none relative grid select-none gap-2"
role="presentation"
>
<div
className={clsx(
"bg-slate-800 p-4 rounded-lg shadow transition",
state.messageVisible ? "opacity-100" : "opacity-0 -translate-y-2",
"rounded-lg bg-slate-800 p-4 shadow transition",
state.messageVisible ? "opacity-100" : "-translate-y-2 opacity-0",
)}
>
<div className="flex items-start gap-4">
<div className="w-12 h-12 p-2 rounded-full bg-no-repeat bg-contain bg-black/25">
<div className="h-12 w-12 rounded-full bg-black/25 bg-contain bg-no-repeat p-2">
<img
src={blobComfyUrl}
alt=""
className="object-contain scale-90 w-full h-full"
className="h-full w-full scale-90 object-contain"
/>
</div>
<div>
@@ -148,13 +148,13 @@ export function LandingAnimation() {
<div className="mt-2 flex flex-row gap-3">
<div
ref={addRef}
className="bg-emerald-700 text-white py-1.5 px-3 text-sm rounded"
className="rounded bg-emerald-700 px-3 py-1.5 text-sm text-white"
>
+1
</div>
<div
ref={deleteRef}
className="bg-red-700 text-white py-1.5 px-3 text-sm rounded"
className="rounded bg-red-700 px-3 py-1.5 text-sm text-white"
>
🗑 delete
</div>
@@ -163,12 +163,12 @@ export function LandingAnimation() {
</div>
</div>
<div
className="bg-slate-700 pb-2 pt-1.5 px-4 rounded-lg shadow"
className="rounded-lg bg-slate-700 px-4 pb-2 pt-1.5 shadow"
ref={chatInputRef}
>
<span
className={clsx(
"text-sm after:content-[attr(data-after)] after:relative after:-top-px after:-left-[2px]",
"text-sm after:relative after:-left-[2px] after:-top-px after:content-[attr(data-after)]",
state.chatInputCursorVisible
? "after:opacity-100"
: "after:opacity-0",
@@ -176,7 +176,7 @@ export function LandingAnimation() {
data-after="|"
>
{state.chatInputText || (
<span className="opacity-50 block absolute translate-y-1">
<span className="absolute block translate-y-1 opacity-50">
Message #showing-off-reacord
</span>
)}
@@ -186,7 +186,7 @@ export function LandingAnimation() {
<img
src={cursorUrl}
alt=""
className="transition-all duration-500 absolute scale-75 bg-transparent"
className="absolute scale-75 bg-transparent transition-all duration-500"
style={{ left: state.cursorLeft, bottom: state.cursorBottom }}
ref={cursorRef}
/>

View File

@@ -1,12 +1,12 @@
---
export type Props = {
icon: (props: { class?: string; className?: string }) => any
export interface Props {
icon: (props: { class?: string; className?: string }) => unknown
label: string
}
---
<div
class="px-3 py-2 transition text-left font-medium block w-full opacity-50 inline-flex gap-1 items-center hover:opacity-100 hover:text-emerald-500"
class="flex w-full items-center gap-1 px-3 py-2 text-left font-medium opacity-50 transition hover:text-emerald-500 hover:opacity-100"
>
<Astro.props.icon class="inline-icon" className="inline-icon" />
<span class="flex-1">{Astro.props.label}</span>

View File

@@ -79,8 +79,8 @@ To reply to a command interaction, use the `.reply()` function. This function re
```jsx
import { Client } from "discord.js"
import * as React from "react"
import { Button, ReacordDiscordJs } from "reacord"
import * as React from "react"
const client = new Client({ intents: [] })
const reacord = new ReacordDiscordJs(client)

View File

@@ -1,2 +1,3 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

View File

@@ -6,7 +6,7 @@ import Layout from "~/components/layout.astro"
import MainNavigation from "~/components/main-navigation.astro"
import NavLink from "~/components/nav-link.astro"
export type Props = {
export interface Props {
guide: CollectionEntry<"guides">
}

View File

@@ -7,7 +7,7 @@
@apply outline-none;
}
:focus-visible {
@apply ring-2 ring-emerald-500 ring-inset;
@apply ring-2 ring-inset ring-emerald-500;
}
pre,
@@ -26,10 +26,10 @@
}
.link {
@apply font-medium inline-block relative opacity-60 hover:opacity-100 transition-opacity;
@apply relative inline-block font-medium opacity-60 transition-opacity hover:opacity-100;
}
.link::after {
@apply content-[''] bottom-[-2px] absolute block w-full h-px bg-current translate-y-[3px] opacity-0 transition;
@apply absolute bottom-[-2px] block h-px w-full translate-y-[3px] bg-current opacity-0 transition content-[''];
}
.link:hover::after {
@apply -translate-y-px opacity-50;
@@ -39,7 +39,7 @@
}
.button {
@apply inline-block mt-4 px-4 py-2.5 text-xl transition rounded-lg bg-black/25 hover:bg-black/40 hover:-translate-y-0.5 hover:shadow active:translate-y-0 active:transition-none;
@apply mt-4 inline-block rounded-lg bg-black/25 px-4 py-2.5 text-xl transition hover:-translate-y-0.5 hover:bg-black/40 hover:shadow active:translate-y-0 active:transition-none;
}
.button-solid {
@apply bg-emerald-700 hover:bg-emerald-800;

View File

@@ -1,18 +0,0 @@
// @ts-nocheck
module.exports = {
content: ["./src/**/*.{ts,tsx,md,astro}"],
theme: {
fontFamily: {
sans: ["RubikVariable", "sans-serif"],
monospace: ["'JetBrains Mono'", "monospace"],
},
boxShadow: {
DEFAULT: "0 2px 9px 0 rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3)",
},
extend: {},
},
corePlugins: {
container: false,
},
plugins: [require("@tailwindcss/typography")],
}

View File

@@ -1,5 +1,5 @@
{
"extends": "@itsmapleleaf/configs/tsconfig.base",
"extends": "../../tsconfig.base",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react",

5957
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

20
tailwind.config.ts Normal file
View File

@@ -0,0 +1,20 @@
import typography from "@tailwindcss/typography"
import { type Config } from "tailwindcss"
export default {
content: ["./packages/*/src/**/*.{ts,tsx,md,astro}"],
theme: {
fontFamily: {
sans: ["RubikVariable", "sans-serif"],
monospace: ["'JetBrains Mono'", "monospace"],
},
boxShadow: {
DEFAULT: "0 2px 9px 0 rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3)",
},
extend: {},
},
corePlugins: {
container: false,
},
plugins: [typography],
} satisfies Config

16
tsconfig.base.json Normal file
View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"allowImportingTsExtensions": true,
"allowJs": true,
"checkJs": true,
"module": "esnext",
"moduleResolution": "bundler",
"noEmit": true,
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "esnext",
"composite": true
}
}

View File

@@ -1,8 +1,4 @@
{
"files": [],
"references": [
{ "path": "packages/reacord" },
{ "path": "packages/website" },
{ "path": "packages/helpers" }
]
"extends": "./tsconfig.base.json",
"exclude": ["node_modules", "packages"]
}