diff --git a/editorial/production/README.md b/editorial/production/README.md index 9a99aea..2a0296e 100644 --- a/editorial/production/README.md +++ b/editorial/production/README.md @@ -1,6 +1,6 @@ # Производство редакционных партий -На 31 июля 2026 года строгий аудит проходит 36 из 358 созданных материалов. Остальные 322 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. +На 31 июля 2026 года строгий аудит проходит 37 из 358 созданных материалов. Остальные 321 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. ## Одна партия diff --git a/editorial/reviews/2019-01-field-draft.md b/editorial/reviews/2019-01-field-draft.md new file mode 100644 index 0000000..a4f9548 --- /dev/null +++ b/editorial/reviews/2019-01-field-draft.md @@ -0,0 +1,63 @@ +# О-01 · 2019-01 · jQuery + Webpack: legacy-плагин в production + +Статус: **принят в публикационный слой 31 июля 2026 года**. Ревизия +накладывается registry только по стабильному slug; дата и автор остаются в +базовом архиве. + +## Граница материала + +- Единственный slug: `editorial-2019-01-field-jquery-webpack`. +- Голос: М2, 2019 год. У автора уже есть опыт PHP/Bitrix и первые заметки о сборке; здесь он расширяет его до границы модулей, global-объекта и production-артефакта. +- Главный вопрос: как отличить отсутствие `window.jQuery`, неправильный порядок исполнения legacy-плагина и второй экземпляр `jquery`. +- Артефакт: учебный ESM/webpack-сценарий плюс отдельная команда, которую следует запускать только в реальном проекте для получения `stats.json` и проверки сетевых чанков. +- Не заявляются: результат настоящей production-сборки, browser trace, состав зависимостей или поведение конкретного legacy-плагина вне учебного файла. + +## Pass 1 — факты и техника + +- Проверена причинная модель: `jQuery.fn.extend()` расширяет прототип конкретного объекта jQuery; поэтому сравнение `window.jQuery === $` отличает один объект от двух. +- Сверены первичные источники Webpack 4: shimming/ProvidePlugin, code splitting, stats data и `devtool`; сверена первичная документация jQuery по `jQuery.fn.extend()`. +- В тексте не приписывается `ProvidePlugin` автоматическая запись в `window`. Для плагина, который явно читает `window.jQuery`, указан отдельный bridge. +- Уточнён порядок: статический `import './vendor/legacy-mask'` не является строковым вызовом после присваивания ниже; в учебном bridge `require()` расположен после `window.jQuery = $` именно ради наблюдаемого порядка. +- В команде production-диагностики явно сказано, что она не запускалась для этого черновика. Source map ограничен политикой публикации исходников. +- Вердикт: соответствует. Локальный аудит подтвердил единственную ревизию и ссылку на asset. + +## Pass 2 — редактура и голос + +- Симптом и цена ошибки стоят в первых двух предложениях: отсутствует метод в `$.fn`, риск — неработающая форма в релизе и импульсивная правка сборки. +- Текст держит один маршрут «симптом → причина → проверка → действие»: фиксируем идентичность объектов, строим bridge, затем ищем resolved-пути в production-статистике. +- Удалены общие оценки и ложная уверенность. Короткие фразы привязаны к объекту, команде, условию или наблюдаемому результату. +- В словаре М2 используются `entry`, `chunk`, `source map`, `resolved-путь`, `runtime`; каждый термин появляется рядом с артефактом проверки, а не как декорация. +- Один учебный ESM-пример отделён от реального репозитория. В конце есть проверяемый критерий: плагин находится на том же `$.fn`, который использует экран. +- Вердикт: соответствует голосу 2019 года; нет зрелых рамок 2025–2027 и выдуманных метрик. + +## Pass 3 — визуал и выпуск + +- SVG `jquery-webpack-production-bridge-2019.svg` показывает оба порядка: ошибочный статический импорт и bridge; отдельный нижний блок показывает проверку второго экземпляра. +- Alt описывает не оформление, а причинную модель. Подпись возвращает читателя к проверке «порядок → идентичность». +- Таблица имеет `thead` и `scope="col"`; она пригодна для узкого экрана через существующий контейнер `table-scroll`. +- В статье есть пять воспроизводимых блоков кода, нумерованная последовательность проверки, ограничения и пять первичных источников. +- Перед выпуском выполнены: синтаксис Node, JSON-only CLI, import-safe проверка через draft gate, XML-проверка SVG и ручной просмотр SVG. Production-сборка сайта не входит в этот автономный черновик и не заявляется. +- Рендер сначала выявил обрезание длинного заголовка в горизонтальной версии схемы. SVG перестроен в вертикальную композицию; повторный рендер на ширине 343 px показывает весь заголовок и все три диагностических блока без обрезания. +- Вердикт: выпускной проход завершён для автономного черновика. Интеграционное ревью понадобится только перед изменением архива. + +## Фактические проверки + +| Проверка | Команда или метод | Результат | +| --- | --- | --- | +| Node syntax | `node --check scripts/upgrade-2019-01-field.mjs` из `web/` | успешно | +| JSON-only CLI | `node scripts/upgrade-2019-01-field.mjs --print-revisions` | успешно внутри draft gate: stdout парсится как JSON, stderr пустой, import возвращает те же данные | +| Import-safe и content gate | `npm run audit:draft -- scripts/upgrade-2019-01-field.mjs --expected-count=1` из `web/` | `PASS editorial-2019-01-field-jquery-webpack: 10910 body chars` | +| XML | `xmllint --noout public/assets/editorial/2019/jquery-webpack-production-bridge-2019.svg` из `web/` | успешно | +| Визуал | SVG отрендерен через Sharp в 720×1460 и 343×696; выполнен ручной просмотр | успешно после перевода схемы в вертикальный формат | + +Основной прозаический текст без кода, таблицы, рисунка и источников — **8 488 знаков**. Метрика draft gate, которая считает основной HTML-контент без источников, — **10 910 знаков**. + +## Выпусковой вердикт + +После интеграции основной редактор повторно прогнал строгий аудит: ревизия +прошла объём 10 910 знаков, figure, таблицу, пять примеров кода, маршрут +действий и источники. `npm run build` завершился с кодом 0 и сгенерировал +374 статические страницы. + +Ревизия принята к публикации. `articles.json` не менялся: registry подменяет +только редакционные поля по стабильному slug. diff --git a/web/data/editorial-revisions.mjs b/web/data/editorial-revisions.mjs index 4977e91..b4edbf6 100644 --- a/web/data/editorial-revisions.mjs +++ b/web/data/editorial-revisions.mjs @@ -7,6 +7,7 @@ import { revisions as september2018Revisions } from '../scripts/upgrade-2018-09. import { revisions as october2018January2019Revisions } from '../scripts/upgrade-2018-10-2019-01.mjs'; import { revisions as november2018Revisions } from '../scripts/upgrade-2018-11.mjs'; import { revisions as december2018Revisions } from '../scripts/upgrade-2018-12.mjs'; +import { revisions as january2019FieldRevisions } from '../scripts/upgrade-2019-01-field.mjs'; // This layer replaces archived source entries without losing their stable slug and date. export const editorialRevisions = [ @@ -19,4 +20,5 @@ export const editorialRevisions = [ ...october2018January2019Revisions, ...november2018Revisions, ...december2018Revisions, + ...january2019FieldRevisions, ]; diff --git a/web/public/assets/editorial/2019/jquery-webpack-production-bridge-2019.svg b/web/public/assets/editorial/2019/jquery-webpack-production-bridge-2019.svg new file mode 100644 index 0000000..d8553c9 --- /dev/null +++ b/web/public/assets/editorial/2019/jquery-webpack-production-bridge-2019.svg @@ -0,0 +1,66 @@ + + Диагностика legacy jQuery-плагина в production-сборке Webpack + Вертикальная схема показывает ошибочный статический импорт, мост с правильным порядком и проверку того, что глобальный и импортированный jQuery являются одним объектом. + + + + + + + + + + + + + + + + + jQuery + Webpack: + порядок и один экземпляр + Начинаем с момента, когда legacy-плагин читает window.jQuery. + + + Сбой: статический импорт + bootstrap.bad.js + + + import './legacy-mask' + зависимость выполняется первой + + + + legacy-mask читает + window.jQuery = отсутствует + + + + window.jQuery = $ + присваивание произошло поздно + + + Мост: явный порядок + legacy-jquery-bridge.js + + + import $ from 'jquery' + один объект в модуле + + + + window.jQuery = $ + legacy-контракт выполнен + + + + require('./legacy-mask') + плагин расширяет тот же $.fn + + + Проверка второго экземпляра + window.jQuery === imported$ ? + false: ищем другой resolved-путь или entry. + + + diff --git a/web/scripts/upgrade-2019-01-field.mjs b/web/scripts/upgrade-2019-01-field.mjs new file mode 100644 index 0000000..1c26038 --- /dev/null +++ b/web/scripts/upgrade-2019-01-field.mjs @@ -0,0 +1,261 @@ +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +function escapeHtml(value) { + return String(value) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function paragraph(text) { + return '

' + text + '

'; +} + +function heading(text) { + return '

' + text + '

'; +} + +function codeBlock(code) { + return '
' + escapeHtml(String(code).trim()) + '
'; +} + +function figure(src, alt, caption) { + return '
' + alt + '
' + caption + '
'; +} + +function orderedList(items) { + return '
    ' + items.map((item) => '
  1. ' + item + '
  2. ').join('') + '
'; +} + +function bulletList(items) { + return ''; +} + +function dataTable(headers, rows) { + const head = '' + headers.map((header) => '' + header + '').join('') + ''; + const body = '' + rows.map((row) => '' + row.map((cell) => '' + cell + '').join('') + '').join('') + ''; + return '
' + head + body + '
'; +} + +function sourceList(items) { + return ''; +} + +function visibleText(html) { + return html + .replace(/<[^>]*>/g, ' ') + .replaceAll(' ', ' ') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('&', '&') + .replace(/\s+/g, ' ') + .trim(); +} + +function proseText(html) { + return visibleText( + html + .replace(/
[\s\S]*?<\/code><\/pre>/g, '')
+      .replace(/
[\s\S]*?<\/figure>/g, '') + .replace(/
[\s\S]*?<\/div>/g, ''), + ); +} + +function createRevision(meta, bodyParts, sources) { + const bodyHtml = bodyParts.join('\n'); + const proseLength = proseText(bodyHtml).length; + + if (proseLength < 5000 || proseLength > 15000) { + throw new Error(meta.slug + ': prose length must be 5000–15000, got ' + proseLength); + } + + if (sources.length < 2) { + throw new Error(meta.slug + ': at least two primary sources are required'); + } + + const contentHtml = [ + bodyHtml, + heading('Проверяемые источники'), + sourceList(sources), + ].join('\n'); + + return { + ...meta, + contentHtml, + proseLength, + }; +} + +const webpackShimming = { + title: 'Webpack 4: Shimming', + url: 'https://v4.webpack.js.org/guides/shimming/', + note: 'назначение ProvidePlugin, legacy-модули с глобальными зависимостями и границы shim-подхода', +}; + +const webpackCodeSplitting = { + title: 'Webpack 4: Code Splitting', + url: 'https://v4.webpack.js.org/guides/code-splitting/', + note: 'entry, динамические импорты и дублирование модуля между несколькими entry', +}; + +const webpackStats = { + title: 'Webpack 4: Stats Data', + url: 'https://v4.webpack.js.org/api/stats/', + note: 'структура JSON-статистики сборки: assets, chunks, modules, errors и warnings', +}; + +const webpackDevtool = { + title: 'Webpack 4: devtool', + url: 'https://v4.webpack.js.org/configuration/devtool/', + note: 'варианты source map и компромиссы между удобством диагностики, скоростью и раскрытием исходного кода', +}; + +const jqueryExtend = { + title: 'jQuery API: jQuery.fn.extend()', + url: 'https://api.jquery.com/jQuery.fn.extend/', + note: 'плагин добавляет метод в прототип конкретного объекта jQuery, то есть в конкретный $.fn', +}; + +const fieldArticle = createRevision( + { + slug: 'editorial-2019-01-field-jquery-webpack', + title: 'jQuery + Webpack: почему legacy-плагин исчезает только в production', + categories: ['JavaScript', 'Webpack', 'jQuery', 'Разбор'], + cover: '/assets/editorial/2019/jquery-webpack-production-bridge-2019.svg', + excerpt: 'Плагин виден в development, но после production-сборки у $.fn нет метода. Разделяем три причины: глобал не создан, плагин выполнился рано или приложение держит второй jQuery.', + readingMinutes: 11, + }, + [ + paragraph('В development старый плагин маски ввода работает, а в production вызов $(input).legacyMask() падает: метода нет в $.fn. Цена ошибки выше сломанного поля — сборка может уйти в релиз с нерабочим оформлением или проверкой номера, а срочная правка начнёт менять Webpack вслепую.'), + paragraph('Разберём один вопрос: как доказать, что плагин потерялся именно на границе window.jQuery, порядка исполнения или второго экземпляра jquery, а не из-за минификации. Это учебный сценарий для Webpack 4 и jQuery; он показывает форму проверки. Ни production-бандл конкретного проекта, ни браузерную трассу я здесь не выдаю за выполненные.'), + heading('Сначала фиксируем, что именно исчезло'), + paragraph('Плагин jQuery обычно добавляет функцию в $.fn. Значит, выражение typeof $.fn.legacyMask отвечает не на вопрос «плагин подключён вообще», а на более полезный: установил ли он метод на тот экземпляр jQuery, которым пользуется код страницы. Официальный API описывает jQuery.fn.extend() как расширение прототипа jQuery. Если в окне живут два разных объекта jQuery, метод может оказаться только на одном из них.'), + paragraph('Development часто скрывает границу. На локальной странице jQuery мог приехать отдельным тегом script раньше vendor-файла, а dev-сервер мог не собрать второй entry в тот же путь. Production собирает зависимости в модули и чанки. Это не делает Webpack виновником: меняется способ доставки, и скрытая зависимость плагина от глобального объекта становится видна.'), + dataTable( + ['Наблюдение', 'Вероятная причина', 'Короткая проверка', 'Действие после проверки'], + [ + ['window.jQuery пустой до старого плагина', 'Импорт есть только в области модуля', 'Остановиться перед загрузкой плагина и вывести window.jQuery', 'Создать явный legacy-мост до выполнения плагина'], + ['Метод есть у window.jQuery.fn, но нет у $ .fn приложения', 'Плагин и приложение держат разные экземпляры', 'Сравнить window.jQuery === $', 'Убрать второй путь резолвации и оставить одну зависимость'], + ['Метода нет у обоих объектов', 'Файл плагина не выполнился или выполнился до моста', 'Проверить порядок модулей, ошибку в Console и включение файла в stats', 'Подключить адаптер после моста, затем проверить production-артефакт'], + ['Сбой только при втором entry', 'jQuery попал в несколько entry или загрузка чанков не согласована', 'Посмотреть modules/chunks в stats.json', 'Настроить общую зависимость и проверить все точки входа'], + ], + ), + paragraph('В таблице есть важное ограничение: это дерево гипотез, не диагноз вашего сайта. Один и тот же текст ошибки может появиться при 404 vendor-файла, при CSP или при версии плагина, которая не совместима с закреплённой jQuery. Поэтому сначала собираем идентичности объектов и факт выполнения кода, а уже затем трогаем оптимизацию.'), + heading('Учебный пример: плагин читает window.jQuery'), + paragraph('Ниже минимальный legacy-файл. Он намеренно не импортирует jQuery: автор плагина рассчитывает, что глобальная переменная уже существует. Такой код неудобен для модулей, но он встречается в старых виджетах. Ошибка в примере воспроизводима без сервера: если в момент выполнения root.jQuery отсутствует, модуль бросает понятное исключение; если объект есть, метод появляется именно на его fn.'), + codeBlock(String.raw` +// src/vendor/legacy-mask.js +(function installLegacyMask(root) { + var $ = root.jQuery; + + if (!$ || !$.fn) { + throw new Error('legacy-mask ожидает window.jQuery до своего запуска'); + } + + $.fn.legacyMask = function legacyMask() { + return this.addClass('has-legacy-mask'); + }; +}(window)); +`), + paragraph('Соблазнительный вариант ниже выглядит как правильный порядок сверху вниз: сначала импорт, затем присваивание, затем импорт плагина. Но статический import описывает зависимость модуля, а не вызов, который исполняется на этой строке после предыдущего оператора. Зависимый legacy-mask.js должен быть подготовлен и выполнен до тела модуля bootstrap.bad.js; присваивание в window для него опаздывает.'), + codeBlock(String.raw` +// src/bootstrap.bad.js +import $ from 'jquery'; +import './vendor/legacy-mask'; + +window.jQuery = window.$ = $; // для legacy-mask это уже поздно + +$('.js-phone').legacyMask(); +`), + paragraph('Для одного старого плагина не обязательно переводить весь код назад на CommonJS. Достаточно сделать маленький адаптер с явной границей. В нём require() вызывается после записи в window; поэтому legacy-файл получает именно тот объект, который импортировал bootstrap. Этот приём выбран здесь ради порядка выполнения, а не как общий стиль нового кода.'), + codeBlock(String.raw` +// src/legacy-jquery-bridge.js +import $ from 'jquery'; + +window.jQuery = window.$ = $; +require('./vendor/legacy-mask'); + +export default $; + +// src/bootstrap.js +import $ from './legacy-jquery-bridge'; + +$('.js-phone').legacyMask(); +`), + paragraph('После такого моста минимальная проверка не должна ограничиваться отсутствием исключения. Нужно удостовериться, что глобальный объект и импорт имеют одну ссылку, а метод лежит на том же $.fn, с которым работает приложение. Так мы отделяем проблему порядка от ситуации, когда плагин успешно установился, но на чужой копии jQuery.'), + codeBlock(String.raw` +// В Console страницы или во временном диагностическом модуле. +import $ from './legacy-jquery-bridge'; + +var report = { + hasGlobal: Boolean(window.jQuery), + sameInstance: window.jQuery === $, + pluginOnImport: typeof $.fn.legacyMask, + pluginOnGlobal: window.jQuery && typeof window.jQuery.fn.legacyMask, +}; + +console.table(report); +console.assert(report.sameInstance, 'jQuery резолвится в разные экземпляры'); +console.assert(report.pluginOnImport === 'function', 'плагин не установлен на импорт'); +`), + heading('Почему ProvidePlugin не заменяет мост'), + paragraph('У ProvidePlugin другая задача. Документация Webpack 4 описывает его как способ сделать пакет переменной в модулях, которые компилирует Webpack, когда встречается нужный идентификатор. Это полезно для legacy-модуля, который обращается к свободной переменной jQuery, но не является автоматическим присваиванием свойства window.jQuery.'), + paragraph('Поэтому решение должно следовать из самого плагина. Если файл принимает jQuery как параметр или использует свободную переменную, можно проверить настройку ProvidePlugin. Если он явно читает window.jQuery, нужен мост в window либо отдельная настройка loader, которая документированно передаёт зависимость. Подменять оба варианта одной конфигурацией опасно: в development симптом может исчезнуть из-за случайного порядка тегов, а в production вернуться.'), + figure('/assets/editorial/2019/jquery-webpack-production-bridge-2019.svg', 'Схема диагностики legacy jQuery-плагина: неправильный статический импорт запускает плагин до назначения window.jQuery, а проверка равенства выявляет второй экземпляр jQuery', 'Проверка идёт от порядка исполнения к идентичности экземпляра: сначала мост, затем плагин, затем сравнение объектов.'), + heading('Как искать второй экземпляр без догадки по размеру bundle'), + paragraph('Два пакета не обязаны выглядеть как два одинаковых больших файла. Один экземпляр мог войти в entry страницы, другой — в зависимость виджета по другому пути или версии. Поэтому размер итогового JavaScript — слабое доказательство. Надёжнее сравнить объекты в рантайме и посмотреть список модулей в статистике именно той production-сборки, которая будет доставлена пользователю.'), + paragraph('Webpack 4 документирует JSON-статистику как представление assets, chunks и modules. В реальном репозитории её можно запросить отдельной командой. Команду ниже не запускали для этой статьи: она является маршрутом диагностики, который нужно выполнять в ветке проекта и сохранять рядом с версией lock-файла. Source map включается только на время разбора и только если правила публикации исходников это позволяют.'), + codeBlock(String.raw` +# Команды для реального проекта, а не результат этого учебного примера. +npx webpack --mode production --devtool source-map --profile --json > dist/stats.json +npm ls jquery + +# В stats.json ищем пути и чанки, где встречается jquery. +# Затем в браузере сравниваем window.jQuery с импортом из legacy-моста. +`), + paragraph('Если npm ls jquery показывает несколько веток, это ещё не приговор: сборщик может дедуплицировать совместимые зависимости. Если в stats.json видны несколько модулей jQuery или разные resolved-пути, это также не доказывает, что два объекта дошли до страницы. Диагноз закрывается только вместе с равенством window.jQuery === $, фактом появления метода и сетевой записью тех чанков, которые фактически загрузил браузер.'), + heading('Порядок проверки production-сценария'), + orderedList([ + 'Зафиксировать точный симптом: имя метода, страницу, один input и версию собранных ассетов. Не менять конфигурацию до записи исходной ошибки.', + 'В точке перед вызовом плагина вывести window.jQuery, импорт $, их равенство и тип $.fn.legacyMask. Это отделяет отсутствие глобала от второго экземпляра.', + 'Открыть Network и убедиться, что нужные entry и lazy-chunk действительно получили ответ, а не старую страницу из кеша. Записать имена файлов и hash, не делать вывод по одному названию bundle.', + 'Собрать production-статистику на той же версии lock-файла. Найти resolved-пути jquery, их chunks и причины подключения.', + 'Если плагин читает глобал, создать один изолированный bridge до его загрузки; если он принимает импорт, удалить глобальную зависимость вместо добавления нового shim.', + 'Повторить проверку на чистой странице и в каждом entry, где живёт виджет. Готовность — одинаковый объект jQuery и функция плагина на его fn, а не только исчезнувшая ошибка в Console.', + ]), + heading('Что считать исправлением'), + paragraph('Исправлением является не строка window.$ = $ сама по себе. Нужен маленький контракт: bridge импортирует одну зависимость, публикует её в global до legacy-файла, подключает нужный side effect и экспортирует тот же объект приложению. Так следующий разработчик видит, где заканчивается старый API и почему этот файл нельзя переставить среди обычных импортов.'), + paragraph('Если обнаружен второй экземпляр, сначала устраняем причину его попадания в граф: разный resolved-путь, несовместимая версия или отдельный entry без общей оптимизации. Webpack 4 отдельно предупреждает, что несколько entry могут дублировать общий импорт; для этого есть SplitChunksPlugin. Но включить splitChunks «для лечения плагина» недостаточно: после изменения всё равно проверяем идентичность объектов и метод в runtime.'), + heading('Ограничения и границы примера'), + bulletList([ + 'Пример предполагает браузерный window и Webpack 4. В SSR, worker или тестовой среде global-объект другой; мост надо либо не исполнять на сервере, либо изолировать адаптером окружения.', + 'Вызов require() оставлен только внутри legacy-моста ради наблюдаемого порядка. Он не является рекомендацией смешивать системы модулей во всём новом коде.', + 'Source map может раскрывать пути и исходный код. Его нельзя включать в публичную production-доставку без проверки политики проекта; для расследования подойдёт защищённый артефакт.', + 'CSP, 404 чанка, несовместимая версия плагина и кеш CDN могут дать похожий внешний симптом. Этот текст не заменяет Network, Console и точную версию зависимостей.', + 'Глобальная jQuery — совместимость с конкретным legacy-файлом, а не архитектурная цель. Новый виджет лучше получает зависимость импортом и не меняет window.', + ]), + heading('Итог'), + paragraph('Когда $.fn.legacyMask пропадает только после production-сборки, не стоит первым делом отключать минификацию. Сначала проверяем порядок: есть ли window.jQuery до плагина. Затем проверяем идентичность: тот ли это объект, который импортирует приложение. После этого production-статистика и Network показывают, какой модуль и какой чанк нужно исправлять. Такой маршрут оставляет одно проверяемое условие готовности: плагин установлен на том же $.fn, которым пользуется экран.'), + ], + [webpackShimming, webpackCodeSplitting, webpackStats, webpackDevtool, jqueryExtend], +); + +export const revisions = [fieldArticle] + .map(({ proseLength, ...revision }) => revision); + +const isDirectRun = process.argv[1] + && resolve(process.argv[1]) === fileURLToPath(import.meta.url); + +if (isDirectRun) { + if (process.argv.includes('--print-revisions')) { + process.stdout.write(JSON.stringify(revisions, null, 2) + '\n'); + } else { + process.stderr.write('Usage: node web/scripts/upgrade-2019-01-field.mjs --print-revisions\n'); + process.exitCode = 1; + } +}