@nitra/cursor 1.8.192 → 1.8.193
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 +12 -0
- package/package.json +1 -1
- package/scripts/check-image.mjs +55 -23
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.8.193] - 2026-05-07
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `check-image.mjs`: cleanup AVIF-сиріт більше не зачіпає `.avif` файли всередині пакетів з опт-аутом (`"@nitra/minify-image": { "disable-avif": true }`). Раніше: пакет з опт-аутом не сканувався на refs → його `.avif` потрапляли у список «сиріт» і видалялись, навіть якщо насправді використовувалися через alias / runtime-обчислений шлях. Тепер `checkVueAvifImports` повертає список абсолютних коренів opt-out пакетів, а `cleanupOrphanAvifs` пропускає `.avif` під ними.
|
|
12
|
+
- `check-image.mjs`: запис у `.vue`/`.html` тепер строго послідовний з cleanup (write-then-cleanup): перший виконує `checkVueAvifImports` (per-file `writeFile` після обробки), і тільки після цього `cleanupOrphanAvifs` читає вже оновлені `usedAvifAbs` і видаляє лише дійсних сиріт.
|
|
13
|
+
- `check-image.mjs`: введено агреговані лічильники `RewriteStats` (`rewrittenRefs` / `rewrittenFiles` / `failedRefs`) і єдиний фінальний рядок-підсумок `image: rewrote N references in M files; deleted K orphan AVIFs; failed to rewrite L references` — раніше підсумок дублювався per-package і не виокремлював orphan-cleanup vs failed-rewrites.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `tests/check-image.test.mjs`: 5 нових кейсів — статичний `<img src="a.png">` авто-переписується (за наявності `a.png` і `a.png.avif`); реактивне `:src="dyn"` залишається незмінним і orphan AVIF видаляється; змішані форми у одному файлі (статичний + import + реактивний + `data-src=`) — переписуються лише покривані; opt-out пакет — AVIF всередині не вважається сиротою; ідемпотентність повторного `check image` на чистому стані.
|
|
18
|
+
|
|
7
19
|
## [1.8.192] - 2026-05-07
|
|
8
20
|
|
|
9
21
|
### Added
|
package/package.json
CHANGED
package/scripts/check-image.mjs
CHANGED
|
@@ -250,13 +250,26 @@ function resolveImagePath(importPath, sourceAbsPath) {
|
|
|
250
250
|
return null
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Аґреговані лічильники по проходу `check image`:
|
|
255
|
+
* - `rewrittenRefs` — скільки конкретних посилань (по одному на match) переписано на `.avif`;
|
|
256
|
+
* - `rewrittenFiles` — у скількох `.vue`/`.html` файлах хоч одне посилання змінилося;
|
|
257
|
+
* - `failedRefs` — скільки конкретних посилань не вдалося переписати (`.avif` не існував).
|
|
258
|
+
* @typedef {object} RewriteStats
|
|
259
|
+
* @property {number} rewrittenRefs
|
|
260
|
+
* @property {number} rewrittenFiles
|
|
261
|
+
* @property {number} failedRefs
|
|
262
|
+
*/
|
|
263
|
+
|
|
253
264
|
/**
|
|
254
265
|
* Сканує `.vue` і `.html` файли одного workspace-пакета: де можемо, переписує raster-посилання
|
|
255
|
-
* на `<path>.avif`, де не можемо — фейлимо.
|
|
256
|
-
* лишилось живе
|
|
266
|
+
* на `<path>.avif`, де не можемо — фейлимо. Доповнює `usedAvifAbs` шляхами AVIF-двійників, на
|
|
267
|
+
* які лишилось живе посилання, і `stats` лічильниками rewrite/fail для глобального підсумку.
|
|
257
268
|
*
|
|
258
269
|
* Заміна виконується ТІЛЬКИ якщо AVIF-двійник реально існує на диску. Якщо AVIF немає
|
|
259
270
|
* (наприклад, оригіналу теж немає, тож `--avif` його не згенерував) — фейл, як раніше.
|
|
271
|
+
* Запис файла відбувається ОДРАЗУ після обробки одного файла (write-then-fail): провал на
|
|
272
|
+
* наступному файлі НЕ відкочує вже записані зміни попередніх.
|
|
260
273
|
*
|
|
261
274
|
* Файли, що належать іншим workspace-пакетам, ігноруються — кожен пакет перевіряється рівно
|
|
262
275
|
* один раз (інакше при обході кореня `.` ми б повторно зайшли в `demo/` і подвоїли звіти).
|
|
@@ -265,11 +278,11 @@ function resolveImagePath(importPath, sourceAbsPath) {
|
|
|
265
278
|
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
266
279
|
* @param {Set<string>} usedAvifAbs мутабельна множина абсолютних шляхів `.avif`, що мають
|
|
267
280
|
* хоч одне посилання у `.vue`/`.html` (доповнюється у цій функції)
|
|
268
|
-
* @param {
|
|
281
|
+
* @param {RewriteStats} stats глобальні лічильники, що мутуються тут
|
|
269
282
|
* @param {(msg: string) => void} fail callback при помилці
|
|
270
283
|
* @returns {Promise<void>} резолвиться по завершенню перевірки одного пакета
|
|
271
284
|
*/
|
|
272
|
-
async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePaths, usedAvifAbs,
|
|
285
|
+
async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePaths, usedAvifAbs, stats, fail) {
|
|
273
286
|
const absRoot = join(process.cwd(), packageRoot)
|
|
274
287
|
const label = packageRoot === '.' ? 'корінь' : packageRoot
|
|
275
288
|
/** @type {string[]} */
|
|
@@ -285,8 +298,6 @@ async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePa
|
|
|
285
298
|
)
|
|
286
299
|
if (targetFiles.length === 0) return
|
|
287
300
|
|
|
288
|
-
let violations = 0
|
|
289
|
-
let replacements = 0
|
|
290
301
|
for (const absPath of targetFiles) {
|
|
291
302
|
const rel = relative(process.cwd(), absPath).split('\\').join('/')
|
|
292
303
|
const original = await readFile(absPath, 'utf8')
|
|
@@ -302,11 +313,11 @@ async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePa
|
|
|
302
313
|
const replaced = full.replace(importPath, newImportPath)
|
|
303
314
|
const imageAbs = resolveImagePath(importPath, absPath)
|
|
304
315
|
if (imageAbs && existsSync(`${imageAbs}.avif`)) {
|
|
305
|
-
|
|
316
|
+
stats.rewrittenRefs++
|
|
306
317
|
usedAvifAbs.add(`${imageAbs}.avif`)
|
|
307
318
|
return replaced
|
|
308
319
|
}
|
|
309
|
-
|
|
320
|
+
stats.failedRefs++
|
|
310
321
|
fail(renderFailure(importPath))
|
|
311
322
|
return full
|
|
312
323
|
})
|
|
@@ -333,28 +344,34 @@ async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePa
|
|
|
333
344
|
|
|
334
345
|
if (updated !== original) {
|
|
335
346
|
await writeFile(absPath, updated, 'utf8')
|
|
347
|
+
stats.rewrittenFiles++
|
|
336
348
|
}
|
|
337
349
|
}
|
|
338
|
-
if (violations === 0) {
|
|
339
|
-
const summary = replacements > 0 ? `усі raster-посилання у .vue/.html переписано на .avif (замін: ${replacements})` : 'усі raster-посилання у .vue/.html вже на .avif (або відсутні)'
|
|
340
|
-
pass(`[${label}] ${summary}`)
|
|
341
|
-
}
|
|
342
350
|
}
|
|
343
351
|
|
|
344
352
|
/**
|
|
345
353
|
* Сканує всі workspace-пакети: для кожного перевіряє opt-out і за потреби викликає
|
|
346
354
|
* перевірку Vue-imports. Перевірка пропускається, якщо в репозиторії немає workspaces
|
|
347
355
|
* або немає `.vue`-файлів — тоді `image` правило не для цього проєкту.
|
|
356
|
+
*
|
|
357
|
+
* Повертає список абсолютних коренів пакетів, у яких ввімкнено opt-out (`disable-avif: true`).
|
|
358
|
+
* Це окремий результат, бо AVIF всередині такого пакета НЕ можна вважати «сиротою» лише
|
|
359
|
+
* на підставі відсутності посилань у його `.vue`/`.html` (ми взагалі не сканували його
|
|
360
|
+
* шаблони) — інакше cleanup помилково затирав би AVIF, що використовуються через alias /
|
|
361
|
+
* runtime-обчислений шлях / зовнішні посилання, які тут не видно.
|
|
348
362
|
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
349
363
|
* @param {Set<string>} usedAvifAbs мутабельна множина абсолютних шляхів `.avif`-двійників,
|
|
350
364
|
* на які лишилось хоча б одне посилання у `.vue`/`.html` (заповнюється у викликаних функціях)
|
|
365
|
+
* @param {RewriteStats} stats глобальні лічильники rewrite/fail (мутуються нижче)
|
|
351
366
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
352
367
|
* @param {(msg: string) => void} fail callback при помилці
|
|
353
|
-
* @returns {Promise<
|
|
368
|
+
* @returns {Promise<string[]>} абсолютні шляхи коренів пакетів з активним opt-out
|
|
354
369
|
*/
|
|
355
|
-
async function checkVueAvifImports(ignorePaths, usedAvifAbs, pass, fail) {
|
|
370
|
+
async function checkVueAvifImports(ignorePaths, usedAvifAbs, stats, pass, fail) {
|
|
356
371
|
const roots = await getMonorepoPackageRootDirs()
|
|
357
372
|
const absRootsByRel = new Map(roots.map(r => [r, join(process.cwd(), r)]))
|
|
373
|
+
/** @type {string[]} */
|
|
374
|
+
const optedOutAbs = []
|
|
358
375
|
for (const root of roots) {
|
|
359
376
|
const pkgPath = join(root, 'package.json')
|
|
360
377
|
if (!existsSync(pkgPath)) continue
|
|
@@ -363,11 +380,13 @@ async function checkVueAvifImports(ignorePaths, usedAvifAbs, pass, fail) {
|
|
|
363
380
|
pass(
|
|
364
381
|
`[${root === '.' ? 'корінь' : root}] avif-import enforcement вимкнено через "@nitra/minify-image.disable-avif"`
|
|
365
382
|
)
|
|
383
|
+
optedOutAbs.push(absRootsByRel.get(root) ?? join(process.cwd(), root))
|
|
366
384
|
continue
|
|
367
385
|
}
|
|
368
386
|
const otherRootsAbs = roots.filter(r => r !== root && r !== '.').map(r => absRootsByRel.get(r) ?? '')
|
|
369
|
-
await checkVueAvifImportsInPackage(root, otherRootsAbs, ignorePaths, usedAvifAbs,
|
|
387
|
+
await checkVueAvifImportsInPackage(root, otherRootsAbs, ignorePaths, usedAvifAbs, stats, fail)
|
|
370
388
|
}
|
|
389
|
+
return optedOutAbs
|
|
371
390
|
}
|
|
372
391
|
|
|
373
392
|
/**
|
|
@@ -423,12 +442,18 @@ function runAvifGeneration() {
|
|
|
423
442
|
* Видаляє AVIF-сироти — `<...>.avif` файли, на які не лишилось жодного посилання
|
|
424
443
|
* у `.vue`/`.html` репозиторію. Реалізує умову правила: «AVIF лишається лише там,
|
|
425
444
|
* де заміна реально вдалася».
|
|
445
|
+
*
|
|
446
|
+
* AVIF файли всередині opt-out пакетів (`disable-avif: true`) пропускаються — ми не
|
|
447
|
+
* сканували їх шаблони, тож не маємо права вважати їх AVIF сиротами. Це гарантує
|
|
448
|
+
* ідемпотентність повторного `check image` для пакетів, що навмисно вимкнули правило
|
|
449
|
+
* (наприклад, мобільний бандл, де AVIF підтримка не гарантована).
|
|
426
450
|
* @param {Set<string>} usedAvifAbs абсолютні шляхи `.avif`, що мають живі посилання
|
|
451
|
+
* @param {string[]} optedOutAbs абсолютні шляхи коренів пакетів з опт-аутом —
|
|
452
|
+
* `.avif` під ними не вважаємо сиротами і не видаляємо
|
|
427
453
|
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
428
|
-
* @
|
|
429
|
-
* @returns {Promise<void>} резолвиться після видалення всіх сиріт
|
|
454
|
+
* @returns {Promise<number>} кількість видалених сиріт
|
|
430
455
|
*/
|
|
431
|
-
async function cleanupOrphanAvifs(usedAvifAbs,
|
|
456
|
+
async function cleanupOrphanAvifs(usedAvifAbs, optedOutAbs, ignorePaths) {
|
|
432
457
|
/** @type {string[]} */
|
|
433
458
|
const orphans = []
|
|
434
459
|
await walkDir(
|
|
@@ -436,6 +461,7 @@ async function cleanupOrphanAvifs(usedAvifAbs, ignorePaths, pass) {
|
|
|
436
461
|
absPath => {
|
|
437
462
|
if (!absPath.endsWith('.avif')) return
|
|
438
463
|
if (usedAvifAbs.has(absPath)) return
|
|
464
|
+
if (optedOutAbs.some(root => absPath === root || absPath.startsWith(`${root}/`))) return
|
|
439
465
|
orphans.push(absPath)
|
|
440
466
|
},
|
|
441
467
|
ignorePaths
|
|
@@ -443,9 +469,7 @@ async function cleanupOrphanAvifs(usedAvifAbs, ignorePaths, pass) {
|
|
|
443
469
|
for (const absPath of orphans) {
|
|
444
470
|
await unlink(absPath)
|
|
445
471
|
}
|
|
446
|
-
|
|
447
|
-
pass(`видалено AVIF-сиріт без посилань у .vue/.html: ${orphans.length}`)
|
|
448
|
-
}
|
|
472
|
+
return orphans.length
|
|
449
473
|
}
|
|
450
474
|
|
|
451
475
|
/**
|
|
@@ -493,8 +517,16 @@ export async function check() {
|
|
|
493
517
|
|
|
494
518
|
/** @type {Set<string>} */
|
|
495
519
|
const usedAvifAbs = new Set()
|
|
496
|
-
|
|
497
|
-
|
|
520
|
+
/** @type {RewriteStats} */
|
|
521
|
+
const stats = { rewrittenRefs: 0, rewrittenFiles: 0, failedRefs: 0 }
|
|
522
|
+
const optedOutAbs = await checkVueAvifImports(ignorePaths, usedAvifAbs, stats, pass, fail)
|
|
523
|
+
const orphansDeleted = await cleanupOrphanAvifs(usedAvifAbs, optedOutAbs, ignorePaths)
|
|
524
|
+
|
|
525
|
+
pass(
|
|
526
|
+
`image: rewrote ${stats.rewrittenRefs} reference${stats.rewrittenRefs === 1 ? '' : 's'} in ${stats.rewrittenFiles} file${stats.rewrittenFiles === 1 ? '' : 's'}; ` +
|
|
527
|
+
`deleted ${orphansDeleted} orphan AVIF${orphansDeleted === 1 ? '' : 's'}; ` +
|
|
528
|
+
`failed to rewrite ${stats.failedRefs} reference${stats.failedRefs === 1 ? '' : 's'}`
|
|
529
|
+
)
|
|
498
530
|
|
|
499
531
|
return reporter.getExitCode()
|
|
500
532
|
}
|