hydration abstraction

This commit is contained in:
MapleLeaf
2022-01-03 03:33:04 -06:00
committed by Darius
parent f9ab9ba7f5
commit 80953c6ca9
6 changed files with 83 additions and 90 deletions

View File

@@ -15,6 +15,7 @@
"@tinyhttp/app": "^2.0.15",
"@tinyhttp/logger": "^1.3.0",
"clsx": "^1.1.1",
"esbuild": "^0.14.10",
"express": "^4.17.2",
"gray-matter": "^4.0.3",
"http-terminator": "^3.0.4",

View File

@@ -0,0 +1,23 @@
import * as React from "react"
import { mainLinks } from "../data/main-links"
import type { AppLinkProps } from "./app-link"
import { AppLink } from "./app-link"
import { PopoverMenu } from "./popover-menu"
export type MainNavigationMobileMenuData = {
guideLinks: AppLinkProps[]
}
export function render(data: MainNavigationMobileMenuData) {
return (
<PopoverMenu>
{mainLinks.map((link) => (
<AppLink {...link} key={link.to} className={PopoverMenu.itemClass} />
))}
<hr className="border-0 h-[2px] bg-black/50" />
{data.guideLinks.map((link) => (
<AppLink {...link} key={link.to} className={PopoverMenu.itemClass} />
))}
</PopoverMenu>
)
}

View File

@@ -1,21 +0,0 @@
import * as React from "react"
import { createRoot } from "react-dom"
import { mainLinks } from "../data/main-links"
import { AppLink } from "./app-link"
import type { MainNavigationClientData } from "./main-navigation"
import { PopoverMenu } from "./popover-menu"
const dataScript = document.querySelector("#main-navigation-popover-data")!
const data: MainNavigationClientData = JSON.parse(dataScript.innerHTML)
createRoot(document.querySelector("#main-navigation-popover")!).render(
<PopoverMenu>
{mainLinks.map((link) => (
<AppLink {...link} key={link.to} className={PopoverMenu.itemClass} />
))}
<hr className="border-0 h-[2px] bg-black/50" />
{data.guideLinks.map((link) => (
<AppLink {...link} key={link.to} className={PopoverMenu.itemClass} />
))}
</PopoverMenu>,
)

View File

@@ -1,38 +1,14 @@
import { build } from "esbuild"
import { readFile } from "node:fs/promises"
import { dirname } from "node:path"
import React from "react"
import { guideLinks } from "../data/guide-links"
import { mainLinks } from "../data/main-links"
import { createHydrater } from "../helpers/hydration"
import { linkClass } from "../styles/components"
import type { AppLinkProps } from "./app-link"
import { AppLink } from "./app-link"
import { PopoverMenu } from "./popover-menu"
import { Script } from "./script"
import type { MainNavigationMobileMenuData } from "./main-navigation-mobile-menu"
const clientSourcePath = new URL(
"./main-navigation.client.tsx",
import.meta.url,
).pathname
const clientOutput = await build({
bundle: true,
stdin: {
contents: await readFile(clientSourcePath, "utf-8"),
sourcefile: clientSourcePath,
loader: "tsx",
resolveDir: dirname(clientSourcePath),
},
target: ["chrome89", "firefox89"],
format: "esm",
write: false,
})
export type MainNavigationClientData = {
guideLinks: AppLinkProps[]
}
const data: MainNavigationClientData = { guideLinks }
const MenuHydrater = await createHydrater<MainNavigationMobileMenuData>(
new URL("./main-navigation-mobile-menu.tsx", import.meta.url).pathname,
)
export function MainNavigation() {
return (
@@ -46,28 +22,8 @@ export function MainNavigation() {
))}
</div>
<div className="md:hidden" id="main-navigation-popover">
<PopoverMenu>
{mainLinks.map((link) => (
<AppLink
{...link}
key={link.to}
className={PopoverMenu.itemClass}
/>
))}
<hr className="border-0 h-[2px] bg-black/50" />
{data.guideLinks.map((link) => (
<AppLink
{...link}
key={link.to}
className={PopoverMenu.itemClass}
/>
))}
</PopoverMenu>
<MenuHydrater data={{ guideLinks }} />
</div>
<Script id="main-navigation-popover-data" type="application/json">
{JSON.stringify(data)}
</Script>
<Script>{clientOutput.outputFiles[0]?.text!}</Script>
</nav>
)
}

View File

@@ -0,0 +1,51 @@
import { build } from "esbuild"
import { readFile } from "node:fs/promises"
import { dirname } from "node:path"
import React from "react"
import { Script } from "../components/script"
let nextId = 0
export async function createHydrater<Data>(scriptFilePath: string) {
const id = `hydrate-root-${nextId}`
nextId += 1
const scriptSource = await readFile(scriptFilePath, "utf-8")
const scriptBuild = await build({
bundle: true,
stdin: {
contents: [scriptSource, clientBootstrap(id)].join(";\n"),
sourcefile: scriptFilePath,
loader: "tsx",
resolveDir: dirname(scriptFilePath),
},
target: ["chrome89", "firefox89"],
format: "esm",
write: false,
})
const serverModule = await import(scriptFilePath)
return function Hydrater({ data }: { data: Data }) {
return (
<>
<div id={id} data-server-data={JSON.stringify(data)}>
{serverModule.render(data)}
</div>
<Script>{scriptBuild.outputFiles[0]?.text!}</Script>
</>
)
}
}
function clientBootstrap(id: string) {
return /* ts */ `
import { createRoot } from "react-dom"
const rootElement = document.querySelector("#${id}")
const data = JSON.parse(rootElement.dataset.serverData)
createRoot(rootElement).render(render(data))
`
}