Add template embedding and generation script

This commit is contained in:
Chris Wanstrath 2026-03-02 21:35:13 -08:00
parent 6dc7ad8608
commit 7a0a9fc731
5 changed files with 146 additions and 83 deletions

3
.gitignore vendored
View File

@ -5,6 +5,9 @@ node_modules
pub/client/index.js pub/client/index.js
toes/ toes/
# generated
src/lib/templates.data.ts
# output # output
out out
dist dist

View File

@ -15,7 +15,7 @@
"toes": "src/cli/index.ts" "toes": "src/cli/index.ts"
}, },
"scripts": { "scripts": {
"check": "bunx tsc --noEmit", "check": "bun run templates && bunx tsc --noEmit",
"build": "./scripts/build.sh", "build": "./scripts/build.sh",
"cli:build": "bun run scripts/build.ts", "cli:build": "bun run scripts/build.ts",
"cli:build:all": "bun run scripts/build.ts --all", "cli:build:all": "bun run scripts/build.ts --all",
@ -24,7 +24,7 @@
"cli:uninstall": "sudo rm /usr/local/bin", "cli:uninstall": "sudo rm /usr/local/bin",
"deploy": "./scripts/deploy.sh", "deploy": "./scripts/deploy.sh",
"debug": "DEBUG=1 bun run dev", "debug": "DEBUG=1 bun run dev",
"dev": "rm -f pub/client/index.js && bun run --hot src/server/index.tsx", "dev": "bun run templates && rm -f pub/client/index.js && bun run --hot src/server/index.tsx",
"remote:deploy": "./scripts/deploy.sh", "remote:deploy": "./scripts/deploy.sh",
"remote:migrate": "bun run scripts/migrate.ts", "remote:migrate": "bun run scripts/migrate.ts",
"remote:install": "./scripts/remote-install.sh", "remote:install": "./scripts/remote-install.sh",
@ -33,6 +33,7 @@
"remote:start": "./scripts/remote-start.sh", "remote:start": "./scripts/remote-start.sh",
"remote:stop": "./scripts/remote-stop.sh", "remote:stop": "./scripts/remote-stop.sh",
"start": "bun run src/server/index.tsx", "start": "bun run src/server/index.tsx",
"templates": "bun run scripts/embed-templates.ts",
"test": "bun test" "test": "bun test"
}, },
"devDependencies": { "devDependencies": {

View File

@ -23,47 +23,6 @@ const TARGETS: BuildTarget[] = [
{ os: 'linux', arch: 'x64', name: 'toes-linux-x64' }, { os: 'linux', arch: 'x64', name: 'toes-linux-x64' },
] ]
// Ensure dist directory exists
if (!existsSync(DIST_DIR)) {
mkdirSync(DIST_DIR, { recursive: true })
}
// Parse command line args
const args = process.argv.slice(2)
const buildAll = args.includes('--all')
const targetArg = args.find(arg => arg.startsWith('--target='))?.split('=')[1]
async function buildTarget(target: BuildTarget) {
console.log(`Building ${target.name}...`)
const output = join(DIST_DIR, target.name)
const proc = Bun.spawn([
'bun',
'build',
ENTRY_POINT,
'--compile',
'--target',
`bun-${target.os}-${target.arch}`,
'--minify',
'--sourcemap=external',
'--outfile',
output,
], {
stdout: 'inherit',
stderr: 'inherit',
})
const exitCode = await proc.exited
if (exitCode === 0) {
console.log(`✓ Built ${target.name}`)
} else {
console.error(`✗ Failed to build ${target.name}`)
process.exit(exitCode)
}
}
async function buildCurrent() { async function buildCurrent() {
const platform = process.platform const platform = process.platform
const arch = process.arch const arch = process.arch
@ -100,6 +59,57 @@ async function buildCurrent() {
} }
} }
async function buildTarget(target: BuildTarget) {
console.log(`Building ${target.name}...`)
const output = join(DIST_DIR, target.name)
const proc = Bun.spawn([
'bun',
'build',
ENTRY_POINT,
'--compile',
'--target',
`bun-${target.os}-${target.arch}`,
'--minify',
'--sourcemap=external',
'--outfile',
output,
], {
stdout: 'inherit',
stderr: 'inherit',
})
const exitCode = await proc.exited
if (exitCode === 0) {
console.log(`✓ Built ${target.name}`)
} else {
console.error(`✗ Failed to build ${target.name}`)
process.exit(exitCode)
}
}
// Embed template files before compiling
const embedProc = Bun.spawn(['bun', 'run', join(import.meta.dir, 'embed-templates.ts')], {
stdout: 'inherit',
stderr: 'inherit',
})
if (await embedProc.exited !== 0) {
console.error('✗ Failed to embed templates')
process.exit(1)
}
// Ensure dist directory exists
if (!existsSync(DIST_DIR)) {
mkdirSync(DIST_DIR, { recursive: true })
}
// Parse command line args
const args = process.argv.slice(2)
const buildAll = args.includes('--all')
const targetArg = args.find(arg => arg.startsWith('--target='))?.split('=')[1]
// Main build logic // Main build logic
if (buildAll) { if (buildAll) {
console.log('Building for all targets...\n') console.log('Building for all targets...\n')

View File

@ -0,0 +1,71 @@
#!/usr/bin/env bun
// Generates src/lib/templates.data.ts with embedded template file contents.
// Run: bun run templates
import { readdirSync, readFileSync, statSync } from 'fs'
import { extname, join, relative } from 'path'
const BINARY_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.svg'])
const TEMPLATES_DIR = join(import.meta.dir, '..', 'templates')
const binary: Record<string, Record<string, string>> = {}
const shared: Record<string, string> = {}
const templates: Record<string, Record<string, string>> = {}
const isBinary = (path: string) =>
BINARY_EXTENSIONS.has(extname(path))
function readDir(dir: string): string[] {
const files: string[] = []
for (const entry of readdirSync(dir)) {
const path = join(dir, entry)
if (statSync(path).isDirectory()) {
files.push(...readDir(path))
} else {
files.push(path)
}
}
return files.sort()
}
// First pass: collect shared files (root level)
for (const entry of readdirSync(TEMPLATES_DIR).sort()) {
const path = join(TEMPLATES_DIR, entry)
if (!statSync(path).isDirectory()) {
shared[entry] = readFileSync(path, 'utf-8')
}
}
// Second pass: build template maps with shared files folded in
for (const entry of readdirSync(TEMPLATES_DIR).sort()) {
const path = join(TEMPLATES_DIR, entry)
if (statSync(path).isDirectory()) {
templates[entry] = { ...shared }
binary[entry] = {}
for (const filePath of readDir(path)) {
const filename = relative(path, filePath)
if (isBinary(filePath)) {
binary[entry]![filename] = readFileSync(filePath).toString('base64')
} else {
templates[entry]![filename] = readFileSync(filePath, 'utf-8')
}
}
if (Object.keys(binary[entry]!).length === 0) {
delete binary[entry]
}
}
}
// Generate TypeScript module
const lines: string[] = [
'// Auto-generated by scripts/embed-templates.ts',
'// Run `bun run templates` to regenerate',
'',
`export const TEMPLATES: Record<string, Record<string, string>> = ${JSON.stringify(templates, null, 2)}`,
'',
`export const BINARY: Record<string, Record<string, string>> = ${JSON.stringify(binary, null, 2)}`,
'',
]
const outPath = join(import.meta.dir, '..', 'src', 'lib', 'templates.data.ts')
await Bun.write(outPath, lines.join('\n'))
console.log(`✓ Embedded templates → ${relative(join(import.meta.dir, '..'), outPath)}`)

View File

@ -1,10 +1,10 @@
import { DEFAULT_EMOJI } from '@types' import { DEFAULT_EMOJI } from '@types'
import { readdirSync, readFileSync, statSync } from 'fs'
import { join, relative } from 'path' import { BINARY, TEMPLATES } from './templates.data'
export type TemplateType = 'ssr' | 'bare' | 'spa' export type TemplateType = 'ssr' | 'bare' | 'spa'
export type AppTemplates = Record<string, string> export type AppTemplates = Record<string, string | Uint8Array>
interface TemplateOptions { interface TemplateOptions {
tool?: boolean tool?: boolean
@ -15,28 +15,6 @@ interface TemplateVars {
APP_NAME: string APP_NAME: string
} }
const SHARED_FILES = ['CLAUDE.md', '.gitignore', '.npmrc', 'package.json', 'tsconfig.json']
const TEMPLATES_DIR = join(import.meta.dir, '../../templates')
function readDir(dir: string): string[] {
const files: string[] = []
for (const entry of readdirSync(dir)) {
const path = join(dir, entry)
if (statSync(path).isDirectory()) {
files.push(...readDir(path))
} else {
files.push(path)
}
}
return files
}
function replaceVars(content: string, vars: TemplateVars): string {
return content
.replace(/\$\$APP_NAME\$\$/g, vars.APP_NAME)
.replace(/\$\$APP_EMOJI\$\$/g, vars.APP_EMOJI)
}
export function generateTemplates(appName: string, template: TemplateType = 'ssr', options: TemplateOptions = {}): AppTemplates { export function generateTemplates(appName: string, template: TemplateType = 'ssr', options: TemplateOptions = {}): AppTemplates {
const vars: TemplateVars = { const vars: TemplateVars = {
APP_EMOJI: DEFAULT_EMOJI, APP_EMOJI: DEFAULT_EMOJI,
@ -45,29 +23,29 @@ export function generateTemplates(appName: string, template: TemplateType = 'ssr
const result: AppTemplates = {} const result: AppTemplates = {}
// Read shared files from templates/ // Text files (shared + template-specific, merged at embed time)
for (const filename of SHARED_FILES) { for (const [filename, content] of Object.entries(TEMPLATES[template] ?? {})) {
const path = join(TEMPLATES_DIR, filename) let processed = replaceVars(content, vars)
let content = readFileSync(path, 'utf-8')
content = replaceVars(content, vars)
// Add tool option to package.json if specified
if (filename === 'package.json' && options.tool) { if (filename === 'package.json' && options.tool) {
const pkg = JSON.parse(content) const pkg = JSON.parse(processed)
pkg.toes.tool = true pkg.toes.tool = true
content = JSON.stringify(pkg, null, 2) + '\n' processed = JSON.stringify(pkg, null, 2) + '\n'
} }
result[filename] = content result[filename] = processed
} }
// Read template-specific files // Binary files (base64 encoded)
const templateDir = join(TEMPLATES_DIR, template) for (const [filename, base64] of Object.entries(BINARY[template] ?? {})) {
for (const path of readDir(templateDir)) { result[filename] = Buffer.from(base64, 'base64')
const filename = relative(templateDir, path)
const content = readFileSync(path, 'utf-8')
result[filename] = replaceVars(content, vars)
} }
return result return result
} }
function replaceVars(content: string, vars: TemplateVars): string {
return content
.replace(/\$\$APP_NAME\$\$/g, vars.APP_NAME)
.replace(/\$\$APP_EMOJI\$\$/g, vars.APP_EMOJI)
}