# 0029. On-prem дистрибутив: npm-пакет с bundled CLI + Dockerfile - Status: proposed - Date: 2026-06-07 ## Context and Problem Statement До сих пор log-viewer выкладывался только на GitHub Pages: статика по пути `/log-viewer/`, multi-page билд (лендинг + app), workflow [.github/workflows/deploy.yml](../../.github/workflows/deploy.yml). Для пользователей внутри закрытого корпоративного контура (нет интернета, есть npm-proxy типа Nexus/Verdaccio) такой деплой бесполезен: - статика не выложена ни в один распространяемый формат, кроме GH Pages; - pull-based решения (`docker pull` из публичных реестров) часто заблокированы; - `base: '/log-viewer/'` хардкодит путь, под который собрана PWA — поднять на другом префиксе нельзя. Приложение полностью клиентское: нет backend, fetch на API отсутствует, всё хранится в OPFS через [@sqlite.org/sqlite-wasm](../../package.json). Это значит, что «развернуть on-prem» = «отдать статику с правильными MIME, HEAD/GET, SPA-fallback». Никакого rendering, сессий, БД на сервере не нужно. Требуется опубликовать артефакт, который: - ставится через npm/pnpm из приватного зеркала или прямо как tarball; - сам поднимает HTTP-сервер (чтобы не зависеть от nginx и не плодить вторую упаковку для веб-сервера); - упаковывается в Docker одной командой через готовый `Dockerfile` в репо; - не ломает существующий GH Pages деплой. ## Considered Options - **Option A — npm-пакет `@log-viewer/app` с встроенным Node CLI + Dockerfile в репо.** Сборка `BUILD_TARGET=onprem` кладёт только app в `dist/app/`, `bin/cli.mjs` отдаёт его через `node:http`. `Dockerfile` ставит пакет из настраиваемого `--build-arg NPM_REGISTRY=…`. Один артефакт работает локально (`npx`), в контейнере, и за reverse proxy. - **Option B — nginx-образ в GHCR.** Multi-stage Docker build → `nginx:alpine` со статикой. Минимум Node в runtime, но: два разных способа запуска (Docker vs локально), need MIME-config'и для wasm/webmanifest и SPA-rewrite в nginx.conf — пользователь будет настраивать сам. - **Option C — Tarball на GitHub Releases (статика).** Дешевле всех, но пользователь сам поднимает любой web-сервер и сам правит MIME/SPA fallback. Самый частый foot-gun: `.wasm` отдаётся как `application/octet-stream` → sqlite-wasm streaming compile падает молча. - **Option D — Готовый Docker image в GHCR.** Самый удобный для конечного пользователя, но требует GHCR-зеркала в закрытом контуре, которого у клиентов обычно нет; npm-зеркала встречаются чаще. - **Option E — Do nothing.** Оставить только GH Pages. ## Decision Outcome Chosen option: **"Option A — npm-пакет с bundled CLI + Dockerfile в репо"**. Решение балансирует распространённость npm-зеркал в закрытых контурах с отсутствием серверной логики у самого приложения. CLI инкапсулирует все правила раздачи (`.wasm → application/wasm`, immutable cache для `assets/*`, SPA-fallback на `index.html`, `/healthz`-эндпоинт для Docker) — это код, который писать всё равно надо, и его можно покрыть тестами. `Dockerfile` — тонкая обёртка над `npm install`, в которой пользователь только меняет `NPM_REGISTRY` под своё зеркало. Полностью offline-сценарий покрывается классическим `docker save`/`docker load` готового образа на стороне сборщика — не требует магии в Dockerfile. Сопутствующие решения: - **Имя пакета `@log-viewer/app`** (личный scope, поскольку unscoped `log-viewer` занят на npmjs). Имя бинаря — `log-viewer` (`npx @log-viewer/app`). - **Один `vite.config.ts` с env-флагом `BUILD_TARGET=onprem`.** Альтернатива (два конфига) ведёт к дрейфу версий плагинов; env-флаг хорошо ложится на CI matrix. - **`base: '/'` для on-prem, mount только в root.** `'./'` ломает SW (VitePWA генерирует абсолютные URL в precache manifest и worker URLs). Развёртывание под произвольный префикс отложено до явного запроса. - **HTTP-сервер на встроенном `node:http`, без зависимостей.** Для закрытого контура каждая транзитивная зависимость — это supply-chain поверхность; `sirv`/`@fastify/static` подтягивают `mrmime`/`totalist`/etc. — лишнее. - **COOP/COEP не ставим.** sqlite-wasm в текущей сборке использует `OpfsAsyncProxy` через MessageChannel (видно по `sqlite3-opfs-async-proxy-*.js` в dist), `SharedArrayBuffer` не нужен. Если в будущем переключимся на `OpfsSAHPool` — CLI придётся научить этим заголовкам, фиксируем как known follow-up. - **Service Worker оставлен по умолчанию**, с CLI-флагом `--no-sw` для downgrade-сценариев (deploy за TLS-terminating proxy недоступен). - **Публикация в npmjs.org с `--provenance`** через отдельный workflow [.github/workflows/publish-npm.yml](../../.github/workflows/publish-npm.yml), триггер `release: published`. release-please продолжает быть единственным источником правды о версии. - **GitHub Pages деплой не трогаем** — `pnpm build` без env-флага идёт в default-режим `'pages'`. ### Consequences - Good: один артефакт обслуживает все три сценария: локальный `npx`, Docker, offline через `docker save`. Закрытый контур ставит из своего npm-зеркала без особой инфраструктуры. - Good: правила раздачи (MIME, кеш, fallback, healthz, path-traversal guard) собраны в одном CLI-файле — версионируются вместе с приложением, не разъезжаются с nginx-конфигом. - Good: `BUILD_TARGET` — единственная точка переключения между публичной и on-prem сборкой; добавлять новые таргеты (например, embed-mode для iframe) можно тем же механизмом. - Bad: добавлен `bin/cli.mjs` (~200 строк) как новый поддерживаемый код. Альтернатива (nginx) перекладывала бы это в чужой конфиг, который пользователь всё равно бы переписывал. - Bad: пакет перестаёт быть `private: true`. Это требует осознанной публикации — release-please будет триггерить `npm publish` на каждом релизе. Снять `private` навсегда — необратимое решение. - Bad: PWA в on-prem требует TLS (или `localhost`). По голому HTTP-IP SW не зарегистрируется, OPFS не инициализируется. Документируется в README как обязательное требование к deploy. - Neutral: Версия `@log-viewer/app` синхронизирована с git-тегом релиза (release-please bump-ит `package.json:version`). Любые расхождения между npm и GH Releases — баг, а не фича. - Neutral: `vite-plugin-pwa@1.2.0` peer-mismatch с Vite 8 — известный риск, новый билд его не усугубляет. ## Diagram ```mermaid flowchart LR Dev[release-please merge] -->|GitHub Release published| Pub[publish-npm.yml] Pub --> Build[pnpm build:onprem] Build --> Pack[pnpm pack] Pack --> NPM[(npmjs.org)] NPM -. proxied .-> Mirror[(Closed-network npm mirror
Verdaccio / Nexus)] Mirror --> Docker[docker build
--build-arg NPM_REGISTRY=…] Docker --> Image[log-viewer image] Image --> Runtime[node bin/cli.mjs] Runtime --> Browser[Browser PWA] NPM --> Local[npx @log-viewer/app] Local --> Browser ``` ## Links - План: [docs/plans/npm-recursive-rainbow.md](../plans/npm-recursive-rainbow.md) - Связанные ADR: - [0026. Versioning, CHANGELOG, and release automation via Release Please + Conventional Commits](0026-release-please-and-conventional-commits.md) - [0005. SQLite (wa-sqlite) + FTS5 в OPFS как индекс/БД для логов](0005-sqlite-fts5-opfs-index.md) — даёт контекст про OPFS требования. - [npm provenance](https://docs.npmjs.com/generating-provenance-statements) - [vite-plugin-pwa](https://vite-pwa-org.netlify.app/)