@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.
@@ -1,500 +0,0 @@
1
- /**
2
- * Перевіряє відповідність репозиторію правилу image.mdc для оптимізації зображень
3
- * через `@nitra/minify-image` ≥ 3.2.0 (локально — у CI лінт зображень не запускається).
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` (на додачу до валідацій):
22
- * 1. `npx @nitra/minify-image --src=. --write --avif` — генерує AVIF-двійники.
23
- * 2. У кожному workspace-пакеті переписує raster-посилання у `.vue`/`.html` на `.avif`
24
- * (де AVIF-двійник реально існує на диску). Pakety з `"@nitra/minify-image": {
25
- * "disable-avif": true }` у `package.json` пропускаються.
26
- * 3. Прибирає AVIF-сироти — `<name>.<ext>.avif`, на які не лишилось жодного посилання
27
- * у `.vue`/`.html` репозиторію, видаляються (умова правила: «AVIF лишається лише
28
- * там, де заміна вдалася»).
29
- *
30
- * Якщо raster-посилання у `.vue`/`.html` не вдалось переписати (наприклад, оригіналу
31
- * нема на диску → `.avif` теж не згенерувався) — фейл на конкретний файл, як раніше.
32
- */
33
- import { existsSync } from 'node:fs'
34
- import { readFile, unlink, writeFile } from 'node:fs/promises'
35
- import { join, relative } from 'node:path'
36
- import { spawnSync } from 'node:child_process'
37
- import { env } from 'node:process'
38
-
39
- import { createCheckReporter } from './utils/check-reporter.mjs'
40
- import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
41
- import { walkDir } from './utils/walkDir.mjs'
42
- import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
43
-
44
- /** Імʼя CLI-пакета: рядок у `lint-image` і заборонений у залежностях. */
45
- const MINIFY_PACKAGE_NAME = '@nitra/minify-image'
46
-
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
- /** Поле в `package.json` для конфігу @nitra/minify-image (наприклад, `disable-avif`). */
54
- const PKG_CONFIG_FIELD = '@nitra/minify-image'
55
-
56
- /**
57
- * Регексп для імпортів raster-зображень у `.vue` файлах.
58
- * Захоплює `import name from '...ext'` (як default, так і type-only форми не потрібні —
59
- * type-imports asset-ів не існує). Захоплюється повний шлях у групі 1.
60
- */
61
- const VUE_RASTER_IMPORT_RE = /import\s+\w[\w$]*\s+from\s+['"]([^'"\n]+\.(?:png|jpe?g|gif))['"]/giu
62
-
63
- /**
64
- * Регексп для прямих посилань на raster-зображення у HTML-атрибуті `src="..."` шаблона `.vue`
65
- * (наприклад `<img src="./hero.png" />`). Vite перетворює такі шляхи на asset-імпорти на етапі
66
- * збірки, тож для них теж діє вимога вживати AVIF-двійник.
67
- *
68
- * Лукбехайнд `(?<![:\-_.])` виключає реактивне `:src="..."` (там JS-вираз — змінна або виклик,
69
- * перевіряється через імпорт), `data-src="..."` і `obj.src=...` у `<script>`.
70
- */
71
- const VUE_RASTER_STATIC_SRC_RE = /(?<![:\-_.])\bsrc\s*=\s*['"]([^'"\s]+\.(?:png|jpe?g|gif))['"]/giu
72
-
73
- /**
74
- * Регексп для готових AVIF-посилань у `.vue`/`.html` (як `import x from '...png.avif'`,
75
- * так і `<img src="....png.avif" />`). Потрібен лише для збору множини «живих» AVIF —
76
- * щоб після авто-заміни знати, які `<...>.avif` файли ще на щось посилаються, а які
77
- * є сиротами і підлягають видаленню.
78
- */
79
- const VUE_AVIF_REF_RE = /['"]([^'"\s]+\.(?:png|jpe?g|gif)\.avif)['"]/giu
80
-
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
- /**
223
- * Чи у `package.json` пакета вимкнено avif-перевірку Vue-імпортів.
224
- * Очікувана форма: `"@nitra/minify-image": { "disable-avif": true }`.
225
- * @param {Record<string, unknown>} pkg розібраний package.json пакета
226
- * @returns {boolean} true, якщо опт-аут активовано
227
- */
228
- function packageHasAvifDisabled(pkg) {
229
- const cfg = pkg[PKG_CONFIG_FIELD]
230
- return Boolean(
231
- cfg && typeof cfg === 'object' && /** @type {Record<string, unknown>} */ (cfg)['disable-avif'] === true
232
- )
233
- }
234
-
235
- /**
236
- * Резолвить шлях зображення з імпорта/атрибуту відносно файла, що його містить, до абсолютного
237
- * шляху файла на диску. Шляхи, що не починаються з `.` чи `/`, не резолвимо (alias-resolver
238
- * Vite/тощо невідомий тут — залишаємо такі посилання як є).
239
- * @param {string} importPath шлях у `import x from '...'` або `src="..."`
240
- * @param {string} sourceAbsPath абсолютний шлях файла з посиланням
241
- * @returns {string|null} абсолютний шлях зображення або `null`, якщо резолвити не можемо
242
- */
243
- function resolveImagePath(importPath, sourceAbsPath) {
244
- if (importPath.startsWith('.')) {
245
- return join(sourceAbsPath, '..', importPath)
246
- }
247
- if (importPath.startsWith('/')) {
248
- return join(process.cwd(), importPath)
249
- }
250
- return null
251
- }
252
-
253
- /**
254
- * Сканує `.vue` і `.html` файли одного workspace-пакета: де можемо, переписує raster-посилання
255
- * на `<path>.avif`, де не можемо — фейлимо. Повертає множину `.avif`-двійників, на які
256
- * лишилось живе посилання після проходу — потрібно для подальшого прибирання сиріт.
257
- *
258
- * Заміна виконується ТІЛЬКИ якщо AVIF-двійник реально існує на диску. Якщо AVIF немає
259
- * (наприклад, оригіналу теж немає, тож `--avif` його не згенерував) — фейл, як раніше.
260
- *
261
- * Файли, що належать іншим workspace-пакетам, ігноруються — кожен пакет перевіряється рівно
262
- * один раз (інакше при обході кореня `.` ми б повторно зайшли в `demo/` і подвоїли звіти).
263
- * @param {string} packageRoot відносний шлях до кореня пакета (наприклад `'.'` або `'demo'`)
264
- * @param {string[]} otherRootsAbs абсолютні шляхи інших workspace-коренів — їх піддерева пропускаємо
265
- * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
266
- * @param {Set<string>} usedAvifAbs мутабельна множина абсолютних шляхів `.avif`, що мають
267
- * хоч одне посилання у `.vue`/`.html` (доповнюється у цій функції)
268
- * @param {(msg: string) => void} pass callback при успішній перевірці
269
- * @param {(msg: string) => void} fail callback при помилці
270
- * @returns {Promise<void>} резолвиться по завершенню перевірки одного пакета
271
- */
272
- async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePaths, usedAvifAbs, pass, fail) {
273
- const absRoot = join(process.cwd(), packageRoot)
274
- const label = packageRoot === '.' ? 'корінь' : packageRoot
275
- /** @type {string[]} */
276
- const targetFiles = []
277
- await walkDir(
278
- absRoot,
279
- absPath => {
280
- if (!absPath.endsWith('.vue') && !absPath.endsWith('.html')) return
281
- if (otherRootsAbs.some(other => absPath.startsWith(`${other}/`))) return
282
- targetFiles.push(absPath)
283
- },
284
- ignorePaths
285
- )
286
- if (targetFiles.length === 0) return
287
-
288
- let violations = 0
289
- let replacements = 0
290
- for (const absPath of targetFiles) {
291
- const rel = relative(process.cwd(), absPath).split('\\').join('/')
292
- const original = await readFile(absPath, 'utf8')
293
- let updated = original
294
-
295
- /**
296
- * @param {RegExp} regex з групою 1 = шлях до зображення
297
- * @param {(srcPath: string) => string} renderFailure повідомлення помилки
298
- */
299
- const processMatches = (regex, renderFailure) => {
300
- updated = updated.replaceAll(regex, (full, importPath) => {
301
- const newImportPath = `${importPath}.avif`
302
- const replaced = full.replace(importPath, newImportPath)
303
- const imageAbs = resolveImagePath(importPath, absPath)
304
- if (imageAbs && existsSync(`${imageAbs}.avif`)) {
305
- replacements++
306
- usedAvifAbs.add(`${imageAbs}.avif`)
307
- return replaced
308
- }
309
- violations++
310
- fail(renderFailure(importPath))
311
- return full
312
- })
313
- }
314
-
315
- processMatches(
316
- VUE_RASTER_IMPORT_RE,
317
- importPath =>
318
- `[${label}] ${rel}: import з '${importPath}' має посилатись на AVIF-двійник '${importPath}.avif' ` +
319
- `(\`npx @nitra/cursor check image\` створює його поряд, якщо оригінал є на диску). Вимкнути локально: "@nitra/minify-image": { "disable-avif": true } у package.json пакета`
320
- )
321
- processMatches(
322
- VUE_RASTER_STATIC_SRC_RE,
323
- srcPath =>
324
- `[${label}] ${rel}: пряме \`src="${srcPath}"\` у шаблоні має використовувати AVIF-двійник \`src="${srcPath}.avif"\` ` +
325
- `(або винеси у import + \`:src="..."\`). Вимкнути локально: "@nitra/minify-image": { "disable-avif": true } у package.json пакета`
326
- )
327
-
328
- for (const match of updated.matchAll(VUE_AVIF_REF_RE)) {
329
- const avifPath = match[1]
330
- const avifAbs = resolveImagePath(avifPath, absPath)
331
- if (avifAbs) usedAvifAbs.add(avifAbs)
332
- }
333
-
334
- if (updated !== original) {
335
- await writeFile(absPath, updated, 'utf8')
336
- }
337
- }
338
- if (violations === 0) {
339
- const summary = replacements > 0 ? `усі raster-посилання у .vue/.html переписано на .avif (замін: ${replacements})` : 'усі raster-посилання у .vue/.html вже на .avif (або відсутні)'
340
- pass(`[${label}] ${summary}`)
341
- }
342
- }
343
-
344
- /**
345
- * Сканує всі workspace-пакети: для кожного перевіряє opt-out і за потреби викликає
346
- * перевірку Vue-imports. Перевірка пропускається, якщо в репозиторії немає workspaces
347
- * або немає `.vue`-файлів — тоді `image` правило не для цього проєкту.
348
- * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
349
- * @param {Set<string>} usedAvifAbs мутабельна множина абсолютних шляхів `.avif`-двійників,
350
- * на які лишилось хоча б одне посилання у `.vue`/`.html` (заповнюється у викликаних функціях)
351
- * @param {(msg: string) => void} pass callback при успішній перевірці
352
- * @param {(msg: string) => void} fail callback при помилці
353
- * @returns {Promise<void>} резолвиться по завершенню перевірки всіх workspace-пакетів
354
- */
355
- async function checkVueAvifImports(ignorePaths, usedAvifAbs, pass, fail) {
356
- const roots = await getMonorepoPackageRootDirs()
357
- const absRootsByRel = new Map(roots.map(r => [r, join(process.cwd(), r)]))
358
- for (const root of roots) {
359
- const pkgPath = join(root, 'package.json')
360
- if (!existsSync(pkgPath)) continue
361
- const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
362
- if (packageHasAvifDisabled(pkg)) {
363
- pass(
364
- `[${root === '.' ? 'корінь' : root}] avif-import enforcement вимкнено через "@nitra/minify-image.disable-avif"`
365
- )
366
- continue
367
- }
368
- const otherRootsAbs = roots.filter(r => r !== root && r !== '.').map(r => absRootsByRel.get(r) ?? '')
369
- await checkVueAvifImportsInPackage(root, otherRootsAbs, ignorePaths, usedAvifAbs, pass, fail)
370
- }
371
- }
372
-
373
- /**
374
- * Чи є в репозиторії хоч один raster-файл, який мав би сенс конвертувати у AVIF.
375
- * Якщо немає — `npx @nitra/minify-image` нема що робити, тож зайвий запуск пропускаємо
376
- * (важливо у тестах: фікстурні `.png`-імпорти посилаються на неіснуючі файли, тож
377
- * minify-image все одно нічого не згенерує — а зайвий npx-спавн повільний і робить шум).
378
- * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
379
- * @returns {Promise<boolean>} `true`, якщо знайдено принаймні один `.png/.jpe?g/.gif`
380
- */
381
- async function hasAnyRasterImage(ignorePaths) {
382
- let found = false
383
- await walkDir(
384
- process.cwd(),
385
- absPath => {
386
- if (found) return
387
- if (/\.(?:png|jpe?g|gif)$/iu.test(absPath)) found = true
388
- },
389
- ignorePaths
390
- )
391
- return found
392
- }
393
-
394
- /**
395
- * Запускає `npx @nitra/minify-image --src=. --write --avif` для генерації AVIF-двійників.
396
- *
397
- * Виклик best-effort: якщо мережа/кеш недоступні чи бінарника нема — лог-варн без падіння
398
- * перевірки (валідації package.json і vue-refs все одно прогоняться, vue-refs на
399
- * відсутні `.avif` фейлять окремо). У тестах та інших ізольованих середовищах npx
400
- * можна вимкнути через `NITRA_CURSOR_NO_AVIF_RUN=1` — тоді ця функція no-op.
401
- * @returns {void}
402
- */
403
- function runAvifGeneration() {
404
- if (env.NITRA_CURSOR_NO_AVIF_RUN === '1') return
405
- const result = spawnSync('npx', [MINIFY_PACKAGE_NAME, '--src=.', '--write', '--avif'], {
406
- stdio: 'inherit',
407
- env
408
- })
409
- if (result.error) {
410
- console.log(
411
- ` ⚠️ не вдалося запустити \`npx ${MINIFY_PACKAGE_NAME} --avif\`: ${result.error.message} — vue/html-перевірка покаже файли, для яких не вистачає .avif`
412
- )
413
- return
414
- }
415
- if (typeof result.status === 'number' && result.status !== 0) {
416
- console.log(
417
- ` ⚠️ \`npx ${MINIFY_PACKAGE_NAME} --avif\` завершився з кодом ${result.status} — vue/html-перевірка покаже файли, для яких не вистачає .avif`
418
- )
419
- }
420
- }
421
-
422
- /**
423
- * Видаляє AVIF-сироти — `<...>.avif` файли, на які не лишилось жодного посилання
424
- * у `.vue`/`.html` репозиторію. Реалізує умову правила: «AVIF лишається лише там,
425
- * де заміна реально вдалася».
426
- * @param {Set<string>} usedAvifAbs абсолютні шляхи `.avif`, що мають живі посилання
427
- * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
428
- * @param {(msg: string) => void} pass callback при успішній перевірці
429
- * @returns {Promise<void>} резолвиться після видалення всіх сиріт
430
- */
431
- async function cleanupOrphanAvifs(usedAvifAbs, ignorePaths, pass) {
432
- /** @type {string[]} */
433
- const orphans = []
434
- await walkDir(
435
- process.cwd(),
436
- absPath => {
437
- if (!absPath.endsWith('.avif')) return
438
- if (usedAvifAbs.has(absPath)) return
439
- orphans.push(absPath)
440
- },
441
- ignorePaths
442
- )
443
- for (const absPath of orphans) {
444
- await unlink(absPath)
445
- }
446
- if (orphans.length > 0) {
447
- pass(`видалено AVIF-сиріт без посилань у .vue/.html: ${orphans.length}`)
448
- }
449
- }
450
-
451
- /**
452
- * Перевіряє кореневий `package.json`: скрипти, заборонені залежності, агрегований `lint`.
453
- * @param {(msg: string) => void} pass callback при успішній перевірці
454
- * @param {(msg: string) => void} fail callback при помилці
455
- * @returns {Promise<boolean>} `true`, якщо `package.json` знайдено й оброблено; `false` — нема
456
- */
457
- async function checkPackageJsonImage(pass, fail) {
458
- if (!existsSync('package.json')) {
459
- fail('package.json не знайдено в корені — додай (image.mdc)')
460
- return false
461
- }
462
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
463
- const scripts = /** @type {Record<string, unknown>} */ (pkg.scripts || {})
464
- checkLintImageScript(typeof scripts['lint-image'] === 'string' ? scripts['lint-image'] : undefined, pass, fail)
465
- checkLintAggregateIncludesImage(typeof scripts.lint === 'string' ? scripts.lint : undefined, pass, fail)
466
- checkMinifyImageNotInDeps(pkg, pass, fail)
467
- return true
468
- }
469
-
470
- /**
471
- * Перевіряє відповідність проєкту правилам `image.mdc` (split-cache 3.2.0):
472
- * `lint-image` через `npx @nitra/minify-image --src=. --write` (без `--avif`!), агрегований `lint`,
473
- * `.n-minify-image.tsv` НЕ в `.gitignore` (committed source of truth), застарілий
474
- * `.minify-image-cache.tsv` видалений. Окремо виконуються дії: запуск AVIF-генерації,
475
- * авто-заміна raster-посилань у `.vue`/`.html`, видалення AVIF-сиріт. CI-workflow
476
- * для image не вимагається — лінт зображень виконується лише локально.
477
- * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
478
- */
479
- export async function check() {
480
- const reporter = createCheckReporter()
481
- const { pass, fail } = reporter
482
-
483
- const pkgFound = await checkPackageJsonImage(pass, fail)
484
- if (pkgFound) {
485
- await checkHashCacheNotIgnored(pass, fail)
486
- await checkLegacyCacheRemoved(pass, fail)
487
- }
488
- const ignorePaths = await loadCursorIgnorePaths(process.cwd())
489
-
490
- if (await hasAnyRasterImage(ignorePaths)) {
491
- runAvifGeneration()
492
- }
493
-
494
- /** @type {Set<string>} */
495
- const usedAvifAbs = new Set()
496
- await checkVueAvifImports(ignorePaths, usedAvifAbs, pass, fail)
497
- await cleanupOrphanAvifs(usedAvifAbs, ignorePaths, pass)
498
-
499
- return reporter.getExitCode()
500
- }