personal website
at main 224 lines 6.9 kB view raw
1#!/usr/bin/env bun 2import plugin from "bun-plugin-tailwind"; 3import { existsSync } from "fs"; 4import { rm, cp, readdir, writeFile, unlink } from "fs/promises"; 5import path from "path"; 6 7export function buildVibesHtml(files: string[]) { 8 const items = files.map((f) => { 9 const src = `/vibes/${f}` 10 if (/\.(mp4|mov|webm)$/i.test(f)) 11 return `<video src="${src}" controls loop muted preload="none"></video>` 12 return `<img src="${src}" alt="" loading="lazy">` 13 }).join("\n ") 14 15 return `<!DOCTYPE html> 16<html lang="en"> 17<head> 18 <meta charset="UTF-8"> 19 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 20 <title>vibes</title> 21 <style> 22 * { margin: 0; padding: 0; box-sizing: border-box; } 23 body { background: #1a1a1a; } 24 a.back { 25 position: fixed; top: 1.5rem; left: 1.5rem; z-index: 10; 26 padding: 0.4rem 1rem; border-radius: 999px; 27 background: #2a2a2a; color: #ccc; text-decoration: none; 28 font: 0.85rem monospace; border: 1px solid #444; 29 } 30 a.back:hover { background: #333; color: #fff; } 31 .gallery { 32 columns: 4 180px; column-gap: 6px; 33 padding: 5rem 1rem 2rem; 34 } 35 .gallery img, .gallery video { 36 width: 100%; height: auto; display: block; margin-bottom: 6px; 37 } 38 </style> 39</head> 40<body> 41 <a class="back" href="/">← back</a> 42 <div class="gallery"> 43 ${items} 44 </div> 45</body> 46</html>` 47} 48 49if (process.argv.includes("--help") || process.argv.includes("-h")) { 50 console.log(` 51🏗️ Bun Build Script 52 53Usage: bun run build.ts [options] 54 55Common Options: 56 --outdir <path> Output directory (default: "dist") 57 --minify Enable minification (or --minify.whitespace, --minify.syntax, etc) 58 --sourcemap <type> Sourcemap type: none|linked|inline|external 59 --target <target> Build target: browser|bun|node 60 --format <format> Output format: esm|cjs|iife 61 --splitting Enable code splitting 62 --packages <type> Package handling: bundle|external 63 --public-path <path> Public path for assets 64 --env <mode> Environment handling: inline|disable|prefix* 65 --conditions <list> Package.json export conditions (comma separated) 66 --external <list> External packages (comma separated) 67 --banner <text> Add banner text to output 68 --footer <text> Add footer text to output 69 --define <obj> Define global constants (e.g. --define.VERSION=1.0.0) 70 --help, -h Show this help message 71 72Example: 73 bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom 74`); 75 process.exit(0); 76} 77 78const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, g => g[1].toUpperCase()); 79 80const parseValue = (value: string): any => { 81 if (value === "true") return true; 82 if (value === "false") return false; 83 84 if (/^\d+$/.test(value)) return parseInt(value, 10); 85 if (/^\d*\.\d+$/.test(value)) return parseFloat(value); 86 87 if (value.includes(",")) return value.split(",").map(v => v.trim()); 88 89 return value; 90}; 91 92function parseArgs(): Partial<Bun.BuildConfig> { 93 const config: Partial<Bun.BuildConfig> = {}; 94 const args = process.argv.slice(2); 95 96 for (let i = 0; i < args.length; i++) { 97 const arg = args[i]; 98 if (arg === undefined) continue; 99 if (!arg.startsWith("--")) continue; 100 101 if (arg.startsWith("--no-")) { 102 const key = toCamelCase(arg.slice(5)); 103 config[key] = false; 104 continue; 105 } 106 107 if (!arg.includes("=") && (i === args.length - 1 || args[i + 1]?.startsWith("--"))) { 108 const key = toCamelCase(arg.slice(2)); 109 config[key] = true; 110 continue; 111 } 112 113 let key: string; 114 let value: string; 115 116 if (arg.includes("=")) { 117 [key, value] = arg.slice(2).split("=", 2) as [string, string]; 118 } else { 119 key = arg.slice(2); 120 value = args[++i] ?? ""; 121 } 122 123 key = toCamelCase(key); 124 125 if (key.includes(".")) { 126 const [parentKey, childKey] = key.split("."); 127 config[parentKey] = config[parentKey] || {}; 128 config[parentKey][childKey] = parseValue(value); 129 } else { 130 config[key] = parseValue(value); 131 } 132 } 133 134 return config; 135} 136 137const formatFileSize = (bytes: number): string => { 138 const units = ["B", "KB", "MB", "GB"]; 139 let size = bytes; 140 let unitIndex = 0; 141 142 while (size >= 1024 && unitIndex < units.length - 1) { 143 size /= 1024; 144 unitIndex++; 145 } 146 147 return `${size.toFixed(2)} ${units[unitIndex]}`; 148}; 149 150console.log("\n🚀 Starting build process...\n"); 151 152const cliConfig = parseArgs(); 153const outdir = cliConfig.outdir || path.join(process.cwd(), "dist"); 154 155if (existsSync(outdir)) { 156 console.log(`🗑️ Cleaning previous build at ${outdir}`); 157 await rm(outdir, { recursive: true, force: true }); 158} 159 160const start = performance.now(); 161 162const entrypoints = [...new Bun.Glob("**.html").scanSync("src")] 163 .map(a => path.resolve("src", a)) 164 .filter(dir => !dir.includes("node_modules")); 165console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`); 166 167const result = await Bun.build({ 168 entrypoints, 169 outdir, 170 plugins: [plugin], 171 minify: true, 172 target: "browser", 173 sourcemap: "linked", 174 define: { 175 "process.env.NODE_ENV": JSON.stringify("production"), 176 }, 177 ...cliConfig, 178}); 179 180const end = performance.now(); 181 182const outputTable = result.outputs.map(output => ({ 183 File: path.relative(process.cwd(), output.path), 184 Type: output.kind, 185 Size: formatFileSize(output.size), 186})); 187 188console.table(outputTable); 189const buildTime = (end - start).toFixed(2); 190 191// Copy public folder to dist 192const publicDir = path.join(process.cwd(), "public"); 193if (existsSync(publicDir)) { 194 console.log("📁 Copying public folder to dist..."); 195 await cp(publicDir, outdir, { recursive: true }); 196} 197 198// Convert vibes images to WebP in dist (originals in public/ stay untouched) 199const distVibesDir = path.join(outdir, "vibes"); 200if (existsSync(distVibesDir)) { 201 const files = await readdir(distVibesDir); 202 const convertable = files.filter((f) => /\.(jpg|jpeg|png|gif|avif|bmp|tiff)$/i.test(f)); 203 204 if (convertable.length > 0) { 205 console.log(`\n🖼️ Converting ${convertable.length} images to WebP...`); 206 await Promise.all( 207 convertable.map(async (f) => { 208 const src = path.join(distVibesDir, f); 209 const dest = path.join(distVibesDir, f.replace(/\.[^.]+$/, ".webp")); 210 const proc = Bun.spawn(["magick", src, "-quality", "80", dest]); 211 await proc.exited; 212 await unlink(src); 213 }) 214 ); 215 console.log("✅ WebP conversion done"); 216 } 217 218 // Generate vibes/index.html 219 const finalFiles = (await readdir(distVibesDir)).filter((f) => f !== "index.html"); 220 await writeFile(path.join(distVibesDir, "index.html"), buildVibesHtml(finalFiles)); 221 console.log(`📦 Generated vibes/index.html with ${finalFiles.length} files`); 222} 223 224console.log(`\n✅ Build completed in ${buildTime}ms\n`);