docs: huge migration to runtime asset cache

This commit is contained in:
MapleLeaf
2022-01-07 02:32:50 -06:00
parent 666215319e
commit ba603fca9e
41 changed files with 270 additions and 450 deletions

View File

@@ -6,9 +6,6 @@ COPY / ./
RUN ls -R
RUN npm install -g pnpm
RUN pnpm install --unsafe-perm --frozen-lockfile
RUN pnpm -C packages/docs build
RUN pnpm install --prod --unsafe-perm --frozen-lockfile
RUN pnpm store prune
CMD [ "pnpm", "-C", "packages/docs", "start" ]

2
packages/docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.asset-cache
node_modules

View File

@@ -1,12 +0,0 @@
import type { BuildOptions } from "esbuild"
import packageJson from "./package.json"
export const esbuildConfig: BuildOptions = {
entryPoints: [packageJson.source],
bundle: true,
outfile: packageJson.main,
format: "esm",
target: "node16",
platform: "node",
external: Object.keys(packageJson.dependencies),
}

View File

@@ -2,16 +2,14 @@
"name": "reacord-docs-new",
"type": "module",
"private": true,
"source": "./src/main.tsx",
"main": "./dist/main.js",
"scripts": {
"build": "esmo --no-warnings scripts/build.ts",
"dev": "esmo --no-warnings scripts/dev.ts | pino-colada",
"start": "NODE_ENV=production pnpm serve | pino-colada",
"serve": "node --experimental-import-meta-resolve --experimental-json-modules --no-warnings --enable-source-maps dist/main.js",
"serve": "esmo --experimental-import-meta-resolve --experimental-json-modules --no-warnings --enable-source-maps src/main.tsx | pino-colada",
"dev": "nodemon --exec \"pnpm serve\" --watch src --ext ts,tsx,md,css",
"start": "NODE_ENV=production pnpm serve",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@heroicons/react": "^1.0.5",
"@tailwindcss/typography": "^0.5.0",
"clsx": "^1.1.1",
"compression": "^1.7.4",
@@ -34,7 +32,6 @@
"tailwindcss": "^3.0.8"
},
"devDependencies": {
"@heroicons/react": "^1.0.5",
"@types/browser-sync": "^2.26.3",
"@types/compression": "^1.7.2",
"@types/express": "^4.17.13",
@@ -46,8 +43,8 @@
"@types/wait-on": "^5.3.1",
"autoprefixer": "^10.4.1",
"browser-sync": "^2.27.7",
"chokidar": "^3.5.2",
"execa": "^6.0.0",
"nodemon": "^2.0.15",
"rxjs": "^7.5.1",
"tsup": "^5.11.10",
"type-fest": "^2.8.0",

View File

@@ -1,3 +0,0 @@
import { build } from "esbuild"
import { esbuildConfig } from "../esbuild.config"
await build(esbuildConfig)

View File

@@ -1,138 +0,0 @@
import browserSync from "browser-sync"
import chokidar from "chokidar"
import type { ExecaChildProcess } from "execa"
import { execa } from "execa"
import pino from "pino"
import { concatMap, debounceTime, Observable, tap } from "rxjs"
import waitOn from "wait-on"
import packageJson from "../package.json"
const console = pino()
function awaitChildStopped(child: ExecaChildProcess) {
if (child.killed) return
return new Promise((resolve) => child.once("close", resolve))
}
class App {
app: ExecaChildProcess | undefined
async start() {
console.info(this.app ? "Restarting app..." : "Starting app...")
await this.stop()
const [command, ...flags] = packageJson.scripts.serve.split(/\s+/)
this.app = execa(command!, flags, {
stdio: "inherit",
detached: true,
})
this.app.catch((error) => {
if (error.signal !== "SIGINT") {
console.error(error)
}
})
void this.app.on("close", () => {
this.app = undefined
})
await waitOn({ resources: ["http-get://localhost:3000"] })
console.info("App running")
}
async stop() {
if (this.app) {
if (this.app.pid != undefined) {
process.kill(-this.app.pid, "SIGINT")
} else {
this.app.kill("SIGINT")
}
await awaitChildStopped(this.app)
}
}
}
class Builder {
child = execa("tsup", ["--watch"], {
stdio: "inherit",
})
async stop() {
this.child.kill()
await awaitChildStopped(this.child)
}
}
class Browser {
browser = browserSync.create()
constructor() {
this.browser.emitter.on("init", () => {
console.info("Browsersync started")
})
this.browser.emitter.on("browser:reload", () => {
console.info("Browser reloaded")
})
}
init() {
this.browser.init({
proxy: "http://localhost:3000",
port: 3001,
ui: false,
logLevel: "silent",
})
}
reload() {
this.browser.reload()
}
stop() {
this.browser.exit()
}
}
class Watcher {
subscription = new Observable<string>((subscriber) => {
chokidar
.watch(packageJson.main, { ignored: /node_modules/, ignoreInitial: true })
.on("all", (_, path) => subscriber.next(path))
})
.pipe(
tap((path) => console.info(`Changed:`, path)),
debounceTime(100),
concatMap(async () => {
await this.app.start()
this.browser.reload()
}),
)
.subscribe()
constructor(private app: App, private browser: Browser) {}
stop() {
this.subscription.unsubscribe()
}
}
const app = new App()
const builder = new Builder()
const browser = new Browser()
const watcher = new Watcher(app, browser)
process.on("SIGINT", async () => {
console.info("Shutting down...")
try {
await Promise.all([app, browser, watcher, builder].map((it) => it.stop()))
} catch (error) {
console.error(error)
}
process.exit()
})
await app.start()
browser.init()

View File

@@ -0,0 +1,10 @@
import { createContext, useContext } from "react"
import { AssetBuilder } from "../asset-builder/asset-builder.js"
import { raise } from "../helpers/raise.js"
const Context = createContext<AssetBuilder>()
export const AssetBuilderProvider = Context.Provider
export const useAssetBuilder = () =>
useContext(Context) ?? raise("AssetBuilderProvider not found")

View File

@@ -0,0 +1,85 @@
import { RequestHandler } from "express"
import { createHash } from "node:crypto"
import { readFileSync } from "node:fs"
import { mkdir, stat, writeFile } from "node:fs/promises"
import { dirname, extname, join, parse } from "node:path"
export type Asset = {
file: string
url: string
content: Buffer
}
export type AssetTransformer = {
transform: (asset: Asset) => Promise<AssetTransformResult | undefined>
}
export type AssetTransformResult = {
content: string
type: string
}
export class AssetBuilder {
// map of asset urls to asset objects
private library = new Map<string, Asset>()
constructor(
private cacheFolder: string,
private transformers: AssetTransformer[],
) {}
// accepts a path to a file, then returns a url to where the built file will be served
// the url will include a hash of the file contents
file(file: string | URL): string {
if (file instanceof URL) {
file = file.pathname
}
const existing = this.library.get(file)
if (existing) {
return existing.url
}
const content = readFileSync(file)
const hash = createHash("sha256").update(content).digest("hex").slice(0, 8)
const { name, ext } = parse(file)
const url = `/${name}.${hash}${ext}`
this.library.set(url, { file, url, content })
return url
}
middleware(): RequestHandler {
return async (req, res, next) => {
try {
const asset = this.library.get(req.path)
if (!asset) return next()
const file = join(this.cacheFolder, asset.url)
const extension = extname(file)
const stats = await stat(file).catch(() => undefined)
if (stats?.isFile()) {
res
.status(200)
.type(extension.endsWith("tsx") ? "text/javascript" : extension)
.sendFile(file)
return
}
for (const transformer of this.transformers) {
const result = await transformer.transform(asset)
if (result) {
await mkdir(dirname(file), { recursive: true })
await writeFile(file, result.content)
return res.type(extension).send(result.content)
}
}
next()
} catch (error) {
next(error)
}
}
}
}

View File

@@ -0,0 +1,28 @@
import { build } from "esbuild"
import { readFile } from "node:fs/promises"
import { dirname } from "node:path"
import { AssetTransformer } from "./asset-builder.js"
export const transformEsbuild: AssetTransformer = {
async transform(asset) {
if (asset.file.match(/\.tsx?$/)) {
const scriptBuild = await build({
bundle: true,
stdin: {
contents: await readFile(asset.file, "utf-8"),
sourcefile: asset.file,
loader: "tsx",
resolveDir: dirname(asset.file),
},
target: ["chrome89", "firefox89"],
format: "esm",
write: false,
})
return {
content: scriptBuild.outputFiles[0]!.text,
type: "text/javascript",
}
}
},
}

View File

@@ -0,0 +1,17 @@
import postcss from "postcss"
import tailwindcss from "tailwindcss"
import { AssetTransformer } from "./asset-builder.js"
export const transformPostCss: AssetTransformer = {
async transform(asset) {
if (!asset.file.match(/\.css$/)) return
const result = await postcss(tailwindcss).process(asset.content, {
from: asset.file,
})
return {
content: result.css,
type: "text/css",
}
},
}

View File

@@ -1,23 +0,0 @@
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,16 +0,0 @@
import type { ComponentPropsWithoutRef } from "react"
import React from "react"
import type { Merge } from "type-fest"
export function Script({
children,
...props
}: Merge<ComponentPropsWithoutRef<"script">, { children: string }>) {
return (
<script
type="module"
dangerouslySetInnerHTML={{ __html: children }}
{...props}
/>
)
}

View File

@@ -1,26 +1,13 @@
import clsx from "clsx"
import React from "react"
import { AppLink } from "../components/app-link"
import { MainNavigation } from "../components/main-navigation"
import { guideLinks } from "../data/guide-links"
import { Html } from "../html"
import {
docsProseClass,
linkClass,
maxWidthContainer,
} from "../styles/components"
import { AppLink } from "../navigation/app-link"
import { guideLinks } from "../navigation/guide-links"
import { MainNavigation } from "../navigation/main-navigation"
import { docsProseClass, linkClass, maxWidthContainer } from "../ui/components"
export default function DocsPage({
title,
description,
html,
}: {
title: string
description: string
html: string
}) {
export default function GuidePage({ html }: { html: string }) {
return (
<Html title={`${title} | Reacord`} description={description}>
<>
<header className="bg-slate-700/30 shadow sticky top-0 backdrop-blur-sm transition z-10 flex">
<div className={maxWidthContainer}>
<MainNavigation />
@@ -42,6 +29,6 @@ export default function DocsPage({
dangerouslySetInnerHTML={{ __html: html }}
/>
</main>
</Html>
</>
)
}

View File

@@ -1,51 +0,0 @@
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))
`
}

View File

@@ -0,0 +1,3 @@
export function raise(error: unknown): never {
throw error instanceof Error ? error : new Error(String(error))
}

View File

@@ -1,7 +0,0 @@
import type { Response } from "express"
import { renderToStaticMarkup } from "react-dom/server.js"
export function sendJsx(res: Response, jsx: React.ReactElement) {
res.set("Content-Type", "text/html")
res.send(`<!DOCTYPE html>\n${renderToStaticMarkup(jsx)}`)
}

View File

@@ -1,26 +0,0 @@
import { build } from "esbuild"
import type { RequestHandler } from "express"
import { readFile } from "node:fs/promises"
import { dirname } from "node:path"
export async function serveCompiledScript(
scriptFilePath: string,
): Promise<RequestHandler> {
const scriptBuild = await build({
bundle: true,
stdin: {
contents: await readFile(scriptFilePath, "utf-8"),
sourcefile: scriptFilePath,
loader: "tsx",
resolveDir: dirname(scriptFilePath),
},
target: ["chrome89", "firefox89"],
format: "esm",
write: false,
})
return (req, res) => {
res.setHeader("Content-Type", "application/javascript")
res.end(scriptBuild.outputFiles[0]!.contents)
}
}

View File

@@ -1,5 +0,0 @@
import type { RequestHandler } from "express"
export function serveFile(path: string): RequestHandler {
return (req, res) => res.sendFile(path)
}

View File

@@ -1,25 +0,0 @@
import type { RequestHandler } from "express"
import { readFile } from "node:fs/promises"
import { fileURLToPath } from "node:url"
import type { Result } from "postcss"
import postcss from "postcss"
import tailwindcss from "tailwindcss"
const tailwindTemplatePath = fileURLToPath(
await import.meta.resolve!("tailwindcss/tailwind.css"),
)
const tailwindTemplate = await readFile(tailwindTemplatePath, "utf-8")
let result: Result | undefined
export function serveTailwindCss(): RequestHandler {
return async (req, res) => {
if (!result || process.env.NODE_ENV !== "production") {
result = await postcss(tailwindcss).process(tailwindTemplate, {
from: tailwindTemplatePath,
})
}
res.set("Content-Type", "text/css").send(result.css)
}
}

View File

@@ -1,10 +0,0 @@
import { useState } from "react"
import { useWindowEvent } from "./use-window-event"
export function useScrolled() {
const [scrolled, setScrolled] = useState(
typeof window !== "undefined" ? window.scrollY > 0 : false,
)
useWindowEvent("scroll", () => setScrolled(window.scrollY > 0))
return scrolled
}

View File

@@ -1,11 +0,0 @@
import { useEffect } from "react"
export function useWindowEvent<EventType extends keyof WindowEventMap>(
type: EventType,
handler: (event: WindowEventMap[EventType]) => void,
) {
useEffect(() => {
window.addEventListener(type, handler)
return () => window.removeEventListener(type, handler)
})
}

View File

@@ -1,6 +1,11 @@
import packageJson from "reacord/package.json"
import type { ReactNode } from "react"
import React from "react"
import { useAssetBuilder } from "./asset-builder/asset-builder-context.js"
const tailwindCssPath = new URL(
await import.meta.resolve!("tailwindcss/tailwind.css"),
).pathname
export function Html({
title = "Reacord",
@@ -11,6 +16,7 @@ export function Html({
description?: string
children: ReactNode
}) {
const assets = useAssetBuilder()
return (
<html lang="en" className="bg-slate-900 text-slate-100">
<head>
@@ -28,10 +34,11 @@ export function Html({
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500&family=Rubik:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"
rel="stylesheet"
/>
<link href="/tailwind.css" rel="stylesheet" />
<link href="/prism-theme.css" rel="stylesheet" />
<script type="module" src="/popover-menu.client.js" />
<link href={assets.file(tailwindCssPath)} rel="stylesheet" />
<link
href={assets.file(new URL("ui/prism-theme.css", import.meta.url))}
rel="stylesheet"
/>
<title>{title}</title>
</head>

View File

@@ -0,0 +1,35 @@
import packageJson from "reacord/package.json"
import React from "react"
import { renderMarkdownFile } from "../helpers/markdown"
import { MainNavigation } from "../navigation/main-navigation"
import { maxWidthContainer } from "../ui/components"
const landingExample = await renderMarkdownFile(
new URL("landing-example.md", import.meta.url).pathname,
)
export function Landing() {
return (
<div className="flex flex-col min-w-0 min-h-screen text-center">
<header className={maxWidthContainer}>
<MainNavigation />
</header>
<div className="px-4 pb-8 flex flex-1">
<main className="px-4 py-6 rounded-lg shadow bg-slate-800 space-y-5 m-auto w-full max-w-xl">
<h1 className="text-6xl font-light">reacord</h1>
<section
className="mx-auto text-sm sm:text-base"
dangerouslySetInnerHTML={{ __html: landingExample.html }}
/>
<p className="text-2xl font-light">{packageJson.description}</p>
<a
href="/guides/getting-started"
className="inline-block px-4 py-3 text-xl transition rounded-lg bg-emerald-700 hover:translate-y-[-2px] hover:bg-emerald-800 hover:shadow"
>
Get Started
</a>
</main>
</div>
</div>
)
}

View File

@@ -1,23 +1,35 @@
import compression from "compression"
import type { ErrorRequestHandler, Request } from "express"
import type { ErrorRequestHandler, Request, Response } from "express"
import express from "express"
import Router from "express-promise-router"
import httpTerminator from "http-terminator"
import pino from "pino"
import pinoHttp from "pino-http"
import * as React from "react"
import { renderToStaticMarkup } from "react-dom/server.js"
import { AssetBuilderProvider } from "./asset-builder/asset-builder-context.js"
import { AssetBuilder } from "./asset-builder/asset-builder.js"
import { transformEsbuild } from "./asset-builder/transform-esbuild.js"
import { transformPostCss } from "./asset-builder/transform-postcss.js"
import { fromProjectRoot } from "./constants"
import GuidePage from "./guides/guide-page"
import { renderMarkdownFile } from "./helpers/markdown"
import { sendJsx } from "./helpers/send-jsx"
import { serveCompiledScript } from "./helpers/serve-compiled-script"
import { serveFile } from "./helpers/serve-file"
import { serveTailwindCss } from "./helpers/tailwind"
import DocsPage from "./pages/docs"
import { Landing } from "./pages/landing"
import { Html } from "./html.js"
import { Landing } from "./landing/landing"
const logger = pino()
const port = process.env.PORT || 3000
const assets = new AssetBuilder(fromProjectRoot(".asset-cache"), [
transformEsbuild,
transformPostCss,
])
export function sendJsx(res: Response, jsx: React.ReactElement) {
res.set("Content-Type", "text/html")
res.send(`<!DOCTYPE html>\n${renderToStaticMarkup(jsx)}`)
}
const errorHandler: ErrorRequestHandler = (error, request, response, next) => {
response.status(500).send(error.message)
logger.error(error)
@@ -26,36 +38,31 @@ const errorHandler: ErrorRequestHandler = (error, request, response, next) => {
const router = Router()
.use(pinoHttp({ logger }))
.use(compression())
.get("/tailwind.css", serveTailwindCss())
.use(assets.middleware())
.get(
"/prism-theme.css",
serveFile(fromProjectRoot("src/styles/prism-theme.css")),
)
.get(
"/popover-menu.client.js",
await serveCompiledScript(
fromProjectRoot("src/components/popover-menu.client.tsx"),
),
)
.get("/docs/*", async (req: Request<{ 0: string }>, res) => {
.get("/guides/*", async (req: Request<{ 0: string }>, res) => {
const { html, data } = await renderMarkdownFile(
fromProjectRoot(`src/docs/${req.params[0]}.md`),
new URL(`guides/${req.params[0]}.md`, import.meta.url).pathname,
)
sendJsx(
res,
<DocsPage
title={data.title}
description={data.description}
html={html}
/>,
<AssetBuilderProvider value={assets}>
<Html title={`${data.title} | Reacord`} description={data.description}>
<GuidePage html={html} />
</Html>
</AssetBuilderProvider>,
)
})
.get("/", (req, res) => {
sendJsx(res, <Landing />)
sendJsx(
res,
<AssetBuilderProvider value={assets}>
<Html>
<Landing />
</Html>
</AssetBuilderProvider>,
)
})
.use(errorHandler)

View File

@@ -1,5 +1,5 @@
import React from "react"
import { ExternalLink } from "./external-link"
import { ExternalLink } from "../dom/external-link"
export type AppLinkProps = {
type: "internal" | "external"

View File

@@ -2,15 +2,14 @@ import glob from "fast-glob"
import grayMatter from "gray-matter"
import { readFile } from "node:fs/promises"
import { join } from "node:path"
import type { AppLinkProps } from "../components/app-link"
import { fromProjectRoot } from "../constants"
import type { AppLinkProps } from "./app-link"
const docsFolderPath = fromProjectRoot("src/docs")
const guideFiles = await glob("**/*.md", { cwd: docsFolderPath })
const docsFolder = new URL("../guides", import.meta.url).pathname
const guideFiles = await glob("**/*.md", { cwd: docsFolder })
const entries = await Promise.all(
guideFiles.map(async (file) => {
const content = await readFile(join(docsFolderPath, file), "utf-8")
const content = await readFile(join(docsFolder, file), "utf-8")
const { data } = grayMatter(content)
let order = Number(data.order)
@@ -19,7 +18,7 @@ const entries = await Promise.all(
}
return {
route: `/docs/${file.replace(/\.mdx?$/, "")}`,
route: `/guides/${file.replace(/\.mdx?$/, "")}`,
title: String(data.title || ""),
order,
}

View File

@@ -4,13 +4,13 @@ import {
ExternalLinkIcon,
} from "@heroicons/react/solid/esm"
import React from "react"
import type { AppLinkProps } from "../components/app-link"
import { inlineIconClass } from "../styles/components"
import { inlineIconClass } from "../ui/components"
import type { AppLinkProps } from "./app-link"
export const mainLinks: AppLinkProps[] = [
{
type: "internal",
to: "/docs/getting-started",
to: "/guides/getting-started",
label: (
<>
<DocumentTextIcon className={inlineIconClass} /> Guides
@@ -19,7 +19,7 @@ export const mainLinks: AppLinkProps[] = [
},
{
type: "internal",
to: "/docs/api",
to: "/guides/api",
label: (
<>
<CodeIcon className={inlineIconClass} /> API Reference

View File

@@ -1,9 +1,9 @@
import React from "react"
import { guideLinks } from "../data/guide-links"
import { mainLinks } from "../data/main-links"
import { linkClass } from "../styles/components"
import { linkClass } from "../ui/components"
import { PopoverMenu } from "../ui/popover-menu"
import { AppLink } from "./app-link"
import { PopoverMenu } from "./popover-menu"
import { guideLinks } from "./guide-links"
import { mainLinks } from "./main-links"
export function MainNavigation() {
return (

View File

@@ -1,39 +0,0 @@
import packageJson from "reacord/package.json"
import React from "react"
import { MainNavigation } from "../components/main-navigation"
import { fromProjectRoot } from "../constants"
import { renderMarkdownFile } from "../helpers/markdown"
import { Html } from "../html"
import { maxWidthContainer } from "../styles/components"
const landingExample = await renderMarkdownFile(
fromProjectRoot("src/components/landing-example.md"),
)
export function Landing() {
return (
<Html>
<div className="flex flex-col min-w-0 min-h-screen text-center">
<header className={maxWidthContainer}>
<MainNavigation />
</header>
<div className="px-4 pb-8 flex flex-1">
<main className="px-4 py-6 rounded-lg shadow bg-slate-800 space-y-5 m-auto w-full max-w-xl">
<h1 className="text-6xl font-light">reacord</h1>
<section
className="mx-auto text-sm sm:text-base"
dangerouslySetInnerHTML={{ __html: landingExample.html }}
/>
<p className="text-2xl font-light">{packageJson.description}</p>
<a
href="/docs/getting-started"
className="inline-block px-4 py-3 text-xl transition rounded-lg bg-emerald-700 hover:translate-y-[-2px] hover:bg-emerald-800 hover:shadow"
>
Get Started
</a>
</main>
</div>
</div>
</Html>
)
}

View File

@@ -1,9 +1,11 @@
import { MenuAlt4Icon } from "@heroicons/react/outline/esm"
import clsx from "clsx"
import React from "react"
import { linkClass } from "../styles/components"
import { useAssetBuilder } from "../asset-builder/asset-builder-context.js"
import { linkClass } from "./components"
export function PopoverMenu({ children }: { children: React.ReactNode }) {
const assets = useAssetBuilder()
return (
<div data-popover className="relative">
<button data-popover-button title="Menu" className={linkClass}>
@@ -16,6 +18,10 @@ export function PopoverMenu({ children }: { children: React.ReactNode }) {
>
{children}
</div>
<script
type="module"
src={assets.file(new URL("popover-menu.client.tsx", import.meta.url))}
/>
</div>
)
}

16
pnpm-lock.yaml generated
View File

@@ -51,7 +51,6 @@ importers:
'@types/wait-on': ^5.3.1
autoprefixer: ^10.4.1
browser-sync: ^2.27.7
chokidar: ^3.5.2
clsx: ^1.1.1
compression: ^1.7.4
esbuild: latest
@@ -64,6 +63,7 @@ importers:
http-terminator: ^3.0.4
markdown-it: ^12.3.0
markdown-it-prism: ^2.2.1
nodemon: ^2.0.15
pino: ^7.6.2
pino-colada: ^2.2.2
pino-http: ^6.5.0
@@ -78,6 +78,7 @@ importers:
typescript: ^4.5.4
wait-on: ^6.0.0
dependencies:
'@heroicons/react': 1.0.5_react@18.0.0-rc.0
'@tailwindcss/typography': 0.5.0_tailwindcss@3.0.8
clsx: 1.1.1
compression: 1.7.4
@@ -99,7 +100,6 @@ importers:
react-dom: 18.0.0-rc.0_react@18.0.0-rc.0
tailwindcss: 3.0.8_cefe482e8d38053bbf3d5815e0c551b3
devDependencies:
'@heroicons/react': 1.0.5_react@18.0.0-rc.0
'@types/browser-sync': 2.26.3
'@types/compression': 1.7.2
'@types/express': 4.17.13
@@ -111,8 +111,8 @@ importers:
'@types/wait-on': 5.3.1
autoprefixer: 10.4.1_postcss@8.4.5
browser-sync: 2.27.7
chokidar: 3.5.2
execa: 6.0.0
nodemon: 2.0.15
rxjs: 7.5.1
tsup: 5.11.10_typescript@4.5.4
type-fest: 2.8.0
@@ -355,6 +355,12 @@ packages:
hasBin: true
dev: true
/@babel/parser/7.16.7:
resolution: {integrity: sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA==}
engines: {node: '>=6.0.0'}
hasBin: true
dev: true
/@babel/plugin-syntax-async-generators/7.8.4_@babel+core@7.16.5:
resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
peerDependencies:
@@ -523,7 +529,7 @@ packages:
'@babel/helper-function-name': 7.16.0
'@babel/helper-hoist-variables': 7.16.0
'@babel/helper-split-export-declaration': 7.16.0
'@babel/parser': 7.16.6
'@babel/parser': 7.16.7
'@babel/types': 7.16.7
debug: 4.3.3
globals: 11.12.0
@@ -609,7 +615,7 @@ packages:
react: '>= 16'
dependencies:
react: 18.0.0-rc.0
dev: true
dev: false
/@humanwhocodes/config-array/0.9.2:
resolution: {integrity: sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==}