Add template embedding and generation script
This commit is contained in:
parent
6dc7ad8608
commit
7a0a9fc731
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
71
scripts/embed-templates.ts
Normal file
71
scripts/embed-templates.ts
Normal 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)}`)
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user