
Posted on • Originally published atorlow.dev
An 85 Lines of Code Static Site Generator with Bun and JSX
In this post we'll take a brief look on how to create an SSG (static site generator) with Bun. It will take your components written in JSX and turn them into HTML pages.
This will be a three-step process.
- collect all the pages that we conventionally put in a single
- directory. Nesting directories will be preserved as well,transpile JSX inside the pages into JS files with functions that produce HTML,
- loop over JS files, running them and saving HTML into
.html
files
TS config
First off, we need to drop the X in JSX. To do so, we instruct Bun to use our custom function for transpiling JSX. We'll go with areact-jsx
preset. We'll also define a gutsby path, which is going to be our JSX handler. With this path in place, we set gutsby as ourjsxImportSource
.
//./tsconfig.json{"compilerOptions":{"baseUrl":".","paths":{"gutsby/*":["./gutsby/*"]},"jsx":"react-jsx","jsxImportSource":"gutsby","lib":["ESNext","DOM"],"allowJs":true,"esModuleInterop":true,"allowSyntheticDefaultImports":true,"skipLibCheck":true,"isolatedModules":true,"target":"ESNext","module":"CommonJS","types":["bun-types"]}}
Next we need tobun init
in the root directory since TSConfig will blame us forbun-types. You needBun installed on your machine, of course.
It also makes sense to define a few types for our convenience. I'll go global but you can export them if you like.
// ./src.types.d.tsdeclareglobal{moduleJSX{interfaceIntrinsicElements{[key:string]:any}}/** * A customisable props type for our components. */exporttypePropsWithChildren<CustomPropsextendsRecord<string,unknown>=Record<string,unknown>>=CustomProps&{children?:(Promise<string|string[]>|undefined)[]}}export{}
Now we're ready to make our dirty littlegutsby.
Dealing with JSX in Bun
With a JSX preset we've picked in our tsconfig (react-jsx
), Bun will wrap every JSX element into ajsxDEV
function call. Since we've also instructed it that the import source is thegutsby directory, let's create our JSX handler there. To avoid errors, we need two files: jsx-runtime.ts and jsx-dev-runtime.ts but the content may be completely the same since Bun will refer to the dev in our scenario.
// ./gutsby/jsx-runtime.ts/** * This function is called recursively, so, whether it's a component or an HTML * tag, it will eventually become an HTML string with all children nested as * HTML as well. */exportasyncfunctionjsx(type:string|((props:PropsWithChildren)=>string),props:PropsWithChildren):Promise<string>{// 1. Handling components// This is a component. Run it to get the contents.if(typeoftype==="function")returntype(props)// 2. Handling tags// If children is not an array then it must be.if(!Array.isArray(props.children))props.children=[props.children]// Start opening tag composition.letline=`<${type}`// Get all the props that are not children.constnotChildren=Object.keys(props).filter(key=>key!=="children")// Loop over the props and put them as attributes in our HTML.// Yes, class, not className.for(constpropofnotChildren)line+=`${prop}="${props[prop]}"`// Finish opening tag composition.line+=">"// Loop over the children.for(constchildofprops.children){letnested=awaitchild// If children is not an array then it must be.if(!Array.isArray(nested))nested=[nestedasstring]// Loop over children and put them as inner HTML.for(constitemofnestedasstring[])line+=(awaititem)??""}// Close the tag and return whatever HTML we got.returnline.concat(`</${type}>`)}
// ./gutsby/jsx-dev-runtime.ts/** * This function is called recursively, so, whether it's a component or an HTML * tag, it will eventually become an HTML string with all children nested as * HTML as well. */exportasyncfunctionjsxDEV(type:string|((props:PropsWithChildren)=>string),props:PropsWithChildren):Promise<string>{// 1. Handling components// This is a component. Run it to get the contents.if(typeoftype==="function")returntype(props)// 2. Handling tags// If children is not an array then it must be.if(!Array.isArray(props.children))props.children=[props.children]// Start opening tag composition.letline=`<${type}`// Get all the props that are not children.constnotChildren=Object.keys(props).filter(key=>key!=="children")// Loop over the props and put them as attributes in our HTML.// Yes, class, not className.for(constpropofnotChildren)line+=`${prop}="${props[prop]}"`// Finish opening tag composition.line+=">"// Loop over the children.for(constchildofprops.children){letnested=awaitchild// If children is not an array then it must be.if(!Array.isArray(nested))nested=[nestedasstring]// Loop over children and put them as inner HTML.for(constitemofnestedasstring[])line+=(awaititem)??""}// Close the tag and return whatever HTML we got.returnline.concat(`</${type}>`)}
That is it, actually. Now all your elements will become strings of HTML. Our next step is to create a script that will run turn our top-level components into elements and save them as HTML. We're basically making aReactDOM.render
here, but it renders to HTML files directly.
Building pages
We need two things: a build script, and a page we'll render as a proof of concept. Let's start with the page first. I put mine in asrc/pages directory to keep it separate from the build script and other stuff. A bit of Nextiness.
// ./src/pages/index.tsxconstvalueOutsideComponent="Value outside"constasyncValueOutsideComponentP=Promise.resolve("Async value outside")/** * This is our index page. * * NOTE: It must be a default export as per our build script. * * And yes, it does support * - extracting components * - children provision * - async behavior * - values in closure */exportdefaultasyncfunctionIndex(){constvalueViaChildren="Value via children"return(<htmllang="en"><head><title>Bun,JSXandOrlowdev</title></head><body><main><h1>Myvalues</h1><MyValuesvalueViaProps="Value via props">{valueViaChildren}</MyValues></main></body></html>)}typeP=PropsWithChildren<{valueViaProps:string}>constMyValues=async({valueViaProps,children}:P)=>{constvalueInsideComponent="Value inside"constasyncValueInsideComponent=awaitPromise.resolve("Async value inside")constasyncValueOutsideComponent=awaitasyncValueOutsideComponentPreturn(<ul><li>{valueViaProps}</li><li>{valueInsideComponent}</li><li>{valueOutsideComponent}</li><li>{asyncValueInsideComponent}</li><li>{asyncValueOutsideComponent}</li><li>{children}</li></ul>)}
Now the last part of our tour is to get HTML. We only cover build process here but you can add all sorts of things here, including CSS processing, copying assets, minifying images, and what. The script will runbun build
on the files inside the pages directory and then grab the compiled files and execute them, saving to./dist/*.html.
// ./build.tsimport{promises,existsSync,mkdirSync}from"node:fs"import{execSync}from"node:child_process"// Create all the directories if they do not exist.if(!existsSync("dist"))mkdirSync("dist")if(!existsSync("dist/js"))mkdirSync("dist/js")if(!existsSync("dist/www"))mkdirSync("dist/www")/** * Build JSX, loop over pages and save their content as HTML files with the same name. */constcompileHTML=async()=>{// Bun build with a target of Bun since we are going to run those scripts later with Bun.execSync("bun build src/pages/* --outdir dist/js --target=bun")// Get all available pages.constpages=awaitpromises.readdir("./dist/js")// Loop over pages and generate HTML files for each of them.for(constpageofpages){// Skip if a page is somehow not a JS file.if(!page.endsWith(".js"))continue// Get name of the file without file extension.constname=page.substring(0,page.lastIndexOf("."))// Import default function from the page.constf=awaitimport(`dist/js/${name}`).then(p=>p.default)// Run the function and write whatever it returns to an HTML file with the name of the page.Bun.write(`./dist/www/${name}.html`,awaitf())}}// Go!compileHTML()
And that's it! If you now runbun run build.ts
it should create you adist/www/index.html that will look something like this:
And this wraps up our short lesson. Your optional home assignment is to add:
- serving HTML files (check out Bun.serve)
- watching for changes and rerunning compilation (fs.watch)
- postprocessing for CSS (TailwindCSS, for example)
- copying static assets for your pages
- optimizing images - this is a harder one
If you have any questions, you can reach me out onX.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse