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/**"] }