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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user