This page is available in English only.

Obfuscate a Vite build

@afterpack/vite obfuscates your build in generateBundle, before Vite writes it. It is the reference integration every other Vite-family package wraps.

@afterpack/vite is the reference integration. Astro, Svelte, SvelteKit, Vue, and Nuxt all wrap it, so what this page describes applies to those packages too.

It reads @afterpack directives from your source, then runs the engine in generateBundle, with the finished bundle in memory and before Vite writes any of it.

Install

$ npm install -D @afterpack/vite
// vite.config.ts
import { defineConfig } from "vite";
import { afterpackVite } from "@afterpack/vite";

export default defineConfig({
  plugins: [afterpackVite()],
});

Build

That is the whole setup. npm run build now emits obfuscated output; vite dev is untouched, because the plugin only acts on a build's generateBundle. Vite writes the obfuscated chunks and never the originals, and a failed obfuscation leaves outDir as it was.

What it obfuscates is what Vite built: every chunk in the output bundle. Files Vite copies verbatim from publicDir into the output directory are not built, are not in the bundle, and are not touched. Put anything that needs protecting through the bundler.

The plugin writes:

  • protectionMap.html: one combined map for the build, always written into the gitignored .afterpack/ at your project root. It embeds your original source. Never let it land in a served directory.
  • foo.js.map: the source map, when maps are enabled. When maps are off, which is the production default, Vite's own map entry is dropped rather than written beside the obfuscated chunk.

There is no foo.backup.<hash>.js here. The obfuscated bytes go back into the bundle, so there is no emitted file for a backup to sit beside, and build.backup: true warns instead of writing one.

The plugin also appends .afterpack/, protectionMap.html, *.protectionMap.html, *.backup.* and *.map to your .gitignore, and warns if an artifact lands under a served path.

Verify

vite build sets NODE_ENV=production, so the Protection Map is off by default in a normal production build. Ask for it explicitly when you want to inspect one:

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

With build.sourcemap unset, that also switches Vite's own source maps on, so the map can render your original source. Without a discoverable map, the plugin has nothing to render original source from and says so.

The report is written to .afterpack/protectionMap.html. It contains your full source. Never commit it or serve it, and review it locally only.

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.
Never ship source maps to production

A .map file reaches your original source. Never deploy one.

Options

Every configuration key can be passed here as the options object, for example afterpackVite({ preset: "hard", complexity: 40 }). All of them are optional. The named export is afterpackVite.

preset and complexity set different things. A preset is a bundle: it sets the complexity target, the total output multiplier, and the inflation budget together. complexity is one number inside that bundle:

afterpackVite({ preset: "hard", complexity: 40 }); // hard's size limits, target 40

Directives

Directives are read from your source by the plugin, before any minifier strips comments. The grammar is block comments only:

/* @afterpack preset=hard */
function verifyLicense(token) {
  // ...
}
/* @afterpack end */

// @afterpack … line comments are not scanned and are reported as diagnostics.

Every key a directive can set is on the Directives page.

Capture skips node_modules and non-JS/TS files. A directive inside a .vue or .svelte single-file component is not captured. Put it in a plain .ts/.js module the component imports instead. Set directives.enabled: false to skip the scan entirely.

Edge cases

Library mode

build.lib output is a public contract. AfterPack runs on the finished bundle, so exported names survive; internals are still renamed and restructured.

Multiple outputs

Each Vite build obfuscates its own bundle and nothing else, so separate builds are separate passes even when they write to one directory. Pin the same seed across them if they share a runtime contract.

Next