Code organization with ConstexprJS

Top

constexprjs allows you to execute some of the javascript in your website before it is deployed. I'll refer to such javascript as constexpr. You can use constexpr code for programmatically generating HTML in your website.

The constexpr code is stripped out by the compiler, so the runtime code must not depend on it. Hence, there has to be a clear boundary between the runtime code and compile time code. In this article I'll share some tips for managing this when building a site with constexprjs.

This article requires some familiarity with constexprjs. Read this guide to get up and running with constexprjs.

A simple example

Set up the environment as described in the above linked guide. After that, put this in index.html:

<html> <head> <title>Dynamic Pages Example</title> </head> <body> <p>This page was rendered on: <span id="timestamp"></span></p> </body> <script constexpr> let d = new Date() document.querySelector('#timestamp').textContent = `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}` window._ConstexprJS_.compile() </script> </html>

And run the compiler:

constexprjs --input=. --output=_out --entry=index.htm

The compiler will emit the following static website:

<html> <head> <title>Dynamic Pages Example</title> </head> <body> <p>This page was rendered on: <span id="timestamp">1:58:5</span></p> </body> </html>

Let's add a button to this page that will add all the numbers in the timestamp and alert the result, just to demonstrate how to build non-static websites with constexprjs. Let's first modify our rendering code to add a button to the page:

<script constexpr> let d = new Date() document.querySelector('#timestamp').textContent = `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}` let b = document.createElement('button') b.textContent = "Calculate" document.body.appendChild(b) window._ConstexprJS_.compile() </script>

After that, define a regular, non-constexpr function named runtime_bootstrap that will add an event listener to this button:

<script> function runtime_bootstrap() { document.querySelector('button').addEventListener('click', () => { let result = 0 document.querySelector('#timestamp').textContent.split(':') .forEach(s => result += parseInt(s)) alert(result) }) } </script>

Finally we'll modify the rendering code to add a script tag to our page that calls the bootstrap function:

<script constexpr> ... let s = document.createElement('script') s.textContent = 'runtime_bootstrap()' document.body.appendChild(s) window._ConstexprJS_.compile() </script>

Now when you run the compiler, you will get the following page as output:

<html> <head> <title>Dynamic Pages Example</title> <script> function runtime_bootstrap() { document.querySelector('button').addEventListener('click', () => { let result = 0 document.querySelector('#timestamp').textContent.split(':') .forEach(s => result += parseInt(s)) alert(result) }) } </script> </head> <body> <p>This page was rendered on: <span id="timestamp">2:32:13</span></p> <button>Calculate</button> <script>runtime_bootstrap()</script> </body> </html>

This page contains three things:

  1. Generated HTML (timestamp and button)
  2. Runtime bootstrap function
  3. A script tag that calls the bootstrap function

All the rendering code has been stripped out.

Implementation in this website

The rendering code generates all the site global theming (navbar, sidebars, syntax highlighting etc). I've put this code in /static/js/constexpr/renderer.js. This script is included as constexpr in every page.

The code that bootstraps the dynamic behavior is contained in separate files, /static/js/dynamic-{pre,post}.js. The constexpr code in renderer.js adds these script tags to every page once it has finished rendering:

addRuntimeBootstrapHook({ src: '/static/js/dynamic-pre.js', early: true }) addRuntimeBootstrapHook({ src: '/static/js/dynamic-post.js' })

This is the implementation of addRuntimeBootstrapHook utility:

function addRuntimeBootstrapHook(js) { let el = document.createElement('script') if (js.src) { el.setAttribute('src', js.src) } else if (js.code) { el.textContent = js.code } else { throw "invalid arguments" } if (js.async) { el.setAttribute('async', '') } if (js.early) { document.body.prepend(el) } else { el.setAttribute('defer', '') document.body.appendChild(el) } }
See pages tagged with constexprjs for more guides.