personal website
1#!/usr/bin/env bun
2import plugin from "bun-plugin-tailwind";
3import { existsSync } from "fs";
4import { rm, cp } from "fs/promises";
5import path from "path";
6
7if (process.argv.includes("--help") || process.argv.includes("-h")) {
8 console.log(`
9🏗️ Bun Build Script
10
11Usage: bun run build.ts [options]
12
13Common Options:
14 --outdir <path> Output directory (default: "dist")
15 --minify Enable minification (or --minify.whitespace, --minify.syntax, etc)
16 --sourcemap <type> Sourcemap type: none|linked|inline|external
17 --target <target> Build target: browser|bun|node
18 --format <format> Output format: esm|cjs|iife
19 --splitting Enable code splitting
20 --packages <type> Package handling: bundle|external
21 --public-path <path> Public path for assets
22 --env <mode> Environment handling: inline|disable|prefix*
23 --conditions <list> Package.json export conditions (comma separated)
24 --external <list> External packages (comma separated)
25 --banner <text> Add banner text to output
26 --footer <text> Add footer text to output
27 --define <obj> Define global constants (e.g. --define.VERSION=1.0.0)
28 --help, -h Show this help message
29
30Example:
31 bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom
32`);
33 process.exit(0);
34}
35
36const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, g => g[1].toUpperCase());
37
38const parseValue = (value: string): any => {
39 if (value === "true") return true;
40 if (value === "false") return false;
41
42 if (/^\d+$/.test(value)) return parseInt(value, 10);
43 if (/^\d*\.\d+$/.test(value)) return parseFloat(value);
44
45 if (value.includes(",")) return value.split(",").map(v => v.trim());
46
47 return value;
48};
49
50function parseArgs(): Partial<Bun.BuildConfig> {
51 const config: Partial<Bun.BuildConfig> = {};
52 const args = process.argv.slice(2);
53
54 for (let i = 0; i < args.length; i++) {
55 const arg = args[i];
56 if (arg === undefined) continue;
57 if (!arg.startsWith("--")) continue;
58
59 if (arg.startsWith("--no-")) {
60 const key = toCamelCase(arg.slice(5));
61 config[key] = false;
62 continue;
63 }
64
65 if (!arg.includes("=") && (i === args.length - 1 || args[i + 1]?.startsWith("--"))) {
66 const key = toCamelCase(arg.slice(2));
67 config[key] = true;
68 continue;
69 }
70
71 let key: string;
72 let value: string;
73
74 if (arg.includes("=")) {
75 [key, value] = arg.slice(2).split("=", 2) as [string, string];
76 } else {
77 key = arg.slice(2);
78 value = args[++i] ?? "";
79 }
80
81 key = toCamelCase(key);
82
83 if (key.includes(".")) {
84 const [parentKey, childKey] = key.split(".");
85 config[parentKey] = config[parentKey] || {};
86 config[parentKey][childKey] = parseValue(value);
87 } else {
88 config[key] = parseValue(value);
89 }
90 }
91
92 return config;
93}
94
95const formatFileSize = (bytes: number): string => {
96 const units = ["B", "KB", "MB", "GB"];
97 let size = bytes;
98 let unitIndex = 0;
99
100 while (size >= 1024 && unitIndex < units.length - 1) {
101 size /= 1024;
102 unitIndex++;
103 }
104
105 return `${size.toFixed(2)} ${units[unitIndex]}`;
106};
107
108console.log("\n🚀 Starting build process...\n");
109
110const cliConfig = parseArgs();
111const outdir = cliConfig.outdir || path.join(process.cwd(), "dist");
112
113if (existsSync(outdir)) {
114 console.log(`🗑️ Cleaning previous build at ${outdir}`);
115 await rm(outdir, { recursive: true, force: true });
116}
117
118const start = performance.now();
119
120const entrypoints = [...new Bun.Glob("**.html").scanSync("src")]
121 .map(a => path.resolve("src", a))
122 .filter(dir => !dir.includes("node_modules"));
123console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`);
124
125const result = await Bun.build({
126 entrypoints,
127 outdir,
128 plugins: [plugin],
129 minify: true,
130 target: "browser",
131 sourcemap: "linked",
132 define: {
133 "process.env.NODE_ENV": JSON.stringify("production"),
134 },
135 ...cliConfig,
136});
137
138const end = performance.now();
139
140const outputTable = result.outputs.map(output => ({
141 File: path.relative(process.cwd(), output.path),
142 Type: output.kind,
143 Size: formatFileSize(output.size),
144}));
145
146console.table(outputTable);
147const buildTime = (end - start).toFixed(2);
148
149// Copy public folder to dist
150const publicDir = path.join(process.cwd(), "public");
151if (existsSync(publicDir)) {
152 console.log("📁 Copying public folder to dist...");
153 await cp(publicDir, outdir, { recursive: true });
154}
155
156console.log(`\n✅ Build completed in ${buildTime}ms\n`);