混淆一次 webpack 构建

@afterpack/webpack 挂钩 compilation.hooks.processAssets,在 webpack 写入资产之前对其进行混淆。

@afterpack/webpack 是一个 webpack 5 插件:一个带有 apply(compiler) 方法的类。它会在压缩之前从你的源代码中读取 @afterpack 指令,然后在 webpack 写入之前的最后一个阶段,在 compilation.hooks.processAssets 上运行引擎。

每个资产的字节都会从编译结果中读出,混淆后的替代内容再交还给它,因此 webpack 永远不会写出未混淆的打包产物。一次失败的混淆会让输出目录保持原样。

安装

$ npm install -D @afterpack/webpack
// webpack.config.js
import { AfterpackWebpackPlugin } from "@afterpack/webpack";

export default {
  mode: "production",
  output: { path: "dist" },
  plugins: [new AfterpackWebpackPlugin()],
};

导出的是 AfterpackWebpackPlugin 这个类。

构建

该插件在 webpack 写入之前的最后一个阶段挂钩 compilation.hooks.processAssets。只有某个 chunk 声明的 .js、.mjs 和 .cjs 资产会被重写;.hot-update. chunk 会被跳过。编译器携带但并非自己构建出来的资产,比如另一个插件生成的清单文件或复制过来的文件,会被原样保留。

一个挂钩 afterProcessAssets 的完整性校验插件依然会在这条流水线之后运行,因此它计算出的哈希值是基于混淆后的字节的。

processAssets 在每次编译时都会运行,包括监听模式下的重新构建,每次重新构建都要付出引擎的开销。在开发循环中设置 build.autorun: false(或 AFTERPACK_build_autorun=false),生产构建则保持开启。

验证

Protection Map 默认会在 devtool 生成 source map 且构建不在 CI 中(CI=true 或 CI=1)时写出;生产构建模式不会关闭它。生产环境的 webpack 构建默认没有 devtool,因此 Protection Map 是关闭的。要显式请求它:

new AfterpackWebpackPlugin({ protectionMap: { enabled: true } });

map 的默认值与打包工具是否存在 source map 相耦合,因此如果你想让它渲染出你的原始源码,就设置 devtool: "source-map"。如果没有,插件会警告说没有找到打包工具的 source map。该文件总是写入已被 gitignore 的 .afterpack/protectionMap.html,绝不会写进 dist/。它包含你的完整源码。绝不要提交它或对外提供服务。

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

选项

每一个配置键都可以作为选项对象在这里传入,例如 new AfterpackWebpackPlugin({ preset: "hard", complexity: 40 })。这些名称与 @afterpack/vite 和 @afterpack/next 完全一致,因此配置可以在各个包之间直接移植。

指令

插件会从你的源代码中读取指令。

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

指令只能是块注释:// @afterpack … 这样的行注释不会被扫描,并会作为诊断信息出现。完整语法见指令页面。

捕获会跳过 node_modules 和非 JS/TS 模块。设置 directives.enabled: false 可以完全跳过扫描;手写的 regions 在两种情况下都照常生效。

边界情况

  • 第三方依赖代码。 webpack 会把 node_modules 一起打包进你的 chunk,因此不存在按输出文件名区分它们的规律。AfterPack 会整体处理生成的文件;需要时,paths.exclude 是按文件粒度排除的方式。
  • 失败即中止,没有覆盖选项。 某个文件处理失败或结果为空会让构建失败,而不是发布未混淆的代码。

下一步

  • 框架:完整对照表。
  • Vite:参考实现以及完整的选项语义。
  • 配置:上面每一个选项,以及哪个入口能设置它。
  • 指令:完整的标记语法。
  • 预设:preset 所对应的阶梯。
  • 构建与 CI:种子、开发环境与生产环境的区别、提升同一份字节。
  • Protection Map:如何阅读这份报告。