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 express from "express"
import { createHash } from "node:crypto"
import { mkdir, rm, writeFile } from "node:fs/promises"
import { dirname, join, parse } from "node:path"
import { mkdir, rm } from "node:fs/promises"
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
outputFile: string
url: string
@@ -20,8 +22,6 @@ export type AssetTransformResult = {
}
export class AssetBuilder {
private library = new Map<string, Asset>()
private constructor(
private cacheFolder: string,
private transformers: AssetTransformer[],
@@ -29,19 +29,14 @@ export class AssetBuilder {
static async create(cacheFolder: string, transformers: AssetTransformer[]) {
if (process.env.NODE_ENV !== "production") {
await rm(cacheFolder, { recursive: true }).catch()
await rm(cacheFolder, { recursive: true }).catch(() => {})
}
await mkdir(cacheFolder, { recursive: true })
return new AssetBuilder(cacheFolder, transformers)
}
async build(inputFile: string | URL, name?: string): Promise<string> {
inputFile = normalizeAsFilePath(inputFile)
const existing = this.library.get(inputFile)
if (existing) {
return existing.url
}
async build(input: Promisable<string | URL>, name?: string): Promise<Asset> {
const inputFile = normalizeAsFilePath(await input)
const transformResult = await this.transform(inputFile)
@@ -55,20 +50,8 @@ export class AssetBuilder {
const outputFile = join(this.cacheFolder, url)
await ensureWrite(outputFile, transformResult.content)
this.library.set(inputFile, { inputFile, outputFile, url })
return 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)
return { inputFile, outputFile, url }
}
middleware(): RequestHandler {
@@ -86,12 +69,3 @@ export class AssetBuilder {
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 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
const alpineJs = new URL(await import.meta.resolve!("alpinejs/dist/cdn.js"))
.pathname
import { LocalFileAsset, ModuleAsset } from "./asset-builder/asset.js"
export function Html({
title = "Reacord",
@@ -19,7 +12,6 @@ export function Html({
description?: string
children: ReactNode
}) {
const assets = useAssetBuilder()
return (
<html lang="en" className="bg-slate-900 text-slate-100">
<head>
@@ -38,15 +30,20 @@ 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"
/>
<link rel="stylesheet" href={assets.local(tailwindCssPath)} />
<link
rel="stylesheet"
href={assets.local(new URL("ui/prism-theme.css", import.meta.url))}
/>
<ModuleAsset from="tailwindcss/tailwind.css">
{(url) => <link rel="stylesheet" href={url} />}
</ModuleAsset>
<LocalFileAsset from={new URL("ui/prism-theme.css", import.meta.url)}>
{(url) => <link rel="stylesheet" href={url} />}
</LocalFileAsset>
<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>
<body>{children}</body>
</html>