@nitra/cursor 1.5.2 → 1.5.5

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/mdc/ga.mdc CHANGED
@@ -41,6 +41,9 @@ on:
41
41
  jobs:
42
42
  cleanup_old_workflows:
43
43
  runs-on: ubuntu-latest
44
+ permissions:
45
+ actions: write
46
+ contents: read
44
47
  steps:
45
48
  - name: Delete workflow runs
46
49
  uses: dmvict/clean-workflow-runs@v1.0.0
@@ -67,6 +70,8 @@ on:
67
70
  jobs:
68
71
  cleanup_old_branches:
69
72
  runs-on: ubuntu-latest
73
+ permissions:
74
+ contents: write
70
75
  steps:
71
76
  - id: delete_stuff
72
77
  name: Delete those pesky dead branches
@@ -78,11 +83,68 @@ jobs:
78
83
  dry_run: no
79
84
 
80
85
  - name: Get output
81
- run: "echo 'Deleted branches: ${{ steps.delete_stuff.outputs.deleted_branches }}'"
86
+ env:
87
+ DELETED_BRANCHES: ${{ steps.delete_stuff.outputs.deleted_branches }}
88
+ run: |
89
+ echo "Deleted branches: ${DELETED_BRANCHES}"
82
90
  ```
83
91
 
84
92
  якщо в ignore_branches задані інші бранчі, то це допустимо.
85
93
 
94
+ Повинен бути файл .github/workflows/lint-ga.yml, зі змістом:
95
+
96
+ ```yaml
97
+ name: Lint GA
98
+
99
+ on:
100
+ push:
101
+ branches:
102
+ - dev
103
+ paths:
104
+ - '.github/workflows/**'
105
+ - '.github/zizmor.yml'
106
+ - 'package.json'
107
+ - 'bun.lock'
108
+
109
+ pull_request:
110
+ branches:
111
+ - dev
112
+
113
+ concurrency:
114
+ group: ${{ github.ref }}-${{ github.workflow }}
115
+ cancel-in-progress: true
116
+
117
+ jobs:
118
+ lint-ga:
119
+ runs-on: ubuntu-latest
120
+ permissions:
121
+ contents: read
122
+ steps:
123
+ - uses: actions/checkout@v4
124
+ with:
125
+ persist-credentials: false
126
+
127
+ - uses: oven-sh/setup-bun@v2
128
+
129
+ - name: Cache Bun dependencies
130
+ uses: actions/cache@v4
131
+ with:
132
+ path: |
133
+ ~/.bun/install/cache
134
+ node_modules
135
+ key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
136
+ restore-keys: |
137
+ ${{ runner.os }}-bun-
138
+
139
+ - name: Install dependencies
140
+ run: bun install --frozen-lockfile
141
+
142
+ - uses: astral-sh/setup-uv@v6
143
+
144
+ - name: Lint GA
145
+ run: bun run lint-ga
146
+ ```
147
+
86
148
  в файлі .vscode/extensions.json є налаштування для GitHub Actions:
87
149
 
88
150
  ```json title=".vscode/extensions.json"
@@ -91,6 +153,29 @@ jobs:
91
153
  }
92
154
  ```
93
155
 
156
+ ## actionlint
157
+
158
+ Статична перевірка синтаксису та виразів GitHub Actions: [actionlint](https://github.com/rhysd/actionlint).
159
+
160
+ У кореневому `package.json` у скрипті `lint-ga` викликай **actionlint** через **`bunx node-actionlint`** (пакет [node-actionlint](https://www.npmjs.com/package/node-actionlint) постає бінарник actionlint для Node-екосистеми).
161
+
162
+ ## zizmor
163
+
164
+ Статичний аналіз безпеки для GitHub Actions: [zizmor documentation](https://docs.zizmor.sh).
165
+
166
+ У кореневому `package.json` має бути скрипт:
167
+
168
+ ```json title="package.json"
169
+ "scripts": {
170
+ "lint-ga": "bunx node-actionlint && uvx zizmor --offline --collect=workflows ."
171
+ }
172
+ ```
173
+
174
+ Параметр `--offline` обмежує аналіз офлайн-аудитами (без GitHub API);
175
+
176
+ За замовчуванням audit [unpinned-uses](https://docs.zizmor.sh/audits/#unpinned-uses) вимагає повний commit SHA для кожного `uses:`. У всих проєктах прийняті **семантичні теги** (`@v4`, `@v2` тощо), додай `.github/zizmor.yml` з політикою `ref-pin` (приклад у цьому репозиторії).
177
+
94
178
  ## Перевірка
95
179
 
96
- `npx @nitra/cursor check ga`
180
+ - `bun run lint-ga` — actionlint (node-actionlint) і zizmor
181
+ - `npx @nitra/cursor check ga`
package/mdc/js-lint.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Перевірка JavaScript коду
3
3
  alwaysApply: true
4
- version: '1.4'
4
+ version: '1.5'
5
5
  ---
6
6
 
7
7
  Перевірка виконується за допомогою **oxlint**, **ESLint** та **jscpd** (дублікати коду).
@@ -25,7 +25,7 @@ version: '1.4'
25
25
  "lint-js": "oxlint --fix && bunx eslint --fix . && bunx jscpd ."
26
26
  },
27
27
  "devDependencies": {
28
- "@nitra/eslint-config": "^3.3.0"
28
+ "@nitra/eslint-config": "^3.4.0"
29
29
  }
30
30
  ```
31
31
 
@@ -43,6 +43,17 @@ version: '1.4'
43
43
  }
44
44
  ```
45
45
 
46
+ ## jscpd: рефакторинг і структура
47
+
48
+ Коли **jscpd** знаходить клони, спочатку зменшуй дублювання кодом, а не конфігом.
49
+
50
+ - **Рефакторинг:** винеси спільні фрагменти в функції, модулі, утиліти, composables/hooks, спільні компоненти або базові типи — залежно від контексту.
51
+ - **Структура:** якщо одна й та сама логіка розмазана між файлами чи пакетами, запропонуй зміну структури (наприклад, спільний модуль, `shared/`, внутрішній пакет у monorepo), щоб була **одна канонічна реалізація** і повторні місця лише викликали її.
52
+
53
+ Так ти уникаєш **хибних обходів** перевірки: розширення `ignore` чи завищення `minLines` лише щоб прибрати звіт — не заміна рефакторингу для справжніх клонів. Якщо збіг **семантично випадковий** (генерований код, формальні шаблони без спільної логіки), після оцінки допустимо точковий `ignore` або зміна порогу — з коротким обґрунтуванням.
54
+
55
+ Але обов'язково перед рефакторингом перевір чи є тести на блоки які підлягають зміні, а саме Bun.test для js та playwright для vue. Якщо є, то перевір чи вони покривають блоки які підлягають зміні. Якщо не покривають або тестів немає — спочатку створи їх, перевір що вони покривають і відпрацьовують коректно, а потім роби рефакторинг і ще раз запускай тести, але якщо тести не відпрацьовують коректно після рефакторингу, то не роби рефакторинг.
56
+
46
57
  Додай workflow `.github/workflows/lint-js.yml`:
47
58
 
48
59
  ```yaml
@@ -74,8 +85,12 @@ concurrency:
74
85
  jobs:
75
86
  eslint:
76
87
  runs-on: ubuntu-latest
88
+ permissions:
89
+ contents: read
77
90
  steps:
78
91
  - uses: actions/checkout@v4
92
+ with:
93
+ persist-credentials: false
79
94
 
80
95
  - uses: oven-sh/setup-bun@v2
81
96
 
package/mdc/js-pino.mdc CHANGED
@@ -7,7 +7,7 @@ version: '1.0'
7
7
  Проект використовує @nitra/pino для логування.
8
8
  Якщо в проекті присутній @nitra/bunyan, то він повинен бути замінений на @nitra/pino.
9
9
 
10
- В k8s/base/configmap.yaml повинен бути заданий OTEL_RESOURCE_ATTRIBUTES: 'service.name=<project_name>,service.namespace=<project_namespace>'
10
+ В **/k8s/base/configmap.yaml повинен бути заданий OTEL_RESOURCE_ATTRIBUTES: 'service.name=<project_name>,service.namespace=<project_namespace>'
11
11
  а в директоріях з kustomize повинні бути перевизначені значення OTEL_RESOURCE_ATTRIBUTES і в них service.namespace повинен відповідати namespace, в якому знаходиться дана директорія.
12
12
 
13
13
  Configmap повинен бути з тією самою назвою, що і deployment, якщо у deployment тільки один configmap.
@@ -13,6 +13,52 @@ version: '1.0'
13
13
  - npm/ - файли для npm модуля
14
14
  - package.json
15
15
 
16
+ ## npm publish (GitHub Actions)
17
+
18
+ У `.github/workflows/` має бути workflow `npm-publish.yml`, який публікує пакет з `npm/package.json` на npm при push у `main`, коли змінюється дерево `npm/**`.
19
+
20
+ Приклад вмісту:
21
+
22
+ ```yaml title=".github/workflows/npm-publish.yml"
23
+ name: npm-publish
24
+
25
+ on:
26
+ push:
27
+ paths:
28
+ - 'npm/**'
29
+ branches:
30
+ - main
31
+
32
+ concurrency:
33
+ group: ${{ github.ref }}-${{ github.workflow }}
34
+ cancel-in-progress: true
35
+
36
+ jobs:
37
+ publish:
38
+ runs-on: ubuntu-latest
39
+ permissions:
40
+ contents: read
41
+ id-token: write # КРИТИЧНО для OIDC!
42
+
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ with:
46
+ persist-credentials: false
47
+
48
+ - uses: actions/setup-node@v5
49
+ with:
50
+ node-version: '24' # includes npm@11.6.0
51
+ registry-url: 'https://registry.npmjs.org'
52
+
53
+ - name: Publish package
54
+ uses: JS-DevTools/npm-publish@v4
55
+ with:
56
+ package: npm/package.json
57
+ ```
58
+
59
+ - `id-token: write` потрібен для **trusted publishing** (OIDC) на npm, якщо пакет налаштований у npm без класичного токена в секретах.
60
+ - Шлях `package: npm/package.json` узгоджений зі структурою monorepo (`npm/` — каталог модуля).
61
+
16
62
  ## Перевірка
17
63
 
18
64
  `npx @nitra/cursor check npm-module`
package/mdc/text.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Обробка та перевірка текстових файлів (cspell, markdownlint-cli2, v8r, CI)
3
3
  alwaysApply: true
4
- version: '1.12'
4
+ version: '1.13'
5
5
  ---
6
6
 
7
7
  Правило описує роботу з **текстовими файлами** в репозиторії: перевірка правопису через **cspell**, стиль **Markdown** через **markdownlint-cli2**, валідація **JSON/YAML/TOML** через **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint** у Cursor/VS Code та інтеграція в CI.
@@ -118,8 +118,12 @@ concurrency:
118
118
  jobs:
119
119
  text:
120
120
  runs-on: ubuntu-latest
121
+ permissions:
122
+ contents: read
121
123
  steps:
122
124
  - uses: actions/checkout@v4
125
+ with:
126
+ persist-credentials: false
123
127
 
124
128
  - uses: oven-sh/setup-bun@v2
125
129
 
@@ -206,9 +210,15 @@ jobs:
206
210
 
207
211
  Підлаштуй `language` під проєкт (наприклад додай `ru-ru`, якщо потрібна перевірка російською). Порядок у `import` може впливати на пріоритет словників — тримай корпоративний `@nitra/cspell-dict` там, де зручно для ваших правил.
208
212
 
209
- ## Локальні виключення
213
+ ## Локальні виключення (cspell `words`)
214
+
215
+ Коли **cspell** підсвічує слово, спочатку **виправ текст**, а не розширюй словник:
216
+
217
+ - виправ **друкарські помилки** та неправильні форми;
218
+ - **перефразуй коректною українською** (або англійською, залежно від контексту файлу): заміни кальки й випадкові склади на звичні формулювання зі словників;
219
+ - заміни **жаргон**, якщо є природний еквівалент у тому ж стилі документації (наприклад, у коментарях пиши «функція зворотного виклику» замість розмовного запозичення з англійського `callback`).
210
220
 
211
- У секції `words` у `.cspell.json` додають власні терміни, імена та скорочення, яких немає в словниках.
221
+ У секцію `words` у `.cspell.json` додавай записи **лише якщо переписати коректно неможливо** або **недоречно**: власні назви, стабільні технічні терміни без усталеного перекладу в проєкті, ідентифікатори зовнішніх API тощо.
212
222
 
213
223
  ## Інші мови
214
224
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.5.2",
3
+ "version": "1.5.5",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -25,6 +25,7 @@
25
25
  "files": [
26
26
  "mdc",
27
27
  "bin",
28
+ "schemas",
28
29
  "scripts",
29
30
  "skills",
30
31
  "AGENTS.template.md"
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://unpkg.com/@nitra/cursor/schemas/n-cursor.json",
4
+ "title": "n-cursor project config",
5
+ "description": "Конфігурація правил і skills для CLI @nitra/cursor (файл .n-cursor.json у корені репозиторію).",
6
+ "type": "object",
7
+ "additionalProperties": true,
8
+ "properties": {
9
+ "rules": {
10
+ "type": "array",
11
+ "description": "Ідентифікатори правил без префікса n- (відповідають файлам n-<id>.mdc).",
12
+ "items": {
13
+ "type": "string",
14
+ "minLength": 1
15
+ }
16
+ },
17
+ "skills": {
18
+ "type": "array",
19
+ "description": "Ідентифікатори skills без префікса n- (каталог .cursor/skills).",
20
+ "items": {
21
+ "type": "string",
22
+ "minLength": 1
23
+ }
24
+ }
25
+ },
26
+ "required": ["rules", "skills"]
27
+ }
@@ -30,7 +30,7 @@ export async function check() {
30
30
  pass('Всі workflows мають розширення .yml')
31
31
  }
32
32
 
33
- for (const f of ['clean-ga-workflows.yml', 'clean-merged-branch.yml']) {
33
+ for (const f of ['clean-ga-workflows.yml', 'clean-merged-branch.yml', 'lint-ga.yml']) {
34
34
  if (files.includes(f)) {
35
35
  pass(`${f} існує`)
36
36
  } else {
@@ -33,9 +33,7 @@ export async function check() {
33
33
  fail('lint-js має викликати jscpd — додай "&& bunx jscpd ." у кінець скрипта')
34
34
  }
35
35
  } else {
36
- fail(
37
- 'package.json не містить скрипт "lint-js" — додай: "oxlint --fix && bunx eslint --fix . && bunx jscpd ."'
38
- )
36
+ fail('package.json не містить скрипт "lint-js" — додай: "oxlint --fix && bunx eslint --fix . && bunx jscpd ."')
39
37
  }
40
38
 
41
39
  if (pkg.devDependencies?.['@nitra/eslint-config']) {
@@ -1,10 +1,51 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
+ import { join } from 'node:path'
3
4
 
4
5
  import { pass } from './utils/pass.mjs'
6
+ import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
5
7
 
6
8
  /**
7
- * Перевіряє відповідність проєкту правилам js-pino.mdc
9
+ * Перевіряє відповідність правилам js-pino.mdc для одного workspace-пакета.
10
+ * @param {string} rootDir відносний шлях workspace (не `'.'`)
11
+ * @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
12
+ * @returns {Promise<void>} завершується після перевірок цього пакета
13
+ */
14
+ async function checkWorkspacePackage(rootDir, fail) {
15
+ const label = `[${rootDir}] `
16
+ const pkgPath = join(rootDir, 'package.json')
17
+ if (existsSync(pkgPath)) {
18
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
19
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
20
+
21
+ if (allDeps['@nitra/bunyan']) {
22
+ fail(`${label}@nitra/bunyan знайдено — замінити на @nitra/pino`)
23
+ }
24
+ if (allDeps.bunyan) {
25
+ fail(`${label}bunyan знайдено — замінити на @nitra/pino`)
26
+ }
27
+ }
28
+
29
+ const configmapPath = join(rootDir, 'k8s/base/configmap.yaml')
30
+ if (existsSync(configmapPath)) {
31
+ const content = await readFile(configmapPath, 'utf8')
32
+ if (content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
33
+ pass(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
34
+ if (content.includes('service.name=') && content.includes('service.namespace=')) {
35
+ pass(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
36
+ } else {
37
+ fail(
38
+ `${label}OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>`
39
+ )
40
+ }
41
+ } else {
42
+ fail(`${label}k8s/base/configmap.yaml не містить OTEL_RESOURCE_ATTRIBUTES`)
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Перевіряє відповідність проєкту правилам js-pino.mdc лише для workspace-пакетів (не корінь репо).
8
49
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
9
50
  */
10
51
  export async function check() {
@@ -14,26 +55,18 @@ export async function check() {
14
55
  exitCode = 1
15
56
  }
16
57
 
17
- if (existsSync('package.json')) {
18
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
19
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
58
+ const roots = await getMonorepoPackageRootDirs()
59
+ const workspaceRoots = roots.filter(r => r !== '.')
20
60
 
21
- if (allDeps['@nitra/bunyan']) fail('@nitra/bunyan знайдено — замінити на @nitra/pino')
22
- if (allDeps.bunyan) fail('bunyan знайдено — замінити на @nitra/pino')
61
+ if (workspaceRoots.length === 0) {
62
+ pass(
63
+ 'js-pino: немає workspace-пакетів у кореневому package.json — перевірку залежностей і k8s у пакетах пропущено'
64
+ )
65
+ return exitCode
23
66
  }
24
67
 
25
- if (existsSync('k8s/base/configmap.yaml')) {
26
- const content = await readFile('k8s/base/configmap.yaml', 'utf8')
27
- if (content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
28
- pass('k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES')
29
- if (content.includes('service.name=') && content.includes('service.namespace=')) {
30
- pass('OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace')
31
- } else {
32
- fail('OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>')
33
- }
34
- } else {
35
- fail('k8s/base/configmap.yaml не містить OTEL_RESOURCE_ATTRIBUTES')
36
- }
68
+ for (const r of workspaceRoots) {
69
+ await checkWorkspacePackage(r, fail)
37
70
  }
38
71
 
39
72
  return exitCode
@@ -1,10 +1,98 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
+ import { join } from 'node:path'
3
4
 
4
5
  import { pass } from './utils/pass.mjs'
6
+ import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
5
7
 
6
8
  /**
7
- * Перевіряє відповідність проєкту правилам vue.mdc
9
+ * Формує зрозумілий для людини підпис пакета для повідомлень перевірки.
10
+ * @param {string} rootDir відносний шлях (`'.'` або `site` тощо)
11
+ * @returns {string} підпис для логів перевірки
12
+ */
13
+ function packageLabel(rootDir) {
14
+ return rootDir === '.' ? 'корінь' : rootDir
15
+ }
16
+
17
+ /**
18
+ * Перевіряє залежності та vite.config одного Vue-пакета.
19
+ * @param {string} rootDir відносний шлях до пакета
20
+ * @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
21
+ * @returns {Promise<void>} завершується після перевірок залежностей і Vite
22
+ */
23
+ async function checkVuePackage(rootDir, fail) {
24
+ const label = packageLabel(rootDir)
25
+ const prefix = `[${label}] `
26
+
27
+ const pkgPath = join(rootDir, 'package.json')
28
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
29
+ const deps = pkg.dependencies || {}
30
+ const devDeps = pkg.devDependencies || {}
31
+ const allDeps = { ...deps, ...devDeps }
32
+
33
+ if (deps.vue) {
34
+ pass(`${prefix}vue в dependencies: ${deps.vue}`)
35
+ } else {
36
+ fail(`${prefix}vue відсутній в dependencies`)
37
+ }
38
+
39
+ if (devDeps.vite) {
40
+ const match = devDeps.vite.match(/(\d+)/)
41
+ if (match && Number(match[1]) >= 8) {
42
+ pass(`${prefix}vite >= 8: ${devDeps.vite}`)
43
+ } else {
44
+ fail(`${prefix}vite має бути >= 8, знайдено: ${devDeps.vite}`)
45
+ }
46
+ } else {
47
+ fail(`${prefix}vite відсутній в devDependencies`)
48
+ }
49
+
50
+ if (devDeps['@vitejs/plugin-vue']) {
51
+ pass(`${prefix}@vitejs/plugin-vue: ${devDeps['@vitejs/plugin-vue']}`)
52
+ } else {
53
+ fail(`${prefix}@vitejs/plugin-vue відсутній в devDependencies`)
54
+ }
55
+
56
+ if (allDeps['vue-macros']) {
57
+ pass(`${prefix}vue-macros: ${allDeps['vue-macros']}`)
58
+ } else {
59
+ fail(`${prefix}vue-macros відсутній — bun add -d vue-macros`)
60
+ }
61
+
62
+ if (allDeps['unplugin-auto-import']) {
63
+ pass(`${prefix}unplugin-auto-import присутній`)
64
+ } else {
65
+ fail(`${prefix}unplugin-auto-import відсутній — bun add -d unplugin-auto-import`)
66
+ }
67
+
68
+ if (allDeps['vite-plugin-vue-layouts-next']) {
69
+ pass(`${prefix}vite-plugin-vue-layouts-next присутній`)
70
+ } else {
71
+ fail(`${prefix}vite-plugin-vue-layouts-next відсутній — bun add -d vite-plugin-vue-layouts-next`)
72
+ }
73
+
74
+ const configFiles = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
75
+ const viteConfig = configFiles.find(f => existsSync(join(rootDir, f)))
76
+ if (viteConfig) {
77
+ const relConfig = join(rootDir, viteConfig)
78
+ const content = await readFile(relConfig, 'utf8')
79
+ if (content.includes('VueMacros')) {
80
+ pass(`${prefix}${viteConfig} використовує VueMacros`)
81
+ } else {
82
+ fail(`${prefix}${viteConfig} не містить VueMacros`)
83
+ }
84
+ if (content.includes('AutoImport')) {
85
+ pass(`${prefix}${viteConfig} використовує AutoImport`)
86
+ } else {
87
+ fail(`${prefix}${viteConfig} не містить AutoImport`)
88
+ }
89
+ } else {
90
+ fail(`${prefix}немає vite.config.js|ts|mjs у каталозі пакета`)
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Перевіряє відповідність проєкту правилам vue.mdc (корінь і всі workspace-пакети з `vue` у dependencies).
8
96
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
9
97
  */
10
98
  export async function check() {
@@ -25,70 +113,26 @@ export async function check() {
25
113
  fail('.vscode/extensions.json не існує')
26
114
  }
27
115
 
28
- if (existsSync('package.json')) {
29
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
30
- const deps = pkg.dependencies || {}
31
- const devDeps = pkg.devDependencies || {}
32
- const allDeps = { ...deps, ...devDeps }
33
-
34
- if (deps.vue) {
35
- pass(`vue в dependencies: ${deps.vue}`)
36
- } else {
37
- fail('vue відсутній в dependencies')
38
- }
39
-
40
- if (devDeps.vite) {
41
- const match = devDeps.vite.match(/(\d+)/)
42
- if (match && Number(match[1]) >= 8) {
43
- pass(`vite >= 8: ${devDeps.vite}`)
44
- } else {
45
- fail(`vite має бути >= 8, знайдено: ${devDeps.vite}`)
46
- }
47
- } else {
48
- fail('vite відсутній в devDependencies')
49
- }
50
-
51
- if (devDeps['@vitejs/plugin-vue']) {
52
- pass(`@vitejs/plugin-vue: ${devDeps['@vitejs/plugin-vue']}`)
53
- } else {
54
- fail('@vitejs/plugin-vue відсутній в devDependencies')
55
- }
56
-
57
- if (allDeps['vue-macros']) {
58
- pass(`vue-macros: ${allDeps['vue-macros']}`)
59
- } else {
60
- fail('vue-macros відсутній — bun add -d vue-macros')
61
- }
62
-
63
- if (allDeps['unplugin-auto-import']) {
64
- pass('unplugin-auto-import присутній')
65
- } else {
66
- fail('unplugin-auto-import відсутній — bun add -d unplugin-auto-import')
116
+ const roots = await getMonorepoPackageRootDirs()
117
+ /** @type {string[]} */
118
+ const vueRoots = []
119
+ for (const r of roots) {
120
+ const p = join(r, 'package.json')
121
+ if (existsSync(p)) {
122
+ const pkg = JSON.parse(await readFile(p, 'utf8'))
123
+ if (pkg.dependencies?.vue) vueRoots.push(r)
67
124
  }
125
+ }
68
126
 
69
- if (allDeps['vite-plugin-vue-layouts-next']) {
70
- pass('vite-plugin-vue-layouts-next присутній')
71
- } else {
72
- fail('vite-plugin-vue-layouts-next відсутній — bun add -d vite-plugin-vue-layouts-next')
73
- }
127
+ if (vueRoots.length === 0) {
128
+ fail(
129
+ 'vue не знайдено в dependencies жодного пакета (корінь репо та каталоги з кореневого workspaces)'
130
+ )
131
+ return exitCode
74
132
  }
75
133
 
76
- const configFiles = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
77
- const viteConfig = configFiles.find(f => existsSync(f))
78
- if (viteConfig) {
79
- const content = await readFile(viteConfig, 'utf8')
80
- if (content.includes('VueMacros')) {
81
- pass('vite.config використовує VueMacros')
82
- } else {
83
- fail(`${viteConfig} не містить VueMacros`)
84
- }
85
- if (content.includes('AutoImport')) {
86
- pass('vite.config використовує AutoImport')
87
- } else {
88
- fail(`${viteConfig} не містить AutoImport`)
89
- }
90
- } else {
91
- fail('vite.config.js не існує')
134
+ for (const r of vueRoots) {
135
+ await checkVuePackage(r, fail)
92
136
  }
93
137
 
94
138
  return exitCode
@@ -0,0 +1,52 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { glob, readFile } from 'node:fs/promises'
3
+ import { dirname, join, relative } from 'node:path'
4
+
5
+ /**
6
+ * Нормалізує поле `workspaces` з package.json до масиву шляхів / glob-патернів.
7
+ * @param {unknown} workspaces значення `workspaces` з кореневого package.json
8
+ * @returns {string[]} масив патернів workspaces
9
+ */
10
+ export function normalizeWorkspacePatterns(workspaces) {
11
+ if (!workspaces) return []
12
+ if (Array.isArray(workspaces)) return workspaces
13
+ if (typeof workspaces === 'object' && workspaces !== null && Array.isArray(workspaces.packages)) {
14
+ return workspaces.packages
15
+ }
16
+ return []
17
+ }
18
+
19
+ /**
20
+ * Повертає каталоги з `package.json`: корінь репозиторію та всі пакети з `workspaces`.
21
+ * @param {string} repoRoot зазвичай `process.cwd()`
22
+ * @returns {Promise<string[]>} відносні шляхи до коренів пакетів; `'.'` першим, без дублікатів
23
+ */
24
+ export async function getMonorepoPackageRootDirs(repoRoot = '.') {
25
+ const roots = new Set(['.'])
26
+ const rootPkgPath = join(repoRoot, 'package.json')
27
+ if (!existsSync(rootPkgPath)) {
28
+ return ['.']
29
+ }
30
+ const pkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
31
+ for (const raw of normalizeWorkspacePatterns(pkg.workspaces)) {
32
+ const w = raw.replaceAll('\\', '/').replace(/\/+$/, '') || '.'
33
+ if (w.includes('*')) {
34
+ const globPat = `${w}/package.json`
35
+ for await (const f of glob(globPat, { cwd: repoRoot })) {
36
+ const abs = join(repoRoot, f)
37
+ const rel = relative(repoRoot, dirname(abs))
38
+ roots.add(rel === '' ? '.' : rel)
39
+ }
40
+ } else {
41
+ const pkgJson = join(repoRoot, w, 'package.json')
42
+ if (existsSync(pkgJson)) roots.add(w)
43
+ }
44
+ }
45
+ const list = [...roots]
46
+ list.sort((a, b) => {
47
+ if (a === '.') return -1
48
+ if (b === '.') return 1
49
+ return a.localeCompare(b)
50
+ })
51
+ return list
52
+ }