From 7a0a9fc731de1c855ede9f8d8def34a7519045b5 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Mon, 2 Mar 2026 21:35:13 -0800 Subject: [PATCH] Add template embedding and generation script --- .gitignore | 3 ++ package.json | 5 ++- scripts/build.ts | 92 +++++++++++++++++++++----------------- scripts/embed-templates.ts | 71 +++++++++++++++++++++++++++++ src/lib/templates.ts | 58 ++++++++---------------- 5 files changed, 146 insertions(+), 83 deletions(-) create mode 100644 scripts/embed-templates.ts diff --git a/.gitignore b/.gitignore index d5a3760..6dc3501 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ node_modules pub/client/index.js toes/ +# generated +src/lib/templates.data.ts + # output out dist diff --git a/package.json b/package.json index 04dd7ad..cac5d2f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "toes": "src/cli/index.ts" }, "scripts": { - "check": "bunx tsc --noEmit", + "check": "bun run templates && bunx tsc --noEmit", "build": "./scripts/build.sh", "cli:build": "bun run scripts/build.ts", "cli:build:all": "bun run scripts/build.ts --all", @@ -24,7 +24,7 @@ "cli:uninstall": "sudo rm /usr/local/bin", "deploy": "./scripts/deploy.sh", "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:migrate": "bun run scripts/migrate.ts", "remote:install": "./scripts/remote-install.sh", @@ -33,6 +33,7 @@ "remote:start": "./scripts/remote-start.sh", "remote:stop": "./scripts/remote-stop.sh", "start": "bun run src/server/index.tsx", + "templates": "bun run scripts/embed-templates.ts", "test": "bun test" }, "devDependencies": { diff --git a/scripts/build.ts b/scripts/build.ts index 490fabe..a9ec5a9 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -23,47 +23,6 @@ const TARGETS: BuildTarget[] = [ { 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() { const platform = process.platform 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 if (buildAll) { console.log('Building for all targets...\n') diff --git a/scripts/embed-templates.ts b/scripts/embed-templates.ts new file mode 100644 index 0000000..bb91120 --- /dev/null +++ b/scripts/embed-templates.ts @@ -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> = {} +const shared: Record = {} +const templates: Record> = {} + +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> = ${JSON.stringify(templates, null, 2)}`, + '', + `export const BINARY: Record> = ${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)}`) diff --git a/src/lib/templates.ts b/src/lib/templates.ts index 8c90922..2afa903 100644 --- a/src/lib/templates.ts +++ b/src/lib/templates.ts @@ -1,10 +1,10 @@ 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 AppTemplates = Record +export type AppTemplates = Record interface TemplateOptions { tool?: boolean @@ -15,28 +15,6 @@ interface TemplateVars { 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 { const vars: TemplateVars = { APP_EMOJI: DEFAULT_EMOJI, @@ -45,29 +23,29 @@ export function generateTemplates(appName: string, template: TemplateType = 'ssr const result: AppTemplates = {} - // Read shared files from templates/ - for (const filename of SHARED_FILES) { - const path = join(TEMPLATES_DIR, filename) - let content = readFileSync(path, 'utf-8') - content = replaceVars(content, vars) + // Text files (shared + template-specific, merged at embed time) + for (const [filename, content] of Object.entries(TEMPLATES[template] ?? {})) { + let processed = replaceVars(content, vars) - // Add tool option to package.json if specified if (filename === 'package.json' && options.tool) { - const pkg = JSON.parse(content) + const pkg = JSON.parse(processed) 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 - const templateDir = join(TEMPLATES_DIR, template) - for (const path of readDir(templateDir)) { - const filename = relative(templateDir, path) - const content = readFileSync(path, 'utf-8') - result[filename] = replaceVars(content, vars) + // Binary files (base64 encoded) + for (const [filename, base64] of Object.entries(BINARY[template] ?? {})) { + result[filename] = Buffer.from(base64, 'base64') } 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) +}