use components for non-blocking builds

This commit is contained in:
MapleLeaf
2022-01-08 02:36:04 -06:00
parent f96eb05681
commit f619cd54db
4 changed files with 102 additions and 50 deletions

View File

@@ -1,10 +1,12 @@
import type { RequestHandler } from "express" import type { RequestHandler } from "express"
import express from "express" import express from "express"
import { createHash } from "node:crypto" import { createHash } from "node:crypto"
import { mkdir, rm, writeFile } from "node:fs/promises" import { mkdir, rm } from "node:fs/promises"
import { dirname, join, parse } from "node:path" import { join, parse } from "node:path"
import { Promisable } from "type-fest"
import { ensureWrite, normalizeAsFilePath } from "../helpers/filesystem.js"
type Asset = { export type Asset = {
inputFile: string inputFile: string
outputFile: string outputFile: string
url: string url: string
@@ -20,8 +22,6 @@ export type AssetTransformResult = {
} }
export class AssetBuilder { export class AssetBuilder {
private library = new Map<string, Asset>()
private constructor( private constructor(
private cacheFolder: string, private cacheFolder: string,
private transformers: AssetTransformer[], private transformers: AssetTransformer[],
@@ -29,19 +29,14 @@ export class AssetBuilder {
static async create(cacheFolder: string, transformers: AssetTransformer[]) { static async create(cacheFolder: string, transformers: AssetTransformer[]) {
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
await rm(cacheFolder, { recursive: true }).catch() await rm(cacheFolder, { recursive: true }).catch(() => {})
} }
await mkdir(cacheFolder, { recursive: true }) await mkdir(cacheFolder, { recursive: true })
return new AssetBuilder(cacheFolder, transformers) return new AssetBuilder(cacheFolder, transformers)
} }
async build(inputFile: string | URL, name?: string): Promise<string> { async build(input: Promisable<string | URL>, name?: string): Promise<Asset> {
inputFile = normalizeAsFilePath(inputFile) const inputFile = normalizeAsFilePath(await input)
const existing = this.library.get(inputFile)
if (existing) {
return existing.url
}
const transformResult = await this.transform(inputFile) const transformResult = await this.transform(inputFile)
@@ -55,20 +50,8 @@ export class AssetBuilder {
const outputFile = join(this.cacheFolder, url) const outputFile = join(this.cacheFolder, url)
await ensureWrite(outputFile, transformResult.content) await ensureWrite(outputFile, transformResult.content)
this.library.set(inputFile, { inputFile, outputFile, url })
return url return { inputFile, outputFile, url }
}
local(inputFile: string | URL, name?: string): string {
inputFile = normalizeAsFilePath(inputFile)
const asset = this.library.get(inputFile)
if (asset) {
return asset.url
}
throw this.build(inputFile, name)
} }
middleware(): RequestHandler { middleware(): RequestHandler {
@@ -86,12 +69,3 @@ export class AssetBuilder {
throw new Error(`No transformers found for ${inputFile}`) 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
}

View File

@@ -0,0 +1,70 @@
import React, { ReactNode } from "react"
import { normalizeAsFilePath } from "../helpers/filesystem.js"
import { useAssetBuilder } from "./asset-builder-context.js"
import { Asset, AssetBuilder } from "./asset-builder.js"
type AssetState =
| { status: "building"; promise: Promise<unknown> }
| { status: "built"; asset: Asset }
const cache = new Map<string, AssetState>()
function useAssetBuild(
cacheKey: string,
build: (builder: AssetBuilder) => Promise<Asset>,
) {
const builder = useAssetBuilder()
const state = cache.get(cacheKey)
if (!state) {
const promise = build(builder).then((asset) => {
cache.set(cacheKey, { status: "built", asset })
})
cache.set(cacheKey, { status: "building", promise })
throw promise
}
if (state.status === "building") {
throw state.promise
}
return state.asset.url
}
export function LocalFileAsset({
from,
as: name,
children,
}: {
from: string | URL
as?: string
children: (url: string) => ReactNode
}) {
const inputFile = normalizeAsFilePath(from)
const url = useAssetBuild(inputFile, (builder) => {
return builder.build(inputFile, name)
})
return <>{children(url)}</>
}
export function ModuleAsset({
from,
as: name,
children,
}: {
from: string
as?: string
children: (url: string) => ReactNode
}) {
const cacheKey = `node:${from}`
const url = useAssetBuild(cacheKey, async (builder) => {
const inputFile = await import.meta.resolve!(from)
return await builder.build(inputFile, name)
})
return <>{children(url)}</>
}

View File

@@ -0,0 +1,11 @@
import { mkdir, writeFile } from "node:fs/promises"
import { dirname } from "node:path"
export async function ensureWrite(file: string, content: string) {
await mkdir(dirname(file), { recursive: true })
await writeFile(file, content)
}
export function normalizeAsFilePath(file: string | URL) {
return new URL(file, "file:").pathname
}

View File

@@ -1,14 +1,7 @@
import packageJson from "reacord/package.json" import packageJson from "reacord/package.json"
import type { ReactNode } from "react" import type { ReactNode } from "react"
import React from "react" import React from "react"
import { useAssetBuilder } from "./asset-builder/asset-builder-context.js" import { LocalFileAsset, ModuleAsset } from "./asset-builder/asset.js"
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({ export function Html({
title = "Reacord", title = "Reacord",
@@ -19,7 +12,6 @@ export function Html({
description?: string description?: string
children: ReactNode children: ReactNode
}) { }) {
const assets = useAssetBuilder()
return ( return (
<html lang="en" className="bg-slate-900 text-slate-100"> <html lang="en" className="bg-slate-900 text-slate-100">
<head> <head>
@@ -38,15 +30,20 @@ export function Html({
as="style" 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" 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"
/> />
<link rel="stylesheet" href={assets.local(tailwindCssPath)} />
<link <ModuleAsset from="tailwindcss/tailwind.css">
rel="stylesheet" {(url) => <link rel="stylesheet" href={url} />}
href={assets.local(new URL("ui/prism-theme.css", import.meta.url))} </ModuleAsset>
/>
<LocalFileAsset from={new URL("ui/prism-theme.css", import.meta.url)}>
{(url) => <link rel="stylesheet" href={url} />}
</LocalFileAsset>
<title>{title}</title> <title>{title}</title>
<script defer src={assets.local(alpineJs, "alpine")} /> <ModuleAsset from="alpinejs/dist/cdn.js" as="alpine">
{(url) => <script defer src={url} />}
</ModuleAsset>
</head> </head>
<body>{children}</body> <body>{children}</body>
</html> </html>