From 219bb364bc37e7dd2d677b731de34634b0606823 Mon Sep 17 00:00:00 2001 From: Crimson Date: Sun, 30 Aug 2026 07:44:51 +0500 Subject: [PATCH] =?UTF-8?q?docs:=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D1=83=20?= =?UTF-8?q?=D1=82=D0=B8=D0=BF=D0=BE=D0=B2=20frontend=20release=20gate=20?= =?UTF-8?q?=D0=B8=20=D1=83=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=B2=D0=B5=D1=81=D1=82=D0=BD=D0=BE=D0=B5=20=D0=BE=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=87=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bundle_ui запускает `pnpm run typecheck` перед сборкой bundle. И наличие шага, и его порядок закреплены приёмкой — вместе с требованием vue-tsc версии 3 и выше и с запретом снова совмещать сборку и проверку в build:prod. Из docs/02 убран раздел «Известное ограничение: проверка типов frontend почти ничего не проверяет» и заменён описанием действующего контракта. Прогноз в нём был близок, но неточен: ошибок оказалось 142, а не ~155, и класс DefaultRow/ PeerVo на Element Plus 2.3 не существовал вовсе — он появился вместе с обновлением Element Plus. docs/04 получил описание модели отображения (третий слой рядом с типизированной моделью и сырым YAML) и раздел о том, что страница Hysteria теперь read-only на всех уровнях, а не только визуально. docs/11: команды проверки frontend и dev doctor в раздел запуска, семь новых пунктов приёмки. --- CHANGELOG.md | 64 ++++++++++++++++++++++++++++++ docs/02-build-layer-and-package.md | 46 ++++++++++++--------- docs/04-admin-panel.md | 64 ++++++++++++++++++++++++++++++ docs/11-testing-and-acceptance.md | 13 ++++++ tools/build/lib/acceptance.sh | 28 +++++++++++++ tools/build/lib/package.sh | 12 ++++++ 6 files changed, 209 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b673223..fb894e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,70 @@ Hardening-проход перед релизом `1.0.0`. Основная те `versions.env` и раньше; машина разработчика не проверялась никак, и расхождение обнаруживалось на Debian, внутри release-сборки. +### Изменено — модернизация frontend + +Известное ограничение «проверка типов frontend почти ничего не проверяет» +закрыто и удалено из документации. + +- **`vue-tsc` стал настоящим release gate.** Он был версии `0.35.0` (2022 год) и + шаблоны Vue не типизировал: проверка проходила зелёной, не давая обещанной + гарантии. На Vue 3.5 она к тому же ломается сама, не зная `vue/jsx-runtime`, — + то есть пережить обновление Vue не могла в любом случае. + + Современный `vue-tsc 3.3` на том же коде дал **142 ошибки**, а не ~155 из + прогноза, и картина оказалась однороднее: 141 × `TS18048` и одна `TS2322`, + всё в двух файлах представления Hysteria. Ожидавшегося класса «`DefaultRow` + несовместим с `PeerVo`» на Element Plus 2.3 не существовало — он появился + вместе с обновлением Element Plus до 2.14. + + Скрипты разделены: `typecheck`, `build:prod`, `verify`. Сборка запускает + проверку типов **до** bundle; раньше `vite build && vue-tsc` сначала тратил + время на production bundle и только потом сообщал о типовой ошибке. + +- **Нормализация конфига Hysteria на границе API.** Все 142 ошибки — обращения + к необязательным секциям конфига в шаблоне. Необязательны они правильно: так + устроен upstream YAML. Инвариант «секция есть всегда» существовал, но держался + на порядке присваиваний внутри компонента и типом не выражался. Введён + `Hysteria2ServerConfigView`, выводимый из модели ответа типом, а не вторым + списком полей, — вместо 141 оператора `?.` или `as any`. + +- **Убраны три редактора, которые ничего не сохраняли.** Outbounds, список + значений и словарь «ключ — значение» на странице конфига Hysteria: страница + отрисована с `:disabled="true"`, значения передаются без `v-model`, маршрутов + записи серверного конфига в API нет. Оператор мог добавить outbound, увидеть + его в списке и уйти в уверенности, что изменил конфигурацию сервера. + + У одного цена была ещё и измеримой: `vuedraggable` поставляется UMD-сборкой, + поэтому её `require("vue")` разрешался в полную сборку Vue с + рантайм-компилятором — около полумегабайта в bundle ради перетаскивания тегов + в недоступной для редактирования форме. + +- **`pnpm audit` по всему графу: 33 предупреждения → 0.** Из них четыре + затрагивали production-зависимости (`vue-i18n`, `echarts`), остальные — + build-цепочку. Среди последних был rollup GHSA DOM clobbering, а он + затрагивает **генерируемый bundle**, то есть уезжает в production: `pnpm audit + --prod`, на который смотрит gate сборки, его не показывал. + + Обновлены: Vue 3.2 → 3.5, TypeScript 4.9 → 5.9, Element Plus 2.3 → 2.14, + Vue Router 4.1 → 4.6, Pinia 2.0 → 2.3, VueUse 9 → 14, echarts 5 → 6, + Vite 4.3 → 7.3, eslint 8 → 10 (с переходом на flat config), stylelint 15 → 17. + Pinia 3, Vue Router 5 и Vite 8 сознательно не берутся: Vite 8 — это переезд на + Rolldown, остальные не дают проекту ничего, кроме номера версии. + +- **`vite-plugin-svg-icons` заменён собственным спрайтом.** Плагин не + обновлялся с 2022 года и тянул `svgo 2.8`, `postcss 5.2.18` и + `image-size 0.5.5`, у которой advisory сообщает `Patched versions: <0.0.0` — + исправленной версии не существует. Проверка на реальных ассетах поймала то, + что иначе уехало бы в релиз: три иконки из семнадцати не объявляют `viewBox`, + и без его синтеза из `width`/`height` отрисовывались бы обрезанными. + +- **Разбор bundle через sourcemap** нашёл вторую потерю: `@vueuse/core` + собирался дважды — наш и тот, что тянет Element Plus. Версии сведены. + + Итог по размеру: 2 819 722 байта против 2 404 202 на исходной базовой линии. + Рост в 17% — цена Vue 3.5, Element Plus 2.14, echarts 6 и rollup 4; + промежуточное состояние до двух исправлений выше было 3 046 172. + ### Исправлено — сборка не собиралась - **`build.sh` детектировал сам себя и падал шестым шагом из четырнадцати.** diff --git a/docs/02-build-layer-and-package.md b/docs/02-build-layer-and-package.md index 96facba..e92fea5 100644 --- a/docs/02-build-layer-and-package.md +++ b/docs/02-build-layer-and-package.md @@ -111,28 +111,38 @@ Bun обновляется отдельно от остальных: оркес полного прохода `bun test → tsc → compile → приёмка на Debian`, а не строки в общем патче. -### Известное ограничение: проверка типов frontend почти ничего не проверяет +### Проверка типов frontend — обязательный шаг релиза -`pnpm run build:prod` выполняет `vite build && vue-tsc --noEmit`, но `vue-tsc` -здесь версии `0.35.0` (2022 год) и шаблоны Vue практически не типизирует. -Проверка проходит зелёной, не давая гарантии, которую обещает. +```text +pnpm run typecheck → vue-tsc --noEmit → ОБЯЗАН пройти +pnpm run build:prod → vite build +pnpm run verify → typecheck, затем build +``` -Замер сделан: на паре `typescript@5.9` + `vue-tsc@2.2` тот же исходный код даёт -**около 155 ошибок типов** в четырёх файлах — почти все одного вида -(`possibly 'undefined'` при обращении к необязательным полям модели конфига -Hysteria в шаблоне) плюс несовместимость `DefaultRow` с `PeerVo` в слотах -таблицы пиров. +`bundle_ui()` запускает `typecheck` **до** сборки bundle: собирать production +bundle из кода, который не проходит проверку типов, незачем. Порядок и сам факт +наличия шага проверяются приёмкой. -Это не дефект безопасности и не блокер релиза: ошибки существуют в коде уже -сейчас и ни на что в рантайме не влияют. Но обновление typechecker'а тянет за -собой обновление `vue` (3.2 → 3.5, иначе `vue-tsc` 2.x не разбирает -`JSX.IntrinsicElements`), а за ним — `element-plus`, `pinia` и `vue-router`. -То есть это отдельная работа с собственной проверкой на живой панели, а не -строка в security-патче. +До v1 этой гарантии не было. `build:prod` означал `vite build && vue-tsc +--noEmit`, но `vue-tsc` был версии `0.35.0` (2022 год) и шаблоны Vue +практически не типизировал: проверка проходила зелёной, не давая гарантии, +которую обещает. Хуже того — на Vue 3.5 она ломается сама, потому что не знает +`vue/jsx-runtime`, то есть пережить обновление Vue всё равно не могла. -Ограничение на безопасность не влияет: уязвимые пакеты frontend обновляются -независимо от версии typechecker'а, движением lockfile внутри уже объявленных -диапазонов (см. `pnpm.overrides` в `apps/frontend/package.json`). +Современный `vue-tsc 3.3` на том же коде дал **142 ошибки**: 141 × `TS18048` +(«possibly undefined» при обращении к необязательным секциям конфига Hysteria в +шаблоне) и одна `TS2322`, всё в двух файлах представления Hysteria. Ожидавшегося +класса «`DefaultRow` несовместим с `PeerVo`» на Element Plus 2.3 не было вовсе — +он появился позже, вместе с обновлением Element Plus до 2.14, где слоты таблицы +типизированы строже. + +Закрыто это не подавлением, а границей: `api/config/hysteriaViewModel.ts` +превращает ответ сервера в модель, где присутствие каждой секции — свойство +типа. Подробности — в [docs/04](04-admin-panel.md). + +Контракт теперь читается так: + +> проверка типов SFC-шаблонов проходит, и это доказывает сборка, а не намерение. ### Проверка зависимостей на уязвимости diff --git a/docs/04-admin-panel.md b/docs/04-admin-panel.md index 48dead7..3df8c76 100644 --- a/docs/04-admin-panel.md +++ b/docs/04-admin-panel.md @@ -354,6 +354,70 @@ AES-GCM. Значение без этого префикса — не «форм - будущие версии Hysteria не ломают экспорт только потому, что backend и frontend ещё не научились показывать новый параметр; - это прямое следствие модели «latest stable на сборке»: схема upstream может опережать модель HY2XS. +### Третий слой: модель отображения + +У типизированной модели есть подслой, о котором стоит сказать отдельно, потому +что он определяет, как устроены шаблоны страницы Hysteria. + +`Hysteria2ServerConfig` описывает то, что **приходит по сети**, и почти все его +секции необязательны — ровно так же, как в upstream YAML. Форма же обращается к +ним напрямую: `dataForm.tls.cert`, `dataForm.acme.dns.config`, +`dataForm.resolver.https.sni`. + +Пока проверка типов SFC-шаблонов не работала, это выглядело безобидно. +Современный `vue-tsc` даёт на этом 141 ошибку `TS18048` — и он прав: обращение +через возможно отсутствующий объект падает в рантайме. Спасало то, что форма +строится merge'ем поверх полного объекта значений по умолчанию, то есть +инвариант «секция есть всегда» существовал, но держался на порядке присваиваний +внутри компонента и нигде не был выражен типом. + +Закрыто одним преобразованием на границе, а не 141 оператором `?.` и не +`as any`: + +```text +ответ API (Hysteria2ServerConfig, секции необязательны) + ↓ +normalizeHysteriaViewModel() + ↓ +Hysteria2ServerConfigView — все секции обязательны + ↓ +шаблон +``` + +`Hysteria2ServerConfigView` выводится из `Hysteria2ServerConfig` типом, а не +пишется вторым списком полей. Поэтому новая секция в схеме ломает компиляцию на +объекте значений по умолчанию — то есть поле upstream нельзя молча не +отобразить. + +Побочное следствие: `v-if` в шаблоне перестали проверять присутствие секции и +проверяют только то, что действительно определяет выбор ветки. Например для +обфускации это `dataForm.obfs.type === 'gecko'` вместо +`dataForm.obfs.type === 'gecko' && dataForm.obfs.gecko` — вторая половина +дублировала первую и существовала только из-за необязательности типа. + +Этот слой не участвует в экспорте: выгрузка идёт от исходного YAML и сохраняет +неизвестные поля, поэтому их потеря в модели отображения безвредна. + +### Страница Hysteria — только чтение, и теперь это верно на всех уровнях + +Страница отрисована с `:disabled="true"` и прямо сообщает, что конфигом владеет +`hy2xs-orchestrator reconfigure`. Маршрутов записи серверного конфига в API нет +— они удалены вместе с мёртвым updater/config-write слоем. + +Тем не менее на ней жили три полноценных редактора: outbounds (кнопка «+», +диалог создания, удаление), список значений (перетаскивание тегов, добавление, +удаление) и словарь «ключ — значение». Ни один не мог ничего сохранить: значения +передаются в них как `:outbounds=`, `:tags=`, `:map-object=` — без `v-model`, +то есть у их событий `update:*` нет ни одного слушателя. Оператор мог добавить +outbound, увидеть его в списке и уйти в уверенности, что изменил конфигурацию +сервера; изменения не переживали даже переключения вкладки. + +Все три приведены к отображению. У одного из них цена была ещё и измеримой: +редактор списка значений работал на `vuedraggable`, которая поставляется +UMD-сборкой, поэтому её `require("vue")` разрешался в полную сборку Vue вместе с +рантайм-компилятором шаблонов — около полумегабайта в bundle ради +перетаскивания тегов в недоступной для редактирования форме. + ### Санитайз экспорта Экспортируемый файл покидает сервер, поэтому секреты из него вырезаются: diff --git a/docs/11-testing-and-acceptance.md b/docs/11-testing-and-acceptance.md index 4b2265a..8595047 100644 --- a/docs/11-testing-and-acceptance.md +++ b/docs/11-testing-and-acceptance.md @@ -13,6 +13,12 @@ cd orchestrator && bun install --frozen-lockfile && bun run check && bun test # Тесты и статический анализ HY2XS admin cd apps && go vet ./... && go test ./... +# Проверка типов и сборка frontend +cd apps/frontend && pnpm install --frozen-lockfile && pnpm run verify + +# Сверка среды разработки с versions.env (ничего не меняет) +./tools/dev/doctor.sh + # Полный E2E с реальным клиентом Hysteria (Debian 13 amd64; нужен Go) HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh @@ -866,3 +872,10 @@ hy2xs-orchestrator doctor 41. `apps/go.mod` объявляет `toolchain`, совпадающий с `GO_VERSION` из `versions.env` 42. `tools/dev/doctor.sh` / `doctor.ps1` показывают расхождение среды разработки с `versions.env` 43. маршруты-алиасы `/:id/client-url` и `/:id/qr` удалены и не входят в публичный API v1 +44. `pnpm run typecheck` (`vue-tsc --noEmit`) проходит без ошибок и является обязательным шагом сборки +45. проверка типов идёт до сборки bundle, а не после +46. `vue-tsc` версии 3 и выше: 0.x проверку шаблонов не выполняет +47. `pnpm audit` по всему графу зависимостей frontend не находит уязвимостей +48. локальные SVG-иконки собираются спрайтом из репозитория, без `vite-plugin-svg-icons` +49. каждая иконка задаёт систему координат: `viewBox` либо пара `width`/`height` +50. страница конфига Hysteria не содержит элементов управления, которые ничего не сохраняют diff --git a/tools/build/lib/acceptance.sh b/tools/build/lib/acceptance.sh index e049561..9cfb43f 100644 --- a/tools/build/lib/acceptance.sh +++ b/tools/build/lib/acceptance.sh @@ -187,6 +187,34 @@ run_fix20_acceptance_subset() { log_step "Acceptance: bootstrap peer can pass auth smoke" grep -q 'quota := int64(-1)' apps/dao/sqlite.go || fail "acceptance: bootstrap peer quota must be unlimited (-1), otherwise install auth smoke fails" + log_step "Acceptance: frontend type checking is a release gate" + # Проверка типов SFC-шаблонов была фикцией: vue-tsc 0.35 не находил ничего, а + # на современном том же коде — 142 ошибки. Инвариант обязан жить в сборке, а + # не в намерении. + "$BUN_BIN" -e ' + const pkg = require("./apps/frontend/package.json"); + const scripts = pkg.scripts || {}; + if (!scripts.typecheck || !scripts.typecheck.includes("vue-tsc")) { + throw new Error("apps/frontend: скрипт typecheck должен запускать vue-tsc"); + } + if (scripts["build:prod"].includes("vue-tsc")) { + throw new Error("build:prod снова совмещает сборку и проверку типов: ошибка типов обнаружится только после production bundle"); + } + const major = Number((pkg.devDependencies["vue-tsc"] || "").replace(/^\D*/, "").split(".")[0]); + if (!Number.isInteger(major) || major < 3) { + throw new Error("vue-tsc должен быть версии 3 и выше: 0.x проверку шаблонов не выполняет"); + } + ' || fail "acceptance: контракт проверки типов frontend нарушен" + grep -q '"\$PNPM_BIN" run typecheck' tools/build/lib/package.sh \ + || fail "acceptance: сборка не запускает проверку типов frontend" + "$BUN_BIN" -e ' + const source = require("node:fs").readFileSync("tools/build/lib/package.sh", "utf8"); + const check = source.indexOf("run typecheck"); + const build = source.indexOf("run build:prod"); + if (check < 0 || build < 0) throw new Error("не найдены шаги typecheck/build:prod"); + if (check > build) throw new Error("проверка типов идёт после сборки bundle"); + ' || fail "acceptance: проверка типов обязана предшествовать сборке bundle" + log_step "Acceptance: local SVG icons are rendered from an in-repo sprite" # vite-plugin-svg-icons убран: он не обновлялся с 2022 года и тянул svgo 2.8, # postcss 5.2.18 и image-size 0.5.5, у последней advisory прямо сообщает diff --git a/tools/build/lib/package.sh b/tools/build/lib/package.sh index 3143d45..322d530 100644 --- a/tools/build/lib/package.sh +++ b/tools/build/lib/package.sh @@ -94,6 +94,18 @@ bundle_ui() { log_info "Frontend Node.js options: $NODE_OPTIONS" "$PNPM_BIN" install --frozen-lockfile + + # Проверка типов идёт ПЕРЕД сборкой bundle, и это обязательный шаг релиза. + # + # Раньше её здесь не было вовсе: `build:prod` означал + # `vite build && vue-tsc --noEmit`, то есть сборка сначала тратила время на + # production bundle и только потом сообщала о типовой ошибке. Сам же + # `vue-tsc` был версии 0.35 и не находил ничего — на современном он даёт 142 + # ошибки на том же коде. + # + # Теперь скрипты разделены, и порядок обратный: смысла собирать bundle из + # кода, который не проходит проверку типов, нет. + "$PNPM_BIN" run typecheck "$PNPM_BIN" run build:prod )