rearchitecting wip

This commit is contained in:
MapleLeaf
2021-12-16 19:01:37 -06:00
parent ffc91900d2
commit 460e7cde1a
16 changed files with 224 additions and 289 deletions

View File

@@ -12,7 +12,7 @@
"require": "./dist/main.cjs"
},
"scripts": {
"build": "tsup src/main.ts --clean --target node16 --format cjs,esm --dts"
"build": "tsup src/main.ts --clean --target node16 --format cjs,esm --dts --sourcemap"
},
"keywords": [],
"author": "itsMapleLeaf",

View File

@@ -1,107 +0,0 @@
import type { ColorResolvable, MessageEmbedOptions } from "discord.js"
import { raise } from "reacord-helpers/raise"
import type { ReactNode } from "react"
import * as React from "react"
export type EmbedProps = {
color?: ColorResolvable
children?: ReactNode
}
export function Embed(props: EmbedProps) {
const [instance] = React.useState(() => new EmbedInstance())
return (
<reacord-element
modifyOptions={(options) => ({
...options,
embeds: [
...(options.embeds || []),
{
color: props.color,
description: props.children,
},
],
})}
/>
)
}
export type EmbedTitleProps = {
children?: string
url?: string
}
export function EmbedTitle(props: EmbedTitleProps) {
const { useEmbedChild } = useEmbedContext()
useEmbedChild(
React.useCallback(
(options) => {
options.title = props.children
options.url = props.url
},
[props.children, props.url],
),
)
return <></>
}
function useEmbedContext() {
const instance =
React.useContext(EmbedInstanceContext) ??
raise("Embed instance provider not found")
return React.useMemo(() => {
function useEmbedChild(
modifyEmbedOptions: (options: MessageEmbedOptions) => void,
) {
React.useEffect(() => {
instance.add(modifyEmbedOptions)
return () => instance.remove(modifyEmbedOptions)
}, [modifyEmbedOptions])
}
return { useEmbedChild }
}, [instance])
}
const EmbedInstanceContext = React.createContext<EmbedInstance | undefined>(
undefined,
)
function EmbedInstanceProvider({
instance,
children,
}: {
instance: EmbedInstance
children: ReactNode
}) {
return (
<EmbedInstanceContext.Provider value={instance}>
{children}
</EmbedInstanceContext.Provider>
)
}
class EmbedInstance {
private children = new Set<EmbedChild>()
add(child: EmbedChild) {
this.children.add(child)
}
remove(child: EmbedChild) {
this.children.delete(child)
}
getEmbedOptions(): MessageEmbedOptions {
const options: MessageEmbedOptions = {}
for (const child of this.children) {
child(options)
}
return options
}
}
type EmbedChild = (options: MessageEmbedOptions) => void

View File

@@ -0,0 +1,9 @@
import React from "react"
export type TextProps = {
children?: string
}
export function Text(props: TextProps) {
return <reacord-text {...props} />
}

View File

@@ -1,15 +0,0 @@
import type { MessageOptions } from "discord.js"
export type ReacordElementJsxTag = "reacord-element"
export type ReacordElement = {
modifyOptions: (options: MessageOptions) => void
}
declare global {
namespace JSX {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface IntrinsicElements
extends Record<ReacordElementJsxTag, ReacordElement> {}
}
}

View File

@@ -0,0 +1,12 @@
import type { TextProps } from "./components/text.jsx"
export type ReacordElementMap = {
"reacord-text": TextProps
}
declare global {
namespace JSX {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface IntrinsicElements extends ReacordElementMap {}
}
}

View File

@@ -1,2 +1,2 @@
export * from "./components/embed.js"
export * from "./components/text.js"
export * from "./root.js"

View File

@@ -1,94 +1,76 @@
/* eslint-disable unicorn/no-null */
import { raise } from "reacord-helpers/raise.js"
import ReactReconciler from "react-reconciler"
import type { ReacordContainer } from "./container.js"
import type { ReacordElement, ReacordElementJsxTag } from "./element.js"
import type { ReacordElementMap } from "./elements.js"
import type { ReacordContainer } from "./renderer/container.js"
import { TextElementInstance } from "./renderer/text-element-instance.js"
import { TextInstance } from "./renderer/text-instance.js"
export const reconciler = ReactReconciler<
ReacordElementJsxTag,
ReacordElement,
ReacordContainer,
ReacordElement,
string,
unknown,
unknown,
unknown,
unknown,
unknown,
unknown,
unknown,
unknown
keyof ReacordElementMap | (string & { __autocompleteHack__?: never }), // Type,
Record<string, unknown>, // Props,
ReacordContainer, // Container,
TextElementInstance, // Instance,
TextInstance, // TextInstance,
never, // SuspenseInstance,
never, // HydratableInstance,
never, // PublicInstance,
null, // HostContext,
never, // UpdatePayload,
never, // ChildSet,
unknown, // TimeoutHandle,
unknown // NoTimeout
>({
now: Date.now,
isPrimaryRenderer: true,
supportsMutation: false,
supportsPersistence: true,
supportsMutation: true,
supportsPersistence: false,
supportsHydration: false,
scheduleTimeout: setTimeout,
cancelTimeout: clearTimeout,
noTimeout: -1,
getRootHostContext: () => ({}),
getChildHostContext: () => ({}),
getRootHostContext: () => null,
getChildHostContext: (parentContext) => parentContext,
shouldSetTextContent: () => false,
createInstance: (
type,
props,
rootContainerInstance,
hostContext,
internalInstanceHandle,
) => {
return props
createInstance: (type, props) => {
if (type === "reacord-text") {
return new TextElementInstance()
}
raise(`Unknown element type "${type}"`)
},
createTextInstance: (
text,
rootContainerInstance,
hostContext,
internalInstanceHandle,
) => {
return text
createTextInstance: (text) => {
return new TextInstance(text)
},
prepareForCommit: () => null,
clearContainer: (container) => {
container.clear()
},
appendChildToContainer: (container, child) => {
container.add(child)
},
removeChildFromContainer: (container, child) => {
container.remove(child)
},
appendInitialChild: (parent, child) => {
parent.add(child)
},
removeChild: (parent, child) => {
parent.remove(child)
},
finalizeInitialChildren: () => false,
prepareForCommit: (container) => null,
resetAfterCommit: () => null,
prepareUpdate: () => null,
appendInitialChild: (parent, child) => raise("Not implemented"),
finalizeInitialChildren: (...args) => {
console.log("finalizeInitialChildren", args)
return false
},
getPublicInstance: () => raise("Not implemented"),
prepareUpdate: () => raise("Not implemented"),
preparePortalMount: () => raise("Not implemented"),
createContainerChildSet: (): ReacordElement[] => {
// console.log("createContainerChildSet", [container])
return []
},
appendChildToContainerChildSet: (
children: ReacordElement[],
child: ReacordElement,
) => {
// console.log("appendChildToContainerChildSet", [children, child])
children.push(child)
},
finalizeContainerChildren: (
container: ReacordContainer,
children: ReacordElement[],
) => {
// console.log("finalizeContainerChildren", [container, children])
return false
},
replaceContainerChildren: (
container: ReacordContainer,
children: ReacordElement[],
) => {
console.log("replaceContainerChildren", [container, children])
container.render(children)
},
})

View File

@@ -1,32 +1,51 @@
import type { Message, MessageOptions, TextBasedChannels } from "discord.js"
import type { ReacordElement } from "./element.js"
import type { TextElementInstance } from "./text-element-instance.js"
import type { TextInstance } from "./text-instance.js"
type Action =
| { type: "updateMessage"; options: MessageOptions }
| { type: "deleteMessage" }
type ReacordContainerChild = TextElementInstance | TextInstance
export class ReacordContainer {
private channel: TextBasedChannels
private message?: Message
private actions: Action[] = []
private runningPromise?: Promise<void>
private instances = new Set<ReacordContainerChild>()
constructor(channel: TextBasedChannels) {
this.channel = channel
}
render(instances: ReacordElement[]) {
const messageOptions: MessageOptions = {
content: instances.join("") || undefined, // empty strings are not allowed
add(instance: ReacordContainerChild) {
this.instances.add(instance)
this.render()
}
remove(instance: ReacordContainerChild) {
this.instances.delete(instance)
this.render()
}
clear() {
this.instances.clear()
this.render()
}
render() {
const messageOptions: MessageOptions = {}
for (const instance of this.instances) {
instance.render(messageOptions)
}
const hasContent = messageOptions.content !== undefined
this.addAction(
hasContent
? { type: "updateMessage", options: messageOptions }
: { type: "deleteMessage" },
)
// can't render an empty message
if (!messageOptions.content) {
this.addAction({ type: "deleteMessage" })
} else {
this.addAction({ type: "updateMessage", options: messageOptions })
}
}
completion() {

View File

@@ -0,0 +1,26 @@
import type { MessageOptions } from "discord.js"
import type { TextInstance } from "./text-instance.js"
type TextElementInstanceChild = TextElementInstance | TextInstance
export class TextElementInstance {
children = new Set<TextElementInstanceChild>()
add(child: TextElementInstanceChild) {
this.children.add(child)
}
remove(child: TextElementInstanceChild) {
this.children.delete(child)
}
clear() {
this.children.clear()
}
render(options: MessageOptions) {
for (const child of this.children) {
child.render(options)
}
}
}

View File

@@ -0,0 +1,13 @@
import type { MessageOptions } from "discord.js"
export class TextInstance {
text: string
constructor(text: string) {
this.text = text
}
render(options: MessageOptions) {
options.content = `${options.content ?? ""}${this.text}`
}
}

View File

@@ -1,23 +1,18 @@
/* 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
export function createRoot(target: ReacordRenderTarget) {
const container = new ReacordContainer(target)
// eslint-disable-next-line unicorn/no-null
const containerId = reconciler.createContainer(container, 0, false, null)
return {
render: (content: ReactNode) => {
reconciler.updateContainer(content, containerId)
return container.completion()
},
destroy: () => {
reconciler.updateContainer(null, containerId)
return container.completion()
},
completion: () => container.completion(),
}
}