From b93919edd80bf9e766d1216162cce479553ddd07 Mon Sep 17 00:00:00 2001 From: MapleLeaf <19603573+itsMapleLeaf@users.noreply.github.com> Date: Fri, 7 Jan 2022 14:28:47 -0600 Subject: [PATCH] new asset builder flow makes use of react suspense to asynchronously build assets on the fly within components (!!!) --- packages/docs/package.json | 4 +- .../docs/src/asset-builder/asset-builder.ts | 104 +++++++++--------- .../src/asset-builder/transform-esbuild.ts | 13 +-- .../src/asset-builder/transform-postcss.ts | 13 ++- packages/docs/src/html.tsx | 9 +- packages/docs/src/main.tsx | 18 +-- pnpm-lock.yaml | 28 +++++ 7 files changed, 114 insertions(+), 75 deletions(-) diff --git a/packages/docs/package.json b/packages/docs/package.json index 6922c9d..8191e31 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -5,13 +5,14 @@ "scripts": { "prepare": "node ./scripts/fix-heroicons.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", + "dev": "nodemon --inspect --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", + "alpinejs": "^3.7.1", "clsx": "^1.1.1", "compression": "^1.7.4", "esbuild": "^0.14.10", @@ -46,6 +47,7 @@ "browser-sync": "^2.27.7", "execa": "^6.0.0", "nodemon": "^2.0.15", + "react-ssr-prepass": "^1.5.0", "rxjs": "^7.5.1", "tsup": "^5.11.10", "type-fest": "^2.8.0", diff --git a/packages/docs/src/asset-builder/asset-builder.ts b/packages/docs/src/asset-builder/asset-builder.ts index 0c1737c..f5e86f4 100644 --- a/packages/docs/src/asset-builder/asset-builder.ts +++ b/packages/docs/src/asset-builder/asset-builder.ts @@ -1,17 +1,16 @@ -import { RequestHandler } from "express" +import express, { 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" +import { mkdir, rm, writeFile } from "node:fs/promises" +import { dirname, join, parse } from "node:path" -export type Asset = { - file: string +type Asset = { + inputFile: string + outputFile: string url: string - content: Buffer } export type AssetTransformer = { - transform: (asset: Asset) => Promise + transform: (inputFile: string) => Promise } export type AssetTransformResult = { @@ -20,68 +19,75 @@ export type AssetTransformResult = { } export class AssetBuilder { - // map of asset urls to asset objects private library = new Map() - constructor( + private 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 - } + static async create(cacheFolder: string, transformers: AssetTransformer[]) { + await rm(cacheFolder, { recursive: true }) + return new AssetBuilder(cacheFolder, transformers) + } - const existing = this.library.get(file) + async build(inputFile: string | URL, name?: string): Promise { + inputFile = normalizeAsFilePath(inputFile) + + const existing = this.library.get(inputFile) if (existing) { return existing.url } - const content = readFileSync(file) - const hash = createHash("sha256").update(content).digest("hex").slice(0, 8) + const transformResult = await this.transform(inputFile) + + const hash = createHash("sha256") + .update(transformResult.content) + .digest("hex") + .slice(0, 8) + + const parsedInputFile = parse(inputFile) + const url = `/${name || parsedInputFile.name}.${hash}${parsedInputFile.ext}` + const outputFile = join(this.cacheFolder, url) + + await ensureWrite(outputFile, transformResult.content) + this.library.set(inputFile, { inputFile, outputFile, url }) - 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() + local(inputFile: string | URL, name?: string): string { + inputFile = normalizeAsFilePath(inputFile) - const file = join(this.cacheFolder, asset.url) - const extension = extname(file) - - const stats = await stat(file).catch(() => undefined) - if (!stats?.isFile()) { - const transformResult = await this.transform(asset) - if (!transformResult) return next() - - await mkdir(dirname(file), { recursive: true }) - await writeFile(file, transformResult.content) - } - - res - .status(200) - .type(extension.endsWith("tsx") ? "text/javascript" : extension) - .header("Cache-Control", "public, max-age=604800, immutable") - .sendFile(file) - } catch (error) { - next(error) - } + const asset = this.library.get(inputFile) + if (asset) { + return asset.url } + + throw this.build(inputFile, name) } - private async transform(asset: Asset) { + middleware(): RequestHandler { + return express.static(this.cacheFolder, { + immutable: true, + maxAge: "1y", + }) + } + + private async transform(inputFile: string) { for (const transformer of this.transformers) { - const result = await transformer.transform(asset) + const result = await transformer.transform(inputFile) if (result) return result } + throw new Error(`No transformers found for ${inputFile}`) } } + +async function ensureWrite(file: string, content: string) { + await mkdir(dirname(file), { recursive: true }) + await writeFile(file, content) +} + +function normalizeAsFilePath(file: string | URL) { + return new URL(file, "file:").pathname +} diff --git a/packages/docs/src/asset-builder/transform-esbuild.ts b/packages/docs/src/asset-builder/transform-esbuild.ts index bffd290..59200d4 100644 --- a/packages/docs/src/asset-builder/transform-esbuild.ts +++ b/packages/docs/src/asset-builder/transform-esbuild.ts @@ -1,19 +1,12 @@ 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?$/)) { + async transform(inputFile) { + if (inputFile.match(/\.[jt]sx?$/)) { const scriptBuild = await build({ + entryPoints: [inputFile], 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, diff --git a/packages/docs/src/asset-builder/transform-postcss.ts b/packages/docs/src/asset-builder/transform-postcss.ts index 86d2ff5..e975861 100644 --- a/packages/docs/src/asset-builder/transform-postcss.ts +++ b/packages/docs/src/asset-builder/transform-postcss.ts @@ -1,14 +1,17 @@ +import { readFile } from "fs/promises" 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 + async transform(inputFile) { + if (!inputFile.match(/\.css$/)) return + + const result = await postcss(tailwindcss).process( + await readFile(inputFile), + { from: inputFile }, + ) - const result = await postcss(tailwindcss).process(asset.content, { - from: asset.file, - }) return { content: result.css, type: "text/css", diff --git a/packages/docs/src/html.tsx b/packages/docs/src/html.tsx index 1a55c8b..5063bc8 100644 --- a/packages/docs/src/html.tsx +++ b/packages/docs/src/html.tsx @@ -7,6 +7,9 @@ const tailwindCssPath = new URL( await import.meta.resolve!("tailwindcss/tailwind.css"), ).pathname +const alpineJs = new URL(await import.meta.resolve!("alpinejs/dist/cdn.js")) + .pathname + export function Html({ title = "Reacord", description = packageJson.description, @@ -35,13 +38,15 @@ export function Html({ as="style" 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" /> - + {title} + +