Image without description
  • Jacco Meijer
  • |
  • Mar 20, 2024

Render JSX plugin for Esbuild

Transform Esbuild generated JSX bundles to HTML pages.

Esbuild as a static site generator for MDX

This post is part of a series that originated with the question "can the Contentful-Gatsby-Netlify trio be simplified?".

Image without description
  • Jacco Meijer
  • |
  • Mar 19, 2024

Esbuild as a static site generator for MDX

Static site generators gain popularity. This blog is about using Esbuild as a static site generator for MDX.

Using 11ty is part of the positive answer. The last post concludes with the need for a Render JSX plugin for Esbuild.

This post starts with the code for such a plugin and describes how it works.

The code

Below is the full plugin. It's taken from jaccomeijer/render-jsx-plugin-poc, a small repo that can be used to see the plugin working.

import { writeFile, mkdir } from 'fs/promises'
import path from 'path'
import crypto from 'crypto'
import { render } from 'preact-render-to-string'

const pluginName = 'renderJsxPlugin'

export const renderJsxPlugin = ({ outdir, initialProps }) => {
  const setup = build => {
    build.onEnd(async result => {
      const metafileOutputs = Object.keys(result.metafile.outputs)

      for (const bundlePath of metafileOutputs) {
        const entryPoint = result.metafile.outputs[bundlePath].entryPoint

        if (!entryPoint) {
          continue
        }

        const { dir, name } = path.parse(entryPoint)
        const strippedDir = dir.replace('src/pages', '')
        const outputDir = path.join(outdir, strippedDir)
        const outputPath = path.join(outdir, strippedDir, `${name}.html`)

        const cacheBust = crypto.randomBytes(6).toString('hex')
        const modulePath = path.resolve(`./${bundlePath}?v=${cacheBust}`)
        const module = await import(modulePath)

        console.log(`${pluginName}: ${outputPath}`)

        // Check if the default export is a function
        if (typeof module?.default !== 'function') {
          continue
        }
        const props = Object.assign(initialProps || {})
        const html = render(module.default(props))

        await mkdir(path.resolve(outputDir), { recursive: true })
        await writeFile(path.resolve(outputPath), `<!DOCTYPE html>${html}`)
      }
    })
  }

  return { name: pluginName, setup }
}

More capable version

A more capable version of the plugin is used in the jaccomeijer/green-build repository. That repo is used to build this site (repository here) and adds functionality like:

  • image handling
  • CSS bundling
  • HTML minifying
  • code highlighting
  • page prop with Front Matter support
  • pages prop for menu building
  • cleanup of the javascript bundle
  • custom urls
  • index.html for all pages
  • uses React JSX runtime instead or Preact

The breakdown

The plugin is simple and contains a few distinct steps.

Metadata

Esbuild provides onEnd plugins with build metadata. The plugin starts with using result.metafile to get all bundle paths.

  const metafileOutputs = Object.keys(result.metafile.outputs)

OutputPath

The plugin then loops through every bundlePath and derives an output dir and path.

const outputPath = path.join(outdir, strippedDir, `${name}.html`)

Import bundle

Next, the the JSX bundle created by Esbuild is imported. Adding ?v=${cacheBust} to the module path prevents Node from caching the module.

const modulePath = path.resolve(`./${bundlePath}?v=${cacheBust}`)
const module = await import(modulePath)

Render to HTML

The last part of the plugin does the actual rendering. The plugin imports the render function from Preact.

import { render } from 'preact-render-to-string'

This means that the plugin works with bundles that have the Preact JSX Factory. This Factory is the function that is called for every JSX element. For Esbuild, these are the build options to build with the Preact Factory:

{
  jsx: 'automatic',
  jsxImportSource: 'preact',
  ...
}

The imported bundle contains a JSX component that accepts props like any other JSX function. This demo does not do much with the initialProps, but this is how e.g. global metadata can be passed to all pages. The package jaccomeijer/green-build, mentioned earlier, uses this concept.

const props = Object.assign(initialProps || {})
const html = render(module.default(props))

Write to file

With the static HTML, all that's left is writing the HTML to a file for the Esbuild webserver to render.

All HTML documents must start with a <!DOCTYPE> declaration. This is not a tag, but an id to let the browser know what to expect. This declaration is added here.

await mkdir(path.resolve(outputDir), { recursive: true })
await writeFile(path.resolve(outputPath), `<!DOCTYPE html>${html}`)

JSX

The plugin above uses Preact to transform JSX into static HTML. Theoretically, the JSX framework should not affect the final result because Factory functions are added when bundling and removed again when rendering. In practice, there's some differences in consistency, speed and syntax conventions.

Jeasx

The jsx-async-runtime package contains a very simple and lightweight JSX runtime. It also contains the renderToString function.

This is a very simple runtime which makes it very fast. Drawback is that spaces in pre and code elements are not maintained properly which makes the render unusable for code blocks.

Preact

The preact-render-to-string package provides a render function that renders consistently to static HTML.

The package does not warn about React conventions like the need for key attributes and is a lot faster than React's renderToStaticMarkup.

React

The first version of the jaccomeijer/green-build package created bundles with Preact settings.

Version 2 of the build package switched to React because of compatibility. Creating e.g. a UI library makes the library compatible with frameworks like Astro.

Gaining React compatibility came with the cost of speed. The React Factory is noticeably slower than the other two. Also, creating a React bundle shows all the warnings that come with React's conventions. CamelCase attributes and key attribute requirements for example.

The react-dom/server package contains two functions:

  • renderToString → renders to a string that can be hydrated
  • renderToStaticMarkup → renders a non-interactive string

Of which the Static variant is the most useful here.

Speed and other JSX Factories

As noted above, React is the slowest of the JSX Factories. For this plugin, three JSX Factories have been tried, but many other frameworks with JSX-alike syntax exist. To mention a few, Million, SolidJs and Svelte. All of them possibly expose separate static render functionality.

Adding Javascript

JSX is used as a simple component solution. By wrapping static HTML in a function, this function can be reused. Simply by importing the function. Function arguments, or 'props', make this concept dynamic.

When JSX is rendered, only the output of the JSX functions remains. This results in static HTML.

When client side Javascript is needed, it can be added the same way that Javascript is added to any static HTML. E.g. using a <script> tag.

Shadow DOM without Javascript

Since Feb 20, 2024 all major browsers support 'Declarative Shadow Dom'. The declarative part means that 'Shadow Dom' can be enabled without Javascript:

<template shadowrootmode="open">

Server Side Rendering

This plugin does exactly what Server Side Rendering (SSR) does. The major difference with e.g. React SSR is that the build this plugin provides does not support a Javascript runtime. It provides a pure static build instead.

This means that the React API in this code doesn't work:

const [value, setValue] = useState(0);

To use the React API, the Javascript runtime library of React must be shipped with the static code. Besides that, once the code is loaded by the client browser, event handlers need to be attached to the static elements. This process is known as 'Hydration'.

This does not mean that client side Javascript cannot be used with a static render. The UI library in the next post defines two Custom Elements that depend on client side Javascript.

Conclusion

A simple extension of Esbuild lays the foundation for a fast static site generator.

JSX being just functions, they are easily added to NPM modules for distribution and reuse. MDX is an efficient way to write documentation and stories. Combine the two and creating a reusable and documented UI library is simple. The next post zooms in on this.

Image without description
  • Jacco Meijer
  • |
  • Mar 21, 2024

UI Library with MDX documentation

Using the simple Render JSX plugin for Esbuild this post shows how to setup a simple UI library.


Other posts

Image without description
  • Jacco Meijer
  • |
  • Mar 21, 2024

UI Library with MDX documentation

Using the simple Render JSX plugin for Esbuild this post shows how to setup a simple UI library.

Image without description
  • Jacco Meijer
  • |
  • Mar 20, 2024

Render JSX plugin for Esbuild

Transform Esbuild generated JSX bundles to HTML pages.

Image without description
  • Jacco Meijer
  • |
  • Mar 19, 2024

Esbuild as a static site generator for MDX

Static site generators gain popularity. This blog is about using Esbuild as a static site generator for MDX.

Image without description
  • Jacco Meijer
  • |
  • Mar 18, 2024

11ty and Github pages

Simplifying the Contentful-Gatsby-Netlfy trio.

Image without description
  • Jacco Meijer
  • |
  • Mar 15, 2024

OWASP and CISSP

OWASP recommendations from the independent information security certification CISSP.

Image without description
  • Jacco Meijer
  • |
  • Jun 30, 2022

NPM7 and @npmcli/arborist

@npmcli/arborist is a powerful library that handles the new NPM 7 workspaces. This blog is about a simple make tool that uses the library.

Image without description
  • Jacco Meijer
  • |
  • May 12, 2022

Comparing React app, Nextjs and Gatsby

A new React project starts with a React toolchain. Main tools in the chains are SSR, React server components and GraphQL.

Image without description
  • Jacco Meijer
  • |
  • May 10, 2022

Versioning strategy for NPM modules

It is important to be able to bump the version of a NPM package without side effects.

Image without description
  • Jacco Meijer
  • |
  • Apr 12, 2022

React component themes and CSS variables

Creating React components with flexible themes by using CSS variables.

Image without description
  • Jacco Meijer
  • |
  • Mar 21, 2022

Content modeling with variants

The efficiency of a variant field in a content model.

Image without description
  • Jacco Meijer
  • |
  • Mar 12, 2022

Documentation

Documenting a software project is challenging. Here's a few simple guidelines that help a team writing clear documentation.

Image without description
  • Jacco Meijer
  • |
  • Mar 11, 2022

Javascript history

In 1986 David Ungar and Randall B. Smith developed Self at Xerox PARC. Inspired by Java, Scheme and Self Brendan Eich created Javascript in 1995.

Image without description
  • Jacco Meijer
  • |
  • Mar 10, 2022

On Javascript transpilers, bundlers and modules

There's Javascript transpilers, modules, bundles and bundlers. This is a brief overview of all of these.

Image without description
  • Jacco Meijer
  • |
  • Feb 11, 2022

Agile Scrum

The Agile Scrum framework is flexible enough to be used in many different ways. Here's one way of working.

Image without description
  • Jacco Meijer
  • |
  • Jan 20, 2022

What happened to Wheelroom?

Founded in 2018. Started to fly in 2020 and abandoned in 2021. What happened?

Image without description
  • Jacco Meijer
  • |
  • Jan 19, 2022

Contentful, Netlify and Gatsby four years later

What did we learn from using Contentful for four years?

Image without description
  • Jacco Meijer
  • |
  • Jan 18, 2022

Typescript interface for React UI components

How to define an interface for React UI components that prevents breaking changes.

Image without description
  • Jacco Meijer
  • |
  • Jan 17, 2022

Naming React components

What's in a name? A clear naming strategy helps developers communicate. Most devs rather spend time writing component code than wasting time on a good component name.