@nitra/cursor 1.8.169 → 1.8.170
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -1
- package/mdc/image.mdc +66 -9
- package/package.json +2 -2
- package/scripts/check-image.mjs +187 -34
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.8.170] - 2026-05-03
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `image.mdc` (v1.4) / `check-image.mjs`: правило перейшло на split-cache `@nitra/minify-image` ≥ **3.2.0**. Замість єдиного `.minify-image-cache.tsv` (який раніше мав бути або в `.gitignore`, або у `files`) тепер: (а) `.n-minify-image.tsv` у корені — committed source of truth з SHA-1/originalSize/size; правило вимагає, щоб він НЕ був у `.gitignore`; (б) `node_modules/.cache/@nitra/minify-image/mtime.tsv` — локальний fast-path, авто-gitignored через `node_modules/`, окремої перевірки не потребує. Додано міграційний fail: якщо `.minify-image-cache.tsv` лежить у корені або згадується в `.gitignore` — підказка з командою `git rm --cached` + `rm -f`. README + image.mdc-секція `## Split-cache` пояснюють, чому коміт hash-кешу осмислений (переживає `git clone`/`checkout`, на відміну від mtime).
|
|
12
|
+
|
|
7
13
|
## [1.8.169] - 2026-05-03
|
|
8
14
|
|
|
9
15
|
### Added
|
|
10
16
|
|
|
11
|
-
- `image.mdc` (v1.
|
|
17
|
+
- `image.mdc` (v1.3) / `check-image.mjs`: нове правило `image` для оптимізації зображень через [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image). Перевіряє лише локальну конфігурацію (CI-workflow не вимагається — sharp/svgo тягнуть бінарні залежності, цінність на ubuntu-runner-ах нижча за час прогону): скрипт `lint-image` у `package.json` з обовʼязковим викликом `npx @nitra/minify-image --src=. --write --avif` (авто-оптимізація на місці + AVIF-двійники для PNG/JPEG/GIF), `bun run lint-image` в агрегованому `lint`, заборона `@nitra/minify-image` у `dependencies`/`devDependencies` (CLI лише через `npx`, симетрично до `markdownlint-cli2` у `text.mdc`) і рядок `.minify-image-cache.tsv` у `.gitignore` (або, рідше, у `files` пакета). AVIF-двійники (`<name>.<ext>.avif`) зберігаються в git як готові артефакти для віддачі браузеру.
|
|
18
|
+
- `image.mdc` (v1.3) / `check-image.mjs`: у `.vue` файлах кожного workspace-пакета raster-посилання мають вести на AVIF-двійник (`...png.avif`) у двох формах: (а) `import x from '...png|jpg|jpeg|gif'` (далі `:src="x"`); (б) прямі статичні атрибути `<img src="...png" />` у `<template>` (Vite перетворює їх на asset-імпорти при збірці). Реактивне `:src="..."` не сканується (JS-вираз — резолвиться через імпорт, який ловиться у формі (а)); `data-src=`, `obj.src=` у `<script>`, SVG-імпорти теж пропускаємо. Опт-аут на рівні воркспейс-пакета: `"@nitra/minify-image": { "disable-avif": true }` у `package.json` цього пакета. Дедуплікація обходу: при walk-у кореня `.` піддерева інших workspace-роди пропускаються (інакше `App.vue` у `demo/` доповідався б двічі).
|
|
12
19
|
- `auto-rules.mjs` / `auto-rules.md`: введено граф залежностей між правилами (`AUTO_RULE_DEPENDENCIES`, синтаксис у `auto-rules.md` — `rule - [other]`). Правило `image` описане як `image - [vue]` — варто автододати лише разом з `vue`, без дублювання вихідної умови «`.vue`-файли». Транзитивне розгортання дозволяє ланцюги (`a → b → c`) і поважає `disable-rules` (якщо vue вимкнено — image теж не додається).
|
|
13
20
|
- `vue.mdc` (v1.4) / `check-vue.mjs`: посилено перевірку `vite.config` — окрім згадки `AutoImport` тепер вимагається, щоб у виклику `AutoImport({ imports: [...] })` був присутній рядковий елемент `'vue'`. Без цього `unplugin-auto-import` не надасть `ref` / `createApp` / тощо, і прибирати явні value-імпорти з `'vue'` стає небезпечно (зламає код). Якщо `'vue'` у `imports` відсутній — value-імпорти більше не оголошуються забороненими, а fail зʼявляється на конфізі vite. Балансована екстракція аргументів `AutoImport(...)` через `extractAutoImportCallArgs` працює для багаторядкових об'єктів.
|
|
14
21
|
|
package/mdc/image.mdc
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Оптимізація зображень через @nitra/minify-image у локальному lint
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.4'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
CLI [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image) запускається через `npx` (як `markdownlint-cli2` у text.mdc) і **не** додається в `dependencies` / `devDependencies`. Канонічний `lint-image` — авто-оптимізація з прапорцями `--write --avif`: стискає raster/SVG на місці й створює AVIF-двійники (`<name>.<ext>.avif`) поряд з кожним PNG/JPEG/GIF.
|
|
7
|
+
CLI [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image) (≥ **3.2.0**) запускається через `npx` (як `markdownlint-cli2` у text.mdc) і **не** додається в `dependencies` / `devDependencies`. Канонічний `lint-image` — авто-оптимізація з прапорцями `--write --avif`: стискає raster/SVG на місці й створює AVIF-двійники (`<name>.<ext>.avif`) поряд з кожним PNG/JPEG/GIF. Split-cache робить повторні прогони дешевими — і локально, і після `git clone`.
|
|
8
8
|
|
|
9
9
|
Перевірка лише локальна — у CI `lint-image` не запускаємо (sharp/svgo тягнуть бінарні залежності, цінність на ubuntu-runner-ах нижча за час прогону). Окремий workflow `lint-image.yml` створювати не треба.
|
|
10
10
|
|
|
@@ -21,22 +21,79 @@ CLI [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image)
|
|
|
21
21
|
|
|
22
22
|
Якщо в `package.json` уже є агрегований `lint`, додай у його ланцюжок `bun run lint-image` (як `bun run lint-text`, `bun run lint-js`, `bun run lint-ga`). Так розробник, що локально гонить `bun run lint`, перед фіксацією одразу бачить, чи зросли зображення, і отримує свіжі AVIF-двійники.
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Split-cache
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Починаючи з `@nitra/minify-image` **3.2.0** кеш розбитий на два файли з різною семантикою:
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
### `.n-minify-image.tsv` — source of truth у git
|
|
29
|
+
|
|
30
|
+
У корені сканованого каталогу. Формат: `<rel-path>\t<sha1-hex>\t<originalSize>\t<size>`.
|
|
31
|
+
|
|
32
|
+
Slow-path і джерело даних для `Project lifetime savings`. **Має бути в git** — після `git clone` чи `git checkout` (mtime скидається на час checkout-у) CLI читає файл, рахує SHA-1 і порівнює зі збереженим у TSV хешем; на match локальний mtime-кеш зігрівається без reprocess. Рядки відсортовані алфавітно, hash і size змінюються лише при реальній зміні контенту — diff чистий.
|
|
33
|
+
|
|
34
|
+
### `node_modules/.cache/@nitra/minify-image/mtime.tsv` — локальний fast-path
|
|
35
|
+
|
|
36
|
+
Формат: `<rel-path>\t<mtime>\t<size>`. При збігу `(size, mtime)` CLI пропускає файл без читання — константа per-file.
|
|
37
|
+
|
|
38
|
+
Лежить під `node_modules/`, тож **авто-gitignored** за конвенцією JS-tooling-у (так кешуються ESLint, Babel, webpack, Turbo). Окремий рядок у `.gitignore` не потрібен. `rm -rf node_modules` зносить — наступний запуск відновлює його через slow-path проти `.n-minify-image.tsv`, без reprocess.
|
|
39
|
+
|
|
40
|
+
### Міграція з versions < 3.2
|
|
31
41
|
|
|
32
|
-
|
|
42
|
+
Старий єдиний `.minify-image-cache.tsv` (4 колонки `path\tmtime\toriginalSize\tsize`, зазвичай у `.gitignore`) автоматично читається при першому запуску для seed-у `originalSize` у `.n-minify-image.tsv` (lifetime savings не скидається). Після цього старий файл видаляють вручну:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git rm --cached .minify-image-cache.tsv 2>/dev/null || true
|
|
46
|
+
rm -f .minify-image-cache.tsv
|
|
47
|
+
# прибери відповідний рядок з .gitignore, якщо був
|
|
48
|
+
```
|
|
33
49
|
|
|
34
50
|
AVIF-двійники (`<name>.<ext>.avif`) **зберігаємо в git** — це готові артефакти для віддачі браузеру (без них ефект від `--avif` втрачається на чистому checkout-і).
|
|
35
51
|
|
|
52
|
+
## AVIF-імпорти у `.vue`
|
|
53
|
+
|
|
54
|
+
Раз `--avif` гарантує `<name>.<ext>.avif` поряд з кожним PNG/JPEG/GIF, у `.vue` файлах потрібно посилатись саме на AVIF-двійник, а не на оригінал:
|
|
55
|
+
|
|
56
|
+
```vue title="App.vue (правильно)"
|
|
57
|
+
<script setup>
|
|
58
|
+
import welcomeImage from './assets/welcome.png.avif'
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<img :src="welcomeImage" alt="Welcome" />
|
|
63
|
+
</template>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```vue title="App.vue (неправильно — втрачає AVIF)"
|
|
67
|
+
<script setup>
|
|
68
|
+
import welcomeImage from './assets/welcome.png'
|
|
69
|
+
</script>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Перевірка `check image` сканує `.vue` файли в кожному workspace-пакеті (root + workspaces) і вимагає AVIF-двійник для двох форм:
|
|
73
|
+
|
|
74
|
+
1. **Імпорт-пов'язані** — `import x from '...png|jpg|jpeg|gif'` (далі `:src="x"` у шаблоні).
|
|
75
|
+
2. **Прямі статичні** — `<img src="...png" />` у `<template>` (Vite перетворює такий шлях на asset-імпорт на етапі збірки, тож вимога та сама).
|
|
76
|
+
|
|
77
|
+
Реактивне `:src="..."` (з JS-виразом — змінною, тернарником, викликом тощо) **не сканується** — значення обчислюється у рантаймі й шлях туди потрапляє через імпорт або інший резолвинг, який ловить імпорт-перевірка вище. SVG не торкаємо (vector → AVIF безглуздо). Атрибути `data-src=`, `obj.src=` у `<script>` тощо також пропускаються.
|
|
78
|
+
|
|
79
|
+
### Опт-аут для конкретного пакета
|
|
80
|
+
|
|
81
|
+
У workspace-пакеті, де AVIF-імпорти небажані (наприклад, мобільний бандл, де AVIF-підтримка не гарантована), додай у `package.json` цього пакета:
|
|
82
|
+
|
|
83
|
+
```json title="apps/mobile/package.json"
|
|
84
|
+
{
|
|
85
|
+
"@nitra/minify-image": {
|
|
86
|
+
"disable-avif": true
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Тоді перевірка пропускає `.vue` файли цього пакета. У root-`package.json` опт-аут діє лише для файлів кореня (вкладені workspaces перевіряються незалежно — вмикай прапор у кожному пакеті, де треба).
|
|
92
|
+
|
|
36
93
|
## Заборонені залежності
|
|
37
94
|
|
|
38
95
|
`@nitra/minify-image` не повинен зʼявлятися ні в `dependencies`, ні в `devDependencies` кореневого `package.json` — CLI запускається лише через `npx` (так само, як `markdownlint-cli2` у text.mdc). Якщо потрібен явний пін — закладай діапазон версій npm у самому виклику (`npx @nitra/minify-image@^3 --src=. --write --avif`).
|
|
39
96
|
|
|
40
97
|
## Перевірка
|
|
41
98
|
|
|
42
|
-
`npx @nitra/cursor check image` (охоплює `lint-image`
|
|
99
|
+
`npx @nitra/cursor check image` (охоплює `lint-image` з обовʼязковими `--src=.`, `--write`, `--avif`, агрегований `lint`, `.n-minify-image.tsv` НЕ в `.gitignore` (має бути в git), відсутність застарілого `.minify-image-cache.tsv` у корені, AVIF-імпорти у `.vue` файлах кожного workspace-пакета).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/cursor",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.170",
|
|
4
4
|
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -49,6 +49,6 @@
|
|
|
49
49
|
"node": ">=25"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@nitra/cursor": "^1.8.
|
|
52
|
+
"@nitra/cursor": "^1.8.170"
|
|
53
53
|
}
|
|
54
54
|
}
|
package/scripts/check-image.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Перевіряє відповідність репозиторію правилу image.mdc для оптимізації зображень
|
|
3
|
-
* через `@nitra/minify-image` (локально — у CI лінт зображень не запускається).
|
|
3
|
+
* через `@nitra/minify-image` ≥ 3.2.0 (локально — у CI лінт зображень не запускається).
|
|
4
4
|
*
|
|
5
5
|
* Очікування:
|
|
6
6
|
* - у кореневому `package.json` є скрипт `lint-image`, який викликає `npx @nitra/minify-image`
|
|
@@ -9,19 +9,53 @@
|
|
|
9
9
|
* (симетрично до `lint-text`, `lint-js`, `lint-ga`);
|
|
10
10
|
* - `@nitra/minify-image` не оголошений у `dependencies`/`devDependencies` —
|
|
11
11
|
* CLI запускається лише через `npx` (як `markdownlint-cli2` у `text.mdc`);
|
|
12
|
-
* - `.minify-image
|
|
13
|
-
*
|
|
12
|
+
* - `.n-minify-image.tsv` (committed source of truth з sha1/originalSize/size) НЕ
|
|
13
|
+
* в `.gitignore` — він має бути в git. Локальний mtime-кеш у
|
|
14
|
+
* `node_modules/.cache/@nitra/minify-image/mtime.tsv` авто-gitignored через `node_modules/`,
|
|
15
|
+
* окремої перевірки не вимагає;
|
|
16
|
+
* - застарілий `.minify-image-cache.tsv` (з версій < 3.2) видалений з кореня — інакше
|
|
17
|
+
* проєкт лишається у напівпереміщеному стані;
|
|
18
|
+
* - у `.vue` файлах raster-імпорти (`.png` / `.jpg` / `.jpeg` / `.gif`) посилаються на
|
|
19
|
+
* AVIF-двійники (`...png.avif` тощо), оскільки `--avif` гарантує їх наявність поряд із
|
|
20
|
+
* оригіналами. Можна вимкнути на рівні воркспейс-пакета через `"@nitra/minify-image": {
|
|
21
|
+
* "disable-avif": true }` у його `package.json`.
|
|
14
22
|
*/
|
|
15
23
|
import { existsSync } from 'node:fs'
|
|
16
24
|
import { readFile } from 'node:fs/promises'
|
|
25
|
+
import { join, relative } from 'node:path'
|
|
17
26
|
|
|
18
27
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
28
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
29
|
+
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
19
30
|
|
|
20
31
|
/** Імʼя CLI-пакета: рядок у `lint-image` і заборонений у залежностях. */
|
|
21
32
|
const MINIFY_PACKAGE_NAME = '@nitra/minify-image'
|
|
22
33
|
|
|
23
|
-
/** Імʼя
|
|
24
|
-
const
|
|
34
|
+
/** Імʼя committed-кешу (sha1 + originalSize + size) у `@nitra/minify-image` ≥ 3.2.0. */
|
|
35
|
+
const HASH_CACHE_FILENAME = '.n-minify-image.tsv'
|
|
36
|
+
|
|
37
|
+
/** Імʼя застарілого 4-колонкового кешу (`@nitra/minify-image` < 3.2). Має бути видалений після міграції. */
|
|
38
|
+
const LEGACY_CACHE_FILENAME = '.minify-image-cache.tsv'
|
|
39
|
+
|
|
40
|
+
/** Поле в `package.json` для конфігу @nitra/minify-image (наприклад, `disable-avif`). */
|
|
41
|
+
const PKG_CONFIG_FIELD = '@nitra/minify-image'
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Регексп для імпортів raster-зображень у `.vue` файлах.
|
|
45
|
+
* Захоплює `import name from '...ext'` (як default, так і type-only форми не потрібні —
|
|
46
|
+
* type-imports asset-ів не існує). Захоплюється повний шлях у групі 1.
|
|
47
|
+
*/
|
|
48
|
+
const VUE_RASTER_IMPORT_RE = /import\s+\w[\w$]*\s+from\s+['"]([^'"\n]+\.(?:png|jpe?g|gif))['"]/giu
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Регексп для прямих посилань на raster-зображення у HTML-атрибуті `src="..."` шаблона `.vue`
|
|
52
|
+
* (наприклад `<img src="./hero.png" />`). Vite перетворює такі шляхи на asset-імпорти на етапі
|
|
53
|
+
* збірки, тож для них теж діє вимога вживати AVIF-двійник.
|
|
54
|
+
*
|
|
55
|
+
* Лукбехайнд `(?<![:\-_.])` виключає реактивне `:src="..."` (там JS-вираз — змінна або виклик,
|
|
56
|
+
* перевіряється через імпорт), `data-src="..."` і `obj.src=...` у `<script>`.
|
|
57
|
+
*/
|
|
58
|
+
const VUE_RASTER_STATIC_SRC_RE = /(?<![:\-_.])\bsrc\s*=\s*['"]([^'"\s]+\.(?:png|jpe?g|gif))['"]/giu
|
|
25
59
|
|
|
26
60
|
/**
|
|
27
61
|
* Перевіряє скрипт `lint-image` у `package.json`.
|
|
@@ -100,68 +134,187 @@ function checkMinifyImageNotInDeps(pkg, pass, fail) {
|
|
|
100
134
|
}
|
|
101
135
|
|
|
102
136
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
|
|
137
|
+
* Зчитує всі змістовні рядки `.gitignore` (без коментарів і порожніх). Якщо файла нема — `null`.
|
|
138
|
+
* @returns {Promise<string[] | null>} список trim-нутих рядків або `null`
|
|
139
|
+
*/
|
|
140
|
+
async function readGitignoreLines() {
|
|
141
|
+
if (!existsSync('.gitignore')) return null
|
|
142
|
+
const raw = await readFile('.gitignore', 'utf8')
|
|
143
|
+
return raw
|
|
144
|
+
.split('\n')
|
|
145
|
+
.map(l => l.trim())
|
|
146
|
+
.filter(l => l.length > 0 && !l.startsWith('#'))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Перевіряє, що `.n-minify-image.tsv` НЕ в `.gitignore` — він має бути в git
|
|
151
|
+
* (split-cache 3.2.0: source of truth для slow-path і lifetime savings).
|
|
152
|
+
*
|
|
153
|
+
* Сам факт існування файла НЕ вимагається — на свіжому проєкті без обробки
|
|
154
|
+
* зображень його ще нема, це нормально.
|
|
106
155
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
107
156
|
* @param {(msg: string) => void} fail callback при помилці
|
|
108
157
|
* @returns {Promise<void>}
|
|
109
158
|
*/
|
|
110
|
-
async function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
pass(`.gitignore містить ${CACHE_FILENAME}`)
|
|
119
|
-
return
|
|
120
|
-
}
|
|
159
|
+
async function checkHashCacheNotIgnored(pass, fail) {
|
|
160
|
+
const lines = await readGitignoreLines()
|
|
161
|
+
if (lines && lines.includes(HASH_CACHE_FILENAME)) {
|
|
162
|
+
fail(
|
|
163
|
+
`.gitignore: прибери рядок \`${HASH_CACHE_FILENAME}\` — це закомічений source of truth split-cache 3.2.0 (image.mdc)`
|
|
164
|
+
)
|
|
165
|
+
} else {
|
|
166
|
+
pass(`${HASH_CACHE_FILENAME} не в .gitignore (має бути в git)`)
|
|
121
167
|
}
|
|
122
|
-
|
|
123
|
-
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Перевіряє, що застарілий `.minify-image-cache.tsv` (з версій < 3.2) видалений
|
|
172
|
+
* з кореня. Якщо лежить — користувач не завершив міграцію на split-cache, що
|
|
173
|
+
* залишає файл як орфана у git-історії.
|
|
174
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
175
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
176
|
+
* @returns {Promise<void>}
|
|
177
|
+
*/
|
|
178
|
+
async function checkLegacyCacheRemoved(pass, fail) {
|
|
179
|
+
if (existsSync(LEGACY_CACHE_FILENAME)) {
|
|
180
|
+
fail(
|
|
181
|
+
`${LEGACY_CACHE_FILENAME} застарілий (split-cache 3.2.0) — видали: ` +
|
|
182
|
+
`\`git rm --cached ${LEGACY_CACHE_FILENAME} 2>/dev/null || true && rm -f ${LEGACY_CACHE_FILENAME}\` ` +
|
|
183
|
+
'(також прибери відповідний рядок з .gitignore, якщо є)'
|
|
184
|
+
)
|
|
124
185
|
return
|
|
125
186
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
187
|
+
const lines = await readGitignoreLines()
|
|
188
|
+
if (lines && lines.includes(LEGACY_CACHE_FILENAME)) {
|
|
189
|
+
fail(
|
|
190
|
+
`.gitignore: прибери застарілий рядок \`${LEGACY_CACHE_FILENAME}\` — split-cache 3.2.0 його не використовує`
|
|
191
|
+
)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
pass(`${LEGACY_CACHE_FILENAME} відсутній (міграція на split-cache завершена)`)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Чи у `package.json` пакета вимкнено avif-перевірку Vue-імпортів.
|
|
199
|
+
* Очікувана форма: `"@nitra/minify-image": { "disable-avif": true }`.
|
|
200
|
+
* @param {Record<string, unknown>} pkg розібраний package.json пакета
|
|
201
|
+
* @returns {boolean} true, якщо опт-аут активовано
|
|
202
|
+
*/
|
|
203
|
+
function packageHasAvifDisabled(pkg) {
|
|
204
|
+
const cfg = pkg[PKG_CONFIG_FIELD]
|
|
205
|
+
return Boolean(cfg && typeof cfg === 'object' && /** @type {Record<string, unknown>} */ (cfg)['disable-avif'] === true)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Сканує `.vue` файли одного workspace-пакета на raster-імпорти, що ще не використовують `.avif`.
|
|
210
|
+
*
|
|
211
|
+
* Файли, що належать іншим workspace-пакетам, ігноруються — кожен пакет перевіряється рівно
|
|
212
|
+
* один раз (інакше при обході кореня `.` ми б повторно зайшли в `demo/` і подвоїли звіти).
|
|
213
|
+
* @param {string} packageRoot відносний шлях до кореня пакета (наприклад `'.'` або `'demo'`)
|
|
214
|
+
* @param {string[]} otherRootsAbs абсолютні шляхи інших workspace-коренів — їх піддерева пропускаємо
|
|
215
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
216
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
217
|
+
* @returns {Promise<void>}
|
|
218
|
+
*/
|
|
219
|
+
async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, pass, fail) {
|
|
220
|
+
const absRoot = join(process.cwd(), packageRoot)
|
|
221
|
+
const label = packageRoot === '.' ? 'корінь' : packageRoot
|
|
222
|
+
/** @type {string[]} */
|
|
223
|
+
const vueFiles = []
|
|
224
|
+
await walkDir(absRoot, absPath => {
|
|
225
|
+
if (!absPath.endsWith('.vue')) return
|
|
226
|
+
if (otherRootsAbs.some(other => absPath.startsWith(`${other}/`))) return
|
|
227
|
+
vueFiles.push(absPath)
|
|
228
|
+
})
|
|
229
|
+
if (vueFiles.length === 0) return
|
|
230
|
+
|
|
231
|
+
let violations = 0
|
|
232
|
+
for (const absPath of vueFiles) {
|
|
233
|
+
const rel = relative(process.cwd(), absPath).split('\\').join('/')
|
|
234
|
+
const content = await readFile(absPath, 'utf8')
|
|
235
|
+
for (const match of content.matchAll(VUE_RASTER_IMPORT_RE)) {
|
|
236
|
+
violations++
|
|
237
|
+
const importPath = match[1]
|
|
238
|
+
fail(
|
|
239
|
+
`[${label}] ${rel}: import з '${importPath}' має посилатись на AVIF-двійник '${importPath}.avif' ` +
|
|
240
|
+
`(lint-image --avif створює його поряд). Вимкнути локально: "@nitra/minify-image": { "disable-avif": true } у package.json пакета`
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
for (const match of content.matchAll(VUE_RASTER_STATIC_SRC_RE)) {
|
|
244
|
+
violations++
|
|
245
|
+
const srcPath = match[1]
|
|
246
|
+
fail(
|
|
247
|
+
`[${label}] ${rel}: пряме \`src="${srcPath}"\` у шаблоні має використовувати AVIF-двійник \`src="${srcPath}.avif"\` ` +
|
|
248
|
+
`(або винеси у import + \`:src="..."\`). Вимкнути локально: "@nitra/minify-image": { "disable-avif": true } у package.json пакета`
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (violations === 0) {
|
|
253
|
+
pass(`[${label}] усі raster-посилання у .vue вже на .avif (або відсутні)`)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Сканує всі workspace-пакети: для кожного перевіряє opt-out і за потреби викликає
|
|
259
|
+
* перевірку Vue-imports. Перевірка пропускається, якщо в репозиторії немає workspaces
|
|
260
|
+
* або немає `.vue`-файлів — тоді `image` правило не для цього проєкту.
|
|
261
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
262
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
263
|
+
* @returns {Promise<void>}
|
|
264
|
+
*/
|
|
265
|
+
async function checkVueAvifImports(pass, fail) {
|
|
266
|
+
const roots = await getMonorepoPackageRootDirs()
|
|
267
|
+
const absRootsByRel = new Map(roots.map(r => [r, join(process.cwd(), r)]))
|
|
268
|
+
for (const root of roots) {
|
|
269
|
+
const pkgPath = join(root, 'package.json')
|
|
270
|
+
if (!existsSync(pkgPath)) continue
|
|
271
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
272
|
+
if (packageHasAvifDisabled(pkg)) {
|
|
273
|
+
pass(`[${root === '.' ? 'корінь' : root}] avif-import enforcement вимкнено через "@nitra/minify-image.disable-avif"`)
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
const otherRootsAbs = roots.filter(r => r !== root && r !== '.').map(r => absRootsByRel.get(r) ?? '')
|
|
277
|
+
await checkVueAvifImportsInPackage(root, otherRootsAbs, pass, fail)
|
|
278
|
+
}
|
|
129
279
|
}
|
|
130
280
|
|
|
131
281
|
/**
|
|
132
282
|
* Перевіряє кореневий `package.json`: скрипти, заборонені залежності, агрегований `lint`.
|
|
133
283
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
134
284
|
* @param {(msg: string) => void} fail callback при помилці
|
|
135
|
-
* @returns {Promise<
|
|
285
|
+
* @returns {Promise<boolean>} `true`, якщо `package.json` знайдено й оброблено; `false` — нема
|
|
136
286
|
*/
|
|
137
287
|
async function checkPackageJsonImage(pass, fail) {
|
|
138
288
|
if (!existsSync('package.json')) {
|
|
139
289
|
fail('package.json не знайдено в корені — додай (image.mdc)')
|
|
140
|
-
return
|
|
290
|
+
return false
|
|
141
291
|
}
|
|
142
292
|
const pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
143
293
|
const scripts = /** @type {Record<string, unknown>} */ (pkg.scripts || {})
|
|
144
294
|
checkLintImageScript(typeof scripts['lint-image'] === 'string' ? scripts['lint-image'] : undefined, pass, fail)
|
|
145
295
|
checkLintAggregateIncludesImage(typeof scripts.lint === 'string' ? scripts.lint : undefined, pass, fail)
|
|
146
296
|
checkMinifyImageNotInDeps(pkg, pass, fail)
|
|
147
|
-
return
|
|
297
|
+
return true
|
|
148
298
|
}
|
|
149
299
|
|
|
150
300
|
/**
|
|
151
|
-
* Перевіряє відповідність проєкту правилам `image.mdc
|
|
152
|
-
* `lint-image` через `npx @nitra/minify-image --src
|
|
153
|
-
* `.minify-image
|
|
154
|
-
*
|
|
301
|
+
* Перевіряє відповідність проєкту правилам `image.mdc` (split-cache 3.2.0):
|
|
302
|
+
* `lint-image` через `npx @nitra/minify-image --src=. --write --avif`, агрегований `lint`,
|
|
303
|
+
* `.n-minify-image.tsv` НЕ в `.gitignore` (committed source of truth), застарілий
|
|
304
|
+
* `.minify-image-cache.tsv` видалений, AVIF-імпорти у `.vue` файлах. CI-workflow
|
|
305
|
+
* для image не вимагається — лінт зображень виконується лише локально.
|
|
155
306
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
156
307
|
*/
|
|
157
308
|
export async function check() {
|
|
158
309
|
const reporter = createCheckReporter()
|
|
159
310
|
const { pass, fail } = reporter
|
|
160
311
|
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
163
|
-
await
|
|
312
|
+
const pkgFound = await checkPackageJsonImage(pass, fail)
|
|
313
|
+
if (pkgFound) {
|
|
314
|
+
await checkHashCacheNotIgnored(pass, fail)
|
|
315
|
+
await checkLegacyCacheRemoved(pass, fail)
|
|
164
316
|
}
|
|
317
|
+
await checkVueAvifImports(pass, fail)
|
|
165
318
|
|
|
166
319
|
return reporter.getExitCode()
|
|
167
320
|
}
|