esbuild Plugin

Obfuscate an esbuild bundle

esbuild minifies in milliseconds, but minified code keeps every string and the shape of your logic. One plugin protects what esbuild writes, in the same build() call, whether the output goes to disk or stays in memory. The free options run on your own machine with no account.

Tested 2026-10-29

Real build, same result

esbuild 0.28 and javascriptobfuscator-com 0.5.0, no account, with write: true and write: false. The protected bundle printed exactly what the unprotected one printed.

HookonEnd, after esbuild finishes
ModesOn disk or result.outputFiles
Source mapsRemoved for protected files
Setup

Add the plugin to your build script

esbuild runs plugins only through its JavaScript API, so the obfuscator goes in a build script rather than on the esbuild command line. The plugin ships inside javascriptobfuscator-com as javascriptobfuscator-com/esbuild.

Terminal
npm install --save-dev esbuild javascriptobfuscator-com
build.mjs — run with node build.mjs
import * as esbuild from "esbuild";
import jsoProtector from "javascriptobfuscator-com/esbuild";

await esbuild.build({
  entryPoints: ["src/main.js"],
  bundle: true,
  minify: true,
  sourcemap: true,
  format: "esm",
  outdir: "dist",
  plugins: [jsoProtector()]
});

What happens when the build ends

  1. The plugin registers an onEnd callback, so it runs once esbuild has produced the final, minified files.
  2. On disk (write: true, the default) it reads every .js file under outdir (or next to outfile), protects them in one batch and writes them back.
  3. In memory (write: false) it replaces the contents of each JavaScript entry in result.outputFiles instead — handy when a dev server or test harness consumes the result.
  4. It removes the source map of each protected file, from disk or from the array.
  5. It picks the engine. No API key and only free options: protected locally, nothing uploaded. With a key, paid options run on the hosted engine.
Real Output

Minified versus minified and protected

The sample is a pricing table and a licence-key checksum in three modules. First the bundle from minify: true alone, then the same build with the plugin (the seven-line comment that opens the free output is left out here).

esbuild --minify only — 536 bytes
var n=[{name:"starter",seats:5,monthly:29},{name:"team",seats:25,monthly:49},{name:"business",seats:100,monthly:99}];function c(e,t){let o=n.find(i=>e<=i.seats)||n[n.length-1],a=o.monthly*(t?10:12);return{tier:o.name,total:a,perSeat:Math.round(a/e*100)/100}}var m="acme-license-v2";...
With jsoProtector() — 1,274 bytes
var n=[{name:"\x73\x74\x61\x72\x74\x65\x72",seats:5,monthly:29},{name:"\x74\x65\x61\x6d",seats:25,monthly:49},{name:"\x62\x75\x73\x69\x6e\x65\x73\x73",seats:100,monthly:99}];function c(a,d){let b=n.find(b=>a<=b.seats)||n[n.length-1],c=b.monthly*(d?10:12);return{tier:b.name,total:c,perSeat:Math.round(c/a*100)/100};}var m="\x61\x63\x6d\x65\x2d\x6c\x69\x63\x65\x6e\x73\x65\x2d\x76\x32";...

The licence salt acme-license-v2, readable at a glance in the minified bundle, is no longer searchable in the protected one. Of the 738 bytes added, 546 are the free-version comment. Both files printed the same result when run. Hex-encoded strings are a first layer, not encryption: the paid presets add string encryption, flattening and runtime defences.

Watch Out For

Things specific to esbuild

  • Everything in outdir is in scope. With write: true the plugin walks the whole output folder, so a script you copied there yourself gets protected too. Keep outdir for esbuild’s output, or list such files in exclude.
  • .mjs or .cjs output (via outExtension) is only protected if you add it: extensions: [".js", ".mjs"].
  • Watch mode protects on every rebuild. onEnd fires each time, so add the plugin only to the production build script.
  • Template-literal strings (backticks) are left as written by the free engine; quoted strings are encoded.
Options

The ones you will reach for

  • preset — free by default with no API key; standard, balanced and maximum need a plan.
  • exclude / include — glob patterns relative to outdir, such as ["**/vendor/**"].
  • reservedNames — names the renamer must keep.
  • removeSourceMaps: false — keep maps you store privately.
  • manifest — a JSON record of what was protected, for release evidence.
  • Credentials for paid presets come from JSO_API_KEY and JSO_API_PASSWORD, never the build script.
Frequently Asked

esbuild obfuscation questions

Can I use the obfuscator plugin from the esbuild command line?

No. esbuild runs plugins only through its JavaScript API (esbuild.build or esbuild.context). If your build is a one-line esbuild command, run npx javascriptobfuscator-com --input dist --output dist after it instead — see the CLI page.

Does it work with write: false?

Yes. When esbuild keeps the output in memory, the plugin replaces the contents of each JavaScript entry in result.outputFiles and removes the matching source maps from the array.

Do I need an account to obfuscate an esbuild bundle?

No. With no API key the plugin protects on your own machine with the free engine: local variables renamed and string literals encoded, nothing uploaded, no rate limit. The output starts with a short comment naming the product. Paid options run on the hosted engine and need a plan.

Does esbuild’s own minify make obfuscation unnecessary?

No. Minification shortens names and removes whitespace, but strings, the structure of your code and every property name stay as written — minification and obfuscation compared. Keep minify: true and add the obfuscator on top.

Other Build Tools

Not using esbuild?

The same package protects Rollup, Rspack, Parcel and Gulp builds, or any output folder from the command line.