esbd
TypeScript icon, indicating that this package has built-in type declarations

3.3.3 • Public • Published

esbd

Turn your esbuild config files into CLIs with enhanced DX for browser and Node application development.

Motivation

Esbuild is an incredible tool with the potential to supplant Webpack as the dominant bundler in the JS ecosystem. But for many, it lacks a handful of features that prevent full adoption.

Tools like Vite, Parcel, Snowpack (RIP), and others have embraced esbuild largely as a transpiler for JS dialects, i.e., as an alternative to Babel or TypeScript. These tools offer a diverse set of features, such as fast startup, HMR, low/zero configuration, and the option to eschew bundling altogether and serve ES modules directly to the browser.

But by only using esbuild as a transpiler, these tools exist in the same performance ballpark as Webpack with esbuild-loader. For smaller projects, this might be more than sufficient. And with a small enough internal dependency graph, unbundled development (or even production!) might offer better performance over bundled development via webpack-dev-server + esbuild-loader without losing any features.

Esbd uses a different approach. It delegates as much as possible to esbuild's (incredibly fast) bundler and augments it with a few nice-to-have DX enhancements that the core tool currently lacks. Features such as live-reload with error reporting, HTML entry points, and sideband type-checking enrich the core featureset without slowing esbuild down in any meaningful way.

The difference in performance is staggering. The quoted "10-100x" figure on esbuild's website is not exaggeration, but it's only true when using esbuild as both a bundler and a transpiler. By fully embracing esbuild's bundler, however, esbd is limited in what features it can reasonably offer without sacrificing build performance or significantly increasing complexity. This means HMR or other advanced features that involve source-level transformations that aren't suppored by esbuild can never be supported by esbd.

In an ideal world, esbd would not exist. If any of its extended features get added to esbuild itself, they will be happily removed from esbd. Until then, this project exists to add just enough on top to reduce boilerplate for medium-to-large projects.

Features

  • Uses esbuild's bundler for incredibly fast builds in both dev and prod
  • Every config file becomes a CLI
  • serve mode for web applications and node-dev mode for Node applications
  • HTML entry points
  • Live-reload for web applications and restart-on-change for Node applications
  • Opt-in sideband type checking for TypeScript projects
  • In-browser error overlay for esbuild and TypeScript errors (dismissable)
  • Copy static assets into the build folder
  • Full support for other esbuild plugins (JS only)

Non-features

Due to esbd's design and philosophy (see Motivation), the following features cannot be supported unless they are added to esbuild:

  • Hot module reloading (HMR)
  • "Unbundled" bundling (traversing dependency graph as though bundling, but simply producing transpiled files)

Install

$ npm i -D esbd esbuild

Usage

Create a bundle.js file (or whatever you would like to name it). This will become both your configuration file and a CLI for running build commands.

The following example will produce a build configuration/script for a single HTML entry point.

#!/usr/bin/env node
const { configure } = require('esbd');

configure({
  entryPoints: ['./index.html'],
  absWorkingDir: __dirname,
  outdir: './build',
});

Make the file executable:

$ chmod +x bundle.js

Now you can run the development server:

$ ./bundle.js serve

You can also produce a production build:

$ ./bundle.js build --mode production

Configuration

In order to restrict the number of available CLI options, build parameters must be specified via a configuration file. All of esbuild's build API options may be specified in addition to some custom options described in the API section.

Esbd exports a single function called configure that accepts various configuration formats:

  • Object — configures the build for a single application (i.e., web, node, or library)
  • Array — configures builds for multiple applications (each element should be a configuration object)
  • Function — function that accepts two parameters, mode ("development" or "production") and commandName ("build", "serve", or "node-dev") and returns either a configuration object or array. The function may be async.

Configuration objects in an array must have a name property to distinguish between them.

Configuration object:

#!/usr/bin/env node
const { configure } = require('esbd');

configure({
  absWorkingDir: __dirname,
  entryPoints: ['./src/app.ts', './src/styles.css'],
  outdir: './build',
});

Configuration array:

#!/usr/bin/env node
const { configure } = require('esbd');

configure([
  {
    name: 'web',
    absWorkingDir: __dirname,
    entryPoints: ['./src/client/index.html'],
    outdir: './build/web',
  },
  {
    name: 'server',
    absWorkingDir: __dirname,
    entryPoints: ['./src/server/app.ts'],
    outdir: './build/server',
    platform: 'node',
  },
]);
$ ./bundle.js serve web
$ ./bundle.js node-dev server
$ ./bundle.js build

Configuration function:

#!/usr/bin/env node
const { configure } = require('esbd');

configure(async (mode, command) => {
  const VERSION = (
    await fs.readFile(__dirname + '/../VERSION', {
      encoding: 'utf8',
    })
  ).trim();
  const NODE_ENV = mode === 'production' ? 'production' : 'development';
  const define = {
    '__APP_VERSION__': JSON.stringify(VERSION),
    'process.env': JSON.stringify({ NODE_ENV }),
    'NODE_ENV': JSON.stringify(NODE_ENV),
  };

  return [
    {
      name: 'web',
      absWorkingDir: __dirname,
      define,
      entryPoints: ['./src/client/index.html'],
      outdir: './build/web',
    },
    {
      name: 'server',
      absWorkingDir: __dirname,
      define,
      entryPoints: { www: './src/server/app.ts' },
      outdir: './build/server',
      platform: 'node',
    },
  ];
});

CLI commands

Build scripts define three commands with specific and global flags.

Global flags

  • --log-level, -l — Logging level (verbose, debug, info, warning, error, silent) (default: info)
  • --mode, -m — Build mode (development, production) (default: development)
  • --check, -t — Check types asynchronously with the TypeScript compiler
  • --ts-build-mode — TypeScript "build" mode behavior (readonly, write-output) (default: write-output) — see API for details

build [name]

Produces output for one or all of the defined configurations.

Parameters:

  • name — (optional) configuration name to build. If not specified, all configurations will be built.

Flags:

  • --watch, -w — Rebuild when source files change

node-dev [name]

Starts a Node application and a file watcher that recompiles the application on source change.

Parameters:

  • name — (optional) configuration name to build. If not specified, the first configuration with platform="node" will be chosen

Flags:

  • --respawn, -r — Restart program on exit/error (but quit if 3 restarts happen within 5s)

serve [name]

Starts a web server and a file watcher that serves build output in development mode.

Parameters:

  • name — (optional) configuration name to build. If not specified, the first configuration with platform="web" (or undefined) will be chosen

Flags:

  • --host, -s — Development server IP/host name (default: 127.0.0.1)
  • --port, -p — Development server port (default: 8000)
  • --livereload, -r — Reload the current page when source changes cause a rebuild. Build errors (and TypeScript errors if using --check) will be piped to the browser.
  • --no-rewrite — Disable rewriting of all requests to "index.html" (SPA mode). When disabled, any request that doesn't match a physical file on disk (either served from the build output directory or servedir) will return a 404.
  • --servedir, -d <path> — Directory of additional static assets to serve from the server root

HTML entry points

With esbd, you can use an HTML file as the entry point for a web project.

  • Scripts and styles that use relative paths will be passed to esbuild as entry points. The HTML file will be written to the output directory and the script/style paths will be replaced with their corresponding output paths.
  • Any non-source assets referenced via relative paths, either from inline styles or from link elements, will be copied to the output folder and their paths will be rebased in the HTML output.
  • Scripts, styles, or assets that use absolute paths or URLs will be left as-is.

Take the following HTML entry point as an example:

index.html:

<!-- index.html entry point -->
<!DOCTYPE html>
<html>
  <head>
    <link rel="apple-touch-icon" href="./assets/favicon.png" />
    <link rel="stylesheet" href="./styles/entry.css" />
    <script defer type="module" src="./src/entry.tsx"></script>
    <script async src="https://google.com/gtag.js"></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Assuming an output directory of /out, this will create 4 files:

  • out/index.html
  • out/entry.0.css
  • out/entry.1.js
  • out/favicon.png

The output for index.html will look something like this:

<!-- index.html output -->
<!DOCTYPE html>
<html>
  <head>
    <link rel="apple-touch-icon" href="favicon.png" />
    <link rel="stylesheet" href="entry.0.css" />
    <script defer="" type="module" src="entry.1.js"></script>
    <script async="" src="https://google.com/gtag.js"></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

If src/entry.tsx included any CSS assets, either directly or indirectly via its dependency graph, the corresponding CSS output generated by esbuild would be included via a <link> tag below the other entry points. It is not currently possible to control where the extra link elements are inserted.

There are two configuration options that affect HTML entry point behavior: ignoreAssets and integrity. You can read about them in the API section.

Entry names (output filenames) for entry points may be customized using a data-entry-name attribute. Entry name attributes can optionally reference values that appear in the define config.

<!-- index.html entry point -->
<!DOCTYPE html>
<html>
  <head>
    <!-- Will be written as "styles.css" -->
    <link rel="stylesheet" href="./styles/entry.css" data-entry-name="styles" />

    <!-- Will be written as "script-1.2.3.js" if {{version}} is defined as "1.2.3" -->
    <script
      data-entry-name="script-{{version}}"
      defer
      type="module"
      src="./src/entry.tsx"
    ></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Copying static assets

Esbd can copy static assets to the output directory that would not otherwise be discovered through esbuild's dependency resolution. This works for files referenced by HTML entry points and also via the copy configuration option.

The copy configuration option is specified as an array of entries. Each entry is a tuple representing the source file path and, optionally, the destination file path.

Source paths may be absolute or relative to absWorkingDir. Destination paths may be absolute or relative to outdir. If no destination path is provided, the source file will be copied to outdir with the same name.

For example:

#!/usr/bin/env node
const { configure } = require('esbd');

configure({
  absWorkingDir: __dirname,
  entryPoints: ['./src/app.ts', './src/styles.css'],
  outdir: './build',
  copy: [
    ['./assets/cat.png'], // -> ./build/cat.png
    ['./assets/dog.png', 'assets/horse.png'], // -> ./build/assets/horse.png
    [
      '/Users/me/path/to/repo/src/assets/pig.png',
      '/Users/me/path/to/repo/build/assets/pig.png',
    ], // -> ./build/assets/pig.png
  ],
});

Note that glob patterns are not supported, so if you need to copy an unknown number of assets that match a pattern, you will need to use a glob library like glob or globby.

How does this relate to esbuild's copy loader?

The copy loader works on imported files based on their extension. You might use this if you wanted all imports of a specific filetype to be copied into the output directory without changing their import semantics.

It's also possible to use the copy loader on filetypes that aren't parseable by esbuild, such as HTML files, and to specify those files as entry points to the build. For example, you might specify { '.html': 'copy' } and specify index.html as an entry point, which will copy index.html to the output directory.

esbd's copy option is similar to the latter use case above except it works for individual files, not file extensions, and it doesn't require those files to be specified as entry points. There are many scenarios where both the copy option and the copy loader would make sense to use in the same build config; they are entirely complementary.

API

function configure(config: EsbdConfigResult | ConfigFn): void

See esbuild's build API for descriptions of all supported options (use the JS API toggle). There are also some esbd-specific options described below.

Note that these esbuild options are ignored:

  • metafile
  • watch
  • write

esbd-specific options:

Name Type Default Description
cleanOutdir boolean false Keep the output directory clean.

On the initial build, the output directory will be cleaned before the build starts.

When in watch mode (either directly or via node-dev/serve), output files will be tracked across builds and any files that are no longer produced by esbuild will be removed from the output directory. This ensures that no stale files are left behind when content hash changes cause files to be renamed or removed. Note that this is usually only relevant when ESM/splitting is enabled.
copy [from: string, to?: string][] - Files to copy to the output directory during the build.

Each entry is a tuple representing the source file path to copy and, optionally, the destination file path.

Source paths may be absolute or relative to absWorkingDir. Destination paths may be absolute or relative to outdir. If no destination path is provided, the source file will be copied to outdir with the same name.

If esbd is started in a watch mode (serve, node-dev, or build --watch), source files will be watched and copied whenever they change.

Note that copy does not support glob patterns.
ignoreAssets boolean - By default, assets (images, manifests, scripts, etc.) referenced by <link>, <style> and <script> tags in the HTML template will be collected as esbuild assets if their src attributes are specified as relative paths. The asset paths will be resolved relative to the template file and will be copied to the output directory, taking publicPath into consideration if it has been set.

Absolute paths or URIs will be ignored.

To ignore all src attributes and avoid collecting discovered assets, set this option to true.
integrity "sha256" | "sha384" | "sha512" - If specified, a cryptographic digest for each file referenced by a <link> or <script> tag will be calculated using the specified algorithm and added as an integrity attribute on the associated tag.
name string - Name of this configuration.

This is required for configurations that appear in an array.
cssChunkFilter (absFilePath: string) => undefined | null | boolean - Predicate function that determines whether a CSS output file should be added to the written HTML entry point.

Esbd makes a best effort to determine which output files should be referenced as <script> and <link rel="stylesheet"> tags in the HTML entry point. By default, CSS output files will be cross-referenced with CSS and JS entry points defined in HTML. Both CSS files that are referenced directly in HTML and those that are referenced indirectly from JS entry points will be included.

In some cases, more CSS files will need to be included than the default algorithm can detect, so this function can be used to be more specific since it will receive every CSS output file produced by esbuild.

This function receives an output file path and should return a value indicating whether that file should be referenced in the HTML output. If the function returns null or undefined, the default algorithm will be used.

Dependents (0)

Package Sidebar

Install

npm i esbd

Weekly Downloads

208

Version

3.3.3

License

MIT

Unpacked Size

1.82 MB

Total Files

85

Last publish

Collaborators

  • jgoz