This page is available in English only.

Obfuscate a Parcel build

@afterpack/parcel-optimizer obfuscates each packaged bundle in memory as a Parcel 2 Optimizer, and fails the build closed rather than ship a chunk that 404s in production.

@afterpack/parcel-optimizer is a Parcel 2 Optimizer, registered in .parcelrc. It obfuscates each packaged bundle in memory, during Parcel's own optimize phase, so the bundle never touches disk in cleartext.

Not @afterpack/parcel

The package is @afterpack/parcel-optimizer.

Install

$ npm install -D @afterpack/parcel-optimizer
// .parcelrc
{
  "extends": "@parcel/config-default",
  "optimizers": {
    "*.{js,mjs,cjs}": ["...", "@afterpack/parcel-optimizer"]
  }
}

The "..." spread matters: it keeps Parcel's default optimizers (@parcel/optimizer-swc) and puts AfterPack last. AfterPack runs after minification. Peer dependency: Parcel >=2.9.0.

Build

.parcelrc entries are bare package names with no inline options, so options come from afterpack.json in your project root, the one config file every AfterPack integration reads:

{
  "seed": "git",
  "preset": "medium"
}

Only project-root artifacts land; nothing goes beside your bundles:

  • One protectionMap.html per bundle, at .afterpack/<bundle>.<id>.protectionMap.html.
  • A source map, when the sourceMap policy enables one. It is returned to Parcel rather than written directly, so Parcel names and emits it with its own content hash.

The plugin also appends its guard globs (.afterpack/, *.protectionMap.html, *.backup.*, *.map) to your project .gitignore.

Verify

Because there's no combined report, check the bundle you care about directly: .afterpack/<bundle-name>.<id>.protectionMap.html for each entry or lazy chunk Parcel packaged. Maps are off in production by default; set "sourceMap": { "enabled": true } in afterpack.json to override. Each report contains your full source. Never commit it or serve it.

Options

Every configuration key can be set there, for example { "preset": "hard", "complexity": 40 }. protectionMap.enabled depends on whether the bundle carries a source map rather than on build mode, and always lands in the gitignored .afterpack/. There is no build.backup option on this plugin. Setting it warns. Explicit options always win over the production defaults.

Directives

Directives are read from your source by the plugin. With no usable source map, the pass has nothing to recover and says so.

A directive in your entry module is not recovered. The pass reports DIAG_DIRECTIVE_SOURCE_UNRESOLVED and names the module. Directives in every other, non-entry module work normally once source maps are on. Move the guarded code into an imported module if you need an entry-module directive.

Edge cases

Content-hash placeholders can fail your build

With content hashing on (the default for parcel build) and a target without native ESM, Parcel's content-hash placeholders end up inside obfuscated strings, and every code-split chunk 404s in production.

This plugin detects the collision and fails the build instead of shipping it:

AfterPack obfuscated 1 Parcel content-hash placeholder(s) in app.[hash].js (HASH_REF_...).
  hint: Run `parcel build --no-content-hash` (chunk URLs stop being content-addressed).
  hint: Or set "complexity": 0 in afterpack.json to ship minify-only.

Parcel's default ESM output is unaffected.

Worth knowing

  • One pass per bundle, in worker processes. Parcel runs optimizers per bundle, and bundles packaged in different worker processes cannot share state. Bundles packaged in the same process share one seed. To pin one seed across every worker, set seed explicitly or export AFTERPACK_seed.
  • Parcel caches optimizer output. A rebuild with a warm .parcel-cache and unchanged input does not re-run AfterPack, and does not rotate an unpinned seed. Clear .parcel-cache for a fresh pass; CI builds from a cold cache anyway.
  • Two harmless warnings. Parcel prints ES module dependencies are experimental (every AfterPack package is ESM) and reports that the plugin contains non-statically analyzable dependencies (the native engine's runtime binary resolution). Neither affects the build.

Without the plugin: the CLI

Build with Parcel as usual, then point AfterPack at the output:

$ npx parcel build
$ npx afterpack@latest dist/ --preset=medium --seed=git

The path is a required argument; there's no auto-detection. AfterPack walks the directory, collecting every .js/.mjs/.cjs and obfuscating them in place. A CLI run gets no content-hash guard, and one combined protectionMap.html for the whole directory instead of one per bundle. Fail-closed still holds: a missing build dir, no JS files, an error/critical diagnostic, or a file the engine couldn't obfuscate all exit 1.

Next