# 混淆 Electron 应用

@afterpack/electron 用一次调用、一个种子接入 main、preload 和 renderer，并在 electron-vite 的 bytecodePlugin 和缺乏防护的备份把你的源代码发布出去之前拒绝它们。

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

`@afterpack/electron` 用一次调用混淆一个 Electron 应用所发布的全部三个程序（main、preload、renderer），并让它们共享同一个构建种子。如果手动接入，每一部分都是各自独立的打包工具构建，各自取一个随机种子，于是同一个应用的各个部分产出的结构彼此毫无关联。

electron-builder 不是打包工具。它把已经构建好的应用打包成安装程序，从不触碰你的 JavaScript，因此无论是否使用 AfterPack，它上游都需要一个打包工具。

## 安装

```bash
npm install -D @afterpack/electron
```

```ts
// electron.vite.config.ts
import { withAfterpack } from "@afterpack/electron";
import { defineConfig, externalizeDepsPlugin } from "electron-vite";

export default withAfterpack(
  defineConfig({
    main: { plugins: [externalizeDepsPlugin()] },
    preload: { plugins: [externalizeDepsPlugin()] },
    renderer: {},
  }),
);
```

`withAfterpack` 包裹整个 electron-vite 配置（即 `defineConfig` 返回的对象、函数或 Promise 形式），并把 [`@afterpack/vite`](https://www.afterpack.dev/zh/docs/frameworks/vite) 接入该配置声明的每一部分，未声明的部分则不会接入。对等依赖：`vite ^5 \| ^6 \| ^7 \| ^8`。

## 构建

如何接入这一遍处理，取决于你的工具链是用一次调用配置全部三部分，还是每部分一个配置。

### 一次一个部分

分别配置每一部分的工具链（使用 `@electron-forge/plugin-vite` 的 Electron Forge）改为在每个配置中各调用一次 `afterpackElectron`：

```ts
// vite.main.config.ts   (and vite.preload.config.ts / vite.renderer.config.ts)
import { afterpackElectron } from "@afterpack/electron";

export default { plugins: [afterpackElectron({ leg: "main" })] };
```

`leg` 是必填项。

### 一个种子，三种方式

- **electron-vite（`withAfterpack`）**和 **Forge + `plugin-vite`**：三个配置都在同一个 Node 进程中构建，因此各部分会自动共享一个种子。
- **`vite-plugin-electron`**：同样是一个进程。把 `afterpackElectron({ leg: "renderer" })` 放在 `plugins` 的最后，位于 `vite-plugin-electron` 本身之后，这样 AfterPack 的 `generateBundle` 会对最终完成的 bundle 收尾。
- **把每一部分作为单独命令运行的手写脚本**：没有共享的进程，因此用环境变量在它们之间固定种子：

```jsonc
// package.json
"build": "AFTERPACK_seed=git vite build -c vite.main.ts && vite build -c vite.renderer.ts"
```

`AFTERPACK_seed` 接受的取值与 [`seed`](https://www.afterpack.dev/zh/docs/config#seed) 相同：一个整数、`"git"`（由 `HEAD` 派生），或者任何其他字符串（由引擎进行哈希）。显式的 `seed` 选项仍然优先于它。何时该固定种子、何时不该，参见[种子策略](https://www.afterpack.dev/zh/docs/best-practices#seed-strategy)。

## 验证

[Protection Map](https://www.afterpack.dev/zh/docs/protection-map) 会在能找到 source map、且构建没有在 CI 中（`CI=true` 或 `CI=1`）运行时，按部分写入 `.afterpack/<leg>/protectionMap.html`。[生产](https://www.afterpack.dev/zh/docs/config#build-mode)模式不会关闭它，但除非你要求，electron-vite 不会输出 source map，因此 Protection Map 默认关闭。请分别检查 main、preload 和 renderer。每份报告都包含你的完整源码。绝不要提交或对外提供它。

> **绝不要在应用内发布 source map**
>
> `app.asar` 中的一个 `.map` 就能直达你的原始源码。对于任何要打包的内容，都要保持 source map 输出关闭（生产环境的默认值）。

## 选项

每个[配置键](https://www.afterpack.dev/zh/docs/config)在这里同样可用，可以通过 `withAfterpack(config, options)` 应用到每一部分，或者通过 `afterpackElectron(options)` 按部分传入。以下选项只存在于这个包中：

| 选项 | 类型 | 默认值 |
|---|---|---|
| `leg` | `"main" \| "preload" \| "renderer"` | 在 `afterpackElectron` 上必填 |
| `projectRoot` | `string` | `process.cwd()`。electron-vite 把 renderer 构建的根目录设在 `src/renderer/`。如果你的构建从其他位置运行，请覆盖它 |

[`seed`](https://www.afterpack.dev/zh/docs/config#seed) 默认为一个新值，由同一构建会话中的每一部分共享。[`protectionMap.enabled`](https://www.afterpack.dev/zh/docs/config#protectionMap-enabled) 遵循通常的 source map 规则，每一部分都会把自己的报告写入 `.afterpack/<leg>/protectionMap.html`。

```ts
// Heavier protection, reproducible per commit, applied to every leg:
withAfterpack(config, { preset: "hard", seed: "git" });
```

## 指令

每一部分底层接入的都是 [`@afterpack/vite`](https://www.afterpack.dev/zh/docs/frameworks/vite)，因此[指令捕获](https://www.afterpack.dev/zh/docs/frameworks/vite#directives)在这里的工作方式完全相同。

## 边界情况

### 支持矩阵

| 工具链 | 接入方式 | 自动共享一个种子？ |
|---|---|---|
| electron-vite | `withAfterpack(defineConfig({ … }))`，一次调用 | 是 |
| Forge + `plugin-vite` | 在三个配置中各使用 `afterpackElectron({ leg })` | 是 |
| Forge + `plugin-webpack` | 在每个配置中使用 [`@afterpack/webpack`](https://www.afterpack.dev/zh/docs/frameworks/webpack) | 是 |
| `vite-plugin-electron` | `afterpackElectron({ leg: "renderer" })`，放在**最后** | 是，只要放在该插件之后 |
| electron-builder | 它负责打包安装程序，不负责打包代码；请接入它上游的打包工具 | 不适用 |
| 手写脚本（独立进程） | 每部分一个插件，或 `npx afterpack@latest <dir>` | 否；请固定 `AFTERPACK_seed` |

### 它会拒绝什么

通过这个包接入的任何部分都会应用两项失败即中止的检查。

**`bytecodePlugin`。**electron-vite 自带的 `bytecodePlugin` 会把每个输出的 chunk 替换成一个 3 行的加载器桩代码，而把真正的应用以 V8 字节码（`.jsc`）形式发布。两者无法组合使用，因为字节码替换的正是 Rollup 生成的 chunk。在启用了 `bytecodePlugin` 的那一部分上，或者之后只要输出中出现 `.jsc`/`.cjsc` 文件，构建就会失败。二者只能择其一。

**`build.backup: true`。**[备份](https://www.afterpack.dev/zh/docs/config#build-backup)会被直接拒绝：调用会抛出异常而不是继续执行，因为 `.backup.<hash>.js` 就是你完整的原始源代码，它被写在输出旁边，而这个目录树正是打包工具复制进 `app.asar` 的内容。每个安装该应用的用户都会拿到它。

同样的风险也适用于打包工具不知道要排除的任何产物。electron-builder 默认的 `files` glob 以 `dot: true` 匹配，且默认不排除 `.afterpack/`。请把以下内容从打包范围中排除：

```jsonc
// electron-builder
"files": ["**/*", "!.afterpack/**", "!**/*.backup.*", "!**/*.map"]
// electron-forge
packagerConfig: { ignore: [/^\/\.afterpack/, /\.backup\./, /\.map$/] }
```

[source map](https://www.afterpack.dev/zh/docs/config#sourceMap-enabled) 只会收到警告而不会被拒绝：`app.asar` 中一个带有 `sourcesContent` 的 `.map` 就等于完全去混淆。把 `sourceMap: { enabled: true, emitUrl: true }` 限定在本地调试构建中，并把 `*.map` 从打包范围中排除。

### IPC 通道

通道名在两个 bundle 之间必须逐字节一致。`ipcMain.on("save-file", …)` 和 `ipcRenderer.send("save-file", …)` 两端是同一个字符串。它们是普通输出 JavaScript 中的普通字符串字面量，因此让两部分使用相同的[种子](https://www.afterpack.dev/zh/docs/config#seed)和相同的保护强度（[`preset`](https://www.afterpack.dev/zh/docs/config#preset) / [`complexity`](https://www.afterpack.dev/zh/docs/config#complexity)），就能让两半保持一致。

如果某个通道名是在运行时拼出来的，而不是写成字面量，就把这段构造代码放进一个用 [`paths.exclude`](https://www.afterpack.dev/zh/docs/config#paths-exclude) 排除的模块中，或者通过打包工具插件的 [`regions`](https://www.afterpack.dev/zh/docs/config#regions) 条目降低该区域的保护强度（[Pro](https://www.afterpack.dev/zh/docs/pro#what-pro-buys)）。

### 不使用插件：CLI

不想增加依赖，或者某一部分不是用 Vite 构建的？那就在构建之后，对每个输出目录各运行一次 [`npx afterpack@latest`](https://www.afterpack.dev/zh/docs/cli)：

```bash
npx afterpack@latest out/main --seed=git
npx afterpack@latest out/renderer --seed=git
```

不带[路径参数](https://www.afterpack.dev/zh/docs/cli#usage)时，CLI 会检测 `out/`，即 electron-vite 写入的目录；如果你的目录不同，请指定它。每次运行都会遍历目录，收集每一个 `.js`/`.mjs`/`.cjs`，并[就地](https://www.afterpack.dev/zh/docs/cli#what-a-run-does)混淆它们；也可以把单个文件作为参数。在每次运行中固定同一个 [`--seed`](https://www.afterpack.dev/zh/docs/config#seed)：由当前提交派生的 `git` 是最简单的保证方式。CLI 运行不会执行上述任何拒绝检查；它会混淆找到的所有 JavaScript。请在各次运行中保持相同的 [`--seed`](https://www.afterpack.dev/zh/docs/config#seed) 和 [`--preset`](https://www.afterpack.dev/zh/docs/config#preset)。

## 下一步

- [框架](https://www.afterpack.dev/zh/docs/frameworks)：完整矩阵。
- [Vite](https://www.afterpack.dev/zh/docs/frameworks/vite)：这个包所转发的选项语义。
- [CLI 参考](https://www.afterpack.dev/zh/docs/cli#flags)：备用路径的标志列表。
- [构建与 CI](https://www.afterpack.dev/zh/docs/builds)：种子、开发与生产环境的区别、原样提升同一份字节。
- [保护 Electron 应用的许可证逻辑](https://www.afterpack.dev/zh/docs/use-cases/electron-license-logic)：把这些应用到本地的许可证或试用期检查上。
- [最佳实践 → 种子策略](https://www.afterpack.dev/zh/docs/best-practices#seed-strategy)：何时固定，何时不固定。
- [Protection Map](https://www.afterpack.dev/zh/docs/protection-map)：如何阅读报告。
- [配置](https://www.afterpack.dev/zh/docs/config)：每一个选项，以及哪个配置面可以设置它。
