Що обфускований код зберігає незмінним (семантичний контракт)

Що з поведінки вашої програми зберігає результат AfterPack, як кожен реліз рушія перевіряють на реальному коді, і короткий перелік навмисних відмінностей.

AfterPack змінює те, як ваш код читається, і зберігає те, що він робить. Обфускована збірка повертає ті самі значення, кидає помилки тих самих типів, виводить те саме й змінює сторінку так само, як збірка, яку ви їй передали. Ця сторінка описує цей контракт для рушія 0.2.3 і новіших: що залишається незмінним, як перевіряють кожен реліз і де результат навмисно відрізняється.

Що залишається незмінним

На кожному пресеті і з кожним сідом обфускована збірка зберігає:

  • Результати й помилки. Функції повертають ті самі значення. Код, який кидав помилку, і далі кидає помилку того самого типу.
  • Ефекти та їхній порядок. Вивід у консоль, зміни DOM, мережеві запити й записи у сховище відбуваються в тому самому порядку з тими самими значеннями.
  • Асинхронний порядок. async-функції, проміси й таймери завершуються в тому самому порядку.
  • Глобальна поверхня. Класичний скрипт визначає ті самі глобальні змінні, що й раніше, тож інші скрипти на сторінці й далі можуть викликати його функції та читати його змінні. ES-модуль нічого не додає до window і не перезаписує жодної глобальної змінної. Файл з import чи export читається як модуль; про файл без них рушію повідомляє плагін фреймворку, CLI або sourceType.
  • Правила мови, на які спирається ваш код. Помилки суворого режиму, this та отримувачі викликів, гетери й сетери, поля класів, арифметика BigInt, а також області видимості eval і with поводяться так, як написано.
  • Імена, які читає ваш код. У файлі, який читає this.constructor.name або new.target.name, як це робить власний клас помилки, класи й конструктори зберігають імена, які ви написали, за винятками з розділу навмисних відмінностей.

Дві збірки з різними сідами відрізняються формою, але ніколи поведінкою.

Що змінюється за задумом

  • Текст коду. Змінюються результат fn.toString(), номери рядків і стовпців, а також імена у трасуванні стека. Щоб читати трасування стека з продакшену, зберігайте source map приватно.
  • Розмір і швидкість. Результат більший, а починаючи з medium він виконує більше роботи на кожну операцію. Виміряні числа є на сторінці Продуктивність.
  • Код, що досліджує сам себе. Коли рушій знаходить код, який під час виконання читає власну структуру, наприклад fn.toString(), він зупиняє збірку й називає шаблон. Підтвердьте шаблон через reflection.allow або залиште файл без змін через paths.exclude.

Як перевіряють кожен реліз

Починаючи з рушія 0.2.3, кожен реліз рушія перед виходом проходить семантичну перевірку. Вона запускає кожну програму двічі, у написаному й обфускованому вигляді, і порівнює, що робить кожен запуск: значення, що повертаються, кинуті помилки, вивід у консоль і порядок асинхронної роботи. Будь-яка відмінність блокує реліз, якщо це не одна з навмисних відмінностей, наведених нижче.

Перевірка запускає:

  • Невеликі цільові програми, кожна з яких спрямована на одне правило мови, яке трансформатор коду може порушити: класи й приватні поля, замикання та області видимості, генератори, порядок async, шаблонні літерали, BigInt, eval і with, суворий режим, мітки та switch. Кожен пресет, багато сідів, і нативний рушій, і рушій WebAssembly.
  • Реальні бібліотеки закріплених версій, кожну з яких проганяє невеликий скрипт: dayjs, Moment.js і Luxon для дат, viem, ethers і @noble/curves для криптографії, що інтенсивно використовує BigInt, Comlink, _.template з lodash і get-intrinsic.
  • Сторінки в Chromium, які завантажують кілька класичних скриптів по черзі, ES-модулі з імпортами й без них, вбудовані обробники подій і користувацькі елементи, а також застосунок Vue в одному чанку. Кожна сторінка має поводитися так само й залишати window точно таким, як оригінал: без нових глобальних змінних і без перезаписаних.

Навмисні відмінності

Ці відмінності та імена класів, описані після таблиці, є єдиними, які допускає перевірка. Кожен рядок таблиці стосується коду, який підміняє вбудовані об'єкти, обгортає екземпляри в Proxy, викликає клас без new або читає повідомлення помилок рушія JavaScript, тож більшість застосунків із ними ніколи не стикається. Вони діють усередині класів, які переписує рушій, починаючи з light; minify залишає класи такими, як написано.

Ваш кодНативна поведінкаОбфускований результат
Визначає публічні поля класу на екземплярі, який є Proxy (повернутим із базового конструктора) або має такий у ланцюжку прототипівСпрацьовує лише пастка defineProperty проксіДля кожного поля також спрацьовує пастка has, а для поля, якого в об'єкта немає, ще й пастка set і все, куди вона переспрямовує
Викликає конструктор класу без newКидає TypeError ще до того, як виконуються значення параметрів за замовчуванням чи деструктуризаціяВиконує значення параметрів за замовчуванням і деструктуризацію, а потім кидає той самий TypeError
Викликає приватний метод або super.method() після підміни Function.prototype.call чи на методі з власною властивістю callВикликає метод напрямуВикликає його через підмінений call
Читає повідомлення TypeError від перевірки приватного поля, повторної ініціалізації приватного поля або виклику класу без newВласне повідомлення рушія JavaScriptВласне повідомлення AfterPack з тим самим типом помилки
Підміняє TypeError, Object, Object.defineProperty, Reflect.construct, Reflect.ownKeys, WeakMap або WeakSet до завантаження файлуКласи й далі використовують оригінальні вбудовані об'єктиПереписані класи використовують підміну

Імена класів і конструкторів відрізняються в кількох випадках, на будь-якому пресеті. Файл, який читає їх лише іншими способами, наприклад err.constructor.name або const { name } = new.target, отримує перейменовані імена. У файлі, який читає this.constructor.name або new.target.name, перейменоване ім'я все одно видно для класу, створеного як new (class X {})(), для анонімного класу, що отримує ім'я від параметра за замовчуванням, для функції, яку створюють через new у гілці switch, що стоїть перед гілкою з її оголошенням, і для функції, яку створюють лише через інше посилання, наприклад new ns.F(), псевдонім або Reflect.construct.

Що залишається таким, як написано

Ці конструкції залишаються такими, як написано, скрізь, де точно переписати їх недешево. Код навколо них і далі перейменовується й кодується, за винятком коду навколо with і прямого eval.

  • async-функції та await.
  • delete, а також typeof, застосований до простого імені.
  • Функція, у якій блок catch (e) оголошує var e.
  • let або const, оголошені безпосередньо в гілці switch.
  • Опціональний виклик чи тег, у якого функцію, що викликається, взято в дужки або вона є членом super: (o?.m)?.(), (o?.m)`x`, super.m?.().
  • Клас, який присвоює значення власному імені, викликає член super як опціональний виклик, тег або функцію в дужках, або має похідний конструктор без рівно одного виклику super() на верхньому рівні.
  • Цикли на minify і light.
  • Області видимості навколо інструкції with або прямого eval. Імена в кожній охопній області видимості, аж до верхнього рівня файлу, зберігають оригінальне написання, а глобальні змінні всередині тіла with залишаються такими, як написано. Збірка повідомляє про це кодом DIAG_DYNAMIC_SCOPE_NATIVE. Якщо виконуваному коду не потрібні локальні змінні, викликайте eval опосередковано, (0, eval)(code), або винесіть код, якому потрібен eval чи with, в окремий файл.

До рушія 0.2.3

Рушій 0.2.2 і давніші не виконували цього контракту в деяких випадках, які тепер покриває перевірка: імена верхнього рівня класичних скриптів, спільні з іншими скриптами, ES-модулі без імпортів, об'єкти на кшталт дат усередині шаблонних літералів, функції, які використовують як конструктори, поля класів поверх успадкованих аксесорів, функції, що посилаються на самих себе, арифметика BigInt на medium і вище, eval і with, блок catch, що повторно оголошує свою змінну через var, порядок async, опціональні виклики в дужках, константні вирази ** та імена класів, які читають під час виконання. Перезберіть проєкт на рушії 0.2.3 або новішому; журнал змін перелічує кожне виправлення.

Повідомити про відмінність

Відмінність, якої немає на цій сторінці, є помилкою. Відкрийте issue з невеликим відтворенням на github.com/afterpack-dev/afterpack/issues. Доки її не виправлено, paths.exclude залишає відповідний файл без змін.

Далі