@nitra/cursor 1.8.193 → 1.8.198
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 +35 -0
- package/bin/auto-rules.md +3 -1
- package/mdc/image-avif.mdc +55 -0
- package/mdc/image-compress.mdc +58 -0
- package/package.json +1 -1
- package/scripts/auto-rules.mjs +4 -2
- package/scripts/{check-image.mjs → check-image-avif.mjs} +83 -221
- package/scripts/check-image-compress.mjs +216 -0
- package/scripts/check-js-run.mjs +1 -1
- package/scripts/utils/depcheck-workflow.mjs +3 -3
- package/mdc/image.mdc +0 -96
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,41 @@
|
|
|
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.198] - 2026-05-07
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `image-compress` (mdc v1.0 → v1.1): мінімум `@nitra/minify-image` піднято з **3.2.0** до **3.3.1**. У `3.3.1` upstream CLI порівнює sha1 raster-сорсу зі збереженим у `.n-minify-image.tsv` і автоматично перегенеровує `<source>.avif` при зміні контенту оригіналу — раніше stale `.avif` лишався поки розробник не видаляв його вручну. Додано пояснювальний абзац у правило.
|
|
12
|
+
- `image-avif` (mdc v1.0 → v1.1): крок 1 (`npx @nitra/minify-image --src=. --write --avif`) явно требує ≥ 3.3.1 і документує, що sha1-перевірка для регенерації застарілого AVIF тепер живе у CLI; `@nitra/cursor` цю логіку **не дублює**.
|
|
13
|
+
|
|
14
|
+
## [1.8.197] - 2026-05-07
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `image` правило розщеплене на два самостійні: **`image-compress`** (валідація `lint-image` / `.gitignore` / залежностей — стиснення raster/SVG через `@nitra/minify-image`) і **`image-avif`** (генерація AVIF-двійників, переписування raster-посилань у `.vue`/`.html` на `.avif`, прибирання AVIF-сиріт). Це дозволяє тримати компресію всюди, а AVIF — лише там, де його підтримка гарантована (адмінки), вимикаючи його для публічних сайтів через `disable-rules: ["image-avif"]` у `.n-cursor.json` чи опт-аут на рівні пакета (`"@nitra/minify-image": { "disable-avif": true }` у `package.json` сайту).
|
|
19
|
+
- `auto-rules.md` / `auto-rules.mjs`: автодетект `image-compress - [bun]` (всюди, де є `package.json`), `image-avif - [vue, image-compress]` (лише для проєктів з `.vue`-файлами і вже активним `image-compress`).
|
|
20
|
+
- Видалено `npm/scripts/check-image.mjs` і `npm/mdc/image.mdc` — їх замінили `check-image-compress.mjs` + `check-image-avif.mjs` і `image-compress.mdc` + `image-avif.mdc`.
|
|
21
|
+
- Канонічний `lint-image` залишається без `--avif` (його перевіряє `image-compress`); `npx @nitra/cursor check image-avif` тепер є самостійною командою для AVIF-pipeline.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `tests/auto-rules.test.mjs`: тест на `disable-rules: ["image-compress"]` → `image-avif` теж не додається (транзитивна залежність).
|
|
26
|
+
|
|
27
|
+
## [1.8.194] - 2026-05-07
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- `check-image.mjs`: резолвер `resolveImagePath` був інлайн-наївний (`/path` → `<cwd>/<path>`, голий шлях → `null`), що в реальних Quasar/Vite-проєктах давало 0 rewrite-ів і помилковий ріст `failedRefs`. Замінено на `resolveImageCandidates`, який повертає **впорядкований список кандидатів**:
|
|
32
|
+
- `./x.png` / `../x.png` → відносно файла-джерела;
|
|
33
|
+
- `/x.png` → `<packageRoot>/public/x.png`, потім `<packageRoot>/x.png`, потім `<cwd>/x.png` (legacy fallback);
|
|
34
|
+
- голий шлях з принаймні одним `/` (`assets/img.png`, `start-page-ua/logo.png`) → відносно файла-джерела + `<packageRoot>/public/<path>` (Quasar-конвенція);
|
|
35
|
+
- bare без `/` → alias resolver невідомий, посилання тихо пропускаємо (без fail).
|
|
36
|
+
- `check-image.mjs`: `cleanupOrphanAvifs` тепер пропускає `.avif` у каталогах артефактів збірки (`build`, `android`, `ios`, `.output`, `.nuxt`, `.cache`) — раніше cleanup міг затирати продукт `bun run build` чи Capacitor sync.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- `tests/check-image.test.mjs`: 4 нових кейси — Quasar-style `src="/api-page/1.png"` через `<pkg>/public/`; `<img src="assets/images/x.png">` у `.html` через relative-to-source; `src="start-page-ua/logo.png"` у `.vue` через `<pkg>/public/`; cleanup не чіпає AVIF у `build/`/`android/`/`ios/`/`.output/`/`.nuxt/`/`.cache/`.
|
|
41
|
+
|
|
7
42
|
## [1.8.193] - 2026-05-07
|
|
8
43
|
|
|
9
44
|
### Fixed
|
package/bin/auto-rules.md
CHANGED
|
@@ -22,7 +22,9 @@ graphql - якщо хоч в одному js або vue файлі присут
|
|
|
22
22
|
|
|
23
23
|
hasura - якщо в директорії присутній config.yaml, який містить рядок `metadata_directory: metadata`
|
|
24
24
|
|
|
25
|
-
image - [
|
|
25
|
+
image-compress - [bun]
|
|
26
|
+
|
|
27
|
+
image-avif - [vue, image-compress]
|
|
26
28
|
|
|
27
29
|
js-lint - якщо присутній хоч один js файл
|
|
28
30
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: AVIF-двійники для raster-зображень з ув'язуванням у .vue/.html
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
version: '1.1'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
AVIF-двійники (`<name>.<ext>.avif`) генерує **виключно** `npx @nitra/cursor check image-avif` — у `lint-image` прапорець `--avif` заборонений (це валідує правило `image-compress`). Перевірка робить три кроки в порядку:
|
|
8
|
+
|
|
9
|
+
1. Запускає `npx @nitra/minify-image --src=. --write --avif` (≥ **3.3.1**) — генерує `<name>.<ext>.avif` поряд з кожним PNG/JPEG/GIF. **Перегенерація при оновленні оригіналу:** з 3.3.1 CLI порівнює sha1 кожного raster-сорсу зі збереженим у `.n-minify-image.tsv` і автоматично перезаписує `<source>.avif`, якщо оригінал відредагували після останнього прогону. `@nitra/cursor` цю логіку не дублює — sha1-кеш живе всередині `@nitra/minify-image`.
|
|
10
|
+
2. Сканує `.vue` (а також `.html`) файли в кожному workspace-пакеті (root + workspaces) і автоматично переписує raster-посилання на AVIF-двійник у двох формах:
|
|
11
|
+
- **Імпорт-пов'язані** — `import x from '...png|jpg|jpeg|gif'` (далі `:src="x"` у шаблоні);
|
|
12
|
+
- **Прямі статичні** — `<img src="...png" />` у `<template>` (Vite перетворює такий шлях на asset-імпорт на етапі збірки, тож вимога та сама).
|
|
13
|
+
3. Видаляє AVIF-сироти: ходить по всіх `<...>.avif` у репозиторії; якщо на двійник не лишилось жодного посилання у `.vue`/`.html` — файл видаляється. **AVIF на диску лишається лише там, де заміна реально відбулась** — тому невикористані оригінали не накопичують `.avif`-«хвости».
|
|
14
|
+
|
|
15
|
+
```vue title="App.vue (після check image-avif)"
|
|
16
|
+
<script setup>
|
|
17
|
+
import welcomeImage from './assets/welcome.png.avif'
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<img :src="welcomeImage" alt="Welcome" />
|
|
22
|
+
</template>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Реактивне `:src="..."` (з JS-виразом — змінною, тернарником, викликом тощо) **не сканується** — значення обчислюється у рантаймі й шлях туди потрапляє через імпорт або інший резолвинг, який ловить імпорт-перевірка вище. SVG не торкаємо (vector → AVIF безглуздо). Атрибути `data-src=`, `obj.src=` у `<script>` тощо також пропускаються.
|
|
26
|
+
|
|
27
|
+
Якщо raster-посилання у `.vue`/`.html` не вдалось переписати (наприклад, оригіналу немає на диску, тож і `.avif` не згенерувався) — `check image-avif` падає з помилкою на конкретний файл.
|
|
28
|
+
|
|
29
|
+
AVIF-двійники **зберігаємо в git** — це готові артефакти для віддачі браузеру (без них ефект від AVIF втрачається на чистому checkout-і).
|
|
30
|
+
|
|
31
|
+
## Коли НЕ вмикати правило
|
|
32
|
+
|
|
33
|
+
AVIF ще не підтримується **усіма** браузерами: для публічного сайту, де серед користувачів можуть бути старі/нестандартні браузери, конвертація raster → AVIF як основного джерела ризикована. Для адмінок (де користувачі — співробітники з сучасними браузерами) AVIF безпечний.
|
|
34
|
+
|
|
35
|
+
У монорепо з адмінкою + публічним сайтом стандартна стратегія така: правило `image-avif` присутнє у `.n-cursor.json`, але для пакета-сайту вмикається опт-аут (нижче).
|
|
36
|
+
|
|
37
|
+
## Опт-аут для конкретного пакета
|
|
38
|
+
|
|
39
|
+
У workspace-пакеті, де AVIF-імпорти небажані (наприклад, мобільний бандл або публічний сайт без гарантованої AVIF-підтримки), додай у `package.json` цього пакета:
|
|
40
|
+
|
|
41
|
+
```json title="apps/site/package.json"
|
|
42
|
+
{
|
|
43
|
+
"@nitra/minify-image": {
|
|
44
|
+
"disable-avif": true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Тоді перевірка пропускає `.vue` файли цього пакета і не видаляє наявні `.avif` всередині як «сироти». У root-`package.json` опт-аут діє лише для файлів кореня (вкладені workspaces перевіряються незалежно — вмикай прапор у кожному пакеті, де треба).
|
|
50
|
+
|
|
51
|
+
`image-compress` (раніший крок: lint-image, кеш, заборонені залежності) при цьому продовжує працювати — стиснення raster-зображень виконується незалежно від AVIF.
|
|
52
|
+
|
|
53
|
+
## Перевірка
|
|
54
|
+
|
|
55
|
+
`npx @nitra/cursor check image-avif` (запуск AVIF-генерації + авто-заміна raster-посилань на `.avif` у `.vue`/`.html` кожного workspace-пакета + прибирання AVIF-сиріт; пакети з `"@nitra/minify-image": { "disable-avif": true }` пропускаються).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Оптимізація raster/SVG через @nitra/minify-image у локальному lint
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
version: '1.1'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
CLI [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image) (≥ **3.3.1**) запускається через `npx` (як `markdownlint-cli2` у text.mdc) і **не** додається в `dependencies` / `devDependencies`. Канонічний `lint-image` — авто-оптимізація з прапорцем `--write`: стискає raster/SVG на місці. **AVIF-генерація (`--avif`) у `lint-image` заборонена** — її виконує окреме правило `image-avif` (`npx @nitra/cursor check image-avif`), яке заодно прибирає AVIF-сироти. Split-cache робить повторні прогони дешевими — і локально, і після `git clone`.
|
|
8
|
+
|
|
9
|
+
Мінімум `3.3.1` важливий для правила `image-avif`: починаючи з цієї версії, CLI порівнює sha1 raster-сорсу зі збереженим у `.n-minify-image.tsv` і **перегенеровує `<source>.avif`** при будь-якій зміні контенту оригіналу (раніше stale `.avif` лишався, поки розробник не видаляв його вручну).
|
|
10
|
+
|
|
11
|
+
Перевірка лише локальна — у CI `lint-image` не запускаємо (sharp/svgo тягнуть бінарні залежності, цінність на ubuntu-runner-ах нижча за час прогону). Окремий workflow `lint-image.yml` створювати не треба.
|
|
12
|
+
|
|
13
|
+
## `package.json`
|
|
14
|
+
|
|
15
|
+
```json title="package.json"
|
|
16
|
+
{
|
|
17
|
+
"scripts": {
|
|
18
|
+
"lint": "bun run lint-js && bun run lint-text && bun run lint-ga && bun run lint-image && oxfmt .",
|
|
19
|
+
"lint-image": "npx @nitra/minify-image --src=. --write"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Якщо в `package.json` уже є агрегований `lint`, додай у його ланцюжок `bun run lint-image` (як `bun run lint-text`, `bun run lint-js`, `bun run lint-ga`). Так розробник, що локально гонить `bun run lint`, перед фіксацією одразу бачить, чи зросли зображення.
|
|
25
|
+
|
|
26
|
+
## Split-cache
|
|
27
|
+
|
|
28
|
+
Починаючи з `@nitra/minify-image` **3.2.0** кеш розбитий на два файли з різною семантикою:
|
|
29
|
+
|
|
30
|
+
### `.n-minify-image.tsv` — source of truth у git
|
|
31
|
+
|
|
32
|
+
У корені сканованого каталогу. Формат: `<rel-path>\t<sha1-hex>\t<originalSize>\t<size>`.
|
|
33
|
+
|
|
34
|
+
Slow-path і джерело даних для `Project lifetime savings`. **Має бути в git** — після `git clone` чи `git checkout` (mtime скидається на час checkout-у) CLI читає файл, рахує SHA-1 і порівнює зі збереженим у TSV хешем; на match локальний mtime-кеш зігрівається без reprocess. Рядки відсортовані алфавітно, hash і size змінюються лише при реальній зміні контенту — diff чистий.
|
|
35
|
+
|
|
36
|
+
### `node_modules/.cache/@nitra/minify-image/mtime.tsv` — локальний fast-path
|
|
37
|
+
|
|
38
|
+
Формат: `<rel-path>\t<mtime>\t<size>`. При збігу `(size, mtime)` CLI пропускає файл без читання — константа per-file.
|
|
39
|
+
|
|
40
|
+
Лежить під `node_modules/`, тож **авто-gitignored** за конвенцією JS-tooling-у (так кешуються ESLint, Babel, webpack, Turbo). Окремий рядок у `.gitignore` не потрібен. `rm -rf node_modules` зносить — наступний запуск відновлює його через slow-path проти `.n-minify-image.tsv`, без reprocess.
|
|
41
|
+
|
|
42
|
+
### Міграція з versions < 3.2
|
|
43
|
+
|
|
44
|
+
Старий єдиний `.minify-image-cache.tsv` (4 колонки `path\tmtime\toriginalSize\tsize`, зазвичай у `.gitignore`) автоматично читається при першому запуску для seed-у `originalSize` у `.n-minify-image.tsv` (lifetime savings не скидається). Після цього старий файл видаляють вручну:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git rm --cached .minify-image-cache.tsv 2>/dev/null || true
|
|
48
|
+
rm -f .minify-image-cache.tsv
|
|
49
|
+
# прибери відповідний рядок з .gitignore, якщо був
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Заборонені залежності
|
|
53
|
+
|
|
54
|
+
`@nitra/minify-image` не повинен зʼявлятися ні в `dependencies`, ні в `devDependencies` кореневого `package.json` — CLI запускається лише через `npx` (так само, як `markdownlint-cli2` у text.mdc). Якщо потрібен явний пін — закладай діапазон версій npm у самому виклику (`npx @nitra/minify-image@^3 --src=. --write`).
|
|
55
|
+
|
|
56
|
+
## Перевірка
|
|
57
|
+
|
|
58
|
+
`npx @nitra/cursor check image-compress` (охоплює `lint-image` з обовʼязковими `--src=.`, `--write` і **забороненим** `--avif`; агрегований `lint`; заборону `@nitra/minify-image` у залежностях; `.n-minify-image.tsv` НЕ в `.gitignore` — має бути в git; відсутність застарілого `.minify-image-cache.tsv` у корені).
|
package/package.json
CHANGED
package/scripts/auto-rules.mjs
CHANGED
|
@@ -31,7 +31,8 @@ export const AUTO_RULE_ORDER = Object.freeze([
|
|
|
31
31
|
'ga',
|
|
32
32
|
'graphql',
|
|
33
33
|
'hasura',
|
|
34
|
-
'image',
|
|
34
|
+
'image-avif',
|
|
35
|
+
'image-compress',
|
|
35
36
|
'js-lint',
|
|
36
37
|
'js-mssql',
|
|
37
38
|
'js-bun-db',
|
|
@@ -56,7 +57,8 @@ export const AUTO_SKILL_ORDER = Object.freeze(['abie-kustomize', 'fix', 'lint'])
|
|
|
56
57
|
export const AUTO_RULE_DEPENDENCIES = Object.freeze(
|
|
57
58
|
/** @type {Record<string, readonly string[]>} */ ({
|
|
58
59
|
changelog: Object.freeze(['bun']),
|
|
59
|
-
image: Object.freeze(['vue'])
|
|
60
|
+
'image-avif': Object.freeze(['vue', 'image-compress']),
|
|
61
|
+
'image-compress': Object.freeze(['bun'])
|
|
60
62
|
})
|
|
61
63
|
)
|
|
62
64
|
|
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Перевіряє відповідність репозиторію правилу image.mdc
|
|
3
|
-
*
|
|
2
|
+
* Перевіряє відповідність репозиторію правилу `image-avif.mdc`: AVIF-генерацію та
|
|
3
|
+
* ув'язування `.avif`-двійників з посиланнями у `.vue`/`.html`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* - у кореневому `package.json` є скрипт `lint-image`, який викликає `npx @nitra/minify-image`
|
|
7
|
-
* з обовʼязковими `--src=.` і `--write`. Прапорець `--avif` у `lint-image` заборонений —
|
|
8
|
-
* AVIF-генерацію виконує `check image` (інакше `bun run lint` плодив би `.avif` для
|
|
9
|
-
* зображень, що ніде не вживаються);
|
|
10
|
-
* - якщо в `package.json` є агрегований скрипт `lint`, він викликає `bun run lint-image`
|
|
11
|
-
* (симетрично до `lint-text`, `lint-js`, `lint-ga`);
|
|
12
|
-
* - `@nitra/minify-image` не оголошений у `dependencies`/`devDependencies` —
|
|
13
|
-
* CLI запускається лише через `npx` (як `markdownlint-cli2` у `text.mdc`);
|
|
14
|
-
* - `.n-minify-image.tsv` (committed source of truth з sha1/originalSize/size) НЕ
|
|
15
|
-
* в `.gitignore` — він має бути в git. Локальний mtime-кеш у
|
|
16
|
-
* `node_modules/.cache/@nitra/minify-image/mtime.tsv` авто-gitignored через `node_modules/`,
|
|
17
|
-
* окремої перевірки не вимагає;
|
|
18
|
-
* - застарілий `.minify-image-cache.tsv` (з версій < 3.2) видалений з кореня — інакше
|
|
19
|
-
* проєкт лишається у напівпереміщеному стані.
|
|
20
|
-
*
|
|
21
|
-
* Дії під час `check image` (на додачу до валідацій):
|
|
5
|
+
* Дії під час `check image-avif`:
|
|
22
6
|
* 1. `npx @nitra/minify-image --src=. --write --avif` — генерує AVIF-двійники.
|
|
23
7
|
* 2. У кожному workspace-пакеті переписує raster-посилання у `.vue`/`.html` на `.avif`
|
|
24
8
|
* (де AVIF-двійник реально існує на диску). Pakety з `"@nitra/minify-image": {
|
|
@@ -28,7 +12,12 @@
|
|
|
28
12
|
* там, де заміна вдалася»).
|
|
29
13
|
*
|
|
30
14
|
* Якщо raster-посилання у `.vue`/`.html` не вдалось переписати (наприклад, оригіналу
|
|
31
|
-
* нема на диску → `.avif` теж не згенерувався) — фейл на конкретний
|
|
15
|
+
* нема на диску → `.avif` теж не згенерувався) — фейл на конкретний файл.
|
|
16
|
+
*
|
|
17
|
+
* Правило самостійне від `image-compress`: AVIF можна вмикати лише в адмінках (де AVIF
|
|
18
|
+
* підтримується сучасними браузерами) і не вмикати в публічних сайтах. Перевірка скрипта
|
|
19
|
+
* `lint-image` (заборона `--avif` у ньому) залишається у `image-compress` — тут вона не
|
|
20
|
+
* дублюється.
|
|
32
21
|
*/
|
|
33
22
|
import { existsSync } from 'node:fs'
|
|
34
23
|
import { readFile, unlink, writeFile } from 'node:fs/promises'
|
|
@@ -41,18 +30,20 @@ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
|
41
30
|
import { walkDir } from './utils/walkDir.mjs'
|
|
42
31
|
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
43
32
|
|
|
44
|
-
/** Імʼя CLI
|
|
33
|
+
/** Імʼя CLI-пакета, який генерує AVIF. */
|
|
45
34
|
const MINIFY_PACKAGE_NAME = '@nitra/minify-image'
|
|
46
35
|
|
|
47
|
-
/** Імʼя committed-кешу (sha1 + originalSize + size) у `@nitra/minify-image` ≥ 3.2.0. */
|
|
48
|
-
const HASH_CACHE_FILENAME = '.n-minify-image.tsv'
|
|
49
|
-
|
|
50
|
-
/** Імʼя застарілого 4-колонкового кешу (`@nitra/minify-image` < 3.2). Має бути видалений після міграції. */
|
|
51
|
-
const LEGACY_CACHE_FILENAME = '.minify-image-cache.tsv'
|
|
52
|
-
|
|
53
36
|
/** Поле в `package.json` для конфігу @nitra/minify-image (наприклад, `disable-avif`). */
|
|
54
37
|
const PKG_CONFIG_FIELD = '@nitra/minify-image'
|
|
55
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Імена каталогів, які `cleanupOrphanAvifs` не зачіпає, бо це артефакти збірки/нативні
|
|
41
|
+
* платформи — `.avif` всередині — це продукт попереднього `bun run build`/Capacitor sync,
|
|
42
|
+
* а не кандидати на видалення. `walkDir` уже скіпає `node_modules`, `.git`, `dist`,
|
|
43
|
+
* `coverage`, `.turbo`, `.next` — додатково для cleanup ігноруємо ще ці.
|
|
44
|
+
*/
|
|
45
|
+
const CLEANUP_EXTRA_IGNORE_DIR_NAMES = new Set(['build', 'android', 'ios', '.output', '.nuxt', '.cache'])
|
|
46
|
+
|
|
56
47
|
/**
|
|
57
48
|
* Регексп для імпортів raster-зображень у `.vue` файлах.
|
|
58
49
|
* Захоплює `import name from '...ext'` (як default, так і type-only форми не потрібні —
|
|
@@ -78,147 +69,6 @@ const VUE_RASTER_STATIC_SRC_RE = /(?<![:\-_.])\bsrc\s*=\s*['"]([^'"\s]+\.(?:png|
|
|
|
78
69
|
*/
|
|
79
70
|
const VUE_AVIF_REF_RE = /['"]([^'"\s]+\.(?:png|jpe?g|gif)\.avif)['"]/giu
|
|
80
71
|
|
|
81
|
-
/**
|
|
82
|
-
* Перевіряє скрипт `lint-image` у `package.json`.
|
|
83
|
-
*
|
|
84
|
-
* Має містити виклик `npx @nitra/minify-image` з обовʼязковими прапорцями `--src=.`
|
|
85
|
-
* і `--write` (авто-оптимізація на місці). Прапорець `--avif` у `lint-image`
|
|
86
|
-
* заборонений — AVIF-генерацію виконує `check image`, інакше `bun run lint` плодить
|
|
87
|
-
* `.avif` для зображень, що ніде не вживаються.
|
|
88
|
-
* @param {string|undefined} lintImage значення `scripts['lint-image']`
|
|
89
|
-
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
90
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
91
|
-
* @returns {void}
|
|
92
|
-
*/
|
|
93
|
-
function checkLintImageScript(lintImage, pass, fail) {
|
|
94
|
-
const canonical = `npx ${MINIFY_PACKAGE_NAME} --src=. --write`
|
|
95
|
-
if (typeof lintImage !== 'string' || !lintImage.trim()) {
|
|
96
|
-
fail(`package.json: додай скрипт "lint-image" з \`${canonical}\` (image.mdc)`)
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
if (!lintImage.includes(`npx ${MINIFY_PACKAGE_NAME}`)) {
|
|
100
|
-
fail(`package.json: lint-image має викликати \`npx ${MINIFY_PACKAGE_NAME}\` (image.mdc)`)
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
/** @type {{ flag: string, variants: string[], hint: string }[]} */
|
|
104
|
-
const requiredFlags = [
|
|
105
|
-
{ flag: '--src=.', variants: ['--src=.', '--src .'], hint: '`--src=.`' },
|
|
106
|
-
{ flag: '--write', variants: ['--write'], hint: '`--write` (авто-оптимізація на місці)' }
|
|
107
|
-
]
|
|
108
|
-
const missing = requiredFlags.filter(f => !f.variants.some(v => lintImage.includes(v)))
|
|
109
|
-
if (missing.length > 0) {
|
|
110
|
-
fail(
|
|
111
|
-
`package.json: lint-image має містити ${missing.map(f => f.hint).join(', ')} — канонічний виклик: \`${canonical}\` (image.mdc)`
|
|
112
|
-
)
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
if (lintImage.includes('--avif')) {
|
|
116
|
-
fail(
|
|
117
|
-
`package.json: прибери \`--avif\` з lint-image — AVIF-генерацію виконує \`npx @nitra/cursor check image\` (image.mdc). Канонічний виклик: \`${canonical}\``
|
|
118
|
-
)
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
pass(`package.json: lint-image викликає \`${canonical}\``)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Перевіряє, що агрегований `lint` (якщо є) кличе `bun run lint-image` —
|
|
126
|
-
* симетрично до `lint-text`, `lint-js`, `lint-ga`.
|
|
127
|
-
* @param {string|undefined} lintAggregate значення `scripts.lint`
|
|
128
|
-
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
129
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
130
|
-
* @returns {void}
|
|
131
|
-
*/
|
|
132
|
-
function checkLintAggregateIncludesImage(lintAggregate, pass, fail) {
|
|
133
|
-
if (typeof lintAggregate !== 'string' || !lintAggregate.trim()) {
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
if (lintAggregate.includes('bun run lint-image')) {
|
|
137
|
-
pass('package.json: агрегований `lint` викликає `bun run lint-image`')
|
|
138
|
-
} else {
|
|
139
|
-
fail('package.json: у `lint` додай `bun run lint-image` (image.mdc, симетрично до lint-text / lint-js / lint-ga)')
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Забороняє `@nitra/minify-image` у `dependencies` чи `devDependencies` —
|
|
145
|
-
* CLI завжди запускається через `npx` (як `markdownlint-cli2` у `text.mdc`).
|
|
146
|
-
* @param {{ dependencies?: Record<string, unknown>, devDependencies?: Record<string, unknown> }} pkg розібраний package.json
|
|
147
|
-
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
148
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
149
|
-
* @returns {void}
|
|
150
|
-
*/
|
|
151
|
-
function checkMinifyImageNotInDeps(pkg, pass, fail) {
|
|
152
|
-
const inDeps = Boolean(pkg.dependencies && MINIFY_PACKAGE_NAME in pkg.dependencies)
|
|
153
|
-
const inDevDeps = Boolean(pkg.devDependencies && MINIFY_PACKAGE_NAME in pkg.devDependencies)
|
|
154
|
-
if (inDeps || inDevDeps) {
|
|
155
|
-
fail(
|
|
156
|
-
`package.json: ${MINIFY_PACKAGE_NAME} не додавай у dependencies/devDependencies — лише через \`npx\` (image.mdc)`
|
|
157
|
-
)
|
|
158
|
-
} else {
|
|
159
|
-
pass(`package.json: ${MINIFY_PACKAGE_NAME} не оголошено в dependencies/devDependencies`)
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Зчитує всі змістовні рядки `.gitignore` (без коментарів і порожніх). Якщо файла нема — `null`.
|
|
165
|
-
* @returns {Promise<string[] | null>} список trim-нутих рядків або `null`
|
|
166
|
-
*/
|
|
167
|
-
async function readGitignoreLines() {
|
|
168
|
-
if (!existsSync('.gitignore')) return null
|
|
169
|
-
const raw = await readFile('.gitignore', 'utf8')
|
|
170
|
-
return raw
|
|
171
|
-
.split('\n')
|
|
172
|
-
.map(l => l.trim())
|
|
173
|
-
.filter(l => l.length > 0 && !l.startsWith('#'))
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Перевіряє, що `.n-minify-image.tsv` НЕ в `.gitignore` — він має бути в git
|
|
178
|
-
* (split-cache 3.2.0: source of truth для slow-path і lifetime savings).
|
|
179
|
-
*
|
|
180
|
-
* Сам факт існування файла НЕ вимагається — на свіжому проєкті без обробки
|
|
181
|
-
* зображень його ще нема, це нормально.
|
|
182
|
-
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
183
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
184
|
-
* @returns {Promise<void>}
|
|
185
|
-
*/
|
|
186
|
-
async function checkHashCacheNotIgnored(pass, fail) {
|
|
187
|
-
const lines = await readGitignoreLines()
|
|
188
|
-
if (lines && lines.includes(HASH_CACHE_FILENAME)) {
|
|
189
|
-
fail(
|
|
190
|
-
`.gitignore: прибери рядок \`${HASH_CACHE_FILENAME}\` — це закомічений source of truth split-cache 3.2.0 (image.mdc)`
|
|
191
|
-
)
|
|
192
|
-
} else {
|
|
193
|
-
pass(`${HASH_CACHE_FILENAME} не в .gitignore (має бути в git)`)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Перевіряє, що застарілий `.minify-image-cache.tsv` (з версій < 3.2) видалений
|
|
199
|
-
* з кореня. Якщо лежить — користувач не завершив міграцію на split-cache, що
|
|
200
|
-
* залишає файл як орфана у git-історії.
|
|
201
|
-
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
202
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
203
|
-
* @returns {Promise<void>}
|
|
204
|
-
*/
|
|
205
|
-
async function checkLegacyCacheRemoved(pass, fail) {
|
|
206
|
-
if (existsSync(LEGACY_CACHE_FILENAME)) {
|
|
207
|
-
fail(
|
|
208
|
-
`${LEGACY_CACHE_FILENAME} застарілий (split-cache 3.2.0) — видали: ` +
|
|
209
|
-
`\`git rm --cached ${LEGACY_CACHE_FILENAME} 2>/dev/null || true && rm -f ${LEGACY_CACHE_FILENAME}\` ` +
|
|
210
|
-
'(також прибери відповідний рядок з .gitignore, якщо є)'
|
|
211
|
-
)
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
const lines = await readGitignoreLines()
|
|
215
|
-
if (lines && lines.includes(LEGACY_CACHE_FILENAME)) {
|
|
216
|
-
fail(`.gitignore: прибери застарілий рядок \`${LEGACY_CACHE_FILENAME}\` — split-cache 3.2.0 його не використовує`)
|
|
217
|
-
return
|
|
218
|
-
}
|
|
219
|
-
pass(`${LEGACY_CACHE_FILENAME} відсутній (міграція на split-cache завершена)`)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
72
|
/**
|
|
223
73
|
* Чи у `package.json` пакета вимкнено avif-перевірку Vue-імпортів.
|
|
224
74
|
* Очікувана форма: `"@nitra/minify-image": { "disable-avif": true }`.
|
|
@@ -233,32 +83,62 @@ function packageHasAvifDisabled(pkg) {
|
|
|
233
83
|
}
|
|
234
84
|
|
|
235
85
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
86
|
+
* Будує впорядкований список кандидатів-абсолютних шляхів, по яких треба перевіряти
|
|
87
|
+
* наявність зображення для даного посилання у `.vue`/`.html`. Caller перевіряє кожен
|
|
88
|
+
* кандидат на існування `<candidate>.avif` (для rewrite) або `<candidate>` (для збору
|
|
89
|
+
* вже-вживаного `.avif`) і обирає перший, що існує.
|
|
90
|
+
*
|
|
91
|
+
* Підтримувані форми:
|
|
92
|
+
* - `./x.png`, `../x.png` — відносно файла-джерела (ES-import / asset-relative).
|
|
93
|
+
* - `/x.png` — у Vite/Quasar-конвенції це `<packageRoot>/public/x.png`. Спочатку пробуємо
|
|
94
|
+
* `public/`, потім сам корінь пакета (на випадок mono-репо без `public/`), нарешті
|
|
95
|
+
* `<cwd>/x.png` як legacy fallback (щоб не зламати проєкти з кореневими ассетами).
|
|
96
|
+
* - голий шлях з принаймні одним `/` (`assets/img.png`, `start-page-ua/logo.png`) — у
|
|
97
|
+
* HTML/Vue браузер резолвить його відносно документа, тому повертаємо relative-to-source
|
|
98
|
+
* та `<packageRoot>/public/<path>` як другий кандидат (Quasar-проєкти кладуть public-assets
|
|
99
|
+
* саме туди).
|
|
100
|
+
* - bare без `/` (`foo`) — ймовірно alias resolver (Vite/Webpack), резолвити не вміємо,
|
|
101
|
+
* повертаємо порожній список → caller просто пропускає посилання, не звітує fail.
|
|
102
|
+
* @param {string} importPath шлях з `import x from '...'` або `src="..."`
|
|
103
|
+
* @param {string} sourceAbsPath абсолютний шлях файла-джерела
|
|
104
|
+
* @param {string|null} packageRootAbs абсолютний корінь workspace-пакета, у якому лежить
|
|
105
|
+
* `sourceAbsPath` (для резолвера `/path` як `<root>/public<path>`); `null`, якщо невідомо
|
|
106
|
+
* @returns {string[]} впорядкований список абсолютних шляхів-кандидатів
|
|
242
107
|
*/
|
|
243
|
-
function
|
|
108
|
+
function resolveImageCandidates(importPath, sourceAbsPath, packageRootAbs) {
|
|
244
109
|
if (importPath.startsWith('.')) {
|
|
245
|
-
return join(sourceAbsPath, '..', importPath)
|
|
110
|
+
return [join(sourceAbsPath, '..', importPath)]
|
|
246
111
|
}
|
|
247
112
|
if (importPath.startsWith('/')) {
|
|
248
|
-
|
|
113
|
+
/** @type {string[]} */
|
|
114
|
+
const candidates = []
|
|
115
|
+
if (packageRootAbs) {
|
|
116
|
+
candidates.push(join(packageRootAbs, 'public', importPath))
|
|
117
|
+
candidates.push(join(packageRootAbs, importPath))
|
|
118
|
+
}
|
|
119
|
+
candidates.push(join(process.cwd(), importPath))
|
|
120
|
+
return candidates
|
|
121
|
+
}
|
|
122
|
+
if (importPath.includes('/')) {
|
|
123
|
+
/** @type {string[]} */
|
|
124
|
+
const candidates = [join(sourceAbsPath, '..', importPath)]
|
|
125
|
+
if (packageRootAbs) {
|
|
126
|
+
candidates.push(join(packageRootAbs, 'public', importPath))
|
|
127
|
+
}
|
|
128
|
+
return candidates
|
|
249
129
|
}
|
|
250
|
-
return
|
|
130
|
+
return []
|
|
251
131
|
}
|
|
252
132
|
|
|
253
133
|
/**
|
|
254
|
-
* Аґреговані лічильники по проходу `check image`:
|
|
134
|
+
* Аґреговані лічильники по проходу `check image-avif`:
|
|
255
135
|
* - `rewrittenRefs` — скільки конкретних посилань (по одному на match) переписано на `.avif`;
|
|
256
136
|
* - `rewrittenFiles` — у скількох `.vue`/`.html` файлах хоч одне посилання змінилося;
|
|
257
137
|
* - `failedRefs` — скільки конкретних посилань не вдалося переписати (`.avif` не існував).
|
|
258
138
|
* @typedef {object} RewriteStats
|
|
259
|
-
* @property {number} rewrittenRefs
|
|
260
|
-
* @property {number} rewrittenFiles
|
|
261
|
-
* @property {number} failedRefs
|
|
139
|
+
* @property {number} rewrittenRefs скільки конкретних посилань переписано на `.avif`
|
|
140
|
+
* @property {number} rewrittenFiles у скількох `.vue`/`.html` файлах хоч одне посилання змінилося
|
|
141
|
+
* @property {number} failedRefs скільки конкретних посилань не вдалося переписати (`.avif` не існував)
|
|
262
142
|
*/
|
|
263
143
|
|
|
264
144
|
/**
|
|
@@ -309,12 +189,18 @@ async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePa
|
|
|
309
189
|
*/
|
|
310
190
|
const processMatches = (regex, renderFailure) => {
|
|
311
191
|
updated = updated.replaceAll(regex, (full, importPath) => {
|
|
192
|
+
const candidates = resolveImageCandidates(importPath, absPath, absRoot)
|
|
193
|
+
if (candidates.length === 0) {
|
|
194
|
+
// Bare alias (наприклад, '@/assets/x.png' без `/` — впізнаваний alias у Vite/WP);
|
|
195
|
+
// резолвера тут нема, тому посилання не чіпаємо і не звітуємо як fail.
|
|
196
|
+
return full
|
|
197
|
+
}
|
|
312
198
|
const newImportPath = `${importPath}.avif`
|
|
313
199
|
const replaced = full.replace(importPath, newImportPath)
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
200
|
+
const found = candidates.find(c => existsSync(`${c}.avif`))
|
|
201
|
+
if (found) {
|
|
316
202
|
stats.rewrittenRefs++
|
|
317
|
-
usedAvifAbs.add(`${
|
|
203
|
+
usedAvifAbs.add(`${found}.avif`)
|
|
318
204
|
return replaced
|
|
319
205
|
}
|
|
320
206
|
stats.failedRefs++
|
|
@@ -327,7 +213,7 @@ async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePa
|
|
|
327
213
|
VUE_RASTER_IMPORT_RE,
|
|
328
214
|
importPath =>
|
|
329
215
|
`[${label}] ${rel}: import з '${importPath}' має посилатись на AVIF-двійник '${importPath}.avif' ` +
|
|
330
|
-
`(\`npx @nitra/cursor check image\` створює його поряд, якщо оригінал є на диску). Вимкнути локально: "@nitra/minify-image": { "disable-avif": true } у package.json пакета`
|
|
216
|
+
`(\`npx @nitra/cursor check image-avif\` створює його поряд, якщо оригінал є на диску). Вимкнути локально: "@nitra/minify-image": { "disable-avif": true } у package.json пакета`
|
|
331
217
|
)
|
|
332
218
|
processMatches(
|
|
333
219
|
VUE_RASTER_STATIC_SRC_RE,
|
|
@@ -338,8 +224,10 @@ async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePa
|
|
|
338
224
|
|
|
339
225
|
for (const match of updated.matchAll(VUE_AVIF_REF_RE)) {
|
|
340
226
|
const avifPath = match[1]
|
|
341
|
-
const
|
|
342
|
-
|
|
227
|
+
const candidates = resolveImageCandidates(avifPath, absPath, absRoot)
|
|
228
|
+
for (const cand of candidates) {
|
|
229
|
+
if (existsSync(cand)) usedAvifAbs.add(cand)
|
|
230
|
+
}
|
|
343
231
|
}
|
|
344
232
|
|
|
345
233
|
if (updated !== original) {
|
|
@@ -352,7 +240,7 @@ async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePa
|
|
|
352
240
|
/**
|
|
353
241
|
* Сканує всі workspace-пакети: для кожного перевіряє opt-out і за потреби викликає
|
|
354
242
|
* перевірку Vue-imports. Перевірка пропускається, якщо в репозиторії немає workspaces
|
|
355
|
-
* або немає `.vue`-файлів — тоді `image` правило не для цього проєкту.
|
|
243
|
+
* або немає `.vue`-файлів — тоді `image-avif` правило не для цього проєкту.
|
|
356
244
|
*
|
|
357
245
|
* Повертає список абсолютних коренів пакетів, у яких ввімкнено opt-out (`disable-avif: true`).
|
|
358
246
|
* Це окремий результат, бо AVIF всередині такого пакета НЕ можна вважати «сиротою» лише
|
|
@@ -445,7 +333,7 @@ function runAvifGeneration() {
|
|
|
445
333
|
*
|
|
446
334
|
* AVIF файли всередині opt-out пакетів (`disable-avif: true`) пропускаються — ми не
|
|
447
335
|
* сканували їх шаблони, тож не маємо права вважати їх AVIF сиротами. Це гарантує
|
|
448
|
-
* ідемпотентність повторного `check image` для пакетів, що навмисно вимкнули правило
|
|
336
|
+
* ідемпотентність повторного `check image-avif` для пакетів, що навмисно вимкнули правило
|
|
449
337
|
* (наприклад, мобільний бандл, де AVIF підтримка не гарантована).
|
|
450
338
|
* @param {Set<string>} usedAvifAbs абсолютні шляхи `.avif`, що мають живі посилання
|
|
451
339
|
* @param {string[]} optedOutAbs абсолютні шляхи коренів пакетів з опт-аутом —
|
|
@@ -462,6 +350,8 @@ async function cleanupOrphanAvifs(usedAvifAbs, optedOutAbs, ignorePaths) {
|
|
|
462
350
|
if (!absPath.endsWith('.avif')) return
|
|
463
351
|
if (usedAvifAbs.has(absPath)) return
|
|
464
352
|
if (optedOutAbs.some(root => absPath === root || absPath.startsWith(`${root}/`))) return
|
|
353
|
+
const segments = absPath.split('/')
|
|
354
|
+
if (segments.some(seg => CLEANUP_EXTRA_IGNORE_DIR_NAMES.has(seg))) return
|
|
465
355
|
orphans.push(absPath)
|
|
466
356
|
},
|
|
467
357
|
ignorePaths
|
|
@@ -473,42 +363,14 @@ async function cleanupOrphanAvifs(usedAvifAbs, optedOutAbs, ignorePaths) {
|
|
|
473
363
|
}
|
|
474
364
|
|
|
475
365
|
/**
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
479
|
-
* @returns {Promise<boolean>} `true`, якщо `package.json` знайдено й оброблено; `false` — нема
|
|
480
|
-
*/
|
|
481
|
-
async function checkPackageJsonImage(pass, fail) {
|
|
482
|
-
if (!existsSync('package.json')) {
|
|
483
|
-
fail('package.json не знайдено в корені — додай (image.mdc)')
|
|
484
|
-
return false
|
|
485
|
-
}
|
|
486
|
-
const pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
487
|
-
const scripts = /** @type {Record<string, unknown>} */ (pkg.scripts || {})
|
|
488
|
-
checkLintImageScript(typeof scripts['lint-image'] === 'string' ? scripts['lint-image'] : undefined, pass, fail)
|
|
489
|
-
checkLintAggregateIncludesImage(typeof scripts.lint === 'string' ? scripts.lint : undefined, pass, fail)
|
|
490
|
-
checkMinifyImageNotInDeps(pkg, pass, fail)
|
|
491
|
-
return true
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Перевіряє відповідність проєкту правилам `image.mdc` (split-cache 3.2.0):
|
|
496
|
-
* `lint-image` через `npx @nitra/minify-image --src=. --write` (без `--avif`!), агрегований `lint`,
|
|
497
|
-
* `.n-minify-image.tsv` НЕ в `.gitignore` (committed source of truth), застарілий
|
|
498
|
-
* `.minify-image-cache.tsv` видалений. Окремо виконуються дії: запуск AVIF-генерації,
|
|
499
|
-
* авто-заміна raster-посилань у `.vue`/`.html`, видалення AVIF-сиріт. CI-workflow
|
|
500
|
-
* для image не вимагається — лінт зображень виконується лише локально.
|
|
366
|
+
* Виконує AVIF-етап: запуск AVIF-генерації, авто-заміна raster-посилань у `.vue`/`.html`,
|
|
367
|
+
* видалення AVIF-сиріт. Не валідує `package.json`/`lint-image` — це вже у `image-compress`.
|
|
501
368
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
502
369
|
*/
|
|
503
370
|
export async function check() {
|
|
504
371
|
const reporter = createCheckReporter()
|
|
505
372
|
const { pass, fail } = reporter
|
|
506
373
|
|
|
507
|
-
const pkgFound = await checkPackageJsonImage(pass, fail)
|
|
508
|
-
if (pkgFound) {
|
|
509
|
-
await checkHashCacheNotIgnored(pass, fail)
|
|
510
|
-
await checkLegacyCacheRemoved(pass, fail)
|
|
511
|
-
}
|
|
512
374
|
const ignorePaths = await loadCursorIgnorePaths(process.cwd())
|
|
513
375
|
|
|
514
376
|
if (await hasAnyRasterImage(ignorePaths)) {
|
|
@@ -523,7 +385,7 @@ export async function check() {
|
|
|
523
385
|
const orphansDeleted = await cleanupOrphanAvifs(usedAvifAbs, optedOutAbs, ignorePaths)
|
|
524
386
|
|
|
525
387
|
pass(
|
|
526
|
-
`image: rewrote ${stats.rewrittenRefs} reference${stats.rewrittenRefs === 1 ? '' : 's'} in ${stats.rewrittenFiles} file${stats.rewrittenFiles === 1 ? '' : 's'}; ` +
|
|
388
|
+
`image-avif: rewrote ${stats.rewrittenRefs} reference${stats.rewrittenRefs === 1 ? '' : 's'} in ${stats.rewrittenFiles} file${stats.rewrittenFiles === 1 ? '' : 's'}; ` +
|
|
527
389
|
`deleted ${orphansDeleted} orphan AVIF${orphansDeleted === 1 ? '' : 's'}; ` +
|
|
528
390
|
`failed to rewrite ${stats.failedRefs} reference${stats.failedRefs === 1 ? '' : 's'}`
|
|
529
391
|
)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє відповідність репозиторію правилу `image-compress.mdc`: канонічний скрипт
|
|
3
|
+
* `lint-image` для оптимізації raster/SVG через `@nitra/minify-image` ≥ 3.2.0 (локально).
|
|
4
|
+
*
|
|
5
|
+
* Очікування:
|
|
6
|
+
* - у кореневому `package.json` є скрипт `lint-image`, який викликає `npx @nitra/minify-image`
|
|
7
|
+
* з обовʼязковими `--src=.` і `--write`. Прапорець `--avif` у `lint-image` заборонений —
|
|
8
|
+
* AVIF-генерацію виконує окреме правило `image-avif` (інакше `bun run lint` плодив би `.avif`
|
|
9
|
+
* для зображень, що ніде не вживаються);
|
|
10
|
+
* - якщо в `package.json` є агрегований скрипт `lint`, він викликає `bun run lint-image`
|
|
11
|
+
* (симетрично до `lint-text`, `lint-js`, `lint-ga`);
|
|
12
|
+
* - `@nitra/minify-image` не оголошений у `dependencies`/`devDependencies` —
|
|
13
|
+
* CLI запускається лише через `npx` (як `markdownlint-cli2` у `text.mdc`);
|
|
14
|
+
* - `.n-minify-image.tsv` (committed source of truth з sha1/originalSize/size) НЕ
|
|
15
|
+
* в `.gitignore` — він має бути в git. Локальний mtime-кеш у
|
|
16
|
+
* `node_modules/.cache/@nitra/minify-image/mtime.tsv` авто-gitignored через `node_modules/`,
|
|
17
|
+
* окремої перевірки не вимагає;
|
|
18
|
+
* - застарілий `.minify-image-cache.tsv` (з версій < 3.2) видалений з кореня — інакше
|
|
19
|
+
* проєкт лишається у напівпереміщеному стані.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync } from 'node:fs'
|
|
22
|
+
import { readFile } from 'node:fs/promises'
|
|
23
|
+
|
|
24
|
+
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
25
|
+
|
|
26
|
+
/** Імʼя CLI-пакета: рядок у `lint-image` і заборонений у залежностях. */
|
|
27
|
+
const MINIFY_PACKAGE_NAME = '@nitra/minify-image'
|
|
28
|
+
|
|
29
|
+
/** Імʼя committed-кешу (sha1 + originalSize + size) у `@nitra/minify-image` ≥ 3.2.0. */
|
|
30
|
+
const HASH_CACHE_FILENAME = '.n-minify-image.tsv'
|
|
31
|
+
|
|
32
|
+
/** Імʼя застарілого 4-колонкового кешу (`@nitra/minify-image` < 3.2). Має бути видалений після міграції. */
|
|
33
|
+
const LEGACY_CACHE_FILENAME = '.minify-image-cache.tsv'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Перевіряє скрипт `lint-image` у `package.json`.
|
|
37
|
+
*
|
|
38
|
+
* Має містити виклик `npx @nitra/minify-image` з обовʼязковими прапорцями `--src=.`
|
|
39
|
+
* і `--write` (авто-оптимізація на місці). Прапорець `--avif` у `lint-image`
|
|
40
|
+
* заборонений — AVIF-генерацію виконує `check image-avif`, інакше `bun run lint` плодить
|
|
41
|
+
* `.avif` для зображень, що ніде не вживаються.
|
|
42
|
+
* @param {string|undefined} lintImage значення `scripts['lint-image']`
|
|
43
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
44
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
45
|
+
* @returns {void}
|
|
46
|
+
*/
|
|
47
|
+
function checkLintImageScript(lintImage, pass, fail) {
|
|
48
|
+
const canonical = `npx ${MINIFY_PACKAGE_NAME} --src=. --write`
|
|
49
|
+
if (typeof lintImage !== 'string' || !lintImage.trim()) {
|
|
50
|
+
fail(`package.json: додай скрипт "lint-image" з \`${canonical}\` (image-compress.mdc)`)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (!lintImage.includes(`npx ${MINIFY_PACKAGE_NAME}`)) {
|
|
54
|
+
fail(`package.json: lint-image має викликати \`npx ${MINIFY_PACKAGE_NAME}\` (image-compress.mdc)`)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
/** @type {{ flag: string, variants: string[], hint: string }[]} */
|
|
58
|
+
const requiredFlags = [
|
|
59
|
+
{ flag: '--src=.', variants: ['--src=.', '--src .'], hint: '`--src=.`' },
|
|
60
|
+
{ flag: '--write', variants: ['--write'], hint: '`--write` (авто-оптимізація на місці)' }
|
|
61
|
+
]
|
|
62
|
+
const missing = requiredFlags.filter(f => !f.variants.some(v => lintImage.includes(v)))
|
|
63
|
+
if (missing.length > 0) {
|
|
64
|
+
fail(
|
|
65
|
+
`package.json: lint-image має містити ${missing.map(f => f.hint).join(', ')} — канонічний виклик: \`${canonical}\` (image-compress.mdc)`
|
|
66
|
+
)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
if (lintImage.includes('--avif')) {
|
|
70
|
+
fail(
|
|
71
|
+
`package.json: прибери \`--avif\` з lint-image — AVIF-генерацію виконує \`npx @nitra/cursor check image-avif\` (image-compress.mdc). Канонічний виклик: \`${canonical}\``
|
|
72
|
+
)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
pass(`package.json: lint-image викликає \`${canonical}\``)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Перевіряє, що агрегований `lint` (якщо є) кличе `bun run lint-image` —
|
|
80
|
+
* симетрично до `lint-text`, `lint-js`, `lint-ga`.
|
|
81
|
+
* @param {string|undefined} lintAggregate значення `scripts.lint`
|
|
82
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
83
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
84
|
+
* @returns {void}
|
|
85
|
+
*/
|
|
86
|
+
function checkLintAggregateIncludesImage(lintAggregate, pass, fail) {
|
|
87
|
+
if (typeof lintAggregate !== 'string' || !lintAggregate.trim()) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
if (lintAggregate.includes('bun run lint-image')) {
|
|
91
|
+
pass('package.json: агрегований `lint` викликає `bun run lint-image`')
|
|
92
|
+
} else {
|
|
93
|
+
fail(
|
|
94
|
+
'package.json: у `lint` додай `bun run lint-image` (image-compress.mdc, симетрично до lint-text / lint-js / lint-ga)'
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Забороняє `@nitra/minify-image` у `dependencies` чи `devDependencies` —
|
|
101
|
+
* CLI завжди запускається через `npx` (як `markdownlint-cli2` у `text.mdc`).
|
|
102
|
+
* @param {{ dependencies?: Record<string, unknown>, devDependencies?: Record<string, unknown> }} pkg розібраний package.json
|
|
103
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
104
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
105
|
+
* @returns {void}
|
|
106
|
+
*/
|
|
107
|
+
function checkMinifyImageNotInDeps(pkg, pass, fail) {
|
|
108
|
+
const inDeps = Boolean(pkg.dependencies && MINIFY_PACKAGE_NAME in pkg.dependencies)
|
|
109
|
+
const inDevDeps = Boolean(pkg.devDependencies && MINIFY_PACKAGE_NAME in pkg.devDependencies)
|
|
110
|
+
if (inDeps || inDevDeps) {
|
|
111
|
+
fail(
|
|
112
|
+
`package.json: ${MINIFY_PACKAGE_NAME} не додавай у dependencies/devDependencies — лише через \`npx\` (image-compress.mdc)`
|
|
113
|
+
)
|
|
114
|
+
} else {
|
|
115
|
+
pass(`package.json: ${MINIFY_PACKAGE_NAME} не оголошено в dependencies/devDependencies`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Зчитує всі змістовні рядки `.gitignore` (без коментарів і порожніх). Якщо файла нема — `null`.
|
|
121
|
+
* @returns {Promise<string[] | null>} список trim-нутих рядків або `null`
|
|
122
|
+
*/
|
|
123
|
+
async function readGitignoreLines() {
|
|
124
|
+
if (!existsSync('.gitignore')) return null
|
|
125
|
+
const raw = await readFile('.gitignore', 'utf8')
|
|
126
|
+
return raw
|
|
127
|
+
.split('\n')
|
|
128
|
+
.map(l => l.trim())
|
|
129
|
+
.filter(l => l.length > 0 && !l.startsWith('#'))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Перевіряє, що `.n-minify-image.tsv` НЕ в `.gitignore` — він має бути в git
|
|
134
|
+
* (split-cache 3.2.0: source of truth для slow-path і lifetime savings).
|
|
135
|
+
*
|
|
136
|
+
* Сам факт існування файла НЕ вимагається — на свіжому проєкті без обробки
|
|
137
|
+
* зображень його ще нема, це нормально.
|
|
138
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
139
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
140
|
+
* @returns {Promise<void>}
|
|
141
|
+
*/
|
|
142
|
+
async function checkHashCacheNotIgnored(pass, fail) {
|
|
143
|
+
const lines = await readGitignoreLines()
|
|
144
|
+
if (lines && lines.includes(HASH_CACHE_FILENAME)) {
|
|
145
|
+
fail(
|
|
146
|
+
`.gitignore: прибери рядок \`${HASH_CACHE_FILENAME}\` — це закомічений source of truth split-cache 3.2.0 (image-compress.mdc)`
|
|
147
|
+
)
|
|
148
|
+
} else {
|
|
149
|
+
pass(`${HASH_CACHE_FILENAME} не в .gitignore (має бути в git)`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Перевіряє, що застарілий `.minify-image-cache.tsv` (з версій < 3.2) видалений
|
|
155
|
+
* з кореня. Якщо лежить — користувач не завершив міграцію на split-cache, що
|
|
156
|
+
* залишає файл як орфана у git-історії.
|
|
157
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
158
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
159
|
+
* @returns {Promise<void>}
|
|
160
|
+
*/
|
|
161
|
+
async function checkLegacyCacheRemoved(pass, fail) {
|
|
162
|
+
if (existsSync(LEGACY_CACHE_FILENAME)) {
|
|
163
|
+
fail(
|
|
164
|
+
`${LEGACY_CACHE_FILENAME} застарілий (split-cache 3.2.0) — видали: ` +
|
|
165
|
+
`\`git rm --cached ${LEGACY_CACHE_FILENAME} 2>/dev/null || true && rm -f ${LEGACY_CACHE_FILENAME}\` ` +
|
|
166
|
+
'(також прибери відповідний рядок з .gitignore, якщо є)'
|
|
167
|
+
)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
const lines = await readGitignoreLines()
|
|
171
|
+
if (lines && lines.includes(LEGACY_CACHE_FILENAME)) {
|
|
172
|
+
fail(`.gitignore: прибери застарілий рядок \`${LEGACY_CACHE_FILENAME}\` — split-cache 3.2.0 його не використовує`)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
pass(`${LEGACY_CACHE_FILENAME} відсутній (міграція на split-cache завершена)`)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Перевіряє кореневий `package.json`: скрипти, заборонені залежності, агрегований `lint`.
|
|
180
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
181
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
182
|
+
* @returns {Promise<boolean>} `true`, якщо `package.json` знайдено й оброблено; `false` — нема
|
|
183
|
+
*/
|
|
184
|
+
async function checkPackageJsonImage(pass, fail) {
|
|
185
|
+
if (!existsSync('package.json')) {
|
|
186
|
+
fail('package.json не знайдено в корені — додай (image-compress.mdc)')
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
const pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
190
|
+
const scripts = /** @type {Record<string, unknown>} */ (pkg.scripts || {})
|
|
191
|
+
checkLintImageScript(typeof scripts['lint-image'] === 'string' ? scripts['lint-image'] : undefined, pass, fail)
|
|
192
|
+
checkLintAggregateIncludesImage(typeof scripts.lint === 'string' ? scripts.lint : undefined, pass, fail)
|
|
193
|
+
checkMinifyImageNotInDeps(pkg, pass, fail)
|
|
194
|
+
return true
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Перевіряє відповідність проєкту правилу `image-compress.mdc`: канонічний `lint-image`
|
|
199
|
+
* (через `npx @nitra/minify-image --src=. --write`, без `--avif`!), агрегований `lint`,
|
|
200
|
+
* `@nitra/minify-image` не у залежностях, `.n-minify-image.tsv` НЕ в `.gitignore`,
|
|
201
|
+
* застарілий `.minify-image-cache.tsv` видалений. CI-workflow для image не вимагається —
|
|
202
|
+
* лінт зображень виконується лише локально.
|
|
203
|
+
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
204
|
+
*/
|
|
205
|
+
export async function check() {
|
|
206
|
+
const reporter = createCheckReporter()
|
|
207
|
+
const { pass, fail } = reporter
|
|
208
|
+
|
|
209
|
+
const pkgFound = await checkPackageJsonImage(pass, fail)
|
|
210
|
+
if (pkgFound) {
|
|
211
|
+
await checkHashCacheNotIgnored(pass, fail)
|
|
212
|
+
await checkLegacyCacheRemoved(pass, fail)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return reporter.getExitCode()
|
|
216
|
+
}
|
package/scripts/check-js-run.mjs
CHANGED
|
@@ -358,7 +358,7 @@ async function checkWorkspacePackage(rootDir, ignorePaths, workflows, fail, pass
|
|
|
358
358
|
*/
|
|
359
359
|
function checkDepcheckInWorkflows(rootDir, workflows, label, fail, passFn) {
|
|
360
360
|
if (workflows.length === 0) return
|
|
361
|
-
const violations = findDepcheckViolationsForPackage(workflows, rootDir.
|
|
361
|
+
const violations = findDepcheckViolationsForPackage(workflows, rootDir.replaceAll('\\', '/'))
|
|
362
362
|
if (violations.length === 0) {
|
|
363
363
|
passFn(`${label}depcheck у path-scoped workflow налаштовано (або відсутній path-scoped workflow для пакета)`)
|
|
364
364
|
return
|
|
@@ -37,7 +37,7 @@ const IGNORES_FLAG_RE = /--ignores\s*=?\s*(?:"([^"]*)"|'([^']*)'|([^\s"']+))/u
|
|
|
37
37
|
* @returns {boolean} `true`, якщо знайдено хоча б один підходящий glob
|
|
38
38
|
*/
|
|
39
39
|
export function workflowHasPathsScopedToPackage(root, pkgRoot) {
|
|
40
|
-
const prefix = `${pkgRoot.
|
|
40
|
+
const prefix = `${pkgRoot.replaceAll('\\', '/').replace(/\/+$/, '')}/`
|
|
41
41
|
const on = root?.on
|
|
42
42
|
if (!on || typeof on !== 'object') return false
|
|
43
43
|
for (const event of /** @type {const} */ (['push', 'pull_request'])) {
|
|
@@ -85,8 +85,8 @@ export function extractDepcheckArgs(runText) {
|
|
|
85
85
|
export function stepWorkingDirectoryEquals(step, pkgRoot) {
|
|
86
86
|
const wd = step['working-directory']
|
|
87
87
|
if (typeof wd !== 'string') return false
|
|
88
|
-
const norm = wd.
|
|
89
|
-
const expected = pkgRoot.
|
|
88
|
+
const norm = wd.replaceAll('\\', '/').replace(/\/+$/, '')
|
|
89
|
+
const expected = pkgRoot.replaceAll('\\', '/').replace(/\/+$/, '')
|
|
90
90
|
return norm === expected
|
|
91
91
|
}
|
|
92
92
|
|
package/mdc/image.mdc
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Оптимізація зображень через @nitra/minify-image у локальному lint
|
|
3
|
-
alwaysApply: true
|
|
4
|
-
version: '1.5'
|
|
5
|
-
---
|
|
6
|
-
|
|
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`: стискає raster/SVG на місці. **AVIF-генерація (`--avif`) у `lint-image` заборонена** — її виконує лише `npx @nitra/cursor check image`, який заодно прибирає AVIF-сироти (див. секцію «AVIF-імпорти у `.vue`»). Split-cache робить повторні прогони дешевими — і локально, і після `git clone`.
|
|
8
|
-
|
|
9
|
-
Перевірка лише локальна — у CI `lint-image` не запускаємо (sharp/svgo тягнуть бінарні залежності, цінність на ubuntu-runner-ах нижча за час прогону). Окремий workflow `lint-image.yml` створювати не треба.
|
|
10
|
-
|
|
11
|
-
## `package.json`
|
|
12
|
-
|
|
13
|
-
```json title="package.json"
|
|
14
|
-
{
|
|
15
|
-
"scripts": {
|
|
16
|
-
"lint": "bun run lint-js && bun run lint-text && bun run lint-ga && bun run lint-image && oxfmt .",
|
|
17
|
-
"lint-image": "npx @nitra/minify-image --src=. --write"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Якщо в `package.json` уже є агрегований `lint`, додай у його ланцюжок `bun run lint-image` (як `bun run lint-text`, `bun run lint-js`, `bun run lint-ga`). Так розробник, що локально гонить `bun run lint`, перед фіксацією одразу бачить, чи зросли зображення.
|
|
23
|
-
|
|
24
|
-
## Split-cache
|
|
25
|
-
|
|
26
|
-
Починаючи з `@nitra/minify-image` **3.2.0** кеш розбитий на два файли з різною семантикою:
|
|
27
|
-
|
|
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
|
|
41
|
-
|
|
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
|
-
```
|
|
49
|
-
|
|
50
|
-
AVIF-двійники (`<name>.<ext>.avif`) **зберігаємо в git** — це готові артефакти для віддачі браузеру (без них ефект від AVIF втрачається на чистому checkout-і).
|
|
51
|
-
|
|
52
|
-
## AVIF-імпорти у `.vue`
|
|
53
|
-
|
|
54
|
-
AVIF-двійники (`<name>.<ext>.avif`) генерує **виключно** `npx @nitra/cursor check image` — у `lint-image` прапорець `--avif` заборонений (інакше `bun run lint` плодить непотрібні `.avif` для зображень, що ніде не використовуються). Перевірка робить три кроки в порядку:
|
|
55
|
-
|
|
56
|
-
1. Запускає `npx @nitra/minify-image --src=. --write --avif` — генерує `<name>.<ext>.avif` поряд з кожним PNG/JPEG/GIF.
|
|
57
|
-
2. Сканує `.vue` (а також `.html`) файли в кожному workspace-пакеті (root + workspaces) і автоматично переписує raster-посилання на AVIF-двійник у двох формах:
|
|
58
|
-
- **Імпорт-пов'язані** — `import x from '...png|jpg|jpeg|gif'` (далі `:src="x"` у шаблоні);
|
|
59
|
-
- **Прямі статичні** — `<img src="...png" />` у `<template>` (Vite перетворює такий шлях на asset-імпорт на етапі збірки, тож вимога та сама).
|
|
60
|
-
3. Видаляє AVIF-сироти: ходить по всіх `<...>.avif` у репозиторії; якщо на двійник не лишилось жодного посилання у `.vue`/`.html` — файл видаляється. **AVIF на диску лишається лише там, де заміна реально відбулась** — тому невикористані оригінали не накопичують `.avif`-«хвости».
|
|
61
|
-
|
|
62
|
-
```vue title="App.vue (після check image)"
|
|
63
|
-
<script setup>
|
|
64
|
-
import welcomeImage from './assets/welcome.png.avif'
|
|
65
|
-
</script>
|
|
66
|
-
|
|
67
|
-
<template>
|
|
68
|
-
<img :src="welcomeImage" alt="Welcome" />
|
|
69
|
-
</template>
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Реактивне `:src="..."` (з JS-виразом — змінною, тернарником, викликом тощо) **не сканується** — значення обчислюється у рантаймі й шлях туди потрапляє через імпорт або інший резолвинг, який ловить імпорт-перевірка вище. SVG не торкаємо (vector → AVIF безглуздо). Атрибути `data-src=`, `obj.src=` у `<script>` тощо також пропускаються.
|
|
73
|
-
|
|
74
|
-
Якщо raster-посилання у `.vue`/`.html` не вдалось переписати (наприклад, оригіналу немає на диску, тож і `.avif` не згенерувався) — `check image` падає з помилкою на конкретний файл, як раніше.
|
|
75
|
-
|
|
76
|
-
### Опт-аут для конкретного пакета
|
|
77
|
-
|
|
78
|
-
У workspace-пакеті, де AVIF-імпорти небажані (наприклад, мобільний бандл, де AVIF-підтримка не гарантована), додай у `package.json` цього пакета:
|
|
79
|
-
|
|
80
|
-
```json title="apps/mobile/package.json"
|
|
81
|
-
{
|
|
82
|
-
"@nitra/minify-image": {
|
|
83
|
-
"disable-avif": true
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Тоді перевірка пропускає `.vue` файли цього пакета. У root-`package.json` опт-аут діє лише для файлів кореня (вкладені workspaces перевіряються незалежно — вмикай прапор у кожному пакеті, де треба).
|
|
89
|
-
|
|
90
|
-
## Заборонені залежності
|
|
91
|
-
|
|
92
|
-
`@nitra/minify-image` не повинен зʼявлятися ні в `dependencies`, ні в `devDependencies` кореневого `package.json` — CLI запускається лише через `npx` (так само, як `markdownlint-cli2` у text.mdc). Якщо потрібен явний пін — закладай діапазон версій npm у самому виклику (`npx @nitra/minify-image@^3 --src=. --write`).
|
|
93
|
-
|
|
94
|
-
## Перевірка
|
|
95
|
-
|
|
96
|
-
`npx @nitra/cursor check image` (охоплює `lint-image` з обовʼязковими `--src=.`, `--write` і **забороненим** `--avif`; агрегований `lint`; `.n-minify-image.tsv` НЕ в `.gitignore` — має бути в git; відсутність застарілого `.minify-image-cache.tsv` у корені; запуск AVIF-генерації + авто-заміна raster-посилань на `.avif` у `.vue`/`.html` кожного workspace-пакета + прибирання AVIF-сиріт).
|