From 765c6fadbb30ab3c3166c7b71d1d9c72bd5a02fc Mon Sep 17 00:00:00 2001
From: MapleLeaf <19603573+itsMapleLeaf@users.noreply.github.com>
Date: Wed, 22 Dec 2021 10:35:55 -0600
Subject: [PATCH] refactor and simplify things
---
integration/rendering.test.tsx | 13 +-
jest.config.js | 1 +
package.json | 2 +-
src/action-row.tsx | 45 ------
src/base-instance.ts | 25 ---
src/button.tsx | 77 ----------
src/components/action-row.tsx | 13 ++
src/components/button.tsx | 21 +++
src/components/embed-field.tsx | 17 +++
src/components/embed.tsx | 32 ++++
src/components/text.tsx | 14 ++
src/container-instance.ts | 36 -----
src/embed-field.tsx | 34 -----
src/embed.tsx | 85 -----------
src/helpers/pick.ts | 1 +
src/helpers/types.ts | 1 +
src/helpers/wait-for-with-timeout.ts | 1 +
src/helpers/with-logged-method-calls.ts | 1 +
src/jsx.d.ts | 17 ++-
src/main.ts | 10 +-
src/node-tree.ts | 192 ++++++++++++++++++++++++
src/reconciler.ts | 65 ++++----
src/{container.ts => renderer.ts} | 26 +---
src/root.ts | 4 +-
src/text-instance.ts | 23 ---
src/text.tsx | 36 -----
26 files changed, 351 insertions(+), 441 deletions(-)
delete mode 100644 src/action-row.tsx
delete mode 100644 src/base-instance.ts
delete mode 100644 src/button.tsx
create mode 100644 src/components/action-row.tsx
create mode 100644 src/components/button.tsx
create mode 100644 src/components/embed-field.tsx
create mode 100644 src/components/embed.tsx
create mode 100644 src/components/text.tsx
delete mode 100644 src/container-instance.ts
delete mode 100644 src/embed-field.tsx
delete mode 100644 src/embed.tsx
create mode 100644 src/node-tree.ts
rename src/{container.ts => renderer.ts} (78%)
delete mode 100644 src/text-instance.ts
delete mode 100644 src/text.tsx
diff --git a/integration/rendering.test.tsx b/integration/rendering.test.tsx
index 521c3c5..652227e 100644
--- a/integration/rendering.test.tsx
+++ b/integration/rendering.test.tsx
@@ -74,14 +74,7 @@ test("empty embed fallback", async () => {
test("embed with only author", async () => {
await root.render()
- await assertMessages([
- { embeds: [{ description: "_ _", author: { name: "only author" } }] },
- ])
-})
-
-test("empty embed author", async () => {
- await root.render()
- await assertMessages([{ embeds: [{ description: "_ _" }] }])
+ await assertMessages([{ embeds: [{ author: { name: "only author" } }] }])
})
test("kitchen sink", async () => {
@@ -252,7 +245,7 @@ async function assertMessages(expected: MessageOptions[]) {
}
function extractMessageData(message: Message): MessageOptions {
- return {
+ return pruneUndefinedValues({
content: nonEmptyOrUndefined(message.content),
embeds: nonEmptyOrUndefined(
pruneUndefinedValues(
@@ -305,7 +298,7 @@ function extractMessageData(message: Message): MessageOptions {
}),
})),
),
- }
+ })
}
function pruneUndefinedValues(input: T) {
diff --git a/jest.config.js b/jest.config.js
index f6800c2..f84ab63 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -16,4 +16,5 @@ const config = {
verbose: true,
}
+// eslint-disable-next-line import/no-unused-modules
export default config
diff --git a/package.json b/package.json
index b481c59..20ffe5a 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
"lint": "eslint --ext js,ts,tsx .",
"lint-fix": "pnpm lint -- --fix",
"format": "prettier --write .",
- "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
+ "test": "node --experimental-vm-modules --no-warnings node_modules/jest/bin/jest.js",
"test-watch": "pnpm test -- --watch",
"coverage": "pnpm test -- --coverage",
"typecheck": "tsc --noEmit"
diff --git a/src/action-row.tsx b/src/action-row.tsx
deleted file mode 100644
index b82be05..0000000
--- a/src/action-row.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import type {
- MessageActionRowComponentOptions,
- MessageOptions,
-} from "discord.js"
-import React from "react"
-import { ContainerInstance } from "./container-instance.js"
-
-export type ActionRowProps = {
- children: React.ReactNode
-}
-
-export function ActionRow(props: ActionRowProps) {
- return (
- new ActionRowInstance()}>
- {props.children}
-
- )
-}
-
-class ActionRowInstance extends ContainerInstance {
- readonly name = "ActionRow"
-
- constructor() {
- super({ warnOnNonTextChildren: false })
- }
-
- // eslint-disable-next-line class-methods-use-this
- override renderToMessage(options: MessageOptions) {
- const row = {
- type: "ACTION_ROW" as const,
- components: [] as MessageActionRowComponentOptions[],
- }
-
- for (const child of this.children) {
- if (!child.renderToActionRow) {
- console.warn(`${child.name} is not an action row component`)
- continue
- }
- child.renderToActionRow(row)
- }
-
- options.components ??= []
- options.components.push(row)
- }
-}
diff --git a/src/base-instance.ts b/src/base-instance.ts
deleted file mode 100644
index 10187da..0000000
--- a/src/base-instance.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type {
- MessageActionRowOptions,
- MessageEmbedOptions,
- MessageOptions,
-} from "discord.js"
-
-export abstract class BaseInstance {
- /** The name of the JSX element represented by this instance */
- abstract readonly name: string
-
- /** If the element represents text, the text for this element */
- getText?(): string
-
- /** If this element can be a child of a message,
- * the function to modify the message options */
- renderToMessage?(options: MessageOptions): void
-
- /** If this element can be a child of an embed,
- * the function to modify the embed options */
- renderToEmbed?(options: MessageEmbedOptions): void
-
- /** If this element can be a child of an action row,
- * the function to modify the action row options */
- renderToActionRow?(options: MessageActionRowOptions): void
-}
diff --git a/src/button.tsx b/src/button.tsx
deleted file mode 100644
index 04ac77b..0000000
--- a/src/button.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import type {
- BaseMessageComponentOptions,
- EmojiResolvable,
- MessageActionRowOptions,
- MessageButtonOptions,
- MessageButtonStyle,
- MessageOptions,
-} from "discord.js"
-import { nanoid } from "nanoid"
-import React from "react"
-import { ContainerInstance } from "./container-instance.js"
-import { last } from "./helpers/last.js"
-import { pick } from "./helpers/pick.js"
-import { toUpper } from "./helpers/to-upper.js"
-
-export type ButtonStyle = Exclude, "link">
-
-export type ButtonProps = {
- style?: ButtonStyle
- emoji?: EmojiResolvable
- disabled?: boolean
- children?: React.ReactNode
-}
-
-export function Button(props: ButtonProps) {
- return (
- new ButtonInstance(props)}>
- {props.children}
-
- )
-}
-
-class ButtonInstance extends ContainerInstance {
- readonly name = "Button"
-
- constructor(private readonly props: ButtonProps) {
- super({ warnOnNonTextChildren: true })
- }
-
- private getButtonOptions(): Required &
- MessageButtonOptions {
- return {
- ...pick(this.props, "emoji", "disabled"),
- type: "BUTTON",
- style: this.props.style ? toUpper(this.props.style) : "SECONDARY",
- label: this.getChildrenText(),
- customId: nanoid(),
- }
- }
-
- override renderToMessage(options: MessageOptions) {
- options.components ??= []
-
- // i hate this
- let actionRow:
- | (Required & MessageActionRowOptions)
- | undefined = last(options.components)
-
- if (
- !actionRow ||
- actionRow.components[0]?.type === "SELECT_MENU" ||
- actionRow.components.length >= 5
- ) {
- actionRow = {
- type: "ACTION_ROW",
- components: [],
- }
- options.components.push(actionRow)
- }
-
- actionRow.components.push(this.getButtonOptions())
- }
-
- override renderToActionRow(row: MessageActionRowOptions) {
- row.components.push(this.getButtonOptions())
- }
-}
diff --git a/src/components/action-row.tsx b/src/components/action-row.tsx
new file mode 100644
index 0000000..c68c5ff
--- /dev/null
+++ b/src/components/action-row.tsx
@@ -0,0 +1,13 @@
+import React from "react"
+
+export type ActionRowProps = {
+ children: React.ReactNode
+}
+
+export function ActionRow(props: ActionRowProps) {
+ return (
+ ({ type: "actionRow", children: [] })}>
+ {props.children}
+
+ )
+}
diff --git a/src/components/button.tsx b/src/components/button.tsx
new file mode 100644
index 0000000..7f5dac4
--- /dev/null
+++ b/src/components/button.tsx
@@ -0,0 +1,21 @@
+import type { EmojiResolvable, MessageButtonStyle } from "discord.js"
+import React from "react"
+
+export type ButtonStyle = Exclude, "link">
+
+export type ButtonProps = {
+ style?: ButtonStyle
+ emoji?: EmojiResolvable
+ disabled?: boolean
+ children?: React.ReactNode
+}
+
+export function Button(props: ButtonProps) {
+ return (
+ ({ ...props, type: "button", children: [] })}
+ >
+ {props.children}
+
+ )
+}
diff --git a/src/components/embed-field.tsx b/src/components/embed-field.tsx
new file mode 100644
index 0000000..881bf8b
--- /dev/null
+++ b/src/components/embed-field.tsx
@@ -0,0 +1,17 @@
+import React from "react"
+
+export type EmbedFieldProps = {
+ name: string
+ children: React.ReactNode
+ inline?: boolean
+}
+
+export function EmbedField(props: EmbedFieldProps) {
+ return (
+ ({ ...props, type: "embedField", children: [] })}
+ >
+ {props.children}
+
+ )
+}
diff --git a/src/components/embed.tsx b/src/components/embed.tsx
new file mode 100644
index 0000000..51afa6e
--- /dev/null
+++ b/src/components/embed.tsx
@@ -0,0 +1,32 @@
+import type { ColorResolvable } from "discord.js"
+import type { ReactNode } from "react"
+import React from "react"
+
+export type EmbedProps = {
+ title?: string
+ color?: ColorResolvable
+ url?: string
+ timestamp?: Date | number | string
+ imageUrl?: string
+ thumbnailUrl?: string
+ author?: {
+ name: string
+ url?: string
+ iconUrl?: string
+ }
+ footer?: {
+ text: string
+ iconUrl?: string
+ }
+ children?: ReactNode
+}
+
+export function Embed(props: EmbedProps) {
+ return (
+ ({ ...props, type: "embed", children: [] })}
+ >
+ {props.children}
+
+ )
+}
diff --git a/src/components/text.tsx b/src/components/text.tsx
new file mode 100644
index 0000000..c46a50c
--- /dev/null
+++ b/src/components/text.tsx
@@ -0,0 +1,14 @@
+import type { ReactNode } from "react"
+import React from "react"
+
+export type TextProps = {
+ children?: ReactNode
+}
+
+export function Text(props: TextProps) {
+ return (
+ ({ type: "textElement", children: [] })}>
+ {props.children}
+
+ )
+}
diff --git a/src/container-instance.ts b/src/container-instance.ts
deleted file mode 100644
index bc64ad6..0000000
--- a/src/container-instance.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { BaseInstance } from "./base-instance.js"
-
-// eslint-disable-next-line import/no-unused-modules
-export type ContainerInstanceOptions = {
- /**
- * Whether or not to log a warning when calling getChildrenText() with non-text children
- *
- * Regardless of what this is set to, non-text children will always be skipped */
- warnOnNonTextChildren: boolean
-}
-
-export abstract class ContainerInstance extends BaseInstance {
- readonly children: BaseInstance[] = []
-
- constructor(private readonly options: ContainerInstanceOptions) {
- super()
- }
-
- add(child: BaseInstance) {
- this.children.push(child)
- }
-
- protected getChildrenText(): string {
- let text = ""
- for (const child of this.children) {
- if (!child.getText) {
- if (this.options.warnOnNonTextChildren) {
- console.warn(`${child.name} is not a valid child of ${this.name}`)
- }
- continue
- }
- text += child.getText()
- }
- return text
- }
-}
diff --git a/src/embed-field.tsx b/src/embed-field.tsx
deleted file mode 100644
index df87e03..0000000
--- a/src/embed-field.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import type { MessageEmbedOptions } from "discord.js"
-import React from "react"
-import { ContainerInstance } from "./container-instance.js"
-import { pick } from "./helpers/pick.js"
-
-export type EmbedFieldProps = {
- name: string
- children: React.ReactNode
- inline?: boolean
-}
-
-export function EmbedField(props: EmbedFieldProps) {
- return (
- new EmbedFieldInstance(props)}>
- {props.children}
-
- )
-}
-
-class EmbedFieldInstance extends ContainerInstance {
- readonly name = "EmbedField"
-
- constructor(private readonly props: EmbedFieldProps) {
- super({ warnOnNonTextChildren: true })
- }
-
- override renderToEmbed(options: MessageEmbedOptions) {
- options.fields ??= []
- options.fields.push({
- ...pick(this.props, "name", "inline"),
- value: this.getChildrenText(),
- })
- }
-}
diff --git a/src/embed.tsx b/src/embed.tsx
deleted file mode 100644
index 66e935b..0000000
--- a/src/embed.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import type {
- ColorResolvable,
- MessageEmbedOptions,
- MessageOptions,
-} from "discord.js"
-import type { ReactNode } from "react"
-import React from "react"
-import { ContainerInstance } from "./container-instance.js"
-
-export type EmbedProps = {
- title?: string
- color?: ColorResolvable
- url?: string
- timestamp?: Date | number | string
- imageUrl?: string
- thumbnailUrl?: string
- author?: {
- name?: string
- url?: string
- iconUrl?: string
- }
- footer?: {
- text?: string
- iconUrl?: string
- }
- children?: ReactNode
-}
-
-export function Embed(props: EmbedProps) {
- return (
- new EmbedInstance(props)}>
- {props.children}
-
- )
-}
-
-class EmbedInstance extends ContainerInstance {
- readonly name = "Embed"
-
- constructor(readonly props: EmbedProps) {
- super({ warnOnNonTextChildren: false })
- }
-
- override renderToMessage(message: MessageOptions) {
- message.embeds ??= []
- message.embeds.push(this.getEmbedOptions())
- }
-
- getEmbedOptions(): MessageEmbedOptions {
- const options: MessageEmbedOptions = {
- ...this.props,
- image: this.props.imageUrl ? { url: this.props.imageUrl } : undefined,
- thumbnail: this.props.thumbnailUrl
- ? { url: this.props.thumbnailUrl }
- : undefined,
- author: {
- ...this.props.author,
- iconURL: this.props.author?.iconUrl,
- },
- footer: {
- text: "",
- ...this.props.footer,
- iconURL: this.props.footer?.iconUrl,
- },
- timestamp: this.props.timestamp
- ? new Date(this.props.timestamp) // this _may_ need date-fns to parse this
- : undefined,
- }
-
- for (const child of this.children) {
- if (!child.renderToEmbed) {
- console.warn(`${child.name} is not a valid child of ${this.name}`)
- continue
- }
- child.renderToEmbed(options)
- }
-
- // can't render an empty embed
- if (!options.description) {
- options.description = "_ _"
- }
-
- return options
- }
-}
diff --git a/src/helpers/pick.ts b/src/helpers/pick.ts
index 403bc98..e921acb 100644
--- a/src/helpers/pick.ts
+++ b/src/helpers/pick.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/no-unused-modules
export function pick(
object: T,
...keys: K[]
diff --git a/src/helpers/types.ts b/src/helpers/types.ts
index 5060b24..d89c1c7 100644
--- a/src/helpers/types.ts
+++ b/src/helpers/types.ts
@@ -1,3 +1,4 @@
+/* eslint-disable import/no-unused-modules */
export type MaybePromise = T | Promise
export type ValueOf = Type extends ReadonlyArray
diff --git a/src/helpers/wait-for-with-timeout.ts b/src/helpers/wait-for-with-timeout.ts
index 6a93e85..90aaeea 100644
--- a/src/helpers/wait-for-with-timeout.ts
+++ b/src/helpers/wait-for-with-timeout.ts
@@ -2,6 +2,7 @@ import { rejectAfter } from "./reject-after.js"
import type { MaybePromise } from "./types.js"
import { waitFor } from "./wait-for.js"
+// eslint-disable-next-line import/no-unused-modules
export function waitForWithTimeout(
condition: () => MaybePromise,
timeout = 1000,
diff --git a/src/helpers/with-logged-method-calls.ts b/src/helpers/with-logged-method-calls.ts
index e99eb72..d7e086a 100644
--- a/src/helpers/with-logged-method-calls.ts
+++ b/src/helpers/with-logged-method-calls.ts
@@ -1,5 +1,6 @@
import { inspect } from "node:util"
+// eslint-disable-next-line import/no-unused-modules
export function withLoggedMethodCalls(value: T) {
return new Proxy(value as Record, {
get(target, property) {
diff --git a/src/jsx.d.ts b/src/jsx.d.ts
index eb41a3b..2311575 100644
--- a/src/jsx.d.ts
+++ b/src/jsx.d.ts
@@ -1,11 +1,14 @@
-declare namespace JSX {
- import type { ReactNode } from "react"
+import type { ReactNode } from "react"
+import type { Node } from "./node-tree"
- // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
- interface IntrinsicElements {
- "reacord-element": {
- createInstance: () => unknown
- children?: ReactNode
+declare global {
+ namespace JSX {
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+ interface IntrinsicElements {
+ "reacord-element": {
+ createNode: () => Node
+ children?: ReactNode
+ }
}
}
}
diff --git a/src/main.ts b/src/main.ts
index e9e235c..0e277d2 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,7 +1,7 @@
/* eslint-disable import/no-unused-modules */
-export * from "./action-row.js"
-export * from "./button.js"
-export * from "./embed-field.js"
-export * from "./embed.js"
+export * from "./components/action-row.js"
+export * from "./components/button.js"
+export * from "./components/embed-field.js"
+export * from "./components/embed.js"
+export * from "./components/text.js"
export * from "./root.js"
-export * from "./text.js"
diff --git a/src/node-tree.ts b/src/node-tree.ts
new file mode 100644
index 0000000..a7802cf
--- /dev/null
+++ b/src/node-tree.ts
@@ -0,0 +1,192 @@
+import type {
+ BaseMessageComponentOptions,
+ ColorResolvable,
+ EmojiResolvable,
+ MessageActionRowOptions,
+ MessageEmbedOptions,
+ MessageOptions,
+} from "discord.js"
+import { nanoid } from "nanoid"
+import type { ButtonStyle } from "./components/button.js"
+import { last } from "./helpers/last.js"
+import { toUpper } from "./helpers/to-upper.js"
+
+export type MessageNode = {
+ type: "message"
+ children: Node[]
+}
+
+export type TextNode = {
+ type: "text"
+ text: string
+}
+
+type TextElementNode = {
+ type: "textElement"
+ children: Node[]
+}
+
+type EmbedNode = {
+ type: "embed"
+ title?: string
+ color?: ColorResolvable
+ url?: string
+ timestamp?: Date | number | string
+ imageUrl?: string
+ thumbnailUrl?: string
+ author?: {
+ name: string
+ url?: string
+ iconUrl?: string
+ }
+ footer?: {
+ text: string
+ iconUrl?: string
+ }
+ children: Node[]
+}
+
+type EmbedFieldNode = {
+ type: "embedField"
+ name: string
+ inline?: boolean
+ children: Node[]
+}
+
+type ActionRowNode = {
+ type: "actionRow"
+ children: Node[]
+}
+
+type ButtonNode = {
+ type: "button"
+ style?: ButtonStyle
+ emoji?: EmojiResolvable
+ disabled?: boolean
+ children: Node[]
+}
+
+export type Node =
+ | MessageNode
+ | TextNode
+ | TextElementNode
+ | EmbedNode
+ | EmbedFieldNode
+ | ActionRowNode
+ | ButtonNode
+
+export function getMessageOptions(node: MessageNode): MessageOptions {
+ if (node.children.length === 0) {
+ // can't send an empty message
+ return { content: "_ _" }
+ }
+
+ const options: MessageOptions = {}
+
+ for (const child of node.children) {
+ if (child.type === "text" || child.type === "textElement") {
+ options.content = `${options.content ?? ""}${getNodeText(child)}`
+ }
+
+ if (child.type === "embed") {
+ options.embeds ??= []
+ options.embeds.push(getEmbedOptions(child))
+ }
+
+ if (child.type === "actionRow") {
+ options.components ??= []
+ options.components.push({
+ type: "ACTION_ROW",
+ components: [],
+ })
+ addActionRowItems(options.components, child.children)
+ }
+
+ if (child.type === "button") {
+ options.components ??= []
+ addActionRowItems(options.components, [child])
+ }
+ }
+
+ if (!options.content && !options.embeds?.length) {
+ options.content = "_ _"
+ }
+
+ return options
+}
+
+function getNodeText(node: Node): string | undefined {
+ if (node.type === "text") {
+ return node.text
+ }
+ if (node.type === "textElement") {
+ return node.children.map(getNodeText).join("")
+ }
+}
+
+function getEmbedOptions(node: EmbedNode) {
+ const options: MessageEmbedOptions = {
+ title: node.title,
+ color: node.color,
+ url: node.url,
+ timestamp: node.timestamp ? new Date(node.timestamp) : undefined,
+ image: { url: node.imageUrl },
+ thumbnail: { url: node.thumbnailUrl },
+ description: node.children.map(getNodeText).join(""),
+ author: node.author
+ ? { ...node.author, iconURL: node.author.iconUrl }
+ : undefined,
+ footer: node.footer
+ ? { text: node.footer.text, iconURL: node.footer.iconUrl }
+ : undefined,
+ }
+
+ for (const child of node.children) {
+ if (child.type === "embedField") {
+ options.fields ??= []
+ options.fields.push({
+ name: child.name,
+ value: child.children.map(getNodeText).join("") || "_ _",
+ inline: child.inline,
+ })
+ }
+ }
+
+ if (!options.description && !options.author) {
+ options.description = "_ _"
+ }
+
+ return options
+}
+
+type ActionRowOptions = Required &
+ MessageActionRowOptions
+
+function addActionRowItems(components: ActionRowOptions[], nodes: Node[]) {
+ let actionRow = last(components)
+
+ if (
+ actionRow == undefined ||
+ actionRow.components[0]?.type === "SELECT_MENU" ||
+ actionRow.components.length >= 5
+ ) {
+ actionRow = {
+ type: "ACTION_ROW",
+ components: [],
+ }
+ components.push(actionRow)
+ }
+
+ for (const node of nodes) {
+ if (node.type === "button") {
+ actionRow.components.push({
+ type: "BUTTON",
+ label: node.children.map(getNodeText).join(""),
+ style: node.style ? toUpper(node.style) : "SECONDARY",
+ emoji: node.emoji,
+ disabled: node.disabled,
+ customId: nanoid(),
+ })
+ }
+ }
+}
diff --git a/src/reconciler.ts b/src/reconciler.ts
index fc68e56..cb592ed 100644
--- a/src/reconciler.ts
+++ b/src/reconciler.ts
@@ -1,46 +1,41 @@
/* eslint-disable unicorn/no-null */
import { inspect } from "node:util"
import ReactReconciler from "react-reconciler"
-import { BaseInstance } from "./base-instance.js"
-import { ContainerInstance } from "./container-instance.js"
-import type { ReacordContainer } from "./container.js"
import { raise } from "./helpers/raise.js"
-import { TextInstance } from "./text-instance.js"
+import type { MessageNode, Node, TextNode } from "./node-tree.js"
+import type { MessageRenderer } from "./renderer.js"
type ElementTag = string
type Props = Record
-const createInstance = (type: string, props: Props): BaseInstance => {
+const createInstance = (type: string, props: Props): Node => {
if (type !== "reacord-element") {
raise(`createInstance: unknown type: ${type}`)
}
- if (typeof props.createInstance !== "function") {
- const actual = inspect(props.createInstance)
- raise(`invalid createInstance function, received: ${actual}`)
+ if (typeof props.createNode !== "function") {
+ const actual = inspect(props.createNode)
+ raise(`invalid createNode function, received: ${actual}`)
}
- const instance = props.createInstance()
- if (!(instance instanceof BaseInstance)) {
- raise(`invalid instance: ${inspect(instance)}`)
- }
-
- return instance
+ return props.createNode()
}
+type ChildSet = MessageNode
+
export const reconciler = ReactReconciler<
string, // Type (jsx tag),
Props, // Props,
- ReacordContainer, // Container,
- BaseInstance, // Instance,
- TextInstance, // TextInstance,
+ MessageRenderer, // Container,
+ Node, // Instance,
+ TextNode, // TextInstance,
never, // SuspenseInstance,
never, // HydratableInstance,
never, // PublicInstance,
null, // HostContext,
[], // UpdatePayload,
- BaseInstance[], // ChildSet,
+ ChildSet, // ChildSet,
unknown, // TimeoutHandle,
unknown // NoTimeout
>({
@@ -59,41 +54,37 @@ export const reconciler = ReactReconciler<
createInstance,
- createTextInstance: (text) => new TextInstance(text),
+ createTextInstance: (text) => ({ type: "text", text }),
- createContainerChildSet: () => [],
+ createContainerChildSet: (): ChildSet => ({
+ type: "message",
+ children: [],
+ }),
- appendChildToContainerChildSet: (
- childSet: BaseInstance[],
- child: BaseInstance,
- ) => {
- childSet.push(child)
+ appendChildToContainerChildSet: (childSet: ChildSet, child: Node) => {
+ childSet.children.push(child)
},
- finalizeContainerChildren: (
- container: ReacordContainer,
- children: BaseInstance[],
- ) => false,
+ finalizeContainerChildren: (container: MessageRenderer, children: ChildSet) =>
+ false,
replaceContainerChildren: (
- container: ReacordContainer,
- children: BaseInstance[],
+ container: MessageRenderer,
+ children: ChildSet,
) => {
container.render(children)
},
appendInitialChild: (parent, child) => {
- if (parent instanceof ContainerInstance) {
- parent.add(child)
+ if ("children" in parent) {
+ parent.children.push(child)
} else {
- raise(
- `Cannot append child of type ${child.constructor.name} to ${parent.constructor.name}`,
- )
+ raise(`${parent.type} cannot have children`)
}
},
cloneInstance: (
- instance: BaseInstance,
+ instance: Node,
_: unknown,
type: ElementTag,
oldProps: Props,
diff --git a/src/container.ts b/src/renderer.ts
similarity index 78%
rename from src/container.ts
rename to src/renderer.ts
index 8302e78..3e50cb9 100644
--- a/src/container.ts
+++ b/src/renderer.ts
@@ -1,11 +1,12 @@
import type { Message, MessageOptions, TextBasedChannels } from "discord.js"
-import type { BaseInstance } from "./base-instance.js"
+import type { MessageNode } from "./node-tree.js"
+import { getMessageOptions } from "./node-tree.js"
type Action =
| { type: "updateMessage"; options: MessageOptions }
| { type: "deleteMessage" }
-export class ReacordContainer {
+export class MessageRenderer {
private channel: TextBasedChannels
private message?: Message
private actions: Action[] = []
@@ -15,22 +16,11 @@ export class ReacordContainer {
this.channel = channel
}
- render(children: BaseInstance[]) {
- const options: MessageOptions = {}
- for (const child of children) {
- if (!child.renderToMessage) {
- console.warn(`${child.name} is not a valid message child`)
- continue
- }
- child.renderToMessage(options)
- }
-
- // can't render an empty message
- if (!options?.content && !options.embeds?.length) {
- options.content = "_ _"
- }
-
- this.addAction({ type: "updateMessage", options })
+ render(node: MessageNode) {
+ this.addAction({
+ type: "updateMessage",
+ options: getMessageOptions(node),
+ })
}
destroy() {
diff --git a/src/root.ts b/src/root.ts
index 9043295..98d5cba 100644
--- a/src/root.ts
+++ b/src/root.ts
@@ -1,15 +1,15 @@
/* eslint-disable unicorn/no-null */
import type { TextBasedChannels } from "discord.js"
import type { ReactNode } from "react"
-import { ReacordContainer } from "./container"
import { reconciler } from "./reconciler"
+import { MessageRenderer } from "./renderer"
export type ReacordRenderTarget = TextBasedChannels
export type ReacordRoot = ReturnType
export function createRoot(target: ReacordRenderTarget) {
- const container = new ReacordContainer(target)
+ const container = new MessageRenderer(target)
const containerId = reconciler.createContainer(container, 0, false, null)
return {
render: (content: ReactNode) => {
diff --git a/src/text-instance.ts b/src/text-instance.ts
deleted file mode 100644
index 1f8be1c..0000000
--- a/src/text-instance.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { MessageEmbedOptions, MessageOptions } from "discord.js"
-import { BaseInstance } from "./base-instance.js"
-
-/** Represents raw strings in JSX */
-export class TextInstance extends BaseInstance {
- readonly name = "Text"
-
- constructor(private readonly text: string) {
- super()
- }
-
- override getText() {
- return this.text
- }
-
- override renderToMessage(options: MessageOptions) {
- options.content = (options.content ?? "") + this.getText()
- }
-
- override renderToEmbed(options: MessageEmbedOptions) {
- options.description = (options.description ?? "") + this.getText()
- }
-}
diff --git a/src/text.tsx b/src/text.tsx
deleted file mode 100644
index 89bad7d..0000000
--- a/src/text.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import type { MessageEmbedOptions, MessageOptions } from "discord.js"
-import type { ReactNode } from "react"
-import React from "react"
-import { ContainerInstance } from "./container-instance.js"
-
-export type TextProps = {
- children?: ReactNode
-}
-
-export function Text(props: TextProps) {
- return (
- new TextElementInstance()}>
- {props.children}
-
- )
-}
-
-class TextElementInstance extends ContainerInstance {
- readonly name = "Text"
-
- constructor() {
- super({ warnOnNonTextChildren: true })
- }
-
- override getText() {
- return this.getChildrenText()
- }
-
- override renderToMessage(options: MessageOptions) {
- options.content = (options.content ?? "") + this.getText()
- }
-
- override renderToEmbed(options: MessageEmbedOptions) {
- options.description = (options.description ?? "") + this.getText()
- }
-}