build(frontend): Vite 4.3.1 -> 7.3.6 и уборка мёртвых редакторов конфига

Решение «Vite не трогать» пересмотрено по данным, а не по желанию обновиться.
`pnpm audit --prod` (то, на что смотрит gate сборки) был чист, но полный audit
показывал в build-цепочке два critical и rollup GHSA DOM clobbering — а он
затрагивает ГЕНЕРИРУЕМЫЙ bundle, то есть уезжает в production. Vite 4.3.1 тянул
rollup 3 без исправления.

Взята 7.3.6 — последняя линия до Rolldown. Vite 8 по-прежнему не берётся: это
смена бандлера, отдельная работа с собственной приёмкой.

Вместе с Vite обновлена цепочка плагинов (@vitejs/plugin-vue 4 -> 6,
unplugin-* и unocss с версий 2022-2023 годов) и линтеры (@typescript-eslint
5 -> 8, prettier 2 -> 3, stylelint 15 -> 17). Новый @typescript-eslint нашёл 7
настоящих замечаний — исправлены по существу, а не подавлением правил; среди
них подавление несуществующего правила ban-types и `{}` вместо `object`.

Отдельно — три мёртвых редактора. Outbounds уже был разобран, здесь то же
самое для ImputMultiple (ACME-домены, inline ACL) и MapAdd (параметры ACME DNS,
заголовки masquerade): оба редактировали конфиг Hysteria в форме с
:disabled="true", получали значения без v-model и эмитили события, которых
никто не слушает, на странице без единого маршрута записи.

Цена ImputMultiple была измеримой. vuedraggable поставляется UMD-сборкой,
поэтому её require("vue") разрешался в vue/dist/vue.cjs.prod.js — полную сборку
Vue с рантайм-компилятором шаблонов. В бандл уезжало ~500 КБ исходников
(vuedraggable + sortablejs + compiler-core + compiler-dom) ради перетаскивания
тегов в недоступной для редактирования форме.

Разбор bundle через sourcemap нашёл и вторую потерю: @vueuse/core собирался
дважды — наш 13.9.0 и 14.4.0 из element-plus. Версии сведены.

bundle: 2 813 525 против 2 404 202 на исходной базовой линии (+17% за Vue 3.5,
Element Plus 2.14, echarts 6 и rollup 4). Промежуточное состояние без этих двух
исправлений было 3 046 172.

pnpm audit --prod: чисто. typecheck/eslint/stylelint: 0. go build с dist: OK.
This commit is contained in:
2026-08-30 07:29:53 +05:00
parent 7f4cc10d25
commit ddd1020a3a
22 changed files with 2393 additions and 2702 deletions
+21 -152
View File
@@ -1,71 +1,9 @@
<template>
<div class="flex gap-2">
<el-tag
:key="key"
v-for="(value, key) in mapObject"
@close="handleClose(key)"
@click="handleInfo(key)"
size="large"
closable
>
{{ key }}
<el-tag v-for="entry in entries" :key="entry.key" size="large">
{{ entry.key }}: {{ entry.value }}
</el-tag>
<el-button @click="handleAdd">+</el-button>
<el-dialog
:title="dialog.title"
v-model="dialog.visible"
width="600px"
append-to-body
@close="closeDialog"
>
<el-form
ref="dataFormRef"
:rules="dataFormRules"
label-position="top"
:model="dataForm"
>
<el-form-item label="key" prop="key">
<el-input v-model="dataForm.key" clearable />
</el-form-item>
<el-form-item label="value" prop="value">
<el-input v-model="dataForm.value" clearable />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"
>{{ $t("common.confirm") }}
</el-button>
<el-button @click="closeDialog">{{ $t("common.cancel") }}</el-button>
</div>
</template>
</el-dialog>
<el-dialog
:title="infoDialog.title"
v-model="infoDialog.visible"
width="600px"
append-to-body
@close="infoDialog.visible = false"
>
<el-form label-position="top">
<el-form-item label="key" prop="key">
<el-tag>{{ dataInfo.key }}</el-tag>
</el-form-item>
<el-form-item label="value" prop="value">
<el-tag>{{ dataInfo.value }}</el-tag>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="infoDialog.visible = false"
>{{ $t("common.cancel") }}
</el-button>
</div>
</template>
</el-dialog>
<el-text v-if="!entries.length" type="info"></el-text>
</div>
</template>
@@ -77,13 +15,22 @@ export default {
<script setup lang="ts">
import { PropType } from "vue";
import { useI18n } from "vue-i18n";
interface Form {
key: string;
value: string;
}
/**
* Словарь «ключ — значение» из конфигурации: параметры ACME DNS-провайдера,
* заголовки masquerade.
*
* Как и ImputMultiple, компонент только показывает содержимое. Здесь был
* редактор с диалогом добавления, closable-тегами и `emit("update:mapObject")`,
* и он ничего не сохранял по тем же трём причинам: форма конфига Hysteria
* отрисована с `:disabled="true"`, значения передаются как
* `:map-object="dataForm.acme.dns.config"` без v-model, маршрутов записи
* серверного конфига в API нет.
*
* Значение показывается рядом с ключом: прежний вариант прятал его во второй
* диалог по клику на тег, что для read-only страницы лишний шаг — увидеть
* настройку сервера нужно сразу.
*/
const props = defineProps({
mapObject: {
required: false,
@@ -92,87 +39,9 @@ const props = defineProps({
},
});
const emit = defineEmits<{
(event: "update:mapObject", value: { [key: string]: string }): void;
}>();
const mapObject = useVModel(props, "mapObject", emit);
const { t } = useI18n();
const dataFormRef = ref(ElForm);
const dataFormRules = {
key: [
{
required: true,
message: t("common.required"),
trigger: ["change", "blur"],
},
],
value: [
{
required: true,
message: t("common.required"),
trigger: ["change", "blur"],
},
],
};
const state = reactive({
dataForm: {
key: "",
value: "",
} as Form,
dialog: {
title: "Add",
visible: false,
} as DialogType,
infoDialog: {
title: "Info",
visible: false,
},
dataInfo: {
key: "",
value: "",
} as Form,
});
const { dataForm, dialog, infoDialog, dataInfo } = toRefs(state);
const handleAdd = () => {
state.dialog.visible = true;
};
const handleClose = (key: string): void => {
delete mapObject.value[key];
};
const handleInfo = (key: string) => {
state.dataInfo = {
key: key,
value: mapObject.value[key] || "",
};
state.infoDialog.visible = true;
};
const submitForm = () => {
dataFormRef.value.validate((valid: any) => {
if (valid) {
if (mapObject.value[state.dataForm.key]) {
ElMessage.error(t("common.invalid"));
return;
}
mapObject.value[state.dataForm.key] = state.dataForm.value;
closeDialog();
}
});
};
const closeDialog = (): void => {
state.dialog.visible = false;
dataFormRef.value.resetFields();
dataFormRef.value.clearValidate();
};
const entries = computed(() =>
Object.entries(props.mapObject ?? {}).map(([key, value]) => ({ key, value }))
);
</script>
<style lang="scss" scoped>