build(frontend): убрать неподдерживаемый плагин иконок и EOL-линтеры, audit до нуля
vite-plugin-svg-icons не обновлялся с 2022 года и был единственным источником половины оставшихся предупреждений: svgo 2.8, postcss 5.2.18, braces 2.3.2 и image-size 0.5.5 — у последней advisory прямо сообщает `Patched versions: <0.0.0`, то есть исправленной версии не существует. Задача, которую он решал, заняла один модуль: import.meta.glob собирает семнадцать локальных SVG в скрытый спрайт со <symbol>. Компонентный подход (unplugin-icons) здесь не подходит — имя иконки часто вычисляется в рантайме (onlyOneChild.meta.icon, isFullscreen ? ... : ...), а поиск по id нужен именно для этого. API <svg-icon icon-class="..."> не изменился. Проверка на реальных ассетах поймала то, что иначе уехало бы в релиз: три иконки из семнадцати не объявляют viewBox, задавая только width/height. Без viewBox <use> рисует иконку в натуральную величину и обрезает её. Плагин синтезировал viewBox сам; теперь это делает sprite.ts, а инвариант закреплён приёмкой — иконка без обоих способов задать координаты роняет сборку. Отдельно: typecheck поймал две ошибки уже в самом sprite.ts (SVGSVGElement против HTMLElement из getElementById). Ровно то, ради чего он возвращался. eslint 8 объявлен EOL и оставался вторым источником предупреждений. Переход на eslint 10 потребовал flat config: eslintrc в 9 работает только через переменную окружения, а в 10 удалён, поэтому обновление версии без смены формата было бы отсрочкой на релиз. Набор правил сохранён прежним; .eslintignore свёрнут в ignores, как требует новый формат. Проверено, что flat config действительно линтит, а не молча пропускает: пробный файл с неиспользуемой переменной и необъявленным именем даёт обе ошибки и в .ts, и в .vue. Оставшиеся предупреждения приходили из графов самих eslint 10 и stylelint 17, то есть уже последних версий, — закрыты pnpm.overrides. Override для ajv ограничен `table>ajv`: глобальный ломал eslint, который использует ajv 6. pnpm audit по всему графу: 33 предупреждения -> 0. typecheck/eslint/stylelint/build: 0. go build и go test с встроенным dist: OK.
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
dist
|
||||
node_modules
|
||||
public
|
||||
.vscode
|
||||
.idea
|
||||
*.sh
|
||||
*.md
|
||||
|
||||
src/assets
|
||||
|
||||
.eslintrc.cjs
|
||||
.prettierrc.cjs
|
||||
.stylelintrc.cjs
|
||||
@@ -1,31 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
parser: "vue-eslint-parser", // https://eslint.vuejs.org/user-guide/#bundle-configurations
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-essential",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"./.eslintrc-auto-import.json",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
parser: "@typescript-eslint/parser",
|
||||
},
|
||||
plugins: ["vue", "@typescript-eslint"],
|
||||
rules: {
|
||||
"vue/multi-word-component-names": "off", // Disable multi-word component name requirement: https://eslint.vuejs.org/rules/multi-word-component-names.html
|
||||
"@typescript-eslint/no-empty-function": "off", // Disable empty function checks
|
||||
"@typescript-eslint/no-explicit-any": "off", // Disable warnings for any
|
||||
"vue/no-v-model-argument": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
}, // https://eslint.org/docs/latest/use/configure/language-options#specifying-globals
|
||||
globals: {
|
||||
DialogType: "readonly",
|
||||
OptionType: "readonly",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginVue from "eslint-plugin-vue";
|
||||
import prettier from "eslint-config-prettier";
|
||||
import vueParser from "vue-eslint-parser";
|
||||
|
||||
/**
|
||||
* Конфигурация ESLint во flat-формате.
|
||||
*
|
||||
* Заменяет `.eslintrc.cjs`. Причина не в моде на формат: eslint 8 объявлен
|
||||
* EOL, перестал получать исправления и оставался последним источником десятка
|
||||
* предупреждений `pnpm audit` в проекте (minimatch, brace-expansion, js-yaml,
|
||||
* flatted — всё в его собственном графе). Eslintrc-формат в eslint 9 работает
|
||||
* только через переменную окружения, а в 10 удалён вовсе, поэтому обновление
|
||||
* версии без перехода на flat-config было бы отсрочкой на один релиз.
|
||||
*
|
||||
* Набор правил сохранён ровно тем же, что был в `.eslintrc.cjs`: перевод
|
||||
* формата не место для изменения политики линтинга.
|
||||
*/
|
||||
|
||||
// Глобальные имена авто-импорта генерирует unplugin-auto-import в
|
||||
// eslintrc-формате (`{ globals: { ref: true, ... } }`). Flat-config такой файл
|
||||
// через `extends` не подключает, поэтому список читается напрямую — источник
|
||||
// истины остаётся один, и он по-прежнему пересоздаётся сборкой.
|
||||
const require = createRequire(import.meta.url);
|
||||
const autoImportGlobals = require("./.eslintrc-auto-import.json").globals;
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
// Перенесено из .eslintignore: eslint 9 этот файл больше не читает, и
|
||||
// список исключений обязан жить в самой конфигурации.
|
||||
//
|
||||
// src/assets — SVG и изображения; src/types/*.d.ts — объявления, которые
|
||||
// генерируют unplugin-auto-import и unplugin-vue-components, править их
|
||||
// вручную нельзя, а замечания линтера по ним только шумят.
|
||||
ignores: [
|
||||
"dist/**",
|
||||
"node_modules/**",
|
||||
"public/**",
|
||||
"src/assets/**",
|
||||
"src/types/*.d.ts",
|
||||
],
|
||||
},
|
||||
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...pluginVue.configs["flat/essential"],
|
||||
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
...autoImportGlobals,
|
||||
// Объявлены в src/types/global.d.ts и используются как глобальные типы.
|
||||
DialogType: "readonly",
|
||||
OptionType: "readonly",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Имя компонента из одного слова — устоявшееся соглашение этого проекта.
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/no-v-model-argument": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
// .vue разбирает vue-eslint-parser, а <script lang="ts"> внутри него —
|
||||
// парсер TypeScript. Без второй половины содержимое блока не разбирается.
|
||||
files: ["**/*.vue"],
|
||||
languageOptions: {
|
||||
parser: vueParser,
|
||||
parserOptions: {
|
||||
parser: tseslint.parser,
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Последним: отключает правила форматирования, которыми владеет prettier.
|
||||
//
|
||||
// Ни один из включённых наборов таких правил сейчас не содержит, так что это
|
||||
// страховка, а не исправление: форматированием занимается отдельный скрипт
|
||||
// lint:prettier, и правило-дубликат в линтере привело бы к двум источникам
|
||||
// истины о стиле.
|
||||
prettier
|
||||
);
|
||||
@@ -11,7 +11,7 @@
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"build:prod": "vite build --mode production",
|
||||
"verify": "pnpm run typecheck && pnpm run build:prod",
|
||||
"lint:eslint": "eslint --fix --ext .ts,.js,.vue ./src ",
|
||||
"lint:eslint": "eslint --fix ./src",
|
||||
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
|
||||
"lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix"
|
||||
},
|
||||
@@ -32,17 +32,16 @@
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "10.0.1",
|
||||
"@iconify-json/ep": "^1.2.4",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/path-browserify": "^1.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.68.0",
|
||||
"@typescript-eslint/parser": "^8.68.0",
|
||||
"@vitejs/plugin-vue": "^6.0.8",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"eslint": "^10.9.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-vue": "^10.10.0",
|
||||
"globals": "17.11.0",
|
||||
"postcss": "^8.5.26",
|
||||
"postcss-html": "^2.0.0",
|
||||
"postcss-scss": "^4.0.9",
|
||||
@@ -57,18 +56,23 @@
|
||||
"stylelint-config-standard-scss": "^17.0.0",
|
||||
"stylelint-order": "8.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "8.68.0",
|
||||
"unocss": "^66.8.1",
|
||||
"unplugin-auto-import": "^21.1.0",
|
||||
"unplugin-icons": "^23.0.1",
|
||||
"unplugin-vue-components": "^32.1.0",
|
||||
"vite": "^7.3.6",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vue-eslint-parser": "10.4.1",
|
||||
"vue-tsc": "^3.3.11"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"lodash": ">=4.18.1",
|
||||
"lodash-es": ">=4.18.1"
|
||||
"lodash-es": ">=4.18.1",
|
||||
"brace-expansion": ">=1.1.18",
|
||||
"picomatch": ">=2.3.2",
|
||||
"js-yaml": ">=4.3.1",
|
||||
"table>ajv": ">=8.18.0"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
|
||||
Generated
+285
-1624
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Спрайт локальных SVG-иконок.
|
||||
*
|
||||
* Заменяет `vite-plugin-svg-icons` и его `virtual:svg-icons-register`.
|
||||
*
|
||||
* Почему плагин убран. Он не обновлялся с 2022 года и тянул за собой цепочку,
|
||||
* которую починить нельзя: `svgo@2.8.0`, `postcss@5.2.18`, `braces@2.3.2` и
|
||||
* `image-size@0.5.5` — у последней в advisory прямо указано `Patched versions:
|
||||
* <0.0.0`, то есть исправленной версии не существует. Один build-плагин
|
||||
* оставался единственным источником половины оставшихся предупреждений
|
||||
* `pnpm audit` во всём проекте.
|
||||
*
|
||||
* Задача, которую он решал, здесь занимает сорок строк: собрать семнадцать
|
||||
* файлов в один скрытый `<svg>` со `<symbol>` внутри, чтобы `<use href="#...">`
|
||||
* находил их по имени. Имя часто вычисляется в рантайме
|
||||
* (`onlyOneChild.meta.icon`, `isFullscreen ? 'exit-fullscreen' : 'fullscreen'`),
|
||||
* поэтому подход «компонент на иконку», как у unplugin-icons, здесь не
|
||||
* подходит — спрайт с поиском по id нужен именно для этого.
|
||||
*
|
||||
* Оптимизация через SVGO при этом потеряна. Для семнадцати вручную отобранных
|
||||
* иконок это несколько килобайт, и они не стоят неисправимой зависимости в
|
||||
* сборке.
|
||||
*/
|
||||
|
||||
const SYMBOL_PREFIX = "icon";
|
||||
const SPRITE_ELEMENT_ID = "__hy2xs_svg_sprite__";
|
||||
|
||||
// eager: файлы читаются на этапе сборки и попадают в бандл строками, сетевых
|
||||
// запросов за иконками не возникает.
|
||||
const sources = import.meta.glob<string>("@/assets/icons/*.svg", {
|
||||
query: "?raw",
|
||||
import: "default",
|
||||
eager: true,
|
||||
});
|
||||
|
||||
function iconName(filePath: string): string {
|
||||
return filePath.replace(/^.*\//, "").replace(/\.svg$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Превращает содержимое файла в `<symbol>`.
|
||||
*
|
||||
* Отбрасываются XML-пролог и DOCTYPE: внутри уже существующего документа они
|
||||
* не только бесполезны, но и делают разметку невалидной. `width` и `height`
|
||||
* тоже отбрасываются — размер задаёт компонент.
|
||||
*
|
||||
* `viewBox` обязателен: без него `<use>` не знает систему координат иконки и
|
||||
* рисует её в натуральную величину, обрезая по размеру родительского `<svg>`.
|
||||
* Три иконки из семнадцати (eye, fullscreen, exit-fullscreen) его не имеют и
|
||||
* задают только width/height, поэтому viewBox для них синтезируется — ровно
|
||||
* так же, как это делал заменённый плагин.
|
||||
*/
|
||||
function toSymbol(raw: string, name: string): string {
|
||||
const withoutProlog = raw
|
||||
.replace(/<\?xml[\s\S]*?\?>/gi, "")
|
||||
.replace(/<!DOCTYPE[\s\S]*?>/gi, "")
|
||||
.replace(/<!--[\s\S]*?-->/g, "")
|
||||
.trim();
|
||||
|
||||
const openTag = withoutProlog.match(/<svg\b[^>]*>/i);
|
||||
if (!openTag) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const body = withoutProlog
|
||||
.replace(/^<svg\b[^>]*>/i, "")
|
||||
.replace(/<\/svg>\s*$/i, "");
|
||||
|
||||
const viewBoxAttr = resolveViewBox(openTag[0]);
|
||||
return `<symbol id="${SYMBOL_PREFIX}-${name}"${viewBoxAttr}>${body}</symbol>`;
|
||||
}
|
||||
|
||||
function resolveViewBox(openTag: string): string {
|
||||
const declared = openTag.match(/viewBox="([^"]+)"/i);
|
||||
if (declared) {
|
||||
return ` viewBox="${declared[1]}"`;
|
||||
}
|
||||
|
||||
const width = numericAttribute(openTag, "width");
|
||||
const height = numericAttribute(openTag, "height");
|
||||
if (width !== null && height !== null) {
|
||||
return ` viewBox="0 0 ${width} ${height}"`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/** Читает размер, игнорируя единицы измерения: `128`, `128px`, `128pt`. */
|
||||
function numericAttribute(openTag: string, name: string): number | null {
|
||||
const match = openTag.match(new RegExp(`${name}="([\\d.]+)[a-z%]*"`, "i"));
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const value = Number.parseFloat(match[1]);
|
||||
return Number.isFinite(value) && value > 0 ? value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Вставляет спрайт в документ. Идемпотентна: повторный вызов заменяет
|
||||
* содержимое, а не добавляет второй элемент с теми же id.
|
||||
*/
|
||||
export function registerSvgIcons(): void {
|
||||
if (typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const symbols = Object.entries(sources)
|
||||
.map(([filePath, raw]) => toSymbol(raw, iconName(filePath)))
|
||||
.filter(Boolean)
|
||||
.join("");
|
||||
|
||||
// Тип уточняется явно: getElementById возвращает HTMLElement | null, а
|
||||
// спрайт — SVGSVGElement, и без этого он не примет ни собственный
|
||||
// createElementNS, ни последующие обращения.
|
||||
let sprite = document.getElementById(
|
||||
SPRITE_ELEMENT_ID
|
||||
) as SVGSVGElement | null;
|
||||
|
||||
if (!sprite) {
|
||||
sprite = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
sprite.id = SPRITE_ELEMENT_ID;
|
||||
sprite.setAttribute("aria-hidden", "true");
|
||||
// Скрываем так же, как это делал плагин: элемент обязан остаться в
|
||||
// документе, иначе `<use>` не найдёт symbol, поэтому display:none не
|
||||
// подходит — нужен нулевой размер.
|
||||
sprite.setAttribute(
|
||||
"style",
|
||||
"position:absolute;width:0;height:0;overflow:hidden"
|
||||
);
|
||||
document.body.insertBefore(sprite, document.body.firstChild);
|
||||
}
|
||||
|
||||
sprite.innerHTML = symbols;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { setupDirective } from "@/directive";
|
||||
import "@/permission";
|
||||
|
||||
// Локальные SVG-иконки
|
||||
import "virtual:svg-icons-register";
|
||||
import { registerSvgIcons } from "@/components/SvgIcon/sprite";
|
||||
|
||||
// Локализация
|
||||
import i18n from "@/lang/index";
|
||||
@@ -16,6 +16,8 @@ import i18n from "@/lang/index";
|
||||
import "@/styles/index.scss";
|
||||
import "uno.css";
|
||||
|
||||
registerSvgIcons();
|
||||
|
||||
const app = createApp(App);
|
||||
// Глобальная регистрация пользовательских директив
|
||||
setupDirective(app);
|
||||
|
||||
@@ -9,8 +9,6 @@ import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
|
||||
import Icons from "unplugin-icons/vite";
|
||||
import IconsResolver from "unplugin-icons/resolver";
|
||||
|
||||
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
|
||||
|
||||
import UnoCSS from "unocss/vite";
|
||||
|
||||
import path from "path";
|
||||
@@ -123,13 +121,6 @@ export default defineConfig((): UserConfig => {
|
||||
// Auto install icon collections
|
||||
autoInstall: true,
|
||||
}),
|
||||
|
||||
createSvgIconsPlugin({
|
||||
// Icon folders to cache
|
||||
iconDirs: [path.resolve(pathSrc, "assets/icons")],
|
||||
// symbolId format
|
||||
symbolId: "icon-[dir]-[name]",
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
|
||||
Reference in New Issue
Block a user