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) {}
+}