use components for non-blocking builds
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
70
packages/docs/src/asset-builder/asset.tsx
Normal file
70
packages/docs/src/asset-builder/asset.tsx
Normal 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)}</>
|
||||
}
|
||||
11
packages/docs/src/helpers/filesystem.ts
Normal file
11
packages/docs/src/helpers/filesystem.ts
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user