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
windowni sobrescribe ninguna variable global. Un archivo conimportoexportse lee como módulo; para uno sin ellos, se lo indica al motor un plugin de framework, la CLI osourceType. - Las reglas del lenguaje de las que depende tu código. Los errores del modo estricto,
thisy los receptores de las llamadas, los getters y setters, los campos de clase, la aritméticaBigInty los ámbitos deevalywithse comportan tal como están escritos. - Los nombres que lee tu código. En un archivo que lee
this.constructor.nameonew.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 conreflection.allowo entrega el archivo sin tocar conpaths.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,evalywith, el modo estricto, etiquetas yswitch. 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,_.templatede 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
windowexactamente 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ódigo | Comportamiento nativo | Salida 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 prototipos | Solo se ejecuta la trampa defineProperty del proxy | Tambié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 new | Lanza un TypeError antes de que se evalúe ningún valor por defecto de parámetro ni ninguna desestructuración | Evalú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 call | Llama al método directamente | Pasa 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 new | El mensaje propio del motor de JavaScript | El 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 archivo | Las clases siguen usando los objetos integrados originales | Las 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
asyncyawait. delete, ytypeofaplicado a un nombre simple.- Una función cuyo bloque
catch (e)declaravar e. - Un
letoconstdeclarado directamente en un caso deswitch. - 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
supercomo llamada opcional, etiqueta o función entre paréntesis, o que tiene un constructor derivado sin exactamente una llamada asuper()en el nivel superior. - Los bucles, en
minifyylight. - Los ámbitos alrededor de una sentencia
witho de unevaldirecto. 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 unwithse dejan tal como están escritas. El build lo informa conDIAG_DYNAMIC_SCOPE_NATIVE. Si el código evaluado no necesita variables locales, llama aevalde forma indirecta,(0, eval)(code), o mueve el código que necesitaevalowitha 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
- Diagnósticos: cada código que puede informar un build y qué hacer con él.
- Cómo funciona AfterPack: qué hacen las transformaciones con tu código.
- Presets: qué aplica cada nivel.
- Configuración:
reflection.allow,paths.excludey las demás claves que enlaza esta página.