Integrate esbuild into your Publican build process

804 words, 5-minute read

Publican primarily constructs HTML files and static assets. It can copy CSS and JavaScript files, but a dedicated bundler offers more sophisticated options such as source maps, tree-shaking, and platform-targeting.

The following sections describe how to use esbuild to:

esbuild is optional and not necessary for Publican projects. You can use any build system or local development server.

Bundled files #

The following files will be bundled:

During a development build, files are not minified, retain console and debugger statements, and provide linked source maps. esbuild can watch for changes and automatically rebundle.

esbuild options #

esbuild is configured using JavaScript so you can add instructions to your Publican publican.config.js configuration file. You can use environment variables or .env files to define:

These are imported into publican.config.js – possibly after your call to await publican.build():

publican.config.js excerpt

// may already be defined
const isDev = (process.env.NODE_ENV === 'development');

// esbuild constants
const
  target = (process.env.BROWSER_TARGET || '').split(','),
  logLevel = isDev ? 'info' : 'error',
  minify = !isDev,
  sourcemap = isDev && 'linked';

CSS context #

The esbuild CSS build context is defined as follows:

publican.config.js excerpt

// bundle CSS
const buildCSS = await esbuild.context({

  entryPoints: [ process.env.CSS_DIR ],
  bundle: true,
  target,
  external: ['/images/*'],
  loader: {
    '.woff2': 'file',
    '.png': 'file',
    '.jpg': 'file',
    '.svg': 'dataurl'
  },
  logLevel,
  minify,
  sourcemap,
  outdir: `${ process.env.BUILD_DIR }css/`

});

Note:

JavaScript context #

The esbuild JavaScript build context is defined as follows:

publican.config.js excerpt

// bundle JS
const buildJS = await esbuild.context({

  entryPoints: [ process.env.JS_DIR ],
  format: 'esm',
  bundle: true,
  target,
  external: [],
  define: {
    __ISDEV__: JSON.stringify(isDev)
  },
  drop: isDev ? [] : ['debugger', 'console'],
  logLevel,
  minify,
  sourcemap,
  outdir: `${ process.env.BUILD_DIR }js/`

});

The define string __ISDEV__ is replaced with true or false in the bundle. This makes it possible to add JavaScript which is only present in development mode:

if (__ISDEV__) {
  console.log('Site is running in development mode');
}

This is used for live reloading.

Watch mode #

esbuild launches a development server, watches for file changes, and re-bundles when Publican’s watch mode is enabled:

publican.config.js excerpt

if (publican.config.watch) {

  // watch for file changes
  await buildCSS.watch();
  await buildJS.watch();

  // development server
  await buildCSS.serve({
    servedir: process.env.BUILD_DIR,
    port: parseInt(process.env.SERVE_PORT) || 8000
  });

}

Live reloading #

esbuild can live reload changed styles but you must add this functionality yourself to your main.js entry script:

src/js/main.js excerpt

// development live CSS reload
import './dev/css-reload.js';

The src/js/dev/css-reload.js file listens for server-sent events from esbuild then re-imports changed CSS files. Note that if (__ISDEV__) { ... } removes the whole block from the JavaScript when bundling in production mode.

src/js/dev/css-reload.js

// live reload CSS in development mode
if (__ISDEV__) {

  // esbuild server-sent event
  new EventSource('/esbuild').addEventListener('change', e => {

    const { added, removed, updated } = JSON.parse(e.data);

    // reload when CSS files are added or removed
    if (added.length || removed.length) {
      location.reload();
      return;
    }

    // replace updated CSS files
    Array.from(document.getElementsByTagName('link')).forEach(link => {

      const url = new URL(link.href), path = url.pathname;

      if (updated.includes(path) && url.host === location.host) {

        const css = link.cloneNode();
        css.onload = () => link.remove();
        css.href = `${ path }?${ +new Date() }`;
        link.after(css);

      }

    });

  });

}

Static build #

esbuild bundles CSS and JavaScript once when watch mode is not enabled:

publican.config.js excerpt

else {

  // single build
  await buildCSS.rebuild();
  buildCSS.dispose();

  await buildJS.rebuild();
  buildJS.dispose();

}