esburu

Runs esbuild tasks from a configuration file.

Usage no npm install needed!

<script type="module">
  import esburu from 'https://cdn.skypack.dev/esburu';
</script>

README

ESBuild Task Runner

Latest version Dependency status codecov

Runs esbuild tasks from a configuration file. Reduces the command-line and configuration duplication in projects with multile build targets. Supports parallel execution.

Synopsis

Let us assume the following project structure with two components and their tests:

src/
├── first
│   ├── first.md
│   ├── first.ts
│   └── first.test.ts
└── second
    ├── second.md
    ├── second.ts
    └── second.test.ts

With the output files created in the following locations:

dist/
├── first.js
└── second.js

src/
├── first
│   └── first.test.js
└── second
    └── second.test.js

The build script usually contains commands for building each component and each test:

esbuild --bundle --outfile=dist/first.js src/first/first.ts
esbuild --bundle --outfile=dist/second.js src/second/second.ts
esbuild --bundle --outfile=src/first/first.test.js src/first/first.test.ts
esbuild --bundle --outfile=src/second/second.test.js src/second/second.test.ts

The sequence of commands with options for building components and their tests, which is difficult to maintain in scripts in package.json, can be simplified by creating esburu.config.js, which will be processed just by executing esburu:

modules.exports = [
  {
    entryPoints: ['src/[name]/[name].ts'],
    outfile: 'dist/[name].js',
    bundle: true
  },
  {
    entryPoints: ['src/[name]/[name].test.ts'],
    outfile: 'src/[name]/[name].test.js',
    bundle: true
  }
]

Installation

You can install this package using your favourite Node.js package manager, including the peer dependency on esbuild:

npm i -D esburu esbuild
yarn add -D esburu esbuild
pnpm i -D esburu esbuild

Usage

Run esbuild tasks from a configuration file:

esburu [option ...] [-- arg ...]

Options

-c|--config <file>          load the configuration from an alternative file
-D|--depth <number>         maximum directory depth to look for the config
-e|--esbuild <name>         alternative name of the esbuild module
-P|--parallelism <count>    maximum count of tasks running in parallel
-p|--[no-]parallel-default  execute task sequences in parallel by default
-w|--[no-]watch             stay running and watch for source file changes
-v|--[no-]verbose           increase verbosity of the console output
-d|--[no-]debug             enable debugging output on stderr
-V|--version                print the version number and exit
-h|--help                   print usage instructions and exit

Command-line arguments past -- will be ignored.

Examples

esburu -pw
esburu -c esburu.test.js

Configuration

If no config file is provided, esburu.config.js will be looked for by default. If not found in the current directory, the search will continue to its ancestors with the default maximum depth of 10 directories.

Before the tasks get executed the current directory of the build process will be switched to the location of the config file, so that the file paths in the config file can be relative to it, regardless in what directory was esburu started.

All simple and advanced options of esbuild can be used in a task object.

modules.exports = [
  {
    entryPoints: ['src/index.ts'],
    outfile: 'dist/index.js',
    bundle: true
  }
]

Only entryPoints property with a single source file is supported. The path expansion will not take place if there are more entry points in a task. The name placeholders from the entryPoints property are replaced with * and looked for in the local file system. One build task will be added for each matched file. Placeholders in the outfile property or the outdir property will be replaced with the values from the entryPoints property.

modules.exports = [
  {
    entryPoints: ['src/[name].ts'],
    outfile: 'dist/[name].js',
    bundle: true
  }
]

Parallelism

If you pass the build tasks in an object { parallel, tasks } instead of an array, they will be executed in parallel instead of sequentially:

modules.exports = {
  parallel: true,
  tasks: [
    {
      entryPoints: ['src/index.ts'],
      outfile: 'dist/index.js',
      bundle: true
    },
    {
      entryPoints: ['src/index.ts'],
      outfile: 'dist/index.min.js',
      bundle: true, minify: true
    }
  ]
}

You can nest sequential and parallel lists of build tasks too. An array of tasks or an object { parallel, tasks } can be used instead of a build task object on any level:

modules.exports = [
  {
    parallel: true,
    tasks: [
      {
        entryPoints: ['src/index.ts'],
        outfile: 'dist/index.js',
        bundle: true
      },
      {
        entryPoints: ['src/index.ts'],
        outfile: 'dist/index.min.js',
        bundle: true
      }
    ]
  },
  {
    parallel: true,
    tasks: [{
      entryPoints: ['test/index.ts'],
      outfile: 'test/index.js',
      bundle: true
    }]
  }
]

You can switch the execution of task sequences to be parallel by default using the option -p (--parallel-default):

// Run with `esburu -p`
modules.exports = [
  {
    entryPoints: ['src/index.ts'],
    outfile: 'dist/index.js',
    bundle: true
  },
  {
    entryPoints: ['src/index.ts'],
    outfile: 'dist/index.min.js',
    bundle: true, minify: true
  }
]

Build tasks added by expanding the file path placeholders will be also executed in parallel if you wrap them as single object in an array, as a shorter-syntax alternative to { parallel, tasks }:

modules.exports = [
  {
    entryPoints: ['src/index.ts'],
    outfile: 'dist/index.js',
    bundle: true
  },
  [{ // all src/comp/*.ts will be built in parallel
    entryPoints: ['src/comp/[name].ts'],
    outfile: 'dist/[name].js',
    bundle: true, minify: true
  }]
]

The automatic parallelisation can be suppressed by setting the parallelDefault option to false explicitly (-no-p, --no-parallel-default). If the default parallelisation needs to be applied globally, only not affecting selected tasks, you can enforce the sequential execution for a specific task list:

modules.exports = {
  parallel: false,
  tasks: [
    {
      entryPoints: ['src/index.ts'],
      outfile: 'dist/index.js',
      bundle: true
    },
    {
      entryPoints: ['test/index.ts'],
      outfile: 'test/index.js',
      bundle: true
    }
  ]
}

The default task execution parallelism is set to the count of CPUs, as reported by Node.js.

Arguments

The configuration file can access command-line arguments passed to the esburu process. The arguments after "--" are ignored by the build execution, and they can be used in the build configuration module for conditional behaviour.

For example, a standard parameter "watch" and a custom one "test" can be added to the command line:

esburu --watch -- test

And its presence can be checked in the build configuration module:

const { getOpts } = require('esburu/lib/util')

const { watch, args } = getOpts()
const test = args.includes('test')

modules.exports = { ... }

API

The build can be started programmatically too:

const { performTasks } = require('esburu')

const tasks = [
  {
    entryPoints: ['src/[name]/[name].ts'],
    outfile: 'dist/[name].js',
    bundle: true
  }
]

performTasks(tasks, { parallelDefault: true })
  .catch(({ config, errors, message, stack }) => {
    if (config) // loading the configuration failed in JS parsing
      console.error(stack)
    else if (!errors) // esbuild itself prints errors on the console
      console.error(message)
    process.exitCode = 1
  })

performTasks(config?: string | object, opts?: object): Promise

Preforms a complete build using either a configuration file path or an object with the build configuration from the config argument. The opts argument supports all parameters recognised by the command-line tool.

Do not run more builds in a single process. Once the esbuild module is loaded, it stays set to the same working directory. You would not be able to use paths relative to the directory of the configuration file in those files, if those files are located in different directories.

Contributing

In lieu of a formal styleguide, take care to maintain the existing coding style. Lint and test your code using npm test.

License

Copyright (c) 2021 Ferdinand Prantl

Licensed under the MIT license.