diff --git a/package.json b/package.json index 0afa4c0..27f2afa 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "@types/react-reconciler": "^0.26.4", "immer": "^9.0.7", "nanoid": "^3.1.30", - "react-reconciler": "^0.26.2" + "react-reconciler": "^0.26.2", + "react-tree-reconciler": "^1.2.0" }, "peerDependencies": { "discord.js": "^13.3", diff --git a/playground/main.tsx b/playground/main.tsx index 6ea3165..d2f3e0b 100644 --- a/playground/main.tsx +++ b/playground/main.tsx @@ -1,59 +1,22 @@ -import type { CommandInteraction } from "discord.js" import { Client } from "discord.js" import "dotenv/config" -import * as React from "react" -import { createRoot } from "../src/main.js" -import { Counter } from "./counter.js" +import { InstanceManager } from "../src.new/main.js" +import { createCommandHandler } from "./command-handler.js" const client = new Client({ intents: ["GUILDS"], }) -type Command = { - name: string - description: string - run: (interaction: CommandInteraction) => unknown -} +const manager = new InstanceManager() -const commands: Command[] = [ +createCommandHandler(client, [ { name: "counter", description: "shows a counter button", - run: async (interaction) => { - await interaction.reply("a") - await createRoot(interaction.channel!).render() + run: (interaction) => { + manager.create(interaction).render("hi world") }, }, -] - -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, - ) - } - } - console.info("ready 💖") -}) - -client.on("interactionCreate", async (interaction) => { - if (!interaction.isCommand()) return - - const command = commands.find( - (command) => command.name === interaction.commandName, - ) - if (command) { - try { - await command.run(interaction) - } catch (error) { - console.error(error) - } - } -}) +]) await client.login(process.env.TEST_BOT_TOKEN) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1111dac..603b9a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,7 @@ importers: prettier: ^2.5.1 react: ^17.0.2 react-reconciler: ^0.26.2 + react-tree-reconciler: ^1.2.0 tsup: ^5.11.7 typescript: ^4.5.4 vite: ^2.7.6 @@ -41,6 +42,7 @@ importers: immer: 9.0.7 nanoid: 3.1.30 react-reconciler: 0.26.2_react@17.0.2 + react-tree-reconciler: 1.2.0_cfedea9b3ed0faf0dded75c187406c5e devDependencies: '@itsmapleleaf/configs': 1.1.2 '@typescript-eslint/eslint-plugin': 5.8.0_836011a006f4f5d67178564baf2b6d34 @@ -4828,6 +4830,18 @@ packages: scheduler: 0.20.2 dev: false + /react-tree-reconciler/1.2.0_cfedea9b3ed0faf0dded75c187406c5e: + resolution: {integrity: sha512-DmILQhig+Nnh1tOrYFn7Tary077qW943vdjYqRUWLpYLMP5vS/+k0ICNTPQVNaLQJhh4nDCvVUhFxcSSTzYvHA==} + engines: {node: '>=12'} + peerDependencies: + '@types/react': '*' + dependencies: + '@types/react': 17.0.37 + react-reconciler: 0.26.2_react@17.0.2 + transitivePeerDependencies: + - react + dev: false + /react/17.0.2: resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==} engines: {node: '>=0.10.0'} diff --git a/src.new/components/text.tsx b/src.new/components/text.tsx new file mode 100644 index 0000000..6ce9b59 --- /dev/null +++ b/src.new/components/text.tsx @@ -0,0 +1,14 @@ +import type { ReactNode } from "react" +import React from "react" + +export type TextProps = { + children?: ReactNode +} + +export const TextTag = "reacord-text" + +export function Text(props: TextProps) { + return React.createElement(TextTag, props) +} + +export class TextElementNode {} diff --git a/src.new/context.ts b/src.new/context.ts new file mode 100644 index 0000000..4c4847b --- /dev/null +++ b/src.new/context.ts @@ -0,0 +1 @@ +export type Context = {} diff --git a/src.new/main.ts b/src.new/main.ts new file mode 100644 index 0000000..0a52d7e --- /dev/null +++ b/src.new/main.ts @@ -0,0 +1,33 @@ +import type { CommandInteraction } from "discord.js" +import type { ReactNode } from "react" +import type { OpaqueRoot } from "react-reconciler" +import { reconciler } from "./reconciler.js" +import { RootNode } from "./root-node.js" + +export class InstanceManager { + private instances = new Set() + + create(interaction: CommandInteraction) { + const instance = new Instance(interaction) + this.instances.add(instance) + return instance + } + + destroy(instance: Instance) { + this.instances.delete(instance) + } +} + +class Instance { + private rootNode: RootNode + private container: OpaqueRoot + + constructor(interaction: CommandInteraction) { + this.rootNode = new RootNode(interaction) + this.container = reconciler.createContainer(this.rootNode, 0, false, {}) + } + + render(content: ReactNode) { + reconciler.updateContainer(content, this.container) + } +} diff --git a/src.new/reconciler.ts b/src.new/reconciler.ts new file mode 100644 index 0000000..089020d --- /dev/null +++ b/src.new/reconciler.ts @@ -0,0 +1,69 @@ +import type { HostConfig } from "react-reconciler" +import ReactReconciler from "react-reconciler" +import { raise } from "../src/helpers/raise.js" +import type { RootNode } from "./root-node.js" +import { TextNode } from "./text-node.js" + +const config: HostConfig< + string, // Type, + Record, // Props, + RootNode, // Container, + never, // Instance, + TextNode, // TextInstance, + never, // SuspenseInstance, + never, // HydratableInstance, + never, // PublicInstance, + {}, // HostContext, + never, // UpdatePayload, + never, // ChildSet, + number, // TimeoutHandle, + number // NoTimeout, +> = { + // config + now: Date.now, + supportsMutation: true, + supportsPersistence: false, + supportsHydration: false, + isPrimaryRenderer: true, + scheduleTimeout: global.setTimeout, + cancelTimeout: global.clearTimeout, + noTimeout: -1, + + getRootHostContext: () => ({}), + getChildHostContext: () => ({}), + + createInstance: () => raise("not implemented"), + createTextInstance: (text) => new TextNode(text), + shouldSetTextContent: () => false, + + clearContainer: (root) => { + root.clear() + }, + appendChildToContainer: (root, child) => { + root.add(child) + }, + removeChildFromContainer: (root, child) => { + root.remove(child) + }, + + // eslint-disable-next-line unicorn/no-null + prepareUpdate: () => null, + commitUpdate: () => {}, + commitTextUpdate: (node, oldText, newText) => { + node.text = newText + }, + + // eslint-disable-next-line unicorn/no-null + prepareForCommit: () => null, + resetAfterCommit: (root) => { + root.render() + }, + + preparePortalMount: () => raise("Portals are not supported"), + getPublicInstance: () => raise("Refs are currently not supported"), + + appendInitialChild: () => raise("not implemented"), + finalizeInitialChildren: () => false, +} + +export const reconciler = ReactReconciler(config) diff --git a/src.new/root-node.ts b/src.new/root-node.ts new file mode 100644 index 0000000..210d91f --- /dev/null +++ b/src.new/root-node.ts @@ -0,0 +1,34 @@ +import type { CommandInteraction, MessageOptions } from "discord.js" +import type { TextNode } from "./text-node.js" + +export class RootNode { + private children = new Set() + + constructor(private interaction: CommandInteraction) {} + + add(child: TextNode) { + this.children.add(child) + } + + clear() { + this.children.clear() + } + + remove(child: TextNode) { + this.children.delete(child) + } + + render() { + this.interaction.reply(this.getMessageOptions()).catch(console.error) + } + + getMessageOptions() { + const options: MessageOptions = {} + + for (const child of this.children) { + options.content = (options.content ?? "") + child.text + } + + return options + } +} diff --git a/src.new/text-node.ts b/src.new/text-node.ts new file mode 100644 index 0000000..7d66981 --- /dev/null +++ b/src.new/text-node.ts @@ -0,0 +1,3 @@ +export class TextNode { + constructor(public text: string) {} +}