@nitra/cursor 1.8.168 → 1.8.170
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 +15 -0
- package/bin/auto-rules.md +4 -0
- package/mdc/image.mdc +99 -0
- package/mdc/vue.mdc +1 -1
- package/package.json +2 -2
- package/scripts/auto-rules.mjs +37 -0
- package/scripts/check-image.mjs +320 -0
- package/scripts/check-vue.mjs +67 -4
- package/scripts/sync-claude-config.mjs +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
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.170] - 2026-05-03
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `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).
|
|
12
|
+
|
|
13
|
+
## [1.8.169] - 2026-05-03
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `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 як готові артефакти для віддачі браузеру.
|
|
18
|
+
- `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/` доповідався б двічі).
|
|
19
|
+
- `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 теж не додається).
|
|
20
|
+
- `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` працює для багаторядкових об'єктів.
|
|
21
|
+
|
|
7
22
|
## [1.8.168] - 2026-05-03
|
|
8
23
|
|
|
9
24
|
### Added
|
package/bin/auto-rules.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
## Правила, які автоматично додається до .n-cursor.json
|
|
6
6
|
|
|
7
|
+
Синтаксис `rule - [other]` означає: правило `rule` варто автододати лише якщо всі правила у списку `[other]` вже додані до конфігу (граф залежностей між правилами; умова не дублюється).
|
|
8
|
+
|
|
7
9
|
abie - якщо в кореневому package.json в секції "repository" присутній текст "<https://github.com/abinbevefes/**/>"
|
|
8
10
|
|
|
9
11
|
bun - якщо в корені проекту є package.json
|
|
@@ -18,6 +20,8 @@ graphql - якщо хоч в одному js або vue файлі присут
|
|
|
18
20
|
|
|
19
21
|
hasura - якщо в директорії присутній config.yaml, який містить рядок `metadata_directory: metadata`
|
|
20
22
|
|
|
23
|
+
image - [vue]
|
|
24
|
+
|
|
21
25
|
js-lint - якщо присутній хоч один js файл
|
|
22
26
|
|
|
23
27
|
js-run - якщо це вкладена директорія з package.json (не в корені) та в devDependencies немає vite
|
package/mdc/image.mdc
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Оптимізація зображень через @nitra/minify-image у локальному lint
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
version: '1.4'
|
|
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 --avif`: стискає raster/SVG на місці й створює AVIF-двійники (`<name>.<ext>.avif`) поряд з кожним PNG/JPEG/GIF. 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 --avif"
|
|
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`, перед фіксацією одразу бачить, чи зросли зображення, і отримує свіжі AVIF-двійники.
|
|
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` поряд з кожним 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
|
+
|
|
93
|
+
## Заборонені залежності
|
|
94
|
+
|
|
95
|
+
`@nitra/minify-image` не повинен зʼявлятися ні в `dependencies`, ні в `devDependencies` кореневого `package.json` — CLI запускається лише через `npx` (так само, як `markdownlint-cli2` у text.mdc). Якщо потрібен явний пін — закладай діапазон версій npm у самому виклику (`npx @nitra/minify-image@^3 --src=. --write --avif`).
|
|
96
|
+
|
|
97
|
+
## Перевірка
|
|
98
|
+
|
|
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/mdc/vue.mdc
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/cursor",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.170",
|
|
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/auto-rules.mjs
CHANGED
|
@@ -30,6 +30,7 @@ export const AUTO_RULE_ORDER = Object.freeze([
|
|
|
30
30
|
'ga',
|
|
31
31
|
'graphql',
|
|
32
32
|
'hasura',
|
|
33
|
+
'image',
|
|
33
34
|
'js-lint',
|
|
34
35
|
'js-mssql',
|
|
35
36
|
'js-bun-db',
|
|
@@ -46,6 +47,17 @@ export const AUTO_RULE_ORDER = Object.freeze([
|
|
|
46
47
|
/** Порядок автододавання skills відповідно до `auto-rules.md`. */
|
|
47
48
|
export const AUTO_SKILL_ORDER = Object.freeze(['abie-kustomize', 'fix', 'lint'])
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Граф залежностей між правилами (`auto-rules.md` синтаксис `rule - [other]`).
|
|
52
|
+
* Ключ варто автододати, коли всі правила-залежності вже додані до конфігу — щоб
|
|
53
|
+
* не дублювати вихідну умову, достатньо описати її у залежності.
|
|
54
|
+
*/
|
|
55
|
+
export const AUTO_RULE_DEPENDENCIES = Object.freeze(
|
|
56
|
+
/** @type {Record<string, readonly string[]>} */ ({
|
|
57
|
+
image: Object.freeze(['vue'])
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
|
|
49
61
|
const ABIE_REPOSITORY_URL_MARKER = 'https://github.com/abinbevefes/'
|
|
50
62
|
const HASURA_CONFIG_MARKER = 'metadata_directory: metadata'
|
|
51
63
|
const JS_LIKE_RE = /\.(?:mjs|cjs|js|jsx|ts|tsx)$/iu
|
|
@@ -493,6 +505,30 @@ export async function collectAutoRuleFacts(root) {
|
|
|
493
505
|
return facts
|
|
494
506
|
}
|
|
495
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Транзитивно розгортає правила за `AUTO_RULE_DEPENDENCIES`: повторно проходить
|
|
510
|
+
* усіма парами «правило → залежності» доки на одному з проходів не зʼявляється
|
|
511
|
+
* нове додавання. Це дозволяє ланцюги (`a → b → c`) і не вимагає від автора правил
|
|
512
|
+
* стежити за порядком викликів `addRule`.
|
|
513
|
+
* @param {string[]} detectedRules уже зібрані id правил (мутується через addRule)
|
|
514
|
+
* @param {(ruleId: string) => void} addRule callback із спільної фабрики (поважає `disable-rules` і дублі)
|
|
515
|
+
* @returns {void}
|
|
516
|
+
*/
|
|
517
|
+
function resolveRuleDependencies(detectedRules, addRule) {
|
|
518
|
+
let changed = true
|
|
519
|
+
while (changed) {
|
|
520
|
+
changed = false
|
|
521
|
+
for (const [ruleId, deps] of Object.entries(AUTO_RULE_DEPENDENCIES)) {
|
|
522
|
+
if (detectedRules.includes(ruleId)) continue
|
|
523
|
+
if (deps.every(d => detectedRules.includes(d))) {
|
|
524
|
+
const before = detectedRules.length
|
|
525
|
+
addRule(ruleId)
|
|
526
|
+
if (detectedRules.length > before) changed = true
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
496
532
|
/**
|
|
497
533
|
* Визначає авто-правила та skills згідно з `auto-rules.md`.
|
|
498
534
|
* @param {object} params параметри аналізу
|
|
@@ -588,6 +624,7 @@ export async function detectAutoRulesAndSkills({
|
|
|
588
624
|
if (facts.hasVueSource) {
|
|
589
625
|
addRule('vue')
|
|
590
626
|
}
|
|
627
|
+
resolveRuleDependencies(detectedRules, addRule)
|
|
591
628
|
|
|
592
629
|
const autoSkillChecks = [
|
|
593
630
|
{ enabled: isAbie, id: 'abie-kustomize' },
|
|
@@ -0,0 +1,320 @@
|
|
|
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` (авто-оптимізація з AVIF-двійниками);
|
|
8
|
+
* - якщо в `package.json` є агрегований скрипт `lint`, він викликає `bun run lint-image`
|
|
9
|
+
* (симетрично до `lint-text`, `lint-js`, `lint-ga`);
|
|
10
|
+
* - `@nitra/minify-image` не оголошений у `dependencies`/`devDependencies` —
|
|
11
|
+
* CLI запускається лише через `npx` (як `markdownlint-cli2` у `text.mdc`);
|
|
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`.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync } from 'node:fs'
|
|
24
|
+
import { readFile } from 'node:fs/promises'
|
|
25
|
+
import { join, relative } from 'node:path'
|
|
26
|
+
|
|
27
|
+
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
28
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
29
|
+
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
30
|
+
|
|
31
|
+
/** Імʼя CLI-пакета: рядок у `lint-image` і заборонений у залежностях. */
|
|
32
|
+
const MINIFY_PACKAGE_NAME = '@nitra/minify-image'
|
|
33
|
+
|
|
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
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Перевіряє скрипт `lint-image` у `package.json`.
|
|
62
|
+
*
|
|
63
|
+
* Має містити виклик `npx @nitra/minify-image` з обовʼязковими прапорцями `--src=.`,
|
|
64
|
+
* `--write` (авто-оптимізація на місці) і `--avif` (AVIF-двійники для PNG/JPEG/GIF).
|
|
65
|
+
* Без `--write`/`--avif` лінт лише оцінює економію — для проєктних коммітів цього мало.
|
|
66
|
+
* @param {string|undefined} lintImage значення `scripts['lint-image']`
|
|
67
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
68
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
69
|
+
* @returns {void}
|
|
70
|
+
*/
|
|
71
|
+
function checkLintImageScript(lintImage, pass, fail) {
|
|
72
|
+
const canonical = `npx ${MINIFY_PACKAGE_NAME} --src=. --write --avif`
|
|
73
|
+
if (typeof lintImage !== 'string' || !lintImage.trim()) {
|
|
74
|
+
fail(`package.json: додай скрипт "lint-image" з \`${canonical}\` (image.mdc)`)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
if (!lintImage.includes(`npx ${MINIFY_PACKAGE_NAME}`)) {
|
|
78
|
+
fail(`package.json: lint-image має викликати \`npx ${MINIFY_PACKAGE_NAME}\` (image.mdc)`)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
/** @type {{ flag: string, variants: string[], hint: string }[]} */
|
|
82
|
+
const requiredFlags = [
|
|
83
|
+
{ flag: '--src=.', variants: ['--src=.', '--src .'], hint: '`--src=.`' },
|
|
84
|
+
{ flag: '--write', variants: ['--write'], hint: '`--write` (авто-оптимізація на місці)' },
|
|
85
|
+
{ flag: '--avif', variants: ['--avif'], hint: '`--avif` (AVIF-двійники для PNG/JPEG/GIF)' }
|
|
86
|
+
]
|
|
87
|
+
const missing = requiredFlags.filter(f => !f.variants.some(v => lintImage.includes(v)))
|
|
88
|
+
if (missing.length > 0) {
|
|
89
|
+
fail(
|
|
90
|
+
`package.json: lint-image має містити ${missing.map(f => f.hint).join(', ')} — канонічний виклик: \`${canonical}\` (image.mdc)`
|
|
91
|
+
)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
pass(`package.json: lint-image викликає \`${canonical}\``)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Перевіряє, що агрегований `lint` (якщо є) кличе `bun run lint-image` —
|
|
99
|
+
* симетрично до `lint-text`, `lint-js`, `lint-ga`.
|
|
100
|
+
* @param {string|undefined} lintAggregate значення `scripts.lint`
|
|
101
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
102
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
103
|
+
* @returns {void}
|
|
104
|
+
*/
|
|
105
|
+
function checkLintAggregateIncludesImage(lintAggregate, pass, fail) {
|
|
106
|
+
if (typeof lintAggregate !== 'string' || !lintAggregate.trim()) {
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
if (lintAggregate.includes('bun run lint-image')) {
|
|
110
|
+
pass('package.json: агрегований `lint` викликає `bun run lint-image`')
|
|
111
|
+
} else {
|
|
112
|
+
fail('package.json: у `lint` додай `bun run lint-image` (image.mdc, симетрично до lint-text / lint-js / lint-ga)')
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Забороняє `@nitra/minify-image` у `dependencies` чи `devDependencies` —
|
|
118
|
+
* CLI завжди запускається через `npx` (як `markdownlint-cli2` у `text.mdc`).
|
|
119
|
+
* @param {{ dependencies?: Record<string, unknown>, devDependencies?: Record<string, unknown> }} pkg розібраний package.json
|
|
120
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
121
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
122
|
+
* @returns {void}
|
|
123
|
+
*/
|
|
124
|
+
function checkMinifyImageNotInDeps(pkg, pass, fail) {
|
|
125
|
+
const inDeps = Boolean(pkg.dependencies && MINIFY_PACKAGE_NAME in pkg.dependencies)
|
|
126
|
+
const inDevDeps = Boolean(pkg.devDependencies && MINIFY_PACKAGE_NAME in pkg.devDependencies)
|
|
127
|
+
if (inDeps || inDevDeps) {
|
|
128
|
+
fail(
|
|
129
|
+
`package.json: ${MINIFY_PACKAGE_NAME} не додавай у dependencies/devDependencies — лише через \`npx\` (image.mdc)`
|
|
130
|
+
)
|
|
131
|
+
} else {
|
|
132
|
+
pass(`package.json: ${MINIFY_PACKAGE_NAME} не оголошено в dependencies/devDependencies`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
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
|
+
* зображень його ще нема, це нормально.
|
|
155
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
156
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
157
|
+
* @returns {Promise<void>}
|
|
158
|
+
*/
|
|
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)`)
|
|
167
|
+
}
|
|
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
|
+
)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
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
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Перевіряє кореневий `package.json`: скрипти, заборонені залежності, агрегований `lint`.
|
|
283
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
284
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
285
|
+
* @returns {Promise<boolean>} `true`, якщо `package.json` знайдено й оброблено; `false` — нема
|
|
286
|
+
*/
|
|
287
|
+
async function checkPackageJsonImage(pass, fail) {
|
|
288
|
+
if (!existsSync('package.json')) {
|
|
289
|
+
fail('package.json не знайдено в корені — додай (image.mdc)')
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
const pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
293
|
+
const scripts = /** @type {Record<string, unknown>} */ (pkg.scripts || {})
|
|
294
|
+
checkLintImageScript(typeof scripts['lint-image'] === 'string' ? scripts['lint-image'] : undefined, pass, fail)
|
|
295
|
+
checkLintAggregateIncludesImage(typeof scripts.lint === 'string' ? scripts.lint : undefined, pass, fail)
|
|
296
|
+
checkMinifyImageNotInDeps(pkg, pass, fail)
|
|
297
|
+
return true
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
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 не вимагається — лінт зображень виконується лише локально.
|
|
306
|
+
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
307
|
+
*/
|
|
308
|
+
export async function check() {
|
|
309
|
+
const reporter = createCheckReporter()
|
|
310
|
+
const { pass, fail } = reporter
|
|
311
|
+
|
|
312
|
+
const pkgFound = await checkPackageJsonImage(pass, fail)
|
|
313
|
+
if (pkgFound) {
|
|
314
|
+
await checkHashCacheNotIgnored(pass, fail)
|
|
315
|
+
await checkLegacyCacheRemoved(pass, fail)
|
|
316
|
+
}
|
|
317
|
+
await checkVueAvifImports(pass, fail)
|
|
318
|
+
|
|
319
|
+
return reporter.getExitCode()
|
|
320
|
+
}
|
package/scripts/check-vue.mjs
CHANGED
|
@@ -205,19 +205,57 @@ function checkViteVersion(devDeps, prefix, passFn, fail) {
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Витягує текст аргументів першого виклику `AutoImport(` з vite.config зі збалансованими дужками.
|
|
210
|
+
* Повертає `null`, якщо виклик не знайдено або дужки не збалансовані (тоді перевірка `'vue'`
|
|
211
|
+
* у списку `imports` пропускається — інші чек-сигнали все одно спрацюють).
|
|
212
|
+
* @param {string} content повний текст vite.config
|
|
213
|
+
* @returns {string | null} текст усередині `AutoImport(...)` без зовнішніх дужок, або `null`
|
|
214
|
+
*/
|
|
215
|
+
function extractAutoImportCallArgs(content) {
|
|
216
|
+
const marker = 'AutoImport('
|
|
217
|
+
const idx = content.indexOf(marker)
|
|
218
|
+
if (idx === -1) return null
|
|
219
|
+
const start = idx + marker.length
|
|
220
|
+
let depth = 1
|
|
221
|
+
for (let i = start; i < content.length; i++) {
|
|
222
|
+
const ch = content[i]
|
|
223
|
+
if (ch === '(') depth++
|
|
224
|
+
else if (ch === ')') {
|
|
225
|
+
depth--
|
|
226
|
+
if (depth === 0) return content.slice(start, i)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Чи передано `'vue'` (або `"vue"`) як рядковий елемент у `imports` всередині виклику `AutoImport(...)`.
|
|
234
|
+
* Без auto-import-ів `vue` забороняти явні value-імпорти `from 'vue'` небезпечно — їх видалення
|
|
235
|
+
* зламає код, бо `ref` / `createApp` тощо більше нікому надати.
|
|
236
|
+
* @param {string} content повний текст vite.config
|
|
237
|
+
* @returns {boolean} true, якщо `AutoImport({ imports: [..., 'vue', ...] })` сконфігуровано
|
|
238
|
+
*/
|
|
239
|
+
function viteConfigHasVueInAutoImports(content) {
|
|
240
|
+
const args = extractAutoImportCallArgs(content)
|
|
241
|
+
if (args === null) return false
|
|
242
|
+
return args.includes("'vue'") || args.includes('"vue"')
|
|
243
|
+
}
|
|
244
|
+
|
|
208
245
|
/**
|
|
209
246
|
* Перевіряє vite.config на наявність VueMacros і AutoImport.
|
|
210
247
|
* @param {string} rootDir параметр rootDir
|
|
211
248
|
* @param {string} prefix параметр prefix
|
|
212
249
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
213
250
|
* @param {(msg: string) => void} fail callback при помилці
|
|
251
|
+
* @returns {Promise<{ hasVueAutoImport: boolean }>} ознака успішно сконфігурованого vue-auto-import (для checkVueImportViolations)
|
|
214
252
|
*/
|
|
215
253
|
async function checkViteConfig(rootDir, prefix, passFn, fail) {
|
|
216
254
|
const configFiles = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
|
|
217
255
|
const viteConfig = configFiles.find(f => existsSync(join(rootDir, f)))
|
|
218
256
|
if (!viteConfig) {
|
|
219
257
|
fail(`${prefix}немає vite.config.js|ts|mjs у каталозі пакета`)
|
|
220
|
-
return
|
|
258
|
+
return { hasVueAutoImport: false }
|
|
221
259
|
}
|
|
222
260
|
const content = await readFile(join(rootDir, viteConfig), 'utf8')
|
|
223
261
|
if (ESBUILD_RE.test(content)) {
|
|
@@ -235,23 +273,48 @@ async function checkViteConfig(rootDir, prefix, passFn, fail) {
|
|
|
235
273
|
}
|
|
236
274
|
}
|
|
237
275
|
|
|
276
|
+
const hasVueAutoImport = viteConfigHasVueInAutoImports(content)
|
|
277
|
+
if (content.includes('AutoImport(')) {
|
|
278
|
+
if (hasVueAutoImport) {
|
|
279
|
+
passFn(`${prefix}${viteConfig}: AutoImport({ imports: [..., 'vue', ...] }) — value-імпорти з 'vue' покриті`)
|
|
280
|
+
} else {
|
|
281
|
+
fail(
|
|
282
|
+
`${prefix}${viteConfig}: AutoImport не містить 'vue' у imports — додай 'vue' (інакше прибирати ` +
|
|
283
|
+
`value-імпорти на кшталт \`import { ref } from 'vue'\` небезпечно: ref/createApp тощо нікому буде надати)`
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
238
288
|
if (content.includes('process.env.npm_lifecycle_event')) {
|
|
239
289
|
fail(
|
|
240
290
|
`${prefix}${viteConfig} використовує process.env.npm_lifecycle_event — у Bun це не працює. ` +
|
|
241
291
|
`Перенеси логіку на mode (defineConfig(({ mode }) => ...)) і передавай mode в helper-функції.`
|
|
242
292
|
)
|
|
243
293
|
}
|
|
294
|
+
|
|
295
|
+
return { hasVueAutoImport }
|
|
244
296
|
}
|
|
245
297
|
|
|
246
298
|
/**
|
|
247
299
|
* Сканує джерела пакета на заборонені value-імпорти з vue.
|
|
300
|
+
*
|
|
301
|
+
* Якщо `unplugin-auto-import` не сконфігурований на `'vue'` у `vite.config`, явні value-імпорти
|
|
302
|
+
* формально не заборонені — їх видалення зламає код. У цьому випадку перевірка пропускається,
|
|
303
|
+
* а fail про відсутній `'vue'` у `AutoImport.imports` уже зареєстровано в `checkViteConfig`.
|
|
248
304
|
* @param {string} rootDir параметр rootDir
|
|
249
305
|
* @param {string} absPackageRoot параметр absPackageRoot
|
|
250
306
|
* @param {string} prefix параметр prefix
|
|
307
|
+
* @param {boolean} hasVueAutoImport чи `AutoImport({ imports: [..., 'vue', ...] })` сконфігуровано
|
|
251
308
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
252
309
|
* @param {(msg: string) => void} fail callback при помилці
|
|
253
310
|
*/
|
|
254
|
-
async function checkVueImportViolations(rootDir, absPackageRoot, prefix, passFn, fail) {
|
|
311
|
+
async function checkVueImportViolations(rootDir, absPackageRoot, hasVueAutoImport, prefix, passFn, fail) {
|
|
312
|
+
if (!hasVueAutoImport) {
|
|
313
|
+
passFn(
|
|
314
|
+
`${prefix}value-імпорти з 'vue' не заборонені — спершу додай 'vue' до AutoImport.imports у vite.config`
|
|
315
|
+
)
|
|
316
|
+
return
|
|
317
|
+
}
|
|
255
318
|
/** @type {string[]} */
|
|
256
319
|
const sourcePaths = []
|
|
257
320
|
await walkDir(absPackageRoot, absPath => {
|
|
@@ -323,8 +386,8 @@ async function checkVuePackage(rootDir, fail, passFn) {
|
|
|
323
386
|
'vite-plugin-vue-layouts-next відсутній — bun add -d vite-plugin-vue-layouts-next'
|
|
324
387
|
)
|
|
325
388
|
|
|
326
|
-
await checkViteConfig(rootDir, prefix, passFn, fail)
|
|
327
|
-
await checkVueImportViolations(rootDir, join(process.cwd(), rootDir), prefix, passFn, fail)
|
|
389
|
+
const { hasVueAutoImport } = await checkViteConfig(rootDir, prefix, passFn, fail)
|
|
390
|
+
await checkVueImportViolations(rootDir, join(process.cwd(), rootDir), hasVueAutoImport, prefix, passFn, fail)
|
|
328
391
|
await checkEsbuildMentions(rootDir, join(process.cwd(), rootDir), prefix, passFn, fail)
|
|
329
392
|
}
|
|
330
393
|
|
|
@@ -91,7 +91,7 @@ export function mergeHooks(existing, fromTemplate) {
|
|
|
91
91
|
/** @type {Record<string, HookGroup[]>} */
|
|
92
92
|
const out = {}
|
|
93
93
|
for (const [event, groups] of Object.entries(existing ?? {})) {
|
|
94
|
-
out[event] = Array.isArray(groups) ? groups
|
|
94
|
+
out[event] = Array.isArray(groups) ? [...groups] : []
|
|
95
95
|
}
|
|
96
96
|
for (const [event, templateGroups] of Object.entries(fromTemplate ?? {})) {
|
|
97
97
|
const existingGroups = (out[event] ?? []).filter(g => !isManagedHookGroup(g))
|
|
@@ -111,10 +111,10 @@ export function mergeHooks(existing, fromTemplate) {
|
|
|
111
111
|
*/
|
|
112
112
|
export function mergeSettings(existing, template) {
|
|
113
113
|
/** @type {ClaudeSettings} */
|
|
114
|
-
const merged = { ...
|
|
114
|
+
const merged = { ...existing }
|
|
115
115
|
const mergedAllow = mergeAllowList(existing?.permissions?.allow, template.permissions?.allow)
|
|
116
116
|
if (mergedAllow.length > 0) {
|
|
117
|
-
merged.permissions = { ...
|
|
117
|
+
merged.permissions = { ...existing?.permissions, allow: mergedAllow }
|
|
118
118
|
}
|
|
119
119
|
const mergedHooks = mergeHooks(existing?.hooks, template.hooks)
|
|
120
120
|
if (Object.keys(mergedHooks).length > 0) {
|
|
@@ -132,12 +132,12 @@ export function mergeSettings(existing, template) {
|
|
|
132
132
|
*/
|
|
133
133
|
async function readJsonOrUndefined(path) {
|
|
134
134
|
if (!existsSync(path)) {
|
|
135
|
-
return
|
|
135
|
+
return
|
|
136
136
|
}
|
|
137
137
|
try {
|
|
138
138
|
return JSON.parse(await readFile(path, 'utf8'))
|
|
139
139
|
} catch {
|
|
140
|
-
return
|
|
140
|
+
return
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|