Modern Angular (v17+) builds with @angular-devkit/build-angular:application. That builder is esbuild-based but exposes no plugin hook, so, unlike the Vite, webpack and esbuild plugins, AfterPack cannot hook into the build here.
The integration is a postbuild pass over the emitted browser bundle. @afterpack/angular is a convenience wrapper over that pass. It is not a builder plugin.
Install
$ npm install -D @afterpack/angular// scripts/obfuscate.mjs — run after `ng build`
import { afterpackAngular } from "@afterpack/angular";
await afterpackAngular({ seed: "git" });Or run the CLI directly instead, with nothing to install.
Build
The application builder writes the client bundle to dist/<app>/browser.
With the helper, findAngularBrowserDir(distRoot) finds the browser output. Pass browserDir in a monorepo. findAngularBrowserDir is also exported separately if you want just the locator.
With the CLI, point it at the browser output directly:
// package.json
{
"scripts": {
"build": "ng build && afterpack dist/my-app/browser --seed=git"
}
}Point the CLI at it and every emitted .js chunk is obfuscated in place. The CLI's backup lands in .afterpack/backup/ at the project root, outside dist/, so it does not ship with the deploy; npx afterpack@latest restore undoes the run. The @afterpack/angular helper is the case to watch: build: { backup: true } there writes a .backup.<hash> sibling into dist/, which is your original source inside the tree you deploy.
The builder writes the bundle before the pass can run, so your unobfuscated bundle is on disk until the pass completes, and anything that can read dist/ in that window can read it. A failed pass leaves that output in place: the build fails, so nothing ships, but the files stay until the next build overwrites them.
Fail-closed: afterpackAngular throws if the browser directory holds no .js/.mjs/.cjs, or if the engine reports a failing diagnostic.
Verify
Enable sourceMap: { enabled: true } for the build you want to inspect, so the Protection Map can render your original source. It writes to the gitignored .afterpack/protectionMap.html. It contains your full source. Never commit it or serve it.
Both the CLI and the helper flip the Protection Map off under NODE_ENV=production / CI=true; the helper accepts protectionMap: { enabled: true } to force it back on, with a warning.
A production source map reaches your original source. Generate it for the build, then withhold it from your deployed dist/.
Options
Every configuration key can be passed here as the options object, for example afterpackAngular({ preset: "hard", complexity: 40 }). These options exist only on this package, for locating the build output:
| Option | Type | Default |
|---|---|---|
cwd | string | process.cwd(); the project root that owns .gitignore and .afterpack/ |
distRoot | string | <cwd>/dist; where to look for the browser subdirectory |
browserDir | string | unset. Skips auto-location entirely |
There is no build.autorun option here.
Directives
directives is refused here, in every layer — the options object, AFTERPACK_directives and
afterpack.json alike. Angular's application builder is sealed, so this pass only ever sees
already-minified output and has no hook to capture /* @afterpack ... */ comments from. Setting it
fails the build rather than being silently ignored. Hand-authored
regions still work, because they name byte ranges instead of comments.
Edge cases
Legacy Angular (webpack). Projects still on the older webpack-based builder can obfuscate in-build with @afterpack/webpack via a custom builder. The postbuild path above works uniformly across both builders and is the recommended one.
Next
- Frameworks: the full matrix.
- CLI reference: the flags used above.
- Builds & CI: seeds, dev-vs-prod, promoting the same bytes.
- Protection Map: reading the report.
- Configuration: the engine config surface.
- Diagnostics: what a failing pass returns.