Skip to content
← Public packages

@kentcdodds/invisible-software

Invisible software: a phone-only step-through demo plus a chart of AI agent benchmark progress.

FollowForkOpen app

scripts/build-assets.mjs

69 lines · 3.4 KB · JavaScript
Raw
// Builds the deck page from src/deck.html plus every src/slides/*.html fragment
// (in file-name order), then inlines it and everything under src/c/ into
// app/assets.generated.ts so the package app serves with no external requests.
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
import { join, relative, extname } from 'node:path'

const root = new URL('..', import.meta.url).pathname
const srcDir = join(root, 'src')
const types = {
	'.html': 'text/html; charset=utf-8',
	'.css': 'text/css; charset=utf-8',
	'.js': 'text/javascript; charset=utf-8',
	'.svg': 'image/svg+xml',
	'.png': 'image/png',
	'.ico': 'image/x-icon',
	'.jpg': 'image/jpeg',
	'.webp': 'image/webp',
	'.woff2': 'font/woff2',
	'.txt': 'text/plain; charset=utf-8',
}
const textTypes = new Set(['.html', '.css', '.js', '.svg', '.txt'])

function walk(dir) {
	return readdirSync(dir).flatMap((name) => {
		const p = join(dir, name)
		return statSync(p).isDirectory() ? walk(p) : [p]
	})
}

const slidesDir = join(srcDir, 'slides')
const slideFiles = readdirSync(slidesDir).filter((f) => f.endsWith('.html')).sort((a, b) => (a < b ? -1 : 1))
const deck = readFileSync(join(srcDir, 'deck.html'), 'utf8')
if (!deck.includes('<!-- slides -->')) throw new Error('src/deck.html needs a <!-- slides --> marker')
// Shared CSS and JS are inlined: Chrome can fetch <head> resources before the page's
// runtime <base> tag exists, which would resolve them against the wrong directory.
const inlined = new Set()
const inline = (f) => (inlined.add(`c/${f}`), readFileSync(join(srcDir, 'c', f), 'utf8').trim())
const page = deck
	.replace(/<link rel="stylesheet" href="([\w.-]+)">/g, (_, f) => `<style>\n${inline(f)}\n</style>`)
	.replace(/<script src="([\w.-]+)"><\/script>/g, (_, f) => `<script>\n${inline(f)}\n</script>`)
	.replace('<!-- slides -->', () => slideFiles.map((f) => readFileSync(join(slidesDir, f), 'utf8').trim()).join('\n'))

const entries = walk(join(srcDir, 'c')).sort().filter((file) => !inlined.has(relative(srcDir, file).split('\\').join('/'))).map((file) => {
	const ext = extname(file)
	const type = types[ext]
	if (!type) throw new Error(`No content type for ${file}`)
	const path = relative(srcDir, file).split('\\').join('/')
	const buf = readFileSync(file)
	return textTypes.has(ext)
		? { path, type, text: buf.toString('utf8') }
		: { path, type, base64: buf.toString('base64') }
})
entries.unshift({ path: 'index.html', type: types['.html'], text: page })
// Landing page and the benchmark graph (data inlined from src/data-trend.json, built from data/*.csv).
entries.unshift({ path: 'home.html', type: types['.html'], text: readFileSync(join(srcDir, 'pages/home.html'), 'utf8') })
entries.unshift({ path: 'graph.html', type: types['.html'], text: readFileSync(join(srcDir, 'pages/graph.html'), 'utf8').replace('/* data */null', () => readFileSync(join(srcDir, 'data-trend.json'), 'utf8').trim()) })

const out = `// Generated by scripts/build-assets.mjs. Do not edit by hand.
export type Asset = { type: string; text?: string; base64?: string }

export const assets: Record<string, Asset> = ${JSON.stringify(
	Object.fromEntries(entries.map(({ path, ...rest }) => [path, rest])),
	null,
	'\t',
)}
`
writeFileSync(join(root, 'app/assets.generated.ts'), out)
const bytes = entries.reduce((n, e) => n + (e.text?.length ?? e.base64.length), 0)
console.log(`Composed ${slideFiles.length} slides. Wrote ${entries.length} assets (${(bytes / 1024).toFixed(0)} KiB) to app/assets.generated.ts`)