# 混淆 Parcel 构建

@afterpack/parcel-optimizer 作为 Parcel 2 Optimizer,在内存中混淆每个打包产物,并在可能导致分块在生产环境 404 时使构建失败,而不是继续发布。

Source: https://www.afterpack.dev/zh/docs/frameworks/parcel

`@afterpack/parcel-optimizer` 是一个 Parcel 2 **Optimizer**,注册在 `.parcelrc` 中。它在 Parcel 自身的优化阶段**在内存中**混淆每个打包产物,因此该产物不会以明文形式落盘。

> **不是 @afterpack/parcel**
>
> 该软件包是 `@afterpack/parcel-optimizer`。

## 安装

```bash
npm install -D @afterpack/parcel-optimizer
```

```jsonc
// .parcelrc
{
  "extends": "@parcel/config-default",
  "optimizers": {
    "*.{js,mjs,cjs}": ["...", "@afterpack/parcel-optimizer"]
  }
}
```

`"..."` 展开很重要:它保留了 Parcel 的默认优化器(`@parcel/optimizer-swc`),并把 AfterPack 放在**最后**。AfterPack 在压缩之后运行。对等依赖:Parcel `>=2.9.0`。

## 构建

`.parcelrc` 中的条目是不带内联选项的裸包名,因此选项来自项目根目录下的 `afterpack.json`,这是每个 AfterPack 集成都会读取的唯一配置文件:

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

只有项目根目录会产生构建产物;不会在你的打包产物旁边生成任何文件:

- **每个打包产物一个 [`protectionMap.html`](https://www.afterpack.dev/zh/docs/protection-map)**,位于 `.afterpack/<bundle>.<id>.protectionMap.html`。
- 当 [sourceMap 策略](https://www.afterpack.dev/zh/docs/config#sourceMap-enabled) 启用时生成一个 source map。它会返回给 Parcel 而不是直接写入磁盘,因此由 Parcel 用自己的内容哈希来命名并输出它。

`.afterpack/` 自带一个 `.gitignore`;该插件从不修改你项目的 `.gitignore`。

## 验证

由于没有合并报告,请直接检查你关心的打包产物:对 Parcel 打包的每个入口或延迟加载的代码块,查看 `.afterpack/<bundle-name>.<id>.protectionMap.html`。[生产环境下默认关闭 map](https://www.afterpack.dev/zh/docs/config#sourceMap-enabled);要覆盖此行为,请在 `afterpack.json` 中设置 `"sourceMap": { "enabled": true }`。每份报告都包含你的完整源码,切勿提交或对外提供该文件。

要检查一次部署，请把插件的 reporter 加到构建命令中。之后每次构建都会把一份保护回执写入[项目根目录](https://www.afterpack.dev/zh/docs/cli#files)中的 `.afterpack/receipts/`，每个目标输出目录一份（`dist` 对应 `.afterpack/receipts/dist.json`）。当某个文件在混淆之后被改动，或者没有回执时，[`afterpack verify`](https://www.afterpack.dev/zh/docs/cli#afterpack-verify-dir) 会失败：

```bash
parcel build index.html --reporter @afterpack/parcel-optimizer/reporter
npx afterpack@latest verify dist
```

## 选项

每个[配置项](https://www.afterpack.dev/zh/docs/config)都可以在这里设置,例如 `{ "preset": "hard", "complexity": 40 }`。[`protectionMap.enabled`](https://www.afterpack.dev/zh/docs/config#protectionMap-enabled) 取决于打包产物是否带有 source map,而不是取决于[构建模式](https://www.afterpack.dev/zh/docs/config#build-mode),并且始终写入已被 gitignore 的 `.afterpack/` 中。该插件没有 `build.backup` 选项,设置它会发出警告。显式选项始终优先于生产环境默认值。

## 指令

指令由插件从你的源码中读取。如果没有可用的 source map,该处理阶段就无法恢复任何内容,并会明确说明这一点。

**入口模块中的指令不会被恢复。** 该处理阶段会报告 `DIAG_DIRECTIVE_SOURCE_UNRESOLVED` 并指出具体模块。一旦启用 source map,其他所有非入口模块中的指令都能正常工作。如果你需要在入口模块中使用指令,请把受保护的代码移到一个被导入的模块中。

## 边界情况

### 内容哈希占位符可能导致构建失败

启用内容哈希时(`parcel build` 的默认行为),如果目标环境不支持原生 ESM,Parcel 的内容哈希占位符会落入被混淆的字符串内部,导致每个代码分割出的分块在生产环境中都返回 404。

该插件会检测这种冲突,并使构建失败,而不是继续发布:

```
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 的默认 ESM 输出不受影响。

### 需要了解的情况

- **每个打包产物在工作进程中处理一次。** Parcel 按打包产物运行优化器,不同工作进程中打包的产物无法共享状态。在同一进程中打包的产物共享同一个种子。要在所有工作进程中固定同一个种子,请显式设置 [`seed`](https://www.afterpack.dev/zh/docs/config#seed) 或导出 `AFTERPACK_seed`。
- **Parcel 会缓存优化器输出。** 如果 `.parcel-cache` 是热缓存且输入未变化,重新构建不会重新运行 AfterPack,也不会轮换未固定的种子。要获得全新的处理结果,请清除 `.parcel-cache`;CI 构建反正都是从冷缓存开始的。
- **两条无害的警告。** Parcel 会打印 `ES module dependencies are experimental`(每个 AfterPack 包都是 ESM),并报告该插件 `contains non-statically analyzable dependencies`(原生引擎的运行时二进制文件解析)。二者都不影响构建。

### 不使用插件:改用 CLI

照常使用 Parcel 构建,然后让 AfterPack 处理其输出:

```bash
npx parcel build
```

```bash
npx afterpack@latest dist/ --preset=medium --seed=git
```

不带[路径参数](https://www.afterpack.dev/zh/docs/cli#usage)时,CLI 会检测 `dist/`,即 Parcel 的默认输出目录;如果你的目录不同,请指定它。AfterPack 会遍历该目录,收集所有 `.js`/`.mjs`/`.cjs` 文件并[原地](https://www.afterpack.dev/zh/docs/cli#what-a-run-does)混淆它们。CLI 运行没有内容哈希防护,并且会为整个目录生成一份合并的 [`protectionMap.html`](https://www.afterpack.dev/zh/docs/protection-map),而不是每个打包产物一份。[失败即中止](https://www.afterpack.dev/zh/docs/diagnostics#fail-closed-no-exceptions)依然有效:构建目录缺失、没有 JS 文件、`error`/`critical` [诊断信息](https://www.afterpack.dev/zh/docs/diagnostics),或者某个文件引擎无法混淆,都会以 `1` 退出。

## 下一步

- [框架](https://www.afterpack.dev/zh/docs/frameworks):完整矩阵。
- [esbuild](https://www.afterpack.dev/zh/docs/frameworks/esbuild#directives):另一个在压缩后从 source map 中恢复指令的插件。
- [CLI 参考](https://www.afterpack.dev/zh/docs/cli):兜底路径的完整参数列表。
- [构建与 CI](https://www.afterpack.dev/zh/docs/builds):种子、开发与生产的区别、发布同一份字节。
- [Protection Map](https://www.afterpack.dev/zh/docs/protection-map):如何阅读该报告。
- [配置](https://www.afterpack.dev/zh/docs/config):每个选项,以及哪个入口可以设置它。
- [预设](https://www.afterpack.dev/zh/docs/presets):五级阶梯。
