Qué mantiene igual el código ofuscado (contrato semántico)

Qué conserva la salida de AfterPack del comportamiento de tu programa, cómo se comprueba cada versión del motor con código real y la breve lista de diferencias deliberadas.

AfterPack cambia cómo se lee tu código y mantiene lo que hace. Un build ofuscado devuelve los mismos valores, lanza los mismos tipos de error, escribe la misma salida y hace los mismos cambios en la página que el build que le diste. Esta página describe ese contrato para el motor 0.2.3 y posteriores: qué se mantiene igual, cómo se comprueba cada versión y los pocos casos en que la salida difiere a propósito.

Qué se mantiene igual

En cada preset y con cada semilla, el build ofuscado conserva:

  • Resultados y errores. Las funciones devuelven los mismos valores. El código que lanzaba un error sigue lanzando un error del mismo tipo.
  • Efectos y su orden. La salida de consola, los cambios en el DOM, las peticiones de red y las escrituras en el almacenamiento ocurren en el mismo orden y con los mismos valores.
  • Orden asíncrono. Las funciones async, las promesas y los temporizadores se resuelven en el mismo orden.
  • La superficie global. Un script clásico define las mismas variables globales que antes, así que otros scripts de la página pueden seguir llamando a sus funciones y leyendo sus variables. Un módulo ES no añade nada a window ni sobrescribe ninguna variable global. Un archivo con import o export se lee como módulo; para uno sin ellos, se lo indica al motor un plugin de framework, la CLI o sourceType.
  • Las reglas del lenguaje de las que depende tu código. Los errores del modo estricto, this y los receptores de las llamadas, los getters y setters, los campos de clase, la aritmética BigInt y los ámbitos de eval y with se comportan tal como están escritos.
  • Los nombres que lee tu código. En un archivo que lee this.constructor.name o new.target.name, como hace una clase de error propia, las clases y los constructores conservan los nombres que escribiste, salvo las excepciones de diferencias deliberadas.

Dos builds con semillas distintas difieren en su forma, nunca en su comportamiento.

Qué cambia por diseño

  • El texto del código. Cambian lo que devuelve fn.toString(), los números de línea y columna, y los nombres de una traza de pila. Guarda un source map en privado para leer las trazas de pila de producción.
  • Tamaño y velocidad. La salida es más grande y, a partir de medium, hace más trabajo por operación. Rendimiento tiene las cifras medidas.
  • Código que se inspecciona a sí mismo. Cuando el motor encuentra código que lee su propia estructura en tiempo de ejecución, como fn.toString(), detiene el build y nombra el patrón. Reconoce el patrón con reflection.allow o entrega el archivo sin tocar con paths.exclude.

Cómo se comprueba cada versión

A partir del motor 0.2.3, cada versión del motor pasa una comprobación semántica antes de publicarse. La comprobación ejecuta cada programa dos veces, tal como está escrito y ofuscado, y compara lo que hace cada ejecución: los valores devueltos, los errores lanzados, la salida de consola y el orden del trabajo asíncrono. Cualquier diferencia hace fallar la versión, salvo que sea una de las diferencias deliberadas de abajo.

La comprobación ejecuta:

  • Programas pequeños y específicos, cada uno dirigido a una regla del lenguaje que un transformador de código puede romper: clases y campos privados, closures y ámbitos, generadores, el orden de async, plantillas literales, BigInt, eval y with, el modo estricto, etiquetas y switch. Cada preset, muchas semillas, y tanto el motor nativo como el motor WebAssembly.
  • Bibliotecas reales en versiones fijadas, cada una ejercitada por un pequeño script: dayjs, Moment.js y Luxon para fechas, viem, ethers y @noble/curves para criptografía con uso intensivo de BigInt, Comlink, _.template de lodash y get-intrinsic.
  • Páginas en Chromium que cargan varios scripts clásicos en orden, módulos ES con y sin imports, manejadores de eventos en línea y elementos personalizados, además de una app de Vue en un único chunk. Cada página debe comportarse igual y dejar window exactamente como lo deja el original, sin variables globales nuevas ni sobrescritas.

Diferencias deliberadas

Estas diferencias y los nombres de clase descritos tras la tabla son las únicas que admite la comprobación. Cada fila afecta a código que reemplaza objetos integrados, envuelve instancias en un Proxy, llama a una clase sin new o lee los mensajes de error del motor de JavaScript, así que la mayoría de las aplicaciones nunca se encuentra con ellas. Se aplican dentro de las clases que el motor reescribe, a partir de light; minify deja las clases tal como están escritas.

Tu códigoComportamiento nativoSalida ofuscada
Define campos públicos de clase en una instancia que es un Proxy (devuelto por un constructor base) o que tiene uno en su cadena de prototiposSolo se ejecuta la trampa defineProperty del proxyTambién se ejecuta la trampa has para cada campo y, para un campo que el objeto no tiene, la trampa set y todo aquello a lo que reenvíe
Llama a un constructor de clase sin newLanza un TypeError antes de que se evalúe ningún valor por defecto de parámetro ni ninguna desestructuraciónEvalúa los valores por defecto y la desestructuración de los parámetros, y luego lanza el mismo TypeError
Llama a un método privado o a super.method() después de reemplazar Function.prototype.call, o sobre un método con su propia propiedad callLlama al método directamentePasa por el call reemplazado
Lee el mensaje de un TypeError de una comprobación de campo privado, de una segunda inicialización de un campo privado o de una clase llamada sin newEl mensaje propio del motor de JavaScriptEl mensaje propio de AfterPack, con el mismo tipo de error
Reemplaza TypeError, Object, Object.defineProperty, Reflect.construct, Reflect.ownKeys, WeakMap o WeakSet antes de que se cargue el archivoLas clases siguen usando los objetos integrados originalesLas clases reescritas usan el reemplazo

Los nombres de clases y constructores difieren en unos pocos casos, en todos los presets. Un archivo que los lee solo de otras formas, como err.constructor.name o const { name } = new.target, obtiene los nombres renombrados. En un archivo que sí lee this.constructor.name o new.target.name, el nombre renombrado sigue apareciendo en una clase construida como new (class X {})(), en una clase anónima que recibe su nombre de un parámetro por defecto, en una función construida desde un caso de switch anterior al caso que la declara y en una función construida solo a través de otra referencia, como new ns.F(), un alias o Reflect.construct.

Qué se deja tal como lo escribiste

Estas construcciones se dejan tal como están escritas allí donde reescribirlas con fidelidad no es barato. El código que las rodea se sigue renombrando y codificando, salvo alrededor de with y de un eval directo.

  • Las funciones async y await.
  • delete, y typeof aplicado a un nombre simple.
  • Una función cuyo bloque catch (e) declara var e.
  • Un let o const declarado directamente en un caso de switch.
  • Una llamada opcional o una etiqueta cuya función llamada va entre paréntesis o es un miembro de super: (o?.m)?.(), (o?.m)`x`, super.m?.().
  • Una clase que asigna a su propio nombre, que llama a un miembro de super como llamada opcional, etiqueta o función entre paréntesis, o que tiene un constructor derivado sin exactamente una llamada a super() en el nivel superior.
  • Los bucles, en minify y light.
  • Los ámbitos alrededor de una sentencia with o de un eval directo. Los nombres de cada ámbito que lo contiene, hasta el nivel superior del archivo, conservan su forma original, y las variables globales dentro del cuerpo de un with se dejan tal como están escritas. El build lo informa con DIAG_DYNAMIC_SCOPE_NATIVE. Si el código evaluado no necesita variables locales, llama a eval de forma indirecta, (0, eval)(code), o mueve el código que necesita eval o with a un archivo propio.

Antes del motor 0.2.3

El motor 0.2.2 y anteriores no cumplían este contrato en algunos casos que la comprobación cubre ahora: los nombres de nivel superior de scripts clásicos compartidos con otros scripts, los módulos ES sin imports, los objetos como las fechas dentro de plantillas literales, las funciones usadas como constructores, los campos de clase sobre accesores heredados, las funciones que se refieren a sí mismas, la aritmética BigInt en medium y superiores, eval y with, un bloque catch que vuelve a declarar su variable con var, el orden de async, las llamadas opcionales entre paréntesis, las expresiones constantes con ** y los nombres de clase leídos en tiempo de ejecución. Vuelve a hacer el build con el motor 0.2.3 o posterior; el registro de cambios enumera cada corrección.

Informar de una diferencia

Una diferencia que no aparece en esta página es un bug. Abre un issue con una reproducción pequeña en github.com/afterpack-dev/afterpack/issues. Hasta que se corrija, paths.exclude entrega el archivo afectado sin tocar.

Siguiente