Skip to content

Instantly share code, notes, and snippets.

@DeadWisdom
Created September 24, 2024 08:10
Show Gist options
  • Save DeadWisdom/4f403110b1bd032c0795c2a72950a863 to your computer and use it in GitHub Desktop.
Save DeadWisdom/4f403110b1bd032c0795c2a72950a863 to your computer and use it in GitHub Desktop.
Bun build script
import { Glob, file } from "bun";
import { filesize } from "filesize";
import { resolve, join } from "path";
/// Config
const watchDirs = ['./ui', '../m3-wc/ui'];
const copyFiles = {
'index.html': './ui/index.html',
'styles/m3.css': '../m3-wc/ui/style/base.css',
'styles/base.css': './ui/styles/base.css'
};
const fallback = './index.html';
const buildConfig = {
entrypoints: ['./ui/index.ts', './ui/loader.ts'],
outdir: './build',
naming: {
asset: "[dir]/[name].[ext]"
},
publicPath: "/",
}
/// Building
export async function build(options: { watch?: boolean, extraStatus?: string }) {
let result = await Bun.build({
...buildConfig,
loader: {
".css": "file",
".html": "file",
},
naming: {
asset: "[dir]/[name].[ext]"
},
splitting: true,
minify: true,
sourcemap: 'external'
})
if (!result.success) {
result.logs.forEach(log => console.error(log, '\n\n----\n'));
console.log("\n❌ ", new Date(), "\n");
return;
}
try {
result.outputs = result.outputs.concat(await copyFilesToBuild());
} catch (e) {
result.logs.forEach(log => console.error(e, '\n\n----\n'));
console.log("\n❌ ", new Date(), "\n");
return;
}
let items = result.outputs.filter(art => !art.path.endsWith('.map')).map(art => ({
'name': '📗 .' + art.path,
'loader': art.loader,
'size': "\x1b[33m" + filesize(art.size) + "\x1b[0m"
}));
console.table(items);
console.log("\n✔️ ", new Date(), options.extraStatus || '', "\n");
}
async function copyFilesToBuild() {
let results: any = [];
for (let [dest, src] of Object.entries(copyFiles)) {
let file = Bun.file(src);
let path = resolve(buildConfig.outdir, dest);
results.push({ path, loader: 'copy', size: file.size });
if (!await file.exists()) {
throw new Error("File not found: " + src);
}
await Bun.write(path, file);
}
return results;
}
/// Server
import { Environment, FileSystemLoader } from "nunjucks";
export function serveContent(root: string) {
return Bun.serve({
port: 3000,
async fetch(req) {
const path = new URL(req.url).pathname;
if (path.includes('..') || path.includes('*')) {
return await notFoundResponse();
}
return (
await templateResponse(root, path) ||
await fileResponse(root, path) ||
await indexResponse(root, path) ||
await folderResponse(root, path) ||
await fallbackResponse(root) ||
await notFoundResponse()
);
},
});
}
async function notFoundResponse() {
return new Response(head + "<h1>Page not found</h1>", { status: 404, headers: { 'content-type': 'text/html' } });
}
async function templateResponse(root: string, path: string) {
if (!path.endsWith('.html')) return undefined;
let env = new Environment(new FileSystemLoader(root), { autoescape: true });
let file = Bun.file(join(root, path));
if (!await file.exists()) return undefined;
let src = await file.text();
let html = env.renderString(src, {});
return new Response(html, { 'headers': { 'content-type': 'text/html' } });
}
async function fileResponse(root: string, path: string) {
const file = Bun.file(join(root, path));
if (!await file.exists()) {
return undefined;
}
return new Response(file, { 'headers': { 'content-type': file.type, 'content-length': file.size.toString() } });
}
async function folderResponse(root: string, path: string) {
if (!path.endsWith('/')) path = path + '/';
let glob = new Glob(root + path + '**');
let files = await Array.fromAsync(glob.scan("."));
if (files.length === 0) return undefined;
let parent = '/' + path.split('/').slice(0, -2).join('/');
let results = [
head,
`<h1>Index of ${path}</h1>`,
path !== '/' ? `<div class="path"><a href="${parent}">..</a></div>` : ''
];
for (const file of files) {
let url = file.substring(root.length).replaceAll('\\', '/');
results.push(
'<div class="path"><a href="' + url + '">' + url + '</a></div>'
);
}
return new Response(results.join("\n"), { 'headers': { 'content-type': 'text/html' } });
}
async function indexResponse(root: string, path: string) {
console.log(root, path);
if (path.endsWith('/_')) return await folderResponse(root, path.slice(0, -1));
return await fileResponse(root, join(path, 'index.html'));
}
async function fallbackResponse(root: string) {
return await fileResponse(root, fallback);
}
const head = `
<!DOCTYPE html>
<head>
<style>
body {
padding: 2rem;
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 14px;
color: #333;
background-color: #f8f5f5;
}
.path { margin: 0.5em; }
h1 { margin-top: 0; }
</style>
</head>
<body>
`;
/// CLI
import { watch } from "fs";
import { parseArgs } from "util";
function runOnChanges(dir: string | string[], callback: (values: any) => void) {
let watchers: any[] = [];
if (!Array.isArray(dir)) dir = [dir];
process.on("SIGINT", () => {
console.log("👍\n");
watchers.forEach(w => w.close());
process.exit(0);
});
let last = Date.now();
watchers =
dir.map(d =>
watch(d, { recursive: true }, (event, filename) => {
let now = Date.now();
if (now - last < 500) return;
callback(values);
last = now;
}));
}
const commands = {
build: (values: any) => {
build(values);
if (values.watch) {
runOnChanges(watchDirs, () => build(values));
}
},
serve: (values: any) => {
let server = serveContent(values.outDir);
values.extraStatus = `🪐 ${server.url}`;
build(values);
runOnChanges(watchDirs, () => build(values));
},
};
let { values, positionals } = parseArgs({
args: Bun.argv,
options: {
watch: {
type: 'boolean',
},
outDir: {
type: 'string',
default: './build',
}
},
strict: true,
allowPositionals: true,
});
function exitWithUsage() {
console.log("### Usage ###");
console.log(`> bun ${import.meta.file} <command> [options]`);
console.log("\n### Commands ###");
console.log(" build · build the project");
console.log(" serve · serve the project and watch for changes");
console.log("\n### Options ###");
console.log(" --watch · watch for changes in the src and reload");
console.log(` --outDir · output directory for building (default: ${buildConfig.outdir})`);
console.log("\n### ENV Options ###");
console.log(" PORT · port for serving content");
console.log("");
process.exit(1);
}
// Main ///
if (positionals.length === 2) { positionals.push('build'); }
let command = (commands as any)[positionals[positionals.length - 1]];
if (!command) {
exitWithUsage();
}
command(values);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment