@nitra/cursor 1.8.169 → 1.8.171
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 +14 -1
- package/mdc/abie.mdc +2 -14
- package/mdc/image.mdc +66 -9
- package/package.json +2 -2
- package/scripts/check-abie.mjs +1 -93
- package/scripts/check-image.mjs +187 -34
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,24 @@
|
|
|
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.171] - 2026-05-04
|
|
8
|
+
|
|
9
|
+
### Removed
|
|
10
|
+
|
|
11
|
+
- `abie.mdc` (v1.17) / `check-abie.mjs`: прибрано перевірку `.github/actionlint.yaml` (мітки `self-hosted-runner` `ua` / `dev` / `ru`). Видалено константи `ABIE_REQUIRED_ACTIONLINT_LABELS`, шаблон файлу та функції `parseActionlintSelfHostedLabels`, `abieMissingActionlintLabels`, `ensureAbieActionlintConfig`; знято відповідні юніт- та інтеграційні тести. Файл `.github/actionlint.yaml` більше не створюється і не валідовується правилом abie.
|
|
12
|
+
|
|
13
|
+
## [1.8.170] - 2026-05-03
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `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).
|
|
18
|
+
|
|
7
19
|
## [1.8.169] - 2026-05-03
|
|
8
20
|
|
|
9
21
|
### Added
|
|
10
22
|
|
|
11
|
-
- `image.mdc` (v1.
|
|
23
|
+
- `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 як готові артефакти для віддачі браузеру.
|
|
24
|
+
- `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
25
|
- `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
26
|
- `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
27
|
|
package/mdc/abie.mdc
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила для проєктів AbInBev Efes
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.17'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**, для спільних сервісів **`auth-run-hl`** / **`file-link-hl`** — **`namespace: dev`** у base та patch **`…/backendRefs/…/namespace`** у **ua** / **ru**), у overlay **ru** — кожен **Service** (у т. ч. **headless** / **`-hl`**) → **`spec.type: NodePort`** через **JSON6902** у **`kustomization.yaml`**, видалення **HealthCheckPolicy** у **ru**), гілки **dev**, **ua**, **ru** у **clean-merged-branch**,
|
|
7
|
+
Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**, для спільних сервісів **`auth-run-hl`** / **`file-link-hl`** — **`namespace: dev`** у base та patch **`…/backendRefs/…/namespace`** у **ua** / **ru**), у overlay **ru** — кожен **Service** (у т. ч. **headless** / **`-hl`**) → **`spec.type: NodePort`** через **JSON6902** у **`kustomization.yaml`**, видалення **HealthCheckPolicy** у **ru**), гілки **dev**, **ua**, **ru** у **clean-merged-branch**, а також заборона тримати артефакти **Firebase Hosting** у **підкаталогах першого рівня** (безпосередні діти кореня репозиторію; у самому корені ці імена не вимагаються до видалення).
|
|
8
8
|
|
|
9
9
|
**`npx @nitra/cursor check abie`** виконується лише якщо в **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень.
|
|
10
10
|
|
|
@@ -336,18 +336,6 @@ spec:
|
|
|
336
336
|
|
|
337
337
|
У **кожному** підкаталозі, що лежить **безпосередньо** в корені репозиторію, не тримати конфіг і кеш **Firebase Hosting**: у таких каталогах не повинно бути **`.firebaserc`**, **`firebase.json`** та каталогу **`.firebase/`** (у **самому** корені репозиторію ці імена перевіркою abie **не** розглядаються; `node_modules` / `.git` зі скану вилучаються).
|
|
338
338
|
|
|
339
|
-
## actionlint: self-hosted-runner labels
|
|
340
|
-
|
|
341
|
-
У **`.github/actionlint.yaml`** має бути блок **`self-hosted-runner.labels`** з присутніми мітками **`ua`**, **`dev`**, **`ru`**. Якщо файлу немає — **`npx @nitra/cursor check abie`** створює його з канонічним вмістом. Інші мітки, інший порядок та формат лапок дозволені — перевіряється лише наявність трьох обов'язкових міток (деталі — **`check-abie.mjs`**).
|
|
342
|
-
|
|
343
|
-
```yaml title=".github/actionlint.yaml"
|
|
344
|
-
self-hosted-runner:
|
|
345
|
-
labels:
|
|
346
|
-
- 'ua'
|
|
347
|
-
- 'dev'
|
|
348
|
-
- 'ru'
|
|
349
|
-
```
|
|
350
|
-
|
|
351
339
|
## Git branches
|
|
352
340
|
|
|
353
341
|
У **`.github/workflows/clean-merged-branch.yml`** у кроці **`phpdocker-io/github-actions-delete-abandoned-branches`** значення **`with.ignore_branches`** має містити **dev**, **ua** та **ru** (разом з іншими гілками, якщо потрібно):
|
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.171",
|
|
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-abie.mjs
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* у файлі **`k8s/ru/kustomization.yaml`** того ж пакета (overlay середовища **ru**) — inline **JSON6902** на **`kind: Service`** з тим самим **`target.name`**: **`path: /spec/type`**, **`value: NodePort`**; якщо в base було **`spec.clusterIP: None`** — **`op: remove`** для **`/spec/clusterIP`**; якщо в base **явно** задано **`spec.clusterIPs`** — також **`remove`** для **`/spec/clusterIPs`** (інакше **API** може залишити **`None`** для **NodePort**; без ключа **`clusterIPs`** у base **`remove`** на **`/spec/clusterIPs`** ламає **`kubectl kustomize`**).
|
|
40
40
|
*/
|
|
41
41
|
import { existsSync } from 'node:fs'
|
|
42
|
-
import {
|
|
42
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
43
43
|
import { dirname, join, relative } from 'node:path'
|
|
44
44
|
|
|
45
45
|
import { parseAllDocuments } from 'yaml'
|
|
@@ -118,20 +118,6 @@ const HTTPROUTE_BACKENDREF_PORT_8081_VALUE_FIRST_RE =
|
|
|
118
118
|
/** Гілки, які мають бути в **`ignore_branches`** за abie.mdc. */
|
|
119
119
|
export const ABIE_REQUIRED_IGNORE_BRANCHES = ['dev', 'ua', 'ru']
|
|
120
120
|
|
|
121
|
-
/** Канонічний шлях до конфігу actionlint у репо (abie.mdc). */
|
|
122
|
-
const ABIE_ACTIONLINT_PATH = '.github/actionlint.yaml'
|
|
123
|
-
|
|
124
|
-
/** Канонічний вміст **`.github/actionlint.yaml`**, який ми створюємо за відсутності файлу (abie.mdc). */
|
|
125
|
-
const ABIE_ACTIONLINT_TEMPLATE = `self-hosted-runner:
|
|
126
|
-
labels:
|
|
127
|
-
- 'ua'
|
|
128
|
-
- 'dev'
|
|
129
|
-
- 'ru'
|
|
130
|
-
`
|
|
131
|
-
|
|
132
|
-
/** Мітки **`self-hosted-runner.labels`**, які мають бути присутні в **`.github/actionlint.yaml`** (abie.mdc). */
|
|
133
|
-
export const ABIE_REQUIRED_ACTIONLINT_LABELS = Object.freeze(['ua', 'dev', 'ru'])
|
|
134
|
-
|
|
135
121
|
/**
|
|
136
122
|
* Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед ім'ям файлу) — специфіка abie overlay.
|
|
137
123
|
* @param {string} rel шлях від кореня репозиторію
|
|
@@ -1730,83 +1716,6 @@ async function ensureNoFirebaseHostingArtifacts(root, passFn, failFn) {
|
|
|
1730
1716
|
passFn('Підкаталоги кореня (1-й рівень, без .git/node_modules): артефактів Firebase Hosting не знайдено (abie.mdc)')
|
|
1731
1717
|
}
|
|
1732
1718
|
|
|
1733
|
-
/**
|
|
1734
|
-
* Витягує мітки **`self-hosted-runner.labels`** з тексту `.github/actionlint.yaml`.
|
|
1735
|
-
* @param {string} raw повний вміст файлу (YAML)
|
|
1736
|
-
* @returns {string[] | null} масив рядків-міток або null, якщо ключа/масиву не знайдено
|
|
1737
|
-
*/
|
|
1738
|
-
export function parseActionlintSelfHostedLabels(raw) {
|
|
1739
|
-
let docs
|
|
1740
|
-
try {
|
|
1741
|
-
docs = parseAllDocuments(raw)
|
|
1742
|
-
} catch {
|
|
1743
|
-
return null
|
|
1744
|
-
}
|
|
1745
|
-
for (const doc of docs) {
|
|
1746
|
-
if (doc.errors.length > 0) continue
|
|
1747
|
-
const root = doc.toJSON()
|
|
1748
|
-
if (root === null || typeof root !== 'object' || Array.isArray(root)) continue
|
|
1749
|
-
const block = /** @type {Record<string, unknown>} */ (root)['self-hosted-runner']
|
|
1750
|
-
if (block === null || typeof block !== 'object' || Array.isArray(block)) continue
|
|
1751
|
-
const labels = /** @type {Record<string, unknown>} */ (block).labels
|
|
1752
|
-
if (!Array.isArray(labels)) continue
|
|
1753
|
-
return labels.filter(l => typeof l === 'string')
|
|
1754
|
-
}
|
|
1755
|
-
return null
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
/**
|
|
1759
|
-
* Які з **`ABIE_REQUIRED_ACTIONLINT_LABELS`** відсутні в наданому списку міток (abie.mdc).
|
|
1760
|
-
* @param {string[]} labels мітки **`self-hosted-runner.labels`**
|
|
1761
|
-
* @returns {string[]} відсутні мітки (порожньо — все ок)
|
|
1762
|
-
*/
|
|
1763
|
-
export function abieMissingActionlintLabels(labels) {
|
|
1764
|
-
const present = new Set(labels.map(l => l.trim()))
|
|
1765
|
-
return ABIE_REQUIRED_ACTIONLINT_LABELS.filter(r => !present.has(r))
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
/**
|
|
1769
|
-
* Гарантує наявність **`.github/actionlint.yaml`** із потрібними мітками **`self-hosted-runner`** (abie.mdc):
|
|
1770
|
-
* створює файл із канонічним вмістом, якщо його немає; якщо є — звіряє мітки.
|
|
1771
|
-
* @param {string} root корінь репозиторію
|
|
1772
|
-
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
1773
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
1774
|
-
*/
|
|
1775
|
-
async function ensureAbieActionlintConfig(root, pass, fail) {
|
|
1776
|
-
const abs = join(root, ABIE_ACTIONLINT_PATH)
|
|
1777
|
-
if (!existsSync(abs)) {
|
|
1778
|
-
try {
|
|
1779
|
-
await mkdir(dirname(abs), { recursive: true })
|
|
1780
|
-
await writeFile(abs, ABIE_ACTIONLINT_TEMPLATE, 'utf8')
|
|
1781
|
-
} catch (error) {
|
|
1782
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1783
|
-
fail(`${ABIE_ACTIONLINT_PATH}: не вдалося створити (${msg}) — abie.mdc`)
|
|
1784
|
-
return
|
|
1785
|
-
}
|
|
1786
|
-
pass(`${ABIE_ACTIONLINT_PATH}: створено з self-hosted-runner.labels [ua, dev, ru] (abie.mdc)`)
|
|
1787
|
-
return
|
|
1788
|
-
}
|
|
1789
|
-
let raw
|
|
1790
|
-
try {
|
|
1791
|
-
raw = await readFile(abs, 'utf8')
|
|
1792
|
-
} catch (error) {
|
|
1793
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1794
|
-
fail(`${ABIE_ACTIONLINT_PATH}: не вдалося прочитати (${msg}) — abie.mdc`)
|
|
1795
|
-
return
|
|
1796
|
-
}
|
|
1797
|
-
const labels = parseActionlintSelfHostedLabels(raw)
|
|
1798
|
-
if (labels === null) {
|
|
1799
|
-
fail(`${ABIE_ACTIONLINT_PATH}: не знайдено self-hosted-runner.labels — додай мітки ua, dev, ru (abie.mdc)`)
|
|
1800
|
-
return
|
|
1801
|
-
}
|
|
1802
|
-
const missing = abieMissingActionlintLabels(labels)
|
|
1803
|
-
if (missing.length > 0) {
|
|
1804
|
-
fail(`${ABIE_ACTIONLINT_PATH}: у self-hosted-runner.labels бракує ${missing.join(', ')} (abie.mdc)`)
|
|
1805
|
-
return
|
|
1806
|
-
}
|
|
1807
|
-
pass(`${ABIE_ACTIONLINT_PATH}: self-hosted-runner.labels містить ua, dev, ru (abie.mdc)`)
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
1719
|
/**
|
|
1811
1720
|
* Перевіряє clean-merged-branch.yml на ignore_branches.
|
|
1812
1721
|
* @param {string} root корінь репозиторію
|
|
@@ -2178,7 +2087,6 @@ export async function check() {
|
|
|
2178
2087
|
pass('Правило abie увімкнено — виконуємо перевірки')
|
|
2179
2088
|
await ensureNoFirebaseHostingArtifacts(root, pass, fail)
|
|
2180
2089
|
await checkCleanMergedBranch(root, pass, fail)
|
|
2181
|
-
await ensureAbieActionlintConfig(root, pass, fail)
|
|
2182
2090
|
|
|
2183
2091
|
const yamlFiles = await findK8sYamlFiles(root)
|
|
2184
2092
|
const deploymentDirs = await collectDeploymentDirs(root, yamlFiles, fail)
|
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
|
}
|