@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 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.2) / `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 як готові артефакти для віддачі браузеру.
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.2'
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. Кеш-файл `.minify-image-cache.tsv` робить повторні прогони дешевими.
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
- ## `.gitignore`
24
+ ## Split-cache
25
25
 
26
- `--write` створює `.minify-image-cache.tsv` у корені сканованого каталогу — TSV з рядком на кожне зображення (`<rel-path>\t<mtime>\t<originalSize>\t<size>`). Файл **переписується** на кожному запуску, тому консервативний дефолт для бібліотек / застосунків — ігнорувати:
26
+ Починаючи з `@nitra/minify-image` **3.2.0** кеш розбитий на два файли з різною семантикою:
27
27
 
28
- ```text title=".gitignore"
29
- .minify-image-cache.tsv
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
- Якщо проєкт усе ж зберігає кеш у git це теж дозволено правилом, але тоді він має бути в `files` у `package.json` (для опублікованих npm-пакетів) або просто відсутній у `.gitignore`. Замовчування — рядок у `.gitignore`.
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` у `package.json` з обовʼязковими `--src=.`, `--write`, `--avif`, агрегований `lint` і `.minify-image-cache.tsv` у `.gitignore`).
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.169",
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.169"
52
+ "@nitra/cursor": "^1.8.170"
53
53
  }
54
54
  }
@@ -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-cache.tsv` ігнорується через `.gitignore`,
13
- * або (рідше) явно перерахований у `files` пакета npm.
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
- /** Імʼя кеш-файлу, який CLI створює у режимі `--write`. */
24
- const CACHE_FILENAME = '.minify-image-cache.tsv'
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
- * Перевіряє `.minify-image-cache.tsv`: має бути або у `.gitignore`,
104
- * або явно у `files`-листі пакета (рідкісний кейс для open-source npm-пакетів).
105
- * @param {{ files?: unknown }} pkg розібраний package.json (для перевірки `files`)
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 checkCacheIgnoredOrPublished(pkg, pass, fail) {
111
- if (existsSync('.gitignore')) {
112
- const raw = await readFile('.gitignore', 'utf8')
113
- const lines = raw
114
- .split('\n')
115
- .map(l => l.trim())
116
- .filter(l => l.length > 0 && !l.startsWith('#'))
117
- if (lines.includes(CACHE_FILENAME)) {
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
- if (Array.isArray(pkg.files) && pkg.files.some(f => typeof f === 'string' && f.includes(CACHE_FILENAME))) {
123
- pass(`package.json: ${CACHE_FILENAME} перерахований у \`files\` (комітований кеш)`)
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
- fail(
127
- `.gitignore: додай рядок \`${CACHE_FILENAME}\` (або явно перерахуй у \`files\` package.json) — image.mdc`
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<{ pkg: Record<string, unknown> } | null>} розібраний package.json або `null` якщо немає
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 null
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 { pkg }
297
+ return true
148
298
  }
149
299
 
150
300
  /**
151
- * Перевіряє відповідність проєкту правилам `image.mdc`:
152
- * `lint-image` через `npx @nitra/minify-image --src=.`, агрегований `lint`,
153
- * `.minify-image-cache.tsv` у `.gitignore`. CI-workflow для 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 pkgResult = await checkPackageJsonImage(pass, fail)
162
- if (pkgResult) {
163
- await checkCacheIgnoredOrPublished(pkgResult.pkg, pass, fail)
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
  }