Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for An 85 Lines of Code Static Site Generator with Bun and JSX
Sergei Orlov
Sergei Orlov

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.

  1. collect all the pages that we conventionally put in a single
  2. directory. Nesting directories will be preserved as well,transpile JSX inside the pages into JS files with functions that produce HTML,
  3. 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"]}}
Enter fullscreen modeExit fullscreen mode

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{}
Enter fullscreen modeExit fullscreen mode

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}>`)}
Enter fullscreen modeExit fullscreen mode
// ./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}>`)}
Enter fullscreen modeExit fullscreen mode

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>)}
Enter fullscreen modeExit fullscreen mode

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()
Enter fullscreen modeExit fullscreen mode

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:

A page generated with Bun and JSX

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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

  • Work
    Lead Software Engineer
  • Joined

More fromSergei Orlov

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp