Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12ec45ccd4 | ||
|
|
fc3025baaf | ||
|
|
81f32794b4 | ||
|
|
dbf9640b16 | ||
|
|
a485ebaf74 | ||
|
|
7ef5a7ac9d | ||
|
|
6715756c2b | ||
|
|
6851c5419a | ||
|
|
1ba75492e5 | ||
|
|
aced338d72 | ||
|
|
512c0649d8 | ||
|
|
91b82ca41f | ||
|
|
752ccc080d | ||
|
|
3c2d3b4683 |
25
.eslintrc.cjs
Normal file
25
.eslintrc.cjs
Normal file
@@ -0,0 +1,25 @@
|
||||
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"),
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["packages/website/cypress/**"],
|
||||
parserOptions: {
|
||||
project: require.resolve("./packages/website/cypress/tsconfig.json"),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"extends": ["./node_modules/@itsmapleleaf/configs/eslint"],
|
||||
"ignorePatterns": [
|
||||
"**/node_modules/**",
|
||||
"**/.cache/**",
|
||||
"**/build/**",
|
||||
"**/dist/**",
|
||||
"**/coverage/**",
|
||||
"**/public/**"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.base.json"
|
||||
},
|
||||
"rules": {
|
||||
"import/no-unused-modules": "off",
|
||||
"unicorn/prevent-abbreviations": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["packages/website/cypress/**"],
|
||||
"parserOptions": {
|
||||
"project": "./packages/website/cypress/tsconfig.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,7 @@ node_modules
|
||||
.vscode
|
||||
coverage
|
||||
.env
|
||||
*.code-workspace
|
||||
|
||||
build
|
||||
.cache
|
||||
|
||||
@@ -4,3 +4,4 @@ coverage
|
||||
pnpm-lock.yaml
|
||||
build
|
||||
.cache
|
||||
packages/website/public/api
|
||||
|
||||
21
package.json
21
package.json
@@ -5,22 +5,13 @@
|
||||
"lint-fix": "pnpm lint -- --fix",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@itsmapleleaf/configs": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.9.1",
|
||||
"@typescript-eslint/parser": "^5.9.1",
|
||||
"eslint": "^8.6.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-import-resolver-typescript": "^2.5.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-unicorn": "^40.0.0",
|
||||
"prettier": "^2.5.1",
|
||||
"typescript": "^4.5.4"
|
||||
"@itsmapleleaf/configs": "^1.1.3",
|
||||
"@rushstack/eslint-patch": "^1.1.3",
|
||||
"@types/eslint": "^8.4.1",
|
||||
"eslint": "^8.14.0",
|
||||
"prettier": "^2.6.2",
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"esbuild": "latest"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { inspect } from "node:util"
|
||||
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export function logPretty(value: unknown) {
|
||||
console.info(
|
||||
inspect(value, {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export function omit<Subject extends object, Key extends PropertyKey>(
|
||||
subject: Subject,
|
||||
keys: Key[],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { LoosePick, UnknownRecord } from "./types"
|
||||
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export function pick<T, K extends keyof T | PropertyKey>(
|
||||
object: T,
|
||||
keys: K[],
|
||||
|
||||
33
packages/reacord/helpers/prune-nullish-values.test.ts
Normal file
33
packages/reacord/helpers/prune-nullish-values.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { expect, test } from "vitest"
|
||||
import { PruneNullishValues, pruneNullishValues } from "./prune-nullish-values"
|
||||
|
||||
test("pruneNullishValues", () => {
|
||||
type InputType = {
|
||||
a: string
|
||||
b: string | null | undefined
|
||||
c?: string
|
||||
d: {
|
||||
a: string
|
||||
b: string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
const input: InputType = {
|
||||
a: "a",
|
||||
b: null,
|
||||
c: undefined,
|
||||
d: {
|
||||
a: "a",
|
||||
b: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
const output: PruneNullishValues<InputType> = {
|
||||
a: "a",
|
||||
d: {
|
||||
a: "a",
|
||||
},
|
||||
}
|
||||
|
||||
expect(pruneNullishValues(input)).toEqual(output)
|
||||
})
|
||||
@@ -18,10 +18,25 @@ export function pruneNullishValues<T>(input: T): PruneNullishValues<T> {
|
||||
return result
|
||||
}
|
||||
|
||||
type PruneNullishValues<Input> = Input extends ReadonlyArray<infer Value>
|
||||
? ReadonlyArray<NonNullable<Value>>
|
||||
: Input extends object
|
||||
? {
|
||||
[Key in keyof Input]: NonNullable<Input[Key]>
|
||||
}
|
||||
export type PruneNullishValues<Input> = Input extends object
|
||||
? OptionalKeys<
|
||||
{ [Key in keyof Input]: NonNullable<PruneNullishValues<Input[Key]>> },
|
||||
KeysWithNullishValues<Input>
|
||||
>
|
||||
: Input
|
||||
|
||||
type OptionalKeys<Input, Keys extends keyof Input> = Omit<Input, Keys> & {
|
||||
[Key in Keys]?: Input[Key]
|
||||
}
|
||||
|
||||
type KeysWithNullishValues<Input> = NonNullable<
|
||||
Values<{
|
||||
[Key in keyof Input]: null extends Input[Key]
|
||||
? Key
|
||||
: undefined extends Input[Key]
|
||||
? Key
|
||||
: never
|
||||
}>
|
||||
>
|
||||
|
||||
type Values<Input> = Input[keyof Input]
|
||||
|
||||
21
packages/reacord/helpers/wait-for.ts
Normal file
21
packages/reacord/helpers/wait-for.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { setTimeout } from "timers/promises"
|
||||
|
||||
const maxTime = 1000
|
||||
|
||||
export async function waitFor<Result>(
|
||||
predicate: () => Result,
|
||||
): Promise<Awaited<Result>> {
|
||||
const startTime = Date.now()
|
||||
let lastError: unknown
|
||||
|
||||
while (Date.now() - startTime < maxTime) {
|
||||
try {
|
||||
return await predicate()
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
await setTimeout(50)
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? new Error("Timeout")
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { inspect } from "node:util"
|
||||
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export function withLoggedMethodCalls<T extends object>(value: T) {
|
||||
return new Proxy(value as Record<string | symbol, unknown>, {
|
||||
get(target, property) {
|
||||
|
||||
7
packages/reacord/library/core/file.ts
Normal file
7
packages/reacord/library/core/file.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Readable } from "node:stream"
|
||||
|
||||
export type ReacordFile = {
|
||||
name?: string
|
||||
description?: string
|
||||
data: Buffer | Readable | string
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ReactNode } from "react"
|
||||
import { ReacordFile } from "./file"
|
||||
|
||||
/**
|
||||
* Represents an interactive message, which can later be replaced or deleted.
|
||||
@@ -16,4 +17,7 @@ export type ReacordInstance = {
|
||||
* This prevents it from listening to user interactions.
|
||||
*/
|
||||
deactivate: () => void
|
||||
|
||||
/** Attach a file to the message for this instance */
|
||||
attach: (file: ReacordFile) => void
|
||||
}
|
||||
|
||||
@@ -298,20 +298,18 @@ function createReacordMessage(message: Discord.Message): Message {
|
||||
edit: async (options) => {
|
||||
await message.edit(getDiscordMessageOptions(options))
|
||||
},
|
||||
disableComponents: async () => {
|
||||
for (const actionRow of message.components) {
|
||||
for (const component of actionRow.components) {
|
||||
component.setDisabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
await message.edit({
|
||||
components: message.components,
|
||||
})
|
||||
},
|
||||
delete: async () => {
|
||||
await message.delete()
|
||||
},
|
||||
updateFiles: async (files) => {
|
||||
await message.edit({
|
||||
files: files.map(({ name, description, data }) => ({
|
||||
name,
|
||||
description,
|
||||
attachment: data,
|
||||
})),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +319,7 @@ function createEphemeralReacordMessage(): Message {
|
||||
console.warn("Ephemeral messages can't be edited")
|
||||
return Promise.resolve()
|
||||
},
|
||||
disableComponents: () => {
|
||||
updateFiles: () => {
|
||||
console.warn("Ephemeral messages can't be edited")
|
||||
return Promise.resolve()
|
||||
},
|
||||
@@ -373,7 +371,10 @@ function getDiscordMessageOptions(
|
||||
})),
|
||||
}
|
||||
|
||||
if (!options.content && !options.embeds?.length) {
|
||||
const hasContent =
|
||||
options.content || options.embeds?.length || options.files?.length
|
||||
|
||||
if (!hasContent) {
|
||||
options.content = "_ _"
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,9 @@ export abstract class Reacord {
|
||||
this.renderers = this.renderers.filter((it) => it !== renderer)
|
||||
renderer.destroy()
|
||||
},
|
||||
attach: (file) => {
|
||||
renderer.attach(file)
|
||||
},
|
||||
}
|
||||
|
||||
if (initialContent !== undefined) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ReacordFile } from "../core/file"
|
||||
import type { Message, MessageOptions } from "./message"
|
||||
|
||||
export type Channel = {
|
||||
send(message: MessageOptions): Promise<Message>
|
||||
sendFiles(files: readonly ReacordFile[]): Promise<Message>
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Except } from "type-fest"
|
||||
import { last } from "../../helpers/last"
|
||||
import type { EmbedOptions } from "../core/components/embed-options"
|
||||
import type { SelectProps } from "../core/components/select"
|
||||
import { ReacordFile } from "../main"
|
||||
|
||||
export type MessageOptions = {
|
||||
content: string
|
||||
@@ -49,7 +50,7 @@ export type MessageSelectOptionOptions = {
|
||||
export type Message = {
|
||||
edit(options: MessageOptions): Promise<void>
|
||||
delete(): Promise<void>
|
||||
disableComponents(): Promise<void>
|
||||
updateFiles(files: readonly ReacordFile[]): Promise<void>
|
||||
}
|
||||
|
||||
export function getNextActionRow(options: MessageOptions): ActionRow {
|
||||
|
||||
@@ -52,6 +52,8 @@ const config: HostConfig<
|
||||
},
|
||||
createTextInstance: (text) => new TextNode(text),
|
||||
shouldSetTextContent: () => false,
|
||||
// @ts-expect-error
|
||||
detachDeletedInstance: (instance) => {},
|
||||
|
||||
clearContainer: (renderer) => {
|
||||
renderer.nodes.clear()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ReacordFile } from "../../core/file"
|
||||
import type { Channel } from "../channel"
|
||||
import type { Message, MessageOptions } from "../message"
|
||||
import { Renderer } from "./renderer"
|
||||
@@ -10,4 +11,10 @@ export class ChannelMessageRenderer extends Renderer {
|
||||
protected createMessage(options: MessageOptions): Promise<Message> {
|
||||
return this.channel.send(options)
|
||||
}
|
||||
|
||||
protected createMessageFromFiles(
|
||||
files: readonly ReacordFile[],
|
||||
): Promise<Message> {
|
||||
return this.channel.sendFiles(files)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Subject } from "rxjs"
|
||||
import { concatMap } from "rxjs/operators"
|
||||
import { ReacordFile } from "../../core/file"
|
||||
import { Container } from "../container.js"
|
||||
import type { ComponentInteraction } from "../interaction"
|
||||
import type { Message, MessageOptions } from "../message"
|
||||
@@ -7,15 +8,21 @@ import type { Node } from "../node.js"
|
||||
|
||||
type UpdatePayload =
|
||||
| { action: "update" | "deactivate"; options: MessageOptions }
|
||||
| { action: "files"; files: readonly ReacordFile[] }
|
||||
| { action: "deferUpdate"; interaction: ComponentInteraction }
|
||||
| { action: "destroy" }
|
||||
|
||||
type NewMessagePayload =
|
||||
| { source: "content"; messageOptions?: MessageOptions }
|
||||
| { source: "files"; files?: readonly ReacordFile[] }
|
||||
|
||||
export abstract class Renderer {
|
||||
readonly nodes = new Container<Node<unknown>>()
|
||||
private componentInteraction?: ComponentInteraction
|
||||
private message?: Message
|
||||
private active = true
|
||||
private updates = new Subject<UpdatePayload>()
|
||||
private files: readonly ReacordFile[] = []
|
||||
|
||||
private updateSubscription = this.updates
|
||||
.pipe(concatMap((payload) => this.updateMessage(payload)))
|
||||
@@ -46,6 +53,11 @@ export abstract class Renderer {
|
||||
this.updates.next({ action: "destroy" })
|
||||
}
|
||||
|
||||
attach(file: ReacordFile) {
|
||||
const newFiles = (this.files = [...this.files, file])
|
||||
this.updates.next({ action: "files", files: newFiles })
|
||||
}
|
||||
|
||||
handleComponentInteraction(interaction: ComponentInteraction) {
|
||||
this.componentInteraction = interaction
|
||||
|
||||
@@ -60,7 +72,7 @@ export abstract class Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract createMessage(options: MessageOptions): Promise<Message>
|
||||
protected abstract createMessage(options: NewMessagePayload): Promise<Message>
|
||||
|
||||
private getMessageOptions(): MessageOptions {
|
||||
const options: MessageOptions = {
|
||||
@@ -83,7 +95,17 @@ export abstract class Renderer {
|
||||
|
||||
if (payload.action === "deactivate") {
|
||||
this.updateSubscription.unsubscribe()
|
||||
await this.message?.disableComponents()
|
||||
|
||||
await this.message?.edit({
|
||||
...payload.options,
|
||||
actionRows: payload.options.actionRows.map((row) =>
|
||||
row.map((component) => ({
|
||||
...component,
|
||||
disabled: true,
|
||||
})),
|
||||
),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -92,6 +114,15 @@ export abstract class Renderer {
|
||||
return
|
||||
}
|
||||
|
||||
if (payload.action === "files") {
|
||||
if (this.message) {
|
||||
await this.message?.updateFiles(payload.files)
|
||||
} else {
|
||||
this.message = await this.createMessageFromFiles(payload.files)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (this.componentInteraction) {
|
||||
const promise = this.componentInteraction.update(payload.options)
|
||||
this.componentInteraction = undefined
|
||||
|
||||
@@ -12,6 +12,7 @@ export * from "./core/components/embed-title"
|
||||
export * from "./core/components/link"
|
||||
export * from "./core/components/option"
|
||||
export * from "./core/components/select"
|
||||
export * from "./core/file"
|
||||
export * from "./core/instance"
|
||||
export { useInstance } from "./core/instance-context"
|
||||
export * from "./core/reacord"
|
||||
|
||||
@@ -46,10 +46,10 @@
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-reconciler": "^0.26.4",
|
||||
"nanoid": "^3.1.31",
|
||||
"react-reconciler": "^0.26.2",
|
||||
"rxjs": "^7.5.2"
|
||||
"@types/react-reconciler": "^0.26.6",
|
||||
"nanoid": "^3.3.3",
|
||||
"react-reconciler": "^0.27.0",
|
||||
"rxjs": "^7.5.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"discord.js": "^13.3",
|
||||
@@ -61,24 +61,24 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash-es": "^4.17.5",
|
||||
"c8": "^7.11.0",
|
||||
"discord.js": "^13.5.1",
|
||||
"dotenv": "^11.0.0",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"c8": "^7.11.2",
|
||||
"discord.js": "^13.6.0",
|
||||
"dotenv": "^16.0.0",
|
||||
"esbuild": "latest",
|
||||
"esbuild-jest": "^0.5.0",
|
||||
"esmo": "^0.13.0",
|
||||
"esmo": "^0.14.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nodemon": "^2.0.15",
|
||||
"prettier": "^2.5.1",
|
||||
"prettier": "^2.6.2",
|
||||
"pretty-ms": "^7.0.1",
|
||||
"react": "^17.0.2",
|
||||
"release-it": "^14.12.1",
|
||||
"tsup": "^5.11.11",
|
||||
"type-fest": "^2.9.0",
|
||||
"typescript": "^4.5.4",
|
||||
"vite": "^2.7.10",
|
||||
"vitest": "^0.0.141"
|
||||
"react": "^18.0.0",
|
||||
"release-it": "^14.14.2",
|
||||
"tsup": "^5.12.6",
|
||||
"type-fest": "^2.12.2",
|
||||
"typescript": "^4.6.3",
|
||||
"vite": "^2.9.5",
|
||||
"vitest": "^0.9.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"esbuild": "latest"
|
||||
|
||||
BIN
packages/reacord/playground/anime.jpg
Normal file
BIN
packages/reacord/playground/anime.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 954 KiB |
@@ -8,16 +8,14 @@ type Command = {
|
||||
|
||||
export function createCommandHandler(client: Client, commands: Command[]) {
|
||||
client.on("ready", async () => {
|
||||
for (const command of commands) {
|
||||
for (const guild of client.guilds.cache.values()) {
|
||||
await client.application?.commands.create(
|
||||
{
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
},
|
||||
guild.id,
|
||||
)
|
||||
}
|
||||
for (const guild of client.guilds.cache.values()) {
|
||||
client.application!.commands.set(
|
||||
commands.map(({ name, description }) => ({
|
||||
name,
|
||||
description,
|
||||
})),
|
||||
guild.id,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Client } from "discord.js"
|
||||
import "dotenv/config"
|
||||
import { readFile } from "fs/promises"
|
||||
import { join } from "path"
|
||||
import React from "react"
|
||||
import { fileURLToPath } from "url"
|
||||
import { Button, ReacordDiscordJs, useInstance } from "../library/main"
|
||||
import { createCommandHandler } from "./command-handler"
|
||||
import { Counter } from "./counter"
|
||||
@@ -104,6 +107,19 @@ createCommandHandler(client, [
|
||||
reacord.reply(interaction, <DeleteThis />)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "anime",
|
||||
description: "shows an anime image",
|
||||
run: async (interaction) => {
|
||||
const reply = reacord.reply(interaction)
|
||||
const image = await readFile(
|
||||
join(fileURLToPath(import.meta.url), "../anime.jpg"),
|
||||
)
|
||||
|
||||
reply.attach({ name: "anime.jpg", data: image })
|
||||
// reply.render("anime")
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await client.login(process.env.TEST_BOT_TOKEN)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState } from "react"
|
||||
import { expect, fn, test } from "vitest"
|
||||
import { expect, test, vi } from "vitest"
|
||||
import { Button, Option, Select } from "../library/main"
|
||||
import { ReacordTester } from "./test-adapter"
|
||||
|
||||
test("single select", async () => {
|
||||
const tester = new ReacordTester()
|
||||
const onSelect = fn()
|
||||
const onSelect = vi.fn()
|
||||
|
||||
function TestSelect() {
|
||||
const [value, setValue] = useState<string>()
|
||||
@@ -75,7 +75,7 @@ test("single select", async () => {
|
||||
|
||||
test("multiple select", async () => {
|
||||
const tester = new ReacordTester()
|
||||
const onSelect = fn()
|
||||
const onSelect = vi.fn()
|
||||
|
||||
function TestSelect() {
|
||||
const [values, setValues] = useState<string[]>([])
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* eslint-disable class-methods-use-this */
|
||||
/* eslint-disable require-await */
|
||||
import { nanoid } from "nanoid"
|
||||
import { nextTick } from "node:process"
|
||||
import { promisify } from "node:util"
|
||||
import { setTimeout } from "node:timers/promises"
|
||||
import type { ReactNode } from "react"
|
||||
import { expect } from "vitest"
|
||||
import { logPretty } from "../helpers/log-pretty"
|
||||
import { omit } from "../helpers/omit"
|
||||
import { pruneNullishValues } from "../helpers/prune-nullish-values"
|
||||
import { raise } from "../helpers/raise"
|
||||
import { waitFor } from "../helpers/wait-for"
|
||||
import type {
|
||||
ChannelInfo,
|
||||
GuildInfo,
|
||||
@@ -26,17 +26,10 @@ import type {
|
||||
CommandInteraction,
|
||||
SelectInteraction,
|
||||
} from "../library/internal/interaction"
|
||||
import type {
|
||||
Message,
|
||||
MessageButtonOptions,
|
||||
MessageOptions,
|
||||
MessageSelectOptions,
|
||||
} from "../library/internal/message"
|
||||
import type { Message, MessageOptions } from "../library/internal/message"
|
||||
import { ChannelMessageRenderer } from "../library/internal/renderers/channel-message-renderer"
|
||||
import { InteractionReplyRenderer } from "../library/internal/renderers/interaction-reply-renderer"
|
||||
|
||||
const nextTickPromise = promisify(nextTick)
|
||||
|
||||
export type MessageSample = ReturnType<ReacordTester["sampleMessages"]>[0]
|
||||
|
||||
/**
|
||||
@@ -73,9 +66,10 @@ export class ReacordTester extends Reacord {
|
||||
return this.reply(initialContent)
|
||||
}
|
||||
|
||||
async assertMessages(expected: MessageSample[]) {
|
||||
await nextTickPromise()
|
||||
expect(this.sampleMessages()).toEqual(expected)
|
||||
assertMessages(expected: MessageSample[]) {
|
||||
return waitFor(() => {
|
||||
expect(this.sampleMessages()).toEqual(expected)
|
||||
})
|
||||
}
|
||||
|
||||
async assertRender(content: ReactNode, expected: MessageSample[]) {
|
||||
@@ -108,57 +102,58 @@ export class ReacordTester extends Reacord {
|
||||
}
|
||||
|
||||
findButtonByLabel(label: string) {
|
||||
for (const message of this.messageContainer) {
|
||||
for (const component of message.options.actionRows.flat()) {
|
||||
if (component.type === "button" && component.label === label) {
|
||||
return this.createButtonActions(component, message)
|
||||
}
|
||||
}
|
||||
return {
|
||||
click: () => {
|
||||
return waitFor(() => {
|
||||
for (const [component, message] of this.eachComponent()) {
|
||||
if (component.type === "button" && component.label === label) {
|
||||
this.handleComponentInteraction(
|
||||
new TestButtonInteraction(component.customId, message, this),
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
raise(`Couldn't find button with label "${label}"`)
|
||||
})
|
||||
},
|
||||
}
|
||||
raise(`Couldn't find button with label "${label}"`)
|
||||
}
|
||||
|
||||
findSelectByPlaceholder(placeholder: string) {
|
||||
for (const message of this.messageContainer) {
|
||||
for (const component of message.options.actionRows.flat()) {
|
||||
if (
|
||||
component.type === "select" &&
|
||||
component.placeholder === placeholder
|
||||
) {
|
||||
return this.createSelectActions(component, message)
|
||||
}
|
||||
}
|
||||
return {
|
||||
select: (...values: string[]) => {
|
||||
return waitFor(() => {
|
||||
for (const [component, message] of this.eachComponent()) {
|
||||
if (
|
||||
component.type === "select" &&
|
||||
component.placeholder === placeholder
|
||||
) {
|
||||
this.handleComponentInteraction(
|
||||
new TestSelectInteraction(
|
||||
component.customId,
|
||||
message,
|
||||
values,
|
||||
this,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
raise(`Couldn't find select with placeholder "${placeholder}"`)
|
||||
})
|
||||
},
|
||||
}
|
||||
raise(`Couldn't find select with placeholder "${placeholder}"`)
|
||||
}
|
||||
|
||||
createMessage(options: MessageOptions) {
|
||||
return new TestMessage(options, this.messageContainer)
|
||||
}
|
||||
|
||||
private createButtonActions(
|
||||
button: MessageButtonOptions,
|
||||
message: TestMessage,
|
||||
) {
|
||||
return {
|
||||
click: () => {
|
||||
this.handleComponentInteraction(
|
||||
new TestButtonInteraction(button.customId, message, this),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private createSelectActions(
|
||||
component: MessageSelectOptions,
|
||||
message: TestMessage,
|
||||
) {
|
||||
return {
|
||||
select: (...values: string[]) => {
|
||||
this.handleComponentInteraction(
|
||||
new TestSelectInteraction(component.customId, message, values, this),
|
||||
)
|
||||
},
|
||||
private *eachComponent() {
|
||||
for (const message of this.messageContainer) {
|
||||
for (const component of message.options.actionRows.flat()) {
|
||||
yield [component, message] as const
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,16 +170,6 @@ class TestMessage implements Message {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
async disableComponents(): Promise<void> {
|
||||
for (const row of this.options.actionRows) {
|
||||
for (const action of row) {
|
||||
if (action.type === "button") {
|
||||
action.disabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
this.container.remove(this)
|
||||
}
|
||||
@@ -197,16 +182,14 @@ class TestCommandInteraction implements CommandInteraction {
|
||||
|
||||
constructor(private messageContainer: Container<TestMessage>) {}
|
||||
|
||||
reply(messageOptions: MessageOptions): Promise<Message> {
|
||||
return Promise.resolve(
|
||||
new TestMessage(messageOptions, this.messageContainer),
|
||||
)
|
||||
async reply(messageOptions: MessageOptions): Promise<Message> {
|
||||
await setTimeout()
|
||||
return new TestMessage(messageOptions, this.messageContainer)
|
||||
}
|
||||
|
||||
followUp(messageOptions: MessageOptions): Promise<Message> {
|
||||
return Promise.resolve(
|
||||
new TestMessage(messageOptions, this.messageContainer),
|
||||
)
|
||||
async followUp(messageOptions: MessageOptions): Promise<Message> {
|
||||
await setTimeout()
|
||||
return new TestMessage(messageOptions, this.messageContainer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("useInstance", () => {
|
||||
await tester.assertMessages([messageOutput("parent")])
|
||||
expect(instanceFromHook).toBe(instance)
|
||||
|
||||
tester.findButtonByLabel("create parent").click()
|
||||
await tester.findButtonByLabel("create parent").click()
|
||||
await tester.assertMessages([
|
||||
messageOutput("parent"),
|
||||
messageOutput("child"),
|
||||
@@ -63,10 +63,10 @@ describe("useInstance", () => {
|
||||
|
||||
// this test ensures that the only the child instance is destroyed,
|
||||
// and not the parent instance
|
||||
tester.findButtonByLabel("destroy child").click()
|
||||
await tester.findButtonByLabel("destroy child").click()
|
||||
await tester.assertMessages([messageOutput("parent")])
|
||||
|
||||
tester.findButtonByLabel("destroy parent").click()
|
||||
await tester.findButtonByLabel("destroy parent").click()
|
||||
await tester.assertMessages([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { hydrate } from "react-dom";
|
||||
import { RemixBrowser } from "remix";
|
||||
import { hydrate } from "react-dom"
|
||||
import { RemixBrowser } from "@remix-run/react"
|
||||
|
||||
hydrate(<RemixBrowser />, document);
|
||||
hydrate(<RemixBrowser />, document)
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { RemixServer } from "remix";
|
||||
import type { EntryContext } from "remix";
|
||||
import { renderToString } from "react-dom/server"
|
||||
import type { EntryContext } from "@remix-run/node"
|
||||
import { RemixServer } from "@remix-run/react"
|
||||
|
||||
export default function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
remixContext: EntryContext,
|
||||
) {
|
||||
const markup = renderToString(
|
||||
<RemixServer context={remixContext} url={request.url} />
|
||||
);
|
||||
<RemixServer context={remixContext} url={request.url} />,
|
||||
)
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
responseHeaders.set("Content-Type", "text/html")
|
||||
|
||||
return new Response("<!DOCTYPE html>" + markup, {
|
||||
status: responseStatusCode,
|
||||
headers: responseHeaders
|
||||
});
|
||||
headers: responseHeaders,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComponentPropsWithoutRef } from "react"
|
||||
import { Link } from "remix"
|
||||
import { Link } from "@remix-run/react"
|
||||
import { ExternalLink } from "~/modules/dom/external-link"
|
||||
|
||||
export type AppLinkProps = ComponentPropsWithoutRef<"a"> & {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import packageJson from "reacord/package.json"
|
||||
import type { LinksFunction, LoaderFunction, MetaFunction } from "remix"
|
||||
import type {
|
||||
LinksFunction,
|
||||
LoaderFunction,
|
||||
MetaFunction,
|
||||
} from "@remix-run/node"
|
||||
import {
|
||||
Links,
|
||||
LiveReload,
|
||||
@@ -8,7 +12,7 @@ import {
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
useLoaderData,
|
||||
} from "remix"
|
||||
} from "@remix-run/react"
|
||||
import bannerUrl from "~/assets/banner.png"
|
||||
import faviconUrl from "~/assets/favicon.png"
|
||||
import { GuideLinksProvider } from "~/modules/navigation/guide-links-context"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from "clsx"
|
||||
import { Outlet } from "remix"
|
||||
import { Outlet } from "@remix-run/react"
|
||||
import { ActiveLink } from "~/modules/navigation/active-link"
|
||||
import { AppLink } from "~/modules/navigation/app-link"
|
||||
import { useGuideLinksContext } from "~/modules/navigation/guide-links-context"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
import "./commands"
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"private": true,
|
||||
"name": "website",
|
||||
"scripts": {
|
||||
"prepare": "remix setup node",
|
||||
"dev": "concurrently 'typedoc --watch' 'pnpm tailwind -- --watch' 'remix dev'",
|
||||
"test": "node ./scripts/test.js",
|
||||
"test-dev": "pnpm dev & wait-on http-get://localhost:3000 && cypress open",
|
||||
@@ -12,42 +11,42 @@
|
||||
"typecheck": "tsc --noEmit && tsc --project cypress/tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.4.2",
|
||||
"@heroicons/react": "^1.0.5",
|
||||
"@reach/rect": "^0.16.0",
|
||||
"@remix-run/react": "^1.1.1",
|
||||
"@remix-run/serve": "^1.1.1",
|
||||
"@tailwindcss/typography": "^0.5.0",
|
||||
"@headlessui/react": "^1.5.0",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@reach/rect": "^0.17.0",
|
||||
"@remix-run/node": "^1.4.1",
|
||||
"@remix-run/react": "^1.4.1",
|
||||
"@remix-run/serve": "^1.4.1",
|
||||
"@tailwindcss/typography": "^0.5.2",
|
||||
"clsx": "^1.1.1",
|
||||
"fast-glob": "^3.2.10",
|
||||
"fast-glob": "^3.2.11",
|
||||
"gray-matter": "^4.0.3",
|
||||
"reacord": "workspace:*",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-focus-on": "^3.5.4",
|
||||
"react-router": "^6.2.1",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"remix": "^1.1.1"
|
||||
"react-router": "^6.3.0",
|
||||
"react-router-dom": "^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^1.1.1",
|
||||
"@remix-run/node": "^1.1.1",
|
||||
"@remix-run/dev": "^1.4.1",
|
||||
"@remix-run/node": "^1.4.1",
|
||||
"@testing-library/cypress": "^8.0.2",
|
||||
"@types/node": "*",
|
||||
"@types/react": "^17.0.38",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/tailwindcss": "^3.0.2",
|
||||
"@types/react": "^18.0.6",
|
||||
"@types/react-dom": "^18.0.2",
|
||||
"@types/tailwindcss": "^3.0.10",
|
||||
"@types/wait-on": "^5.3.1",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"concurrently": "^7.0.0",
|
||||
"cypress": "^9.2.1",
|
||||
"execa": "^6.0.0",
|
||||
"postcss": "^8.4.5",
|
||||
"rehype-prism-plus": "^1.3.1",
|
||||
"tailwindcss": "^3.0.13",
|
||||
"typedoc": "^0.22.10",
|
||||
"typescript": "^4.5.4",
|
||||
"wait-on": "^6.0.0"
|
||||
"autoprefixer": "^10.4.4",
|
||||
"concurrently": "^7.1.0",
|
||||
"cypress": "^9.5.4",
|
||||
"execa": "^6.1.0",
|
||||
"postcss": "^8.4.12",
|
||||
"rehype-prism-plus": "^1.3.2",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"typedoc": "^0.22.15",
|
||||
"typescript": "^4.6.3",
|
||||
"wait-on": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
}
|
||||
|
||||
3223
pnpm-lock.yaml
generated
3223
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user