Created
September 24, 2024 08:10
-
-
Save DeadWisdom/4f403110b1bd032c0795c2a72950a863 to your computer and use it in GitHub Desktop.
Bun build script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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