bx24-hmr
v0.6.0
Published
Hot Module Replacement for B24 dev. Launcher + dashboard + Chrome extension. Repo-clean: no in-place bundle writes.
Readme
bx24-hmr
Hot Module Replacement для модулей Bitrix24 с Vue/JS bundle-структурой (<module>/install/js/<module>/...). По умолчанию подключён im (мессенджер); через wizard или CLI можно добавить любое количество модулей — crm, imopenlines, tasks и т.п. Правка Vue 3 компонентов, CSS и Vuex-моделей — без F5 и без модификации артефактов сборки в репозитории modules.
Имя проекта
im-hmrотражает исторический фокус на im v2; сейчас инструмент шире — отслеживаемые модули задаются списком в конфиге.
Состоит из трёх процессов:
bx24-hmr (npm-пакет)
├── launcher HTTP :39125 — дашборд, REST + SSE, мастер первого запуска
├── HMR core WS :39124 + bundle-server :39126 — fs.watch, persistent chef -w, патчи
└── chrome extension загружается отдельно — content-script + DNR + плавающий UIЛаунчер и HMR core живут в одном Node-процессе. Chrome-расширение — отдельно, грузится через chrome://extensions → "Загрузить распакованное".
Установка
Требуется Node.js ≥ 20.
npm install -g bx24-hmr
bx24-hmrВ PATH появится команда bx24-hmr — лаунчер с дашбордом.
macOS при первом запуске спросит разрешение на входящие сетевые соединения — это WS-сервер на :39124, который слушает Chrome-расширение. Жмём Allow.
Обновление:
npm update -g bx24-hmrБинарь без Node
Single-file бинарь (без зависимости от Node) — opt-in, собирается отдельным таском (см. docs/NEXT.md). На время foundation-перехода загрузка бинарей со страницы Releases приостановлена; для машин без Node используйте nvm/fnm или установите Node системно.
Первый запуск — мастер
После запуска открывается http://localhost:39125/ с мастером:
- Repo — путь к локальному клону
modules(Mercurial). Автодетект сканирует~/Projects,~/PhpstormProjects,~/Workspace,~/dev,~/code. - chef — проверка CLI
@bitrix/chef; если не установлен, один клик ставит его глобально черезnpm install -g @bitrix/chef. - Build & VCS — выбор build-tool'а и VCS. По умолчанию автодетект (chef в PATH +
.hg/или.git/в репе). Здесь же можно переключиться на произвольный shell-builder (например,npm run build) или сменить VCS вручную. См. docs/ADAPTERS.md. - Extension — мастер распаковывает расширение в
~/.bx24-hmr/extension/и копирует путь в буфер. Открытьchrome://extensions, включить Developer mode, Load unpacked, вставить путь. - Messenger — открыть вкладку с порталом Bitrix24, дождаться когда расширение подключится к WS.
После окончания мастера на дашборде видно состояние всех частей: HMR core, chef, watcher, hg, подключённые клиенты, последние патчи.
IDE для open-in-editor (PhpStorm / WebStorm / VSCode / Cursor) выбирается позже — в Settings на дашборде.
Какие сайты обслуживаются
Расширение по умолчанию ни на чём не активно. Хосты добавляются в whitelist из popup-а расширения (toggle «Track this site» — добавляет текущий hostname). Управление списком и шаблонами доменов — на chrome-extension://…/options/options.html (Options).
Использование
На сайте из whitelist в углу появляется плавающий бейдж со статусом HMR. Сохранение Vue/CSS/Vuex-файла внутри отслеживаемого модуля (<repo>/<module>/install/js/<module>/**/src/**) — патч долетает до браузера за ~200 мс, F5 не нужен.
Список модулей хранится в config.json под ключом modules:
{
"modules": [
{ "name": "im", "root": "im", "extensions": ["im.v2.application.messenger", "im.v2.component.**", "im.v2.model"], "enabled": true },
{ "name": "crm", "root": "crm", "extensions": ["crm.entity.**"], "enabled": false }
]
}root — имя верхней папки модуля относительно repoPath. Watcher слушает всю папку модуля; фильтрация по src/ встроена. chef поднимается один раз в watch-режиме (chef build ext1 ext2 … -w, cwd = repoPath) и сам пересобирает изменённые extensions — без per-save spawn'а (даёт ~9× ускорение по медиане, см. docs/BENCH_CHEF_WATCH.md). Откатной режим --legacy-chef-spawn возвращает per-save spawn на случай регрессий.
| Что меняется | Стратегия | Что переживает hot-swap |
|---|---|---|
| Vue-компонент | __VUE_HMR_RUNTIME__.reload(id, def) | скролл, попапы, текст в textarea, state соседних компонентов |
| CSS | cache-bust у <link href> | всё |
| Vuex-модель | store.hotUpdate({modules:{...}}) | Vuex state |
| im.v2.application.messenger | unmount + reload + initComponent | Vuex (Core живёт) — локальный UI теряется |
| im.v2.application.core, ui.*, main.* | full page reload + sessionStorage snapshot | то что заснапшотили (черновики, скролл) |
Ошибки сборки
Если chef build падает — лаунчер показывает stderr в баннере на странице и в дашборде. Watcher продолжает работать; следующий валидный сейв триггерит ребилд.
Изменение инфраструктуры
Если меняется главный класс компонента или mount-нода — стратегия эскалируется до remount или полного reload (с попыткой восстановить state из sessionStorage).
Если в дереве im.v2.*/src/ появляется новый extension (новая папка с config.php) — нужен рестарт HMR core: dep-graph и список расширений собираются на старте.
Управление
- Popup расширения (клик по иконке в Chrome): три toggle-а.
- Track this site — добавить/убрать текущий hostname в whitelist.
- Show in-page panel — спрятать/показать раскрывающуюся панель на странице (бейдж и тосты остаются).
- Pause file watching — поставить scheduler на паузу (бэкенд лаунчера). Source of truth —
GET /api/watcher/stateна лаунчере; popup ресинкается при открытии.
- Дашборд лаунчера на
http://localhost:39125— состояние HMR core и watcher, лента событий, конфиг (Settings— IDE, шаблон URL для open-in-editor, порты,extensionsglob). - Hg-pause. Если в
modulesсделатьhg up,hg revert,hg shelve— watcher автоматически встаёт на паузу до стабилизации tree (sliding window 5×2с, max-min ≤ 1). Это видно на дашборде и в баннере.
Self-update
После перехода на npm-дистрибутив встроенный self-updater (скачивание бинаря из GitLab Releases) приостановлен. Обновление — стандартной командой:
npm update -g bx24-hmrЛаунчер всё ещё дёргает GitLab Releases API для отображения баннера «доступна новая версия», но кнопка установки в дашборде на время foundation-перехода отключена. Полноценная интеграция с npm-registry для проверки версии — отдельный таск.
Архитектура
Подробности — в docs/ARCHITECTURE.md. Кратко:
- Launcher (
src/launcher/) — Node-процесс, лайфцикл, конфиг в~/.bx24-hmr/config.json, HTTP-дашборд (:39125), REST/api/*+ SSE/api/events, мастер первого запуска, supervisor HMR core. - HMR core (
src/server/) — persistentchef -w(один долгоживущий процесс на enabled-набор extensions) +@parcel/watcherна<repo>/.../src/**для классификации, hg-monitor, pause-controller, dep-graph для cascade-ребилдов lib-ов, bundle-server на:39126(отдаёт инструментированные bundle'ы по HTTP — без записи на диск в repo), WS-сервер на:39124для отдачи патчей. - Chrome extension — исходники в
src/extension/, сборка вdist/extension/(npm run build:extтранспилирует .ts → .js и копирует ассеты). MV3, три контекста (service worker, content_script в isolated world, page_entry в main world). DNR-правила редиректят запросы за*//bitrix/js/im/v2/**/*.bundle.{js,css}(.map)?на bundle-server лаунчера. Hot-swap живёт вclient/handlers/*(component / css / model / pull / infra / full-reload).
Главное архитектурное свойство: репозиторий modules не модифицируется. Инструментирование bundle'ов происходит в памяти bundle-server'а на каждом HTTP-запросе. hg status остаётся чистым.
Сборка из исходников
Код пишется на TypeScript. Все новые файлы — .ts; оставшиеся .mjs/.js — legacy, мигрируются параллельно. В dev-режиме .ts читаются через tsx (node --import tsx, подключено в npm-скриптах). Для продакшен-дистрибутива всё бандлится через esbuild в dist/ и публикуется в npm registry.
Зависимости:
- Node.js ≥ 20 — для запуска из исходников и для собранного npm-пакета.
- Mercurial (
hg) — для целевого репозиторияmodules. - Git — для самого этого репозитория.
git clone <repo> ~/Projects/im-hmr
cd ~/Projects/im-hmr
npm install
npm run start # запуск из исходников через node --import tsx, открывает дашборд в браузереСборка дистрибутива (что попадёт в npm-пакет):
npm run build # esbuild → dist/launcher, dist/server, dist/tools, dist/extension, dist/launcher/ui
npm run build:clean # то же, но с предварительной очисткой dist/
npm run build:ext # только Chrome-расширение → dist/extension/Результат — в dist/. После npm run build локально можно запустить node bin/bx24-hmr, проверить чисто dist-пайплайн.
Подробности про разработку (smoke-test, инспект состояния, гочи) — в docs/DEV.md.
Релиз
Версия — single source of truth в package.json#version. tools/sync-version.ts пропагирует её в src/extension/manifest.json и в строку-баннер лаунчера. Build-pipeline (tools/build-dist.ts) запускает sync-version перед сборкой.
End-to-end релиз — npm run release (требует npm login под mainтейнером пакета):
npm version patch --no-git-tag-version # бамп package.json без git-tag (тег создаёт release.ts)
git commit -am "v$(node -p 'require(\"./package.json\").version')"
npm run release:dry # печатает план без побочных эффектов
npm run release # build dist → npm publish → git tag → pushprepublishOnly гарантирует, что npm publish всегда происходит со свежесобранным dist/ — даже если запускать npm publish напрямую, мимо tools/release.ts.
Полный гайд для мейнтейнера — в docs/RELEASE.md.
Документация
Код пишется на TypeScript (новые файлы — .ts, миграция оставшихся .mjs/.js идёт параллельно). Dev-run — через node --import tsx, compile-сборка — через Bun (читает .ts нативно). Подробнее про конвенции — docs/STYLE.md, про состояние миграции — docs/TYPECHECK_BASELINE.md.
- docs/ARCHITECTURE.md — детали трёх процессов, bundle-server, pause-controller, communication channels, state storage.
- docs/DEV.md — запуск из исходников, smoke-test, typecheck, hot-paths в коде, гочи. Раздел
Perf baseline— проnpm run benchи big-repo fixture (--fixture=big) для нагрузочных замеров. - docs/STYLE.md — code style: narrative-структура файла, антипаттерны проекта, кандидаты на рефакторинг.
- docs/TYPECHECK_BASELINE.md — состояние миграции на TS, snapshot ошибок
tsc. - docs/RELEASE.md — процесс релиза, флаги
release.mjs, чек-листы. - docs/NEXT.md — текущая итерация (distribution & updates), что уже сделано и что в работе.
Power-user CLI
Старый Node-CLI без дашборда — для тех кому не нужен GUI:
npm run hmr -- --repo /path/to/modules \
--port 39124 \
--module im:im:im.v2.application.messenger,im.v2.component.**,im.v2.modelHealth-эндпоинт: http://127.0.0.1:39125/__hmr_status.
GUI-лаунчер поддерживает headless-флаги для скриптовых сценариев — они перезаписывают ~/.bx24-hmr/config.json на время запуска (без сохранения):
# Подключить два модуля одним вызовом
bx24-hmr --module im --module crm:crm:crm.entity.**
# Build/VCS переопределения
bx24-hmr --builder=shell --builder-cmd="npm" --builder-args="run,build" --vcs=gitСтарый флаг --extensions поддерживается ради обратной совместимости (deprecated, выдаёт warning) — он применяется к extensions первого enabled-модуля. Полный список — bx24-hmr --help.
Профилирование пайплайна
Флаг --profile включает потактовый лог HMR-пайплайна — для случаев, когда патч долетает не за ~200 мс, а за секунды, и непонятно где именно теряется время.
bx24-hmr --profileНа каждой стадии в stdout пишется строка вида [profile] stage=<имя> evt=<id> key=value .... Каждое fs-событие получает короткий evt=<id> (base36-счётчик), который протягивается через весь pipeline — все стадии одного события можно собрать одной командой grep "evt=2 ".
Стадии
| Стадия | Когда логируется |
|-------------------|-------------------------------------------------------------------------------------------------------------------|
| fs-event | fs.watch поймал изменение src-файла — это t0 для total. |
| enqueue | От fs-event до первого резолва extension'а (async classify()) и постановки в pendingExts. В total попадает только первый замер на пару (evt, ext). |
| re-enqueue | Повторный classify() той же пары (evt, ext) в активной цепочке (быстрый бурст сейвов того же файла). В total не входит; считается в dup-enqueue=N строки total. |
| pause-active | Глобально: scheduler вошёл в паузу (reason=hg-pull/branch-switch/hg-merge/src-burst/…). |
| pause-resume | Глобально: scheduler вышел из паузы. ms — длительность паузы. |
| debounce | Время от первого scheduleFlush до flushDebounce (обычно ~250 мс — burst схлопывается). |
| queue-wait | От постановки батча в buildQueue до фактического старта runChefBuild. Растёт, когда builder уже занят. |
| builder-spawn | От spawn() chef-процесса до первого байта stdout/stderr (cold-start стоимости). |
| chef-build | Длительность builder.buildOnce (сам chef). |
| dist-event | fs.watch увидел изменение dist-bundle (между chef-build и build-patch). |
| build-patch | Построение Patch[] в onChange artifact-watcher'а (resolver + classifier + hmr-id postprocess). |
| bundle-instrument | Постпроцессинг bundle при ответе bundle-server'а клиенту (отдельный hot-path). |
| ws-push | Broadcast патчей по ws-клиентам. |
| total | От fs-event до ws-push. Содержит breakdown по основным стадиям + other для всего необъяснённого. |
Пример выхода
[profile] stage=fs-event evt=2 file=/repo/im/install/js/im/v2/component/dialog/src/dialog.vue
[profile] stage=enqueue evt=2 ext=im.v2.component.dialog ms=3.1
[profile] stage=debounce evts=2 pending=1 ms=251.3
[profile] stage=queue-wait evts=2 count=1 ms=0.2
[profile] stage=builder-spawn evts=2 count=1 ms=42.4
[profile] stage=chef-build evts=2 exts=im.v2.component.dialog count=1 ms=1432.7
[profile] stage=dist-event file=/repo/im/install/js/im/v2/component/dialog/dist/dialog.bundle.js ms=0
[profile] stage=build-patch evts=2 files=1 patches=1 ms=6.4
[profile] stage=ws-push evts=2 patches=1 clients=1 ms=0.4
[profile] stage=total evt=2 clients=1 ext=im.v2.component.dialog ms=1736.1 enqueue=3.1 debounce=251.3 queue-wait=0.2 builder-spawn=42.4 chef-build=1432.7 build-patch=6.4 ws-push=0.4Пример outlier'а с паузой (long pull/merge):
[profile] stage=fs-event evt=5 file=…/some.vue
[profile] stage=enqueue evt=5 ext=im.v2.component.dialog ms=2.4
[profile] stage=pause-active reason=hg-pull
… 65 секунд …
[profile] stage=pause-resume reason=hg-pull ms=65120.0
[profile] stage=chef-build evts=5 exts=im.v2.component.dialog count=1 ms=3533.5
[profile] stage=ws-push evts=5 patches=2 clients=2 ms=0.1
[profile] stage=total evt=5 clients=2 ext=im.v2.component.dialog ms=68660.2 pause=65120.0 enqueue=2.4 chef-build=3533.5 ws-push=0.1 other=4.2Здесь pause=65120.0 сразу видно в строке total — задержка не в chef'е, а в hg-паузе.
Чтение breakdown
total ms=…— wall-clock отfs-eventдоws-push.- Перечисленные стадии — длительности соответствующих фаз; сумма примерно равна
total(отклонение может быть из-за overlap'ов). other—totalминус все известные стадии. Если регулярно > 50 мс — стадия пропущена в инструментировании, стоит добавить.dup-enqueue=N— сколько повторных постановок(evt, ext)отброшено в стадии enqueue (быстрые сейвы того же файла, дубли watcher'а). Большое значение — сигнал «шумного» fs-источника.warn=stages-mismatch— сумма стадий превысилаtotalбольше чем на 100 мс. Чаще всего значит, что какая-то стадия атрибутировалась не на тотevt(баг профайлера, а не пайплайна).
Без --profile overhead на горячем пути нулевой (все методы профайлера — no-op'ы).
