From 9828b5c5361f42f2440a704fc7683624ade393a2 Mon Sep 17 00:00:00 2001
From: MapleLeaf <19603573+itsMapleLeaf@users.noreply.github.com>
Date: Sun, 19 Dec 2021 16:38:32 -0600
Subject: [PATCH] decentralization refactor wip
---
.../tests/rendering.test.tsx | 14 +---
packages/reacord/src/components/embed.tsx | 5 +-
packages/reacord/src/components/text.tsx | 3 +-
packages/reacord/src/elements.ts | 14 ----
packages/reacord/src/main.ts | 2 +-
.../reacord/src/renderer/base-instance.ts | 13 +++
.../src/renderer/container-instance.ts | 40 ++++++++++
packages/reacord/src/renderer/container.ts | 12 +--
packages/reacord/src/renderer/elements.d.ts | 13 +++
.../reacord/src/renderer/embed-instance.ts | 20 ++---
.../reacord/src/{ => renderer}/reconciler.ts | 79 ++++++++-----------
packages/reacord/src/{ => renderer}/root.ts | 2 +-
.../src/renderer/text-element-instance.ts | 23 +++---
.../reacord/src/renderer/text-instance.ts | 18 ++++-
tsconfig.json | 3 +
15 files changed, 151 insertions(+), 110 deletions(-)
delete mode 100644 packages/reacord/src/elements.ts
create mode 100644 packages/reacord/src/renderer/base-instance.ts
create mode 100644 packages/reacord/src/renderer/container-instance.ts
create mode 100644 packages/reacord/src/renderer/elements.d.ts
rename packages/reacord/src/{ => renderer}/reconciler.ts (52%)
rename packages/reacord/src/{ => renderer}/root.ts (92%)
diff --git a/packages/integration-tests/tests/rendering.test.tsx b/packages/integration-tests/tests/rendering.test.tsx
index 63be35a..5c5a2d7 100644
--- a/packages/integration-tests/tests/rendering.test.tsx
+++ b/packages/integration-tests/tests/rendering.test.tsx
@@ -40,7 +40,7 @@ test.beforeEach(async () => {
await Promise.all(messages.map((message) => message.delete()))
})
-test.serial("kitchen sink + destroy", async (t) => {
+test.serial.only("kitchen sink + destroy", async (t) => {
const root = createRoot(channel)
await root.render(
@@ -110,18 +110,6 @@ test.serial("empty embed fallback", async (t) => {
await assertMessages(t, [{ embeds: [{ color: null, description: "_ _" }] }])
})
-test.serial("invalid children error", (t) => {
- const root = createRoot(channel)
-
- t.throws(() =>
- root.render(
-
-
- ,
- ),
- )
-})
-
type MessageData = ReturnType
function extractMessageData(message: Message) {
return {
diff --git a/packages/reacord/src/components/embed.tsx b/packages/reacord/src/components/embed.tsx
index f581387..b70b03b 100644
--- a/packages/reacord/src/components/embed.tsx
+++ b/packages/reacord/src/components/embed.tsx
@@ -1,6 +1,7 @@
import type { ColorResolvable } from "discord.js"
import type { ReactNode } from "react"
import React from "react"
+import { EmbedInstance } from "../renderer/embed-instance.js"
export type EmbedProps = {
color?: ColorResolvable
@@ -8,5 +9,7 @@ export type EmbedProps = {
}
export function Embed(props: EmbedProps) {
- return
+ return (
+ new EmbedInstance(props.color)} />
+ )
}
diff --git a/packages/reacord/src/components/text.tsx b/packages/reacord/src/components/text.tsx
index e17e7fa..e4d69f8 100644
--- a/packages/reacord/src/components/text.tsx
+++ b/packages/reacord/src/components/text.tsx
@@ -1,10 +1,11 @@
import type { ReactNode } from "react"
import React from "react"
+import { TextElementInstance } from "../renderer/text-element-instance.js"
export type TextProps = {
children?: ReactNode
}
export function Text(props: TextProps) {
- return
+ return new TextElementInstance()} />
}
diff --git a/packages/reacord/src/elements.ts b/packages/reacord/src/elements.ts
deleted file mode 100644
index e21d235..0000000
--- a/packages/reacord/src/elements.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { EmbedProps } from "./components/embed.js"
-import type { TextProps } from "./components/text.jsx"
-
-export type ReacordElementMap = {
- "reacord-text": TextProps
- "reacord-embed": EmbedProps
-}
-
-declare global {
- namespace JSX {
- // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
- interface IntrinsicElements extends ReacordElementMap {}
- }
-}
diff --git a/packages/reacord/src/main.ts b/packages/reacord/src/main.ts
index 2786406..d988b26 100644
--- a/packages/reacord/src/main.ts
+++ b/packages/reacord/src/main.ts
@@ -1,3 +1,3 @@
export * from "./components/embed.js"
export * from "./components/text.js"
-export * from "./root.js"
+export * from "./renderer/root.js"
diff --git a/packages/reacord/src/renderer/base-instance.ts b/packages/reacord/src/renderer/base-instance.ts
new file mode 100644
index 0000000..041d320
--- /dev/null
+++ b/packages/reacord/src/renderer/base-instance.ts
@@ -0,0 +1,13 @@
+import type { 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
+}
diff --git a/packages/reacord/src/renderer/container-instance.ts b/packages/reacord/src/renderer/container-instance.ts
new file mode 100644
index 0000000..49a80be
--- /dev/null
+++ b/packages/reacord/src/renderer/container-instance.ts
@@ -0,0 +1,40 @@
+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)
+ }
+
+ clear() {
+ this.children.splice(0)
+ }
+
+ 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/packages/reacord/src/renderer/container.ts b/packages/reacord/src/renderer/container.ts
index 70e4378..275c791 100644
--- a/packages/reacord/src/renderer/container.ts
+++ b/packages/reacord/src/renderer/container.ts
@@ -1,14 +1,10 @@
import type { Message, MessageOptions, TextBasedChannels } from "discord.js"
-import type { EmbedInstance } from "./embed-instance.js"
-import type { TextElementInstance } from "./text-element-instance.js"
-import type { TextInstance } from "./text-instance.js"
+import type { BaseInstance } from "./base-instance.js"
type Action =
| { type: "updateMessage"; options: MessageOptions }
| { type: "deleteMessage" }
-type ContainerChild = TextInstance | TextElementInstance | EmbedInstance
-
export class ReacordContainer {
private channel: TextBasedChannels
private message?: Message
@@ -19,9 +15,13 @@ export class ReacordContainer {
this.channel = channel
}
- render(children: ContainerChild[]) {
+ 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)
}
diff --git a/packages/reacord/src/renderer/elements.d.ts b/packages/reacord/src/renderer/elements.d.ts
new file mode 100644
index 0000000..55e0a3f
--- /dev/null
+++ b/packages/reacord/src/renderer/elements.d.ts
@@ -0,0 +1,13 @@
+export type ReacordElementTag = "reacord-element"
+
+export type ReacordElementProps = {
+ createInstance: () => unknown
+}
+
+declare global {
+ namespace JSX {
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+ interface IntrinsicElements
+ extends Record {}
+ }
+}
diff --git a/packages/reacord/src/renderer/embed-instance.ts b/packages/reacord/src/renderer/embed-instance.ts
index 931a45f..e4cb6b8 100644
--- a/packages/reacord/src/renderer/embed-instance.ts
+++ b/packages/reacord/src/renderer/embed-instance.ts
@@ -3,21 +3,17 @@ import type {
MessageEmbedOptions,
MessageOptions,
} from "discord.js"
-import type { TextElementInstance } from "./text-element-instance.js"
-import type { TextInstance } from "./text-instance.js"
+import { ContainerInstance } from "./container-instance.js"
-type EmbedChild = TextInstance | TextElementInstance
+/** Represents an element */
+export class EmbedInstance extends ContainerInstance {
+ readonly name = "Embed"
-export class EmbedInstance {
- children: EmbedChild[] = []
-
- constructor(readonly color: ColorResolvable) {}
-
- add(child: EmbedChild) {
- this.children.push(child)
+ constructor(readonly color?: ColorResolvable) {
+ super({ warnOnNonTextChildren: false })
}
- renderToMessage(message: MessageOptions) {
+ override renderToMessage(message: MessageOptions) {
message.embeds ??= []
message.embeds.push(this.embedOptions)
}
@@ -25,7 +21,7 @@ export class EmbedInstance {
get embedOptions(): MessageEmbedOptions {
return {
color: this.color,
- description: this.children.map((child) => child.text).join("") || "_ _",
+ description: this.getChildrenText() || "_ _",
}
}
}
diff --git a/packages/reacord/src/reconciler.ts b/packages/reacord/src/renderer/reconciler.ts
similarity index 52%
rename from packages/reacord/src/reconciler.ts
rename to packages/reacord/src/renderer/reconciler.ts
index 99a0a36..0884ce8 100644
--- a/packages/reacord/src/reconciler.ts
+++ b/packages/reacord/src/renderer/reconciler.ts
@@ -1,46 +1,46 @@
/* eslint-disable unicorn/no-null */
+import { inspect } from "node:util"
import { raise } from "reacord-helpers/raise.js"
import ReactReconciler from "react-reconciler"
-import type { ReacordElementMap } from "./elements.js"
-import type { ReacordContainer } from "./renderer/container.js"
-import { EmbedInstance } from "./renderer/embed-instance.js"
-import { TextElementInstance } from "./renderer/text-element-instance.js"
-import { TextInstance } from "./renderer/text-instance.js"
+import { BaseInstance } from "./base-instance.js"
+import { ContainerInstance } from "./container-instance.js"
+import type { ReacordContainer } from "./container.js"
+import { TextInstance } from "./text-instance.js"
-// instances that represent an element
-type ElementInstance = TextElementInstance | EmbedInstance
-
-// any instance
-type Instance = ElementInstance | TextInstance
-
-type ElementTag =
- | keyof ReacordElementMap
- | (string & { __autocompleteHack__?: never })
+type ElementTag = string
type Props = Record
-const createInstance = (type: ElementTag, props: Props): ElementInstance => {
- if (type === "reacord-text") {
- return new TextElementInstance()
+const createInstance = (type: string, props: Props): BaseInstance => {
+ if (type !== "reacord-element") {
+ raise(`createInstance: unknown type: ${type}`)
}
- if (type === "reacord-embed") {
- return new EmbedInstance((props as any).color)
+
+ if (typeof props.createInstance !== "function") {
+ const actual = inspect(props.createInstance)
+ raise(`invalid createInstance function, received: ${actual}`)
}
- raise(`Unknown element type "${type}"`)
+
+ const instance = props.createInstance()
+ if (!(instance instanceof BaseInstance)) {
+ raise(`invalid instance: ${inspect(instance)}`)
+ }
+
+ return instance
}
export const reconciler = ReactReconciler<
- ElementTag, // Type,
+ string, // Type (jsx tag),
Props, // Props,
ReacordContainer, // Container,
- ElementInstance, // Instance,
+ BaseInstance, // Instance,
TextInstance, // TextInstance,
never, // SuspenseInstance,
never, // HydratableInstance,
never, // PublicInstance,
null, // HostContext,
never, // UpdatePayload,
- Instance[], // ChildSet,
+ BaseInstance[], // ChildSet,
unknown, // TimeoutHandle,
unknown // NoTimeout
>({
@@ -63,46 +63,37 @@ export const reconciler = ReactReconciler<
createContainerChildSet: () => [],
- appendChildToContainerChildSet: (childSet: Instance[], child: Instance) => {
+ appendChildToContainerChildSet: (
+ childSet: BaseInstance[],
+ child: BaseInstance,
+ ) => {
childSet.push(child)
},
finalizeContainerChildren: (
container: ReacordContainer,
- children: Instance[],
+ children: BaseInstance[],
) => false,
replaceContainerChildren: (
container: ReacordContainer,
- children: Instance[],
+ children: BaseInstance[],
) => {
container.render(children)
},
appendInitialChild: (parent, child) => {
- if (
- parent instanceof TextElementInstance &&
- (child instanceof TextInstance || child instanceof TextElementInstance)
- ) {
+ if (parent instanceof ContainerInstance) {
parent.add(child)
- return
+ } else {
+ raise(
+ `Cannot append child of type ${child.constructor.name} to ${parent.constructor.name}`,
+ )
}
-
- if (
- parent instanceof EmbedInstance &&
- (child instanceof TextInstance || child instanceof TextElementInstance)
- ) {
- parent.add(child)
- return
- }
-
- raise(
- `Cannot append child of type ${child.constructor.name} to ${parent.constructor.name}`,
- )
},
cloneInstance: (
- instance: Instance,
+ instance: BaseInstance,
_: unknown,
type: ElementTag,
oldProps: Props,
diff --git a/packages/reacord/src/root.ts b/packages/reacord/src/renderer/root.ts
similarity index 92%
rename from packages/reacord/src/root.ts
rename to packages/reacord/src/renderer/root.ts
index 5d4be0d..b105595 100644
--- a/packages/reacord/src/root.ts
+++ b/packages/reacord/src/renderer/root.ts
@@ -1,8 +1,8 @@
/* 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 { ReacordContainer } from "./renderer/container"
export type ReacordRenderTarget = TextBasedChannels
diff --git a/packages/reacord/src/renderer/text-element-instance.ts b/packages/reacord/src/renderer/text-element-instance.ts
index c40e3ba..4d0a777 100644
--- a/packages/reacord/src/renderer/text-element-instance.ts
+++ b/packages/reacord/src/renderer/text-element-instance.ts
@@ -1,22 +1,19 @@
import type { MessageOptions } from "discord.js"
-import type { TextInstance } from "./text-instance.js"
+import { ContainerInstance } from "./container-instance.js"
-type TextElementChild = TextElementInstance | TextInstance
+/** Represents a element */
+export class TextElementInstance extends ContainerInstance {
+ readonly name = "Text"
-export class TextElementInstance {
- children = new Set()
-
- add(child: TextElementChild) {
- this.children.add(child)
+ constructor() {
+ super({ warnOnNonTextChildren: true })
}
- renderToMessage(options: MessageOptions) {
- for (const child of this.children) {
- options.content = `${options.content ?? ""}${child.text}`
- }
+ override getText() {
+ return this.getChildrenText()
}
- get text(): string {
- return [...this.children].map((child) => child.text).join("")
+ override renderToMessage(options: MessageOptions) {
+ options.content = (options.content ?? "") + this.getText()
}
}
diff --git a/packages/reacord/src/renderer/text-instance.ts b/packages/reacord/src/renderer/text-instance.ts
index 7e0c54e..5305f5f 100644
--- a/packages/reacord/src/renderer/text-instance.ts
+++ b/packages/reacord/src/renderer/text-instance.ts
@@ -1,9 +1,19 @@
import type { MessageOptions } from "discord.js"
+import { BaseInstance } from "./base-instance.js"
-export class TextInstance {
- constructor(readonly text: string) {}
+/** Represents raw strings in JSX */
+export class TextInstance extends BaseInstance {
+ readonly name = "Text"
- renderToMessage(options: MessageOptions) {
- options.content = `${options.content ?? ""}${this.text}`
+ constructor(private readonly text: string) {
+ super()
+ }
+
+ override getText() {
+ return this.text
+ }
+
+ override renderToMessage(options: MessageOptions) {
+ options.content = (options.content ?? "") + this.getText()
}
}
diff --git a/tsconfig.json b/tsconfig.json
index 19ddfec..c0606c6 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,4 +1,7 @@
{
"extends": "@itsmapleleaf/configs/tsconfig.base",
+ "compilerOptions": {
+ "noImplicitOverride": true
+ },
"exclude": ["**/node_modules/**", "**/coverage/**", "**/dist/**"]
}