docs: сделать проверку типов frontend release gate и убрать известное ограничение
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 в раздел запуска, семь новых пунктов приёмки.
This commit is contained in:
@@ -117,6 +117,70 @@ Hardening-проход перед релизом `1.0.0`. Основная те
|
|||||||
`versions.env` и раньше; машина разработчика не проверялась никак, и
|
`versions.env` и раньше; машина разработчика не проверялась никак, и
|
||||||
расхождение обнаруживалось на Debian, внутри release-сборки.
|
расхождение обнаруживалось на 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` детектировал сам себя и падал шестым шагом из четырнадцати.**
|
- **`build.sh` детектировал сам себя и падал шестым шагом из четырнадцати.**
|
||||||
|
|||||||
@@ -111,28 +111,38 @@ Bun обновляется отдельно от остальных: оркес
|
|||||||
полного прохода `bun test → tsc → compile → приёмка на Debian`, а не строки в
|
полного прохода `bun test → tsc → compile → приёмка на Debian`, а не строки в
|
||||||
общем патче.
|
общем патче.
|
||||||
|
|
||||||
### Известное ограничение: проверка типов frontend почти ничего не проверяет
|
### Проверка типов frontend — обязательный шаг релиза
|
||||||
|
|
||||||
`pnpm run build:prod` выполняет `vite build && vue-tsc --noEmit`, но `vue-tsc`
|
```text
|
||||||
здесь версии `0.35.0` (2022 год) и шаблоны Vue практически не типизирует.
|
pnpm run typecheck → vue-tsc --noEmit → ОБЯЗАН пройти
|
||||||
Проверка проходит зелёной, не давая гарантии, которую обещает.
|
pnpm run build:prod → vite build
|
||||||
|
pnpm run verify → typecheck, затем build
|
||||||
|
```
|
||||||
|
|
||||||
Замер сделан: на паре `typescript@5.9` + `vue-tsc@2.2` тот же исходный код даёт
|
`bundle_ui()` запускает `typecheck` **до** сборки bundle: собирать production
|
||||||
**около 155 ошибок типов** в четырёх файлах — почти все одного вида
|
bundle из кода, который не проходит проверку типов, незачем. Порядок и сам факт
|
||||||
(`possibly 'undefined'` при обращении к необязательным полям модели конфига
|
наличия шага проверяются приёмкой.
|
||||||
Hysteria в шаблоне) плюс несовместимость `DefaultRow` с `PeerVo` в слотах
|
|
||||||
таблицы пиров.
|
|
||||||
|
|
||||||
Это не дефект безопасности и не блокер релиза: ошибки существуют в коде уже
|
До v1 этой гарантии не было. `build:prod` означал `vite build && vue-tsc
|
||||||
сейчас и ни на что в рантайме не влияют. Но обновление typechecker'а тянет за
|
--noEmit`, но `vue-tsc` был версии `0.35.0` (2022 год) и шаблоны Vue
|
||||||
собой обновление `vue` (3.2 → 3.5, иначе `vue-tsc` 2.x не разбирает
|
практически не типизировал: проверка проходила зелёной, не давая гарантии,
|
||||||
`JSX.IntrinsicElements`), а за ним — `element-plus`, `pinia` и `vue-router`.
|
которую обещает. Хуже того — на Vue 3.5 она ломается сама, потому что не знает
|
||||||
То есть это отдельная работа с собственной проверкой на живой панели, а не
|
`vue/jsx-runtime`, то есть пережить обновление Vue всё равно не могла.
|
||||||
строка в security-патче.
|
|
||||||
|
|
||||||
Ограничение на безопасность не влияет: уязвимые пакеты frontend обновляются
|
Современный `vue-tsc 3.3` на том же коде дал **142 ошибки**: 141 × `TS18048`
|
||||||
независимо от версии typechecker'а, движением lockfile внутри уже объявленных
|
(«possibly undefined» при обращении к необязательным секциям конфига Hysteria в
|
||||||
диапазонов (см. `pnpm.overrides` в `apps/frontend/package.json`).
|
шаблоне) и одна `TS2322`, всё в двух файлах представления Hysteria. Ожидавшегося
|
||||||
|
класса «`DefaultRow` несовместим с `PeerVo`» на Element Plus 2.3 не было вовсе —
|
||||||
|
он появился позже, вместе с обновлением Element Plus до 2.14, где слоты таблицы
|
||||||
|
типизированы строже.
|
||||||
|
|
||||||
|
Закрыто это не подавлением, а границей: `api/config/hysteriaViewModel.ts`
|
||||||
|
превращает ответ сервера в модель, где присутствие каждой секции — свойство
|
||||||
|
типа. Подробности — в [docs/04](04-admin-panel.md).
|
||||||
|
|
||||||
|
Контракт теперь читается так:
|
||||||
|
|
||||||
|
> проверка типов SFC-шаблонов проходит, и это доказывает сборка, а не намерение.
|
||||||
|
|
||||||
### Проверка зависимостей на уязвимости
|
### Проверка зависимостей на уязвимости
|
||||||
|
|
||||||
|
|||||||
@@ -354,6 +354,70 @@ AES-GCM. Значение без этого префикса — не «форм
|
|||||||
- будущие версии Hysteria не ломают экспорт только потому, что backend и frontend ещё не научились показывать новый параметр;
|
- будущие версии Hysteria не ломают экспорт только потому, что backend и frontend ещё не научились показывать новый параметр;
|
||||||
- это прямое следствие модели «latest stable на сборке»: схема upstream может опережать модель HY2XS.
|
- это прямое следствие модели «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 ради
|
||||||
|
перетаскивания тегов в недоступной для редактирования форме.
|
||||||
|
|
||||||
### Санитайз экспорта
|
### Санитайз экспорта
|
||||||
|
|
||||||
Экспортируемый файл покидает сервер, поэтому секреты из него вырезаются:
|
Экспортируемый файл покидает сервер, поэтому секреты из него вырезаются:
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ cd orchestrator && bun install --frozen-lockfile && bun run check && bun test
|
|||||||
# Тесты и статический анализ HY2XS admin
|
# Тесты и статический анализ HY2XS admin
|
||||||
cd apps && go vet ./... && go test ./...
|
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)
|
# Полный E2E с реальным клиентом Hysteria (Debian 13 amd64; нужен Go)
|
||||||
HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
|
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`
|
41. `apps/go.mod` объявляет `toolchain`, совпадающий с `GO_VERSION` из `versions.env`
|
||||||
42. `tools/dev/doctor.sh` / `doctor.ps1` показывают расхождение среды разработки с `versions.env`
|
42. `tools/dev/doctor.sh` / `doctor.ps1` показывают расхождение среды разработки с `versions.env`
|
||||||
43. маршруты-алиасы `/:id/client-url` и `/:id/qr` удалены и не входят в публичный API v1
|
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 не содержит элементов управления, которые ничего не сохраняют
|
||||||
|
|||||||
@@ -187,6 +187,34 @@ run_fix20_acceptance_subset() {
|
|||||||
log_step "Acceptance: bootstrap peer can pass auth smoke"
|
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"
|
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"
|
log_step "Acceptance: local SVG icons are rendered from an in-repo sprite"
|
||||||
# vite-plugin-svg-icons убран: он не обновлялся с 2022 года и тянул svgo 2.8,
|
# vite-plugin-svg-icons убран: он не обновлялся с 2022 года и тянул svgo 2.8,
|
||||||
# postcss 5.2.18 и image-size 0.5.5, у последней advisory прямо сообщает
|
# postcss 5.2.18 и image-size 0.5.5, у последней advisory прямо сообщает
|
||||||
|
|||||||
@@ -94,6 +94,18 @@ bundle_ui() {
|
|||||||
log_info "Frontend Node.js options: $NODE_OPTIONS"
|
log_info "Frontend Node.js options: $NODE_OPTIONS"
|
||||||
|
|
||||||
"$PNPM_BIN" install --frozen-lockfile
|
"$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
|
"$PNPM_BIN" run build:prod
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user