@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.
- package/CHANGELOG.md +40 -0
- package/bin/auto-rules.md +3 -1
- package/mdc/image-avif.mdc +55 -0
- package/mdc/image-compress.mdc +56 -0
- package/package.json +1 -1
- package/scripts/auto-rules.mjs +4 -2
- package/scripts/check-image-avif.mjs +394 -0
- package/scripts/check-image-compress.mjs +216 -0
- package/scripts/check-js-run.mjs +1 -1
- package/scripts/utils/depcheck-workflow.mjs +3 -3
- package/mdc/image.mdc +0 -96
- package/scripts/check-image.mjs +0 -500
package/scripts/check-image.mjs
DELETED
|
@@ -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
|
-
}
|