From ba603fca9e90ef7a127a1b07c8018eb38809840a Mon Sep 17 00:00:00 2001 From: MapleLeaf <19603573+itsMapleLeaf@users.noreply.github.com> Date: Fri, 7 Jan 2022 02:32:50 -0600 Subject: [PATCH] docs: huge migration to runtime asset cache --- Dockerfile | 3 - packages/docs/.gitignore | 2 + packages/docs/esbuild.config.ts | 12 -- packages/docs/package.json | 13 +- packages/docs/scripts/build.ts | 3 - packages/docs/scripts/dev.ts | 138 ------------------ .../asset-builder/asset-builder-context.tsx | 10 ++ .../docs/src/asset-builder/asset-builder.ts | 85 +++++++++++ .../src/asset-builder/transform-esbuild.ts | 28 ++++ .../src/asset-builder/transform-postcss.ts | 17 +++ .../main-navigation-mobile-menu.tsx | 23 --- packages/docs/src/components/script.tsx | 16 -- .../src/{components => dom}/external-link.tsx | 0 packages/docs/src/{docs => guides}/buttons.md | 0 packages/docs/src/{docs => guides}/embeds.md | 0 .../src/{docs => guides}/getting-started.md | 0 .../{pages/docs.tsx => guides/guide-page.tsx} | 27 +--- .../docs/src/{docs => guides}/select-menu.md | 0 .../src/{docs => guides}/sending-messages.md | 0 packages/docs/src/helpers/hydration.tsx | 51 ------- packages/docs/src/helpers/raise.ts | 3 + packages/docs/src/helpers/send-jsx.ts | 7 - .../docs/src/helpers/serve-compiled-script.ts | 26 ---- packages/docs/src/helpers/serve-file.ts | 5 - packages/docs/src/helpers/tailwind.ts | 25 ---- packages/docs/src/hooks/dom/use-scrolled.ts | 10 -- .../docs/src/hooks/dom/use-window-event.ts | 11 -- packages/docs/src/html.tsx | 15 +- .../landing-example.md | 0 packages/docs/src/landing/landing.tsx | 35 +++++ packages/docs/src/main.tsx | 63 ++++---- .../{components => navigation}/app-link.tsx | 2 +- .../src/{data => navigation}/guide-links.tsx | 11 +- .../src/{data => navigation}/main-links.tsx | 8 +- .../main-navigation.tsx | 8 +- packages/docs/src/pages/landing.tsx | 39 ----- .../docs/src/{styles => ui}/components.ts | 0 .../popover-menu.client.tsx | 0 .../src/{components => ui}/popover-menu.tsx | 8 +- .../docs/src/{styles => ui}/prism-theme.css | 0 pnpm-lock.yaml | 16 +- 41 files changed, 270 insertions(+), 450 deletions(-) create mode 100644 packages/docs/.gitignore delete mode 100644 packages/docs/esbuild.config.ts delete mode 100644 packages/docs/scripts/build.ts delete mode 100644 packages/docs/scripts/dev.ts create mode 100644 packages/docs/src/asset-builder/asset-builder-context.tsx create mode 100644 packages/docs/src/asset-builder/asset-builder.ts create mode 100644 packages/docs/src/asset-builder/transform-esbuild.ts create mode 100644 packages/docs/src/asset-builder/transform-postcss.ts delete mode 100644 packages/docs/src/components/main-navigation-mobile-menu.tsx delete mode 100644 packages/docs/src/components/script.tsx rename packages/docs/src/{components => dom}/external-link.tsx (100%) rename packages/docs/src/{docs => guides}/buttons.md (100%) rename packages/docs/src/{docs => guides}/embeds.md (100%) rename packages/docs/src/{docs => guides}/getting-started.md (100%) rename packages/docs/src/{pages/docs.tsx => guides/guide-page.tsx} (64%) rename packages/docs/src/{docs => guides}/select-menu.md (100%) rename packages/docs/src/{docs => guides}/sending-messages.md (100%) delete mode 100644 packages/docs/src/helpers/hydration.tsx create mode 100644 packages/docs/src/helpers/raise.ts delete mode 100644 packages/docs/src/helpers/send-jsx.ts delete mode 100644 packages/docs/src/helpers/serve-compiled-script.ts delete mode 100644 packages/docs/src/helpers/serve-file.ts delete mode 100644 packages/docs/src/helpers/tailwind.ts delete mode 100644 packages/docs/src/hooks/dom/use-scrolled.ts delete mode 100644 packages/docs/src/hooks/dom/use-window-event.ts rename packages/docs/src/{components => landing}/landing-example.md (100%) create mode 100644 packages/docs/src/landing/landing.tsx rename packages/docs/src/{components => navigation}/app-link.tsx (90%) rename packages/docs/src/{data => navigation}/guide-links.tsx (64%) rename packages/docs/src/{data => navigation}/main-links.tsx (78%) rename packages/docs/src/{components => navigation}/main-navigation.tsx (84%) delete mode 100644 packages/docs/src/pages/landing.tsx rename packages/docs/src/{styles => ui}/components.ts (100%) rename packages/docs/src/{components => ui}/popover-menu.client.tsx (100%) rename packages/docs/src/{components => ui}/popover-menu.tsx (73%) rename packages/docs/src/{styles => ui}/prism-theme.css (100%) diff --git a/Dockerfile b/Dockerfile index 58ba4a4..0d85823 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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" ] diff --git a/packages/docs/.gitignore b/packages/docs/.gitignore new file mode 100644 index 0000000..33e6952 --- /dev/null +++ b/packages/docs/.gitignore @@ -0,0 +1,2 @@ +.asset-cache +node_modules diff --git a/packages/docs/esbuild.config.ts b/packages/docs/esbuild.config.ts deleted file mode 100644 index ff4ce35..0000000 --- a/packages/docs/esbuild.config.ts +++ /dev/null @@ -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), -} diff --git a/packages/docs/package.json b/packages/docs/package.json index 3cb24d7..d9e5615 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -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", diff --git a/packages/docs/scripts/build.ts b/packages/docs/scripts/build.ts deleted file mode 100644 index b8557f6..0000000 --- a/packages/docs/scripts/build.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { build } from "esbuild" -import { esbuildConfig } from "../esbuild.config" -await build(esbuildConfig) diff --git a/packages/docs/scripts/dev.ts b/packages/docs/scripts/dev.ts deleted file mode 100644 index 01819a8..0000000 --- a/packages/docs/scripts/dev.ts +++ /dev/null @@ -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((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() diff --git a/packages/docs/src/asset-builder/asset-builder-context.tsx b/packages/docs/src/asset-builder/asset-builder-context.tsx new file mode 100644 index 0000000..484163c --- /dev/null +++ b/packages/docs/src/asset-builder/asset-builder-context.tsx @@ -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() + +export const AssetBuilderProvider = Context.Provider + +export const useAssetBuilder = () => + useContext(Context) ?? raise("AssetBuilderProvider not found") diff --git a/packages/docs/src/asset-builder/asset-builder.ts b/packages/docs/src/asset-builder/asset-builder.ts new file mode 100644 index 0000000..a82fdfb --- /dev/null +++ b/packages/docs/src/asset-builder/asset-builder.ts @@ -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 +} + +export type AssetTransformResult = { + content: string + type: string +} + +export class AssetBuilder { + // map of asset urls to asset objects + private library = new Map() + + 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) + } + } + } +} diff --git a/packages/docs/src/asset-builder/transform-esbuild.ts b/packages/docs/src/asset-builder/transform-esbuild.ts new file mode 100644 index 0000000..bffd290 --- /dev/null +++ b/packages/docs/src/asset-builder/transform-esbuild.ts @@ -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", + } + } + }, +} diff --git a/packages/docs/src/asset-builder/transform-postcss.ts b/packages/docs/src/asset-builder/transform-postcss.ts new file mode 100644 index 0000000..86d2ff5 --- /dev/null +++ b/packages/docs/src/asset-builder/transform-postcss.ts @@ -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", + } + }, +} diff --git a/packages/docs/src/components/main-navigation-mobile-menu.tsx b/packages/docs/src/components/main-navigation-mobile-menu.tsx deleted file mode 100644 index 40e104a..0000000 --- a/packages/docs/src/components/main-navigation-mobile-menu.tsx +++ /dev/null @@ -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 ( - - {mainLinks.map((link) => ( - - ))} -
- {data.guideLinks.map((link) => ( - - ))} -
- ) -} diff --git a/packages/docs/src/components/script.tsx b/packages/docs/src/components/script.tsx deleted file mode 100644 index dfc647c..0000000 --- a/packages/docs/src/components/script.tsx +++ /dev/null @@ -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, { children: string }>) { - return ( - - - ) - } -} - -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)) - ` -} diff --git a/packages/docs/src/helpers/raise.ts b/packages/docs/src/helpers/raise.ts new file mode 100644 index 0000000..45fdfe2 --- /dev/null +++ b/packages/docs/src/helpers/raise.ts @@ -0,0 +1,3 @@ +export function raise(error: unknown): never { + throw error instanceof Error ? error : new Error(String(error)) +} diff --git a/packages/docs/src/helpers/send-jsx.ts b/packages/docs/src/helpers/send-jsx.ts deleted file mode 100644 index bffd26d..0000000 --- a/packages/docs/src/helpers/send-jsx.ts +++ /dev/null @@ -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(`\n${renderToStaticMarkup(jsx)}`) -} diff --git a/packages/docs/src/helpers/serve-compiled-script.ts b/packages/docs/src/helpers/serve-compiled-script.ts deleted file mode 100644 index 0f590ba..0000000 --- a/packages/docs/src/helpers/serve-compiled-script.ts +++ /dev/null @@ -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 { - 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) - } -} diff --git a/packages/docs/src/helpers/serve-file.ts b/packages/docs/src/helpers/serve-file.ts deleted file mode 100644 index 63887a9..0000000 --- a/packages/docs/src/helpers/serve-file.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { RequestHandler } from "express" - -export function serveFile(path: string): RequestHandler { - return (req, res) => res.sendFile(path) -} diff --git a/packages/docs/src/helpers/tailwind.ts b/packages/docs/src/helpers/tailwind.ts deleted file mode 100644 index 2fb0c46..0000000 --- a/packages/docs/src/helpers/tailwind.ts +++ /dev/null @@ -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) - } -} diff --git a/packages/docs/src/hooks/dom/use-scrolled.ts b/packages/docs/src/hooks/dom/use-scrolled.ts deleted file mode 100644 index 8fefd62..0000000 --- a/packages/docs/src/hooks/dom/use-scrolled.ts +++ /dev/null @@ -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 -} diff --git a/packages/docs/src/hooks/dom/use-window-event.ts b/packages/docs/src/hooks/dom/use-window-event.ts deleted file mode 100644 index a76294b..0000000 --- a/packages/docs/src/hooks/dom/use-window-event.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useEffect } from "react" - -export function useWindowEvent( - type: EventType, - handler: (event: WindowEventMap[EventType]) => void, -) { - useEffect(() => { - window.addEventListener(type, handler) - return () => window.removeEventListener(type, handler) - }) -} diff --git a/packages/docs/src/html.tsx b/packages/docs/src/html.tsx index e74e963..70d0fd7 100644 --- a/packages/docs/src/html.tsx +++ b/packages/docs/src/html.tsx @@ -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 ( @@ -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" /> - - - -