Ofusca una app de Electron

@afterpack/electron conecta main, preload y renderer con una sola semilla en una única llamada, y rechaza el bytecodePlugin de electron-vite y las copias de seguridad sin protección antes de que puedan publicar tu código fuente.

@afterpack/electron ofusca desde una sola llamada los tres programas que publica una app de Electron (main, preload y renderer), con una semilla de build compartida entre ellos. Si los conectas a mano, cada parte es su propio build del bundler y toma su propia semilla aleatoria, de modo que las piezas de una misma aplicación salen estructuralmente sin relación entre sí.

electron-builder no es un bundler. Empaqueta en un instalador una app ya compilada y nunca toca tu JavaScript, así que necesita un bundler por encima con o sin AfterPack.

Instalación

$ npm install -D @afterpack/electron
// 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 envuelve una configuración completa de electron-vite (la forma de objeto, función o promesa que devuelve defineConfig) y conecta @afterpack/vite en cada parte que declara la configuración, nunca en una que no declara. Dependencia peer: vite ^5 \| ^6 \| ^7 \| ^8.

Build

Cómo conectas la pasada depende de si tu toolchain configura las tres partes en una sola llamada o con una configuración por parte.

Una parte cada vez

Las toolchains que configuran cada parte por separado (Electron Forge con @electron-forge/plugin-vite) llaman a afterpackElectron una vez por configuración:

// 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 es obligatorio.

Una semilla, tres formas

  • electron-vite (withAfterpack) y Forge + plugin-vite: las tres configuraciones se compilan en un solo proceso de Node, así que las partes comparten semilla automáticamente.
  • vite-plugin-electron: también un solo proceso. Coloca afterpackElectron({ leg: "renderer" }) el último en plugins, después del propio vite-plugin-electron, para que el generateBundle de AfterPack selle el bundle terminado.
  • Scripts propios que ejecutan cada parte como un comando independiente: no hay proceso compartido, así que fija la semilla entre ellos con una variable de entorno:
// package.json
"build": "AFTERPACK_seed=git vite build -c vite.main.ts && vite build -c vite.renderer.ts"

AFTERPACK_seed acepta los mismos valores que seed: un entero, "git" (derivado de HEAD) o cualquier otra cadena, a la que el motor aplica un hash. Una opción seed explícita sigue teniendo prioridad sobre ella. Estrategia de semilla explica cuándo fijarla y cuándo no.

Verificación

El Protection Map se escribe por parte, en .afterpack/<leg>/protectionMap.html, cuando había un source map disponible y el build no se ejecuta en CI (CI=true o CI=1). El modo de producción no lo desactiva, pero electron-vite no emite source maps a menos que los pidas, así que el mapa está desactivado por defecto. Revisa main, preload y renderer por separado. Cada informe contiene todo tu código fuente. Nunca lo subas al repositorio ni lo sirvas.

Nunca incluyas source maps dentro de la app

Un .map en app.asar da acceso a tu código fuente original. Mantén desactivada la emisión de source maps (el valor por defecto en producción) en todo lo que empaquetes.

Opciones

Aquí también funciona cada clave de configuración, aplicada a todas las partes mediante withAfterpack(config, options) o pasada a cada parte mediante afterpackElectron(options). Estas opciones existen solo en este paquete:

OpciónTipoValor por defecto
leg"main" | "preload" | "renderer"obligatoria en afterpackElectron
projectRootstringprocess.cwd(). electron-vite toma src/renderer/ como raíz del build del renderer. Sobrescríbela si tu build se ejecuta desde otro sitio

seed toma por defecto un único valor nuevo compartido por todas las partes de una sesión de build. protectionMap.enabled sigue la regla habitual de los source maps, y cada parte escribe su propio informe en .afterpack/<leg>/protectionMap.html.

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

Directivas

Cada parte conecta @afterpack/vite por debajo, así que la captura de directivas funciona allí de la misma manera.

Casos límite

Matriz de compatibilidad

ToolchainConexión¿Una sola semilla automáticamente?
electron-vitewithAfterpack(defineConfig({ … })), una llamadaSí
Forge + plugin-viteafterpackElectron({ leg }) en cada una de las tres configuracionesSí
Forge + plugin-webpack@afterpack/webpack en cada configuraciónSí
vite-plugin-electronafterpackElectron({ leg: "renderer" }), colocado el últimoSí, una vez colocado después de ese plugin
electron-builderempaqueta, no hace bundling; conecta el bundler que está por enciman/a
Scripts propios (procesos independientes)un plugin por parte, o npx afterpack@latest <dir>No; fija AFTERPACK_seed

Qué rechaza

Se aplican dos comprobaciones de fallo seguro (fail closed) en cualquier parte conectada a través de este paquete.

bytecodePlugin. El propio bytecodePlugin de electron-vite sustituye cada chunk emitido por un stub cargador de 3 líneas y publica la aplicación real como bytecode de V8 (.jsc). Los dos no se pueden combinar, porque el bytecode sustituye el chunk que generó Rollup. El build falla en la parte donde está activado bytecodePlugin, o allí donde aparezca después un archivo .jsc/.cjsc en la salida. Quita uno de los dos.

build.backup: true. Las copias de seguridad se rechazan de plano: la llamada lanza un error en lugar de continuar, porque un .backup.<hash>.js es tu código fuente original completo, escrito junto a la salida dentro del árbol que tu empaquetador copia en app.asar. Todos los usuarios que instalaran la app lo recibirían.

El mismo riesgo se extiende a cualquier artefacto que tu empaquetador no sepa excluir. El glob files por defecto de electron-builder hace coincidencias con dot: true y no excluye .afterpack/ por defecto. Excluye esto de lo que empaquetas:

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

Los source maps reciben una advertencia en lugar de un rechazo: un .map con sourcesContent dentro de app.asar equivale a una desofuscación completa. Limita sourceMap: { enabled: true, emitUrl: true } a los builds locales de depuración y excluye *.map de lo que empaquetas.

Canales IPC

Los nombres de canal deben coincidir byte a byte entre los dos bundles. ipcMain.on("save-file", …) e ipcRenderer.send("save-file", …) son la misma cadena en ambos lados. Son literales de cadena normales en JavaScript emitido normal, así que mantener ambas partes con la misma semilla y el mismo nivel de protección (preset / complexity) es lo que mantiene coherentes las dos mitades.

Si un nombre de canal se construye en tiempo de ejecución en lugar de escribirse como literal, deja esa construcción en un módulo que excluyas con paths.exclude, o baja el nivel de protección de esa región con una entrada regions del plugin del bundler (Pro).

Sin el plugin: la CLI

¿Prefieres no añadir una dependencia, o compilas alguna parte con algo que no es Vite? Ejecuta npx afterpack@latest una vez por directorio de salida después del build:

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

Sin un argumento de ruta, la CLI detecta out/, el directorio en el que escribe electron-vite; indica el directorio si el tuyo es otro. Cada ejecución recorre el directorio, recopila cada .js/.mjs/.cjs y los ofusca en el sitio; un único archivo también sirve como argumento. Fija la misma --seed en todas las ejecuciones: git, derivada del commit actual, es la forma más sencilla de garantizarlo. Una ejecución de la CLI no aplica ninguno de los rechazos anteriores; ofusca todo el JavaScript que encuentra. Mantén la misma --seed y el mismo --preset en todas las ejecuciones.

Siguiente