Obfuscate an esbuild build

@afterpack/esbuild hooks build.onEnd and obfuscates the emitted output in place.

@afterpack/esbuild is a standard esbuild plugin ({ name, setup(build) }). It hooks build.onEnd, which fires once esbuild has finished writing output to disk (write !== false, the default).

It handles both output shapes: outdir (many files) and outfile (a single file). It skips a build that already errored.

Install

$ npm install -D @afterpack/esbuild
// build.mjs
import { build } from "esbuild";
import { afterpackEsbuild } from "@afterpack/esbuild";

await build({
  entryPoints: ["src/main.js"],
  bundle: true,
  format: "esm",
  outfile: "dist/app.js",
  plugins: [afterpackEsbuild()],
});

The named export is afterpackEsbuild.

Build

The plugin runs on build.onEnd, after esbuild has finished writing output to disk, for outdir and outfile alike. There is no separate dev command for it to skip: every build() call that writes files runs the plugin.

esbuild exposes no in-pipeline hook, so your unobfuscated bundle is on disk until the pass completes — tens of milliseconds on a real build — and anything that can read the output directory in that window can read it. A failed obfuscation leaves that output in place: the build fails, so nothing ships, but the files stay until the next build overwrites them.

Verify

The Protection Map default depends on whether the bundler emits a source map, and flips off in production. Ask for it explicitly:

afterpackEsbuild({ protectionMap: { enabled: true } });

It lands in the gitignored .afterpack/protectionMap.html. Without sourcemap: true the plugin warns that it found no bundler map to render original source from. The report contains your full source. Never commit it or serve it.

Options

Every configuration key can be passed here as the options object, for example afterpackEsbuild({ preset: "hard", complexity: 40 }).

Directives

Directives are recovered from the bundler's source map, so build with sourcemap: true if you use them. See Directives.

Edge cases

  • Fail-closed. A failed file or an empty output rejects the build.
  • write: false. Nothing is on disk, so the plugin does nothing. AfterPack needs written files.
  • TypeScript. esbuild has already stripped types by the time AfterPack runs; maps chain back to your .ts lines.

Next