Obfuscate a Rollup build

@afterpack/rollup reads directives from your source and runs the engine in generateBundle, before Rollup writes the output.

@afterpack/rollup mirrors @afterpack/vite: Rollup is what Vite builds on, so the same approach applies. It reads directives from your source, then runs the engine in generateBundle, with the finished bundle in memory and before Rollup writes any of it.

Install

$ npm install -D @afterpack/rollup
// rollup.config.mjs
import { afterpackRollup } from "@afterpack/rollup";

export default {
  input: "src/index.js",
  output: { dir: "dist", format: "es", sourcemap: true },
  plugins: [afterpackRollup()],
};

The named export is afterpackRollup.

Build

The plugin runs in generateBundle, with order: "post", so it is the last hook to touch the bundle. It obfuscates every JavaScript chunk the bundle holds and hands them back, so Rollup writes the obfuscated chunks and never the originals. A failed obfuscation leaves the output directory as it was.

Verify

Ask for the Protection Map explicitly on a production build:

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

It lands in the gitignored .afterpack/protectionMap.html. With output.sourcemap on it renders your original source; without one, there is nothing to render from and the pass says so. The report contains your full source. Never commit it or serve it.

entitlements.jsmedium · complexity 8Open full size
entitlements.js, 54 lines, built at the medium preset with a directive around the signature check. Click any token to see what was applied to it.

Options

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

Directives

Directives are read from your source by the plugin, before minification, so a directive survives bundling.

/* @afterpack preset=hard */
export function signRequest(payload) {
  // ...
}
/* @afterpack end */

Capture skips node_modules, virtual modules, and non-JS/TS files. The grammar is the same everywhere.

Edge cases

Multiple outputs

A config with two outputs runs generateBundle once per output, and each run only sees its own bundle. Nothing is obfuscated twice, whatever directories the outputs write to:

output: [
  { dir: "dist/esm", format: "es", entryFileNames: "lib.js" },
  { dir: "dist/cjs", format: "cjs", entryFileNames: "lib.cjs" },
],
plugins: [afterpackRollup({ seed: "git" })],

Pin one seed across outputs that ship together.

Library builds

AfterPack runs on the bundle Rollup built, so every exported name your consumers import survives as written; only internals are renamed and restructured.

Next