@nitra/cursor 1.8.192 → 1.8.197

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.
@@ -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
+ }
@@ -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.replace(/\\/g, '/'))
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.replace(/\\/g, '/').replace(/\/+$/, '')}/`
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.replace(/\\/g, '/').replace(/\/+$/, '')
89
- const expected = pkgRoot.replace(/\\/g, '/').replace(/\/+$/, '')
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-сиріт).