@nitra/cursor 1.5.3 → 1.6.4
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/bun.mdc +2 -2
- package/mdc/ga.mdc +18 -41
- package/mdc/js-lint.mdc +22 -19
- package/mdc/js-pino.mdc +1 -1
- package/mdc/npm-module.mdc +41 -7
- package/mdc/text.mdc +24 -39
- package/package.json +1 -1
- package/scripts/check-bun.mjs +6 -0
- package/scripts/check-ga.mjs +84 -0
- package/scripts/check-js-format.mjs +5 -0
- package/scripts/check-js-lint.mjs +35 -1
- package/scripts/check-js-pino.mjs +53 -18
- package/scripts/check-nginx-default-tpl.mjs +6 -0
- package/scripts/check-npm-module.mjs +28 -0
- package/scripts/check-style-lint.mjs +6 -0
- package/scripts/check-text.mjs +8 -2
- package/scripts/check-vue.mjs +109 -61
- package/scripts/utils/pass.mjs +3 -1
- package/scripts/utils/workspaces.mjs +58 -0
package/mdc/bun.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Bun як єдиний package manager у монорепо
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.2'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
Проект використовує тільки Bun для керування залежностями та запуску скриптів.
|
|
@@ -50,7 +50,7 @@ Lockfile у репозиторії: `bun.lock`.
|
|
|
50
50
|
|
|
51
51
|
Якщо в package.json є поля `packageManager`, то прибрати їх, також прибрати всі директорії та файли для yarn
|
|
52
52
|
|
|
53
|
-
Якщо в проекті використовується npx, то не
|
|
53
|
+
Якщо в проекті використовується npx, то не замінювати його на bunx, а використовувати npx.
|
|
54
54
|
Коли зміна відбувається в Dockerfile, то використовувати
|
|
55
55
|
|
|
56
56
|
```dockerfile
|
package/mdc/ga.mdc
CHANGED
|
@@ -1,30 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила форматів для .github/workflows
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.2'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Всі файли в директорії .github/workflows повинні мати розширення тільки yml
|
|
10
|
-
|
|
11
|
-
Якщо є .github/workflows/apply-k8s.yml, то в ньому обов'язково повинно бути вказано що критерій для запуску це:
|
|
12
|
-
|
|
13
|
-
```yaml
|
|
14
|
-
on:
|
|
15
|
-
push:
|
|
16
|
-
paths:
|
|
17
|
-
- '**/k8s/*.yaml'
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Якщо є .github/workflows/apply-nats-consumer.yml, то в ньому обов'язково повинно бути вказано що критерій для запуску це:
|
|
21
|
-
|
|
22
|
-
```yaml
|
|
23
|
-
on:
|
|
24
|
-
push:
|
|
25
|
-
paths:
|
|
26
|
-
- '**/consumer.yaml'
|
|
27
|
-
```
|
|
7
|
+
У `.github/workflows/` лише **`.yml`**. Мають бути **`clean-ga-workflows.yml`**, **`clean-merged-branch.yml`**, **`lint-ga.yml`**. Якщо є **`apply-k8s.yml`** / **`apply-nats-consumer.yml`** — paths у тригері як у фрагментах.
|
|
28
8
|
|
|
29
9
|
Повинен бути файл .github/workflows/clean-ga-workflows.yml, зі змістом:
|
|
30
10
|
|
|
@@ -53,7 +33,6 @@ jobs:
|
|
|
53
33
|
save_min_runs_number: 0
|
|
54
34
|
|
|
55
35
|
```
|
|
56
|
-
|
|
57
36
|
Повинен бути файл .github/workflows/clean-merged-branch.yml, зі змістом:
|
|
58
37
|
|
|
59
38
|
```yaml
|
|
@@ -89,8 +68,7 @@ jobs:
|
|
|
89
68
|
echo "Deleted branches: ${DELETED_BRANCHES}"
|
|
90
69
|
```
|
|
91
70
|
|
|
92
|
-
|
|
93
|
-
|
|
71
|
+
Інші гілки в `ignore_branches` — допустимо.
|
|
94
72
|
Повинен бути файл .github/workflows/lint-ga.yml, зі змістом:
|
|
95
73
|
|
|
96
74
|
```yaml
|
|
@@ -145,25 +123,13 @@ jobs:
|
|
|
145
123
|
run: bun run lint-ga
|
|
146
124
|
```
|
|
147
125
|
|
|
148
|
-
в файлі .vscode/extensions.json є налаштування для GitHub Actions:
|
|
149
|
-
|
|
150
126
|
```json title=".vscode/extensions.json"
|
|
151
127
|
{
|
|
152
128
|
"recommendations": ["github.vscode-github-actions"]
|
|
153
129
|
}
|
|
154
130
|
```
|
|
155
131
|
|
|
156
|
-
|
|
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` має бути скрипт:
|
|
132
|
+
**Лінт:** [actionlint](https://github.com/rhysd/actionlint) через [node-actionlint](https://www.npmjs.com/package/node-actionlint); [zizmor](https://docs.zizmor.sh) — `uvx`, офлайн. Скрипт у корені:
|
|
167
133
|
|
|
168
134
|
```json title="package.json"
|
|
169
135
|
"scripts": {
|
|
@@ -171,11 +137,22 @@ jobs:
|
|
|
171
137
|
}
|
|
172
138
|
```
|
|
173
139
|
|
|
174
|
-
|
|
140
|
+
**`.github/zizmor.yml`:** для [unpinned-uses](https://docs.zizmor.sh/audits/#unpinned-uses) — політика **`ref-pin`**, якщо в `uses:` семантичні теги. За потреби вимкни [template-injection](https://docs.zizmor.sh/audits/#template-injection):
|
|
141
|
+
|
|
142
|
+
```yaml title=".github/zizmor.yml"
|
|
143
|
+
# https://docs.zizmor.sh/configuration/
|
|
144
|
+
rules:
|
|
145
|
+
unpinned-uses:
|
|
146
|
+
config:
|
|
147
|
+
policies:
|
|
148
|
+
'*': ref-pin
|
|
149
|
+
template-injection:
|
|
150
|
+
disable: true
|
|
151
|
+
```
|
|
175
152
|
|
|
176
|
-
|
|
153
|
+
**MegaLinter:** не використовувати; прибрати workflow, конфіги (`.mega-linter.yml`, `.megalinter.yaml`, `.mega-linter.yaml`), залежності та згадки в CI / pre-commit / документації.
|
|
177
154
|
|
|
178
155
|
## Перевірка
|
|
179
156
|
|
|
180
|
-
- `bun run lint-ga`
|
|
157
|
+
- `bun run lint-ga`
|
|
181
158
|
- `npx @nitra/cursor check ga`
|
package/mdc/js-lint.mdc
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Перевірка JavaScript коду
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.7'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
У файлі `.vscode/extensions.json` мають бути рекомендації розширень:
|
|
7
|
+
**oxlint**, **ESLint**, **jscpd**. У скрипті **`lint-js`:** `oxlint` (без `bunx`), **`bunx eslint`**, **`bunx jscpd`**; у CI — `bunx oxlint` / `bunx eslint` / `bunx jscpd`. Без **prettier** і **@nitra/prettier-config**. Достатньо **`@nitra/eslint-config`** у devDependencies; пакети oxlint/eslint/jscpd не додавай без потреби монорепо.
|
|
10
8
|
|
|
11
9
|
```json title=".vscode/extensions.json"
|
|
12
10
|
{
|
|
@@ -18,19 +16,19 @@ version: '1.4'
|
|
|
18
16
|
}
|
|
19
17
|
```
|
|
20
18
|
|
|
21
|
-
У кореневому `package.json` додай скрипт і залежності:
|
|
22
|
-
|
|
23
19
|
```json title="package.json"
|
|
20
|
+
{
|
|
24
21
|
"scripts": {
|
|
25
22
|
"lint-js": "oxlint --fix && bunx eslint --fix . && bunx jscpd ."
|
|
26
23
|
},
|
|
27
24
|
"devDependencies": {
|
|
28
25
|
"@nitra/eslint-config": "^3.4.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=24"
|
|
29
29
|
}
|
|
30
|
+
}
|
|
30
31
|
```
|
|
31
|
-
|
|
32
|
-
У `devDependencies` достатньо `@nitra/eslint-config` (залежності ESLint підтягуються разом із ним). **oxlint**, **eslint** та **jscpd** у скриптах викликай через `bunx`; окремо не додавай пакети `oxlint` / `eslint` / `jscpd`, якщо цього не вимагає ваш монорепо.
|
|
33
|
-
|
|
34
32
|
У корені проєкту має бути `.jscpd.json`. Мінімум: увімкнути облік `.gitignore`, ненульовий код виходу при знаходженні клонів, консольний звіт. За потреби додай `ignore` (дзеркальні каталоги, шаблони) та `minLines`, щоб відсікти дрібні збіги:
|
|
35
33
|
|
|
36
34
|
```json title=".jscpd.json"
|
|
@@ -43,9 +41,20 @@ version: '1.4'
|
|
|
43
41
|
}
|
|
44
42
|
```
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
## jscpd: рефакторинг і структура
|
|
45
|
+
|
|
46
|
+
Коли **jscpd** знаходить клони, спочатку зменшуй дублювання кодом, а не конфігом.
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
- **Рефакторинг:** винеси спільні фрагменти в функції, модулі, утиліти, composables/hooks, спільні компоненти або базові типи — залежно від контексту.
|
|
49
|
+
- **Структура:** якщо одна й та сама логіка розмазана між файлами чи пакетами, запропонуй зміну структури (наприклад, спільний модуль, `shared/`, внутрішній пакет у monorepo), щоб була **одна канонічна реалізація** і повторні місця лише викликали її.
|
|
50
|
+
|
|
51
|
+
Так ти уникаєш **хибних обходів** перевірки: розширення `ignore` чи завищення `minLines` лише щоб прибрати звіт — не заміна рефакторингу для справжніх клонів. Якщо збіг **семантично випадковий** (генерований код, формальні шаблони без спільної логіки), після оцінки допустимо точковий `ignore` або зміна порогу — з коротким обґрунтуванням.
|
|
52
|
+
|
|
53
|
+
Але обов'язково перед рефакторингом перевір чи є тести на блоки які підлягають зміні, а саме Bun.test для js та playwright для vue. Якщо є, то перевір чи вони покривають блоки які підлягають зміні. Якщо не покривають або тестів немає — спочатку створи їх, перевір що вони покривають і відпрацьовують коректно, а потім роби рефакторинг і ще раз запускай тести, але якщо тести не відпрацьовують коректно після рефакторингу, то не роби рефакторинг.
|
|
54
|
+
|
|
55
|
+
Додай workflow:
|
|
56
|
+
|
|
57
|
+
```yaml title=".github/workflows/lint-js.yml"
|
|
49
58
|
name: Lint JS
|
|
50
59
|
|
|
51
60
|
on:
|
|
@@ -103,9 +112,7 @@ jobs:
|
|
|
103
112
|
bunx jscpd .
|
|
104
113
|
```
|
|
105
114
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
У корені проєкту має бути `eslint.config.js` із розподілом директорій за типом коду:
|
|
115
|
+
Один workflow на лінт JS; зайвий `lint.yml` з тими самими кроками — прибери.
|
|
109
116
|
|
|
110
117
|
```javascript title="eslint.config.js"
|
|
111
118
|
import { getConfig } from '@nitra/eslint-config'
|
|
@@ -118,7 +125,6 @@ export default [
|
|
|
118
125
|
```
|
|
119
126
|
|
|
120
127
|
У монорепо пакети з Vite (frontend) вкажи в секції `vue`, решту — у секції `node` у виклику `getConfig`.
|
|
121
|
-
|
|
122
128
|
## Додаткові js правила
|
|
123
129
|
|
|
124
130
|
Завжди додавай до package.json що підтримується 24+ версія node:
|
|
@@ -128,10 +134,7 @@ export default [
|
|
|
128
134
|
"node": ">=24"
|
|
129
135
|
}
|
|
130
136
|
```
|
|
131
|
-
|
|
132
|
-
Завжди код повинен використовувати синтаксис 24+ версії node
|
|
133
|
-
|
|
134
|
-
Завжди повинен використовуватися top level await
|
|
137
|
+
**Код:** синтаксис Node **24+**, **top level await**.
|
|
135
138
|
|
|
136
139
|
## Перевірка
|
|
137
140
|
|
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.
|
package/mdc/npm-module.mdc
CHANGED
|
@@ -1,17 +1,51 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Оформлення репозиторію для npm модуля
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.2'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Bun monorepo: workspace **`npm/`**, кореневий **`package.json`**, **`.github/workflows/`**; опційно **`demo/`**.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## npm publish
|
|
10
10
|
|
|
11
|
-
- .
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
11
|
+
**`npm-publish.yml`:** push у **`main`**, **`paths: npm/**`**, **`JS-DevTools/npm-publish@v4`**, **`with.package: npm/package.json`**, **`permissions.id-token: write`** (OIDC на npm).
|
|
12
|
+
|
|
13
|
+
```yaml title=".github/workflows/npm-publish.yml"
|
|
14
|
+
name: npm-publish
|
|
15
|
+
|
|
16
|
+
on:
|
|
17
|
+
push:
|
|
18
|
+
paths:
|
|
19
|
+
- 'npm/**'
|
|
20
|
+
branches:
|
|
21
|
+
- main
|
|
22
|
+
|
|
23
|
+
concurrency:
|
|
24
|
+
group: ${{ github.ref }}-${{ github.workflow }}
|
|
25
|
+
cancel-in-progress: true
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
publish:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
permissions:
|
|
31
|
+
contents: read
|
|
32
|
+
id-token: write # КРИТИЧНО для OIDC!
|
|
33
|
+
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
with:
|
|
37
|
+
persist-credentials: false
|
|
38
|
+
|
|
39
|
+
- uses: actions/setup-node@v5
|
|
40
|
+
with:
|
|
41
|
+
node-version: '24' # includes npm@11.6.0
|
|
42
|
+
registry-url: 'https://registry.npmjs.org'
|
|
43
|
+
|
|
44
|
+
- name: Publish package
|
|
45
|
+
uses: JS-DevTools/npm-publish@v4
|
|
46
|
+
with:
|
|
47
|
+
package: npm/package.json
|
|
48
|
+
```
|
|
15
49
|
|
|
16
50
|
## Перевірка
|
|
17
51
|
|
package/mdc/text.mdc
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Обробка та перевірка текстових файлів (cspell, markdownlint-cli2, v8r, CI)
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.16'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## Розширення Cursor / VS Code
|
|
10
|
-
|
|
11
|
-
У файлі `.vscode/extensions.json` **додай** `DavidAnson.vscode-markdownlint` у масив `recommendations` (разом із записами з інших правил, зокрема js-lint / js-format):
|
|
7
|
+
**cspell**, **markdownlint-cli2**, **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint**, workflow **`lint-text`**.
|
|
12
8
|
|
|
13
9
|
```json title=".vscode/extensions.json"
|
|
14
10
|
{
|
|
@@ -21,34 +17,20 @@ version: '1.12'
|
|
|
21
17
|
}
|
|
22
18
|
```
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
У кореневому `package.json` один скрипт **lint-text** має послідовно викликати **cspell**, **markdownlint-cli2** і **v8r** (і решту блоків з розділів нижче в одному файлі):
|
|
20
|
+
**`package.json`:** скрипт **`lint-text`** і devDependencies **`@nitra/cspell-dict`**, **`markdownlint-cli2`**. Для української додай **`@cspell/dict-uk-ua`**. **`v8r`** лише через **`bunx`**, не в devDependencies. Окремий пакет **`markdownlint`** не потрібний.
|
|
27
21
|
|
|
28
22
|
```json title="package.json"
|
|
23
|
+
{
|
|
29
24
|
"scripts": {
|
|
30
25
|
"lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && (bunx v8r \"**/*.json\" || [ $? -eq 98 ]) && (bunx v8r \"**/*.yml\" || [ $? -eq 98 ]) && (bunx v8r \"**/*.yaml\" || [ $? -eq 98 ]) && (bunx v8r \"**/*.toml\" || [ $? -eq 98 ])"
|
|
31
26
|
},
|
|
32
27
|
"devDependencies": {
|
|
33
|
-
"
|
|
28
|
+
"@nitra/cspell-dict": "^1.0.185"
|
|
34
29
|
}
|
|
30
|
+
}
|
|
35
31
|
```
|
|
36
32
|
|
|
37
|
-
**
|
|
38
|
-
|
|
39
|
-
## v8r
|
|
40
|
-
|
|
41
|
-
[v8r](https://chris48s.github.io/v8r/) — CLI-валідатор: за іменем файлу знаходить схему в [Schema Store](https://www.schemastore.org/) і перевіряє вміст. У `lint-text` — чотири послідовні виклики з масками `**/*.json`, `**/*.yml`, `**/*.yaml`, `**/*.toml`, кожен у вигляді `(bunx v8r "<glob>" || [ $? -eq 98 ])`.
|
|
42
|
-
|
|
43
|
-
**Код 98:** якщо для glob **немає жодного файлу**, v8r дає **98**. Це стосується не лише TOML: репозиторій без `.yml`/`.yaml` зламає **один** спільний виклик `v8r` уже на `**/*.yml`. Окремі виклики з дозволом лише **98** покривають і такі репозиторії.
|
|
44
|
-
|
|
45
|
-
За замовчуванням v8r враховує **`.gitignore`**. **`.v8rignore` не створюй завчасно** — файл потрібен **лише коли** `bunx v8r` падає на JSON/YAML без схеми в каталозі (тоді додай у ignore відповідні glob’и).
|
|
46
|
-
|
|
47
|
-
Пакет **`v8r` у `devDependencies` не додавай** — у скрипті достатньо **`bunx v8r`**.
|
|
48
|
-
|
|
49
|
-
## Конфігурація markdownlint-cli2
|
|
50
|
-
|
|
51
|
-
У корені проєкту має бути **`.markdownlint-cli2.jsonc`** (документація: [DavidAnson/markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2)):
|
|
33
|
+
**v8r:** чотири виклики `(bunx v8r "<glob>" || [ $? -eq 98 ])` — exit **98**, якщо glob порожній. **`.v8rignore`** лише коли немає схеми. Враховує `.gitignore`.
|
|
52
34
|
|
|
53
35
|
```json title=".markdownlint-cli2.jsonc"
|
|
54
36
|
{
|
|
@@ -63,19 +45,14 @@ version: '1.12'
|
|
|
63
45
|
}
|
|
64
46
|
```
|
|
65
47
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
За потреби увімкни окремі правила (наприклад `MD013` з `line_length: 120`), щоб узгодити довжину рядків з `.oxfmtrc.json`.
|
|
69
|
-
|
|
70
|
-
`MD041` вимкнено навмисно: файли `.mdc` (Cursor rules) мають YAML frontmatter, після якого перший абзац часто не є заголовком H1.
|
|
48
|
+
**MD041** off навмисно (`.mdc` з frontmatter). Деталі — [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2).
|
|
71
49
|
|
|
72
50
|
## Cspell
|
|
73
51
|
|
|
74
52
|
У корені проєкту має бути `.cspell.json` і залежності для cspell у кореневому `package.json` (зазвичай `devDependencies`).
|
|
75
53
|
|
|
76
54
|
Додай workflow `.github/workflows/lint-text.yml`:
|
|
77
|
-
|
|
78
|
-
```yaml
|
|
55
|
+
```yaml title=".github/workflows/lint-text.yml"
|
|
79
56
|
name: Lint Text
|
|
80
57
|
|
|
81
58
|
on:
|
|
@@ -144,11 +121,11 @@ jobs:
|
|
|
144
121
|
run: bun run lint-text
|
|
145
122
|
```
|
|
146
123
|
|
|
147
|
-
|
|
124
|
+
Не дублюй окремий workflow з тими самими кроками cspell/markdownlint.
|
|
148
125
|
|
|
149
|
-
|
|
126
|
+
**`.cspell.json`**, `version: "0.2"`, **`language`**, **`import`** з `@nitra/cspell-dict`, **`ignorePaths`**, **`words`** лише для назв/термінів, коли не виправити текстом.
|
|
150
127
|
|
|
151
|
-
|
|
128
|
+
Базово (англійська + українська + корпоративний словник):
|
|
152
129
|
|
|
153
130
|
```json title=".cspell.json"
|
|
154
131
|
{
|
|
@@ -167,14 +144,14 @@ jobs:
|
|
|
167
144
|
},
|
|
168
145
|
"devDependencies": {
|
|
169
146
|
"@nitra/cspell-dict": "^1.0.185",
|
|
170
|
-
"
|
|
147
|
+
"@cspell/dict-uk-ua": "^4.0.6"
|
|
171
148
|
}
|
|
172
149
|
}
|
|
173
150
|
```
|
|
174
151
|
|
|
175
152
|
## Проєкт з українською мовою
|
|
176
153
|
|
|
177
|
-
Якщо в репозиторії є українська документація, коментарі або рядки в коді — потрібен **окремий словник** `@cspell/dict-
|
|
154
|
+
Якщо в репозиторії є українська документація, коментарі або рядки в коді — потрібен **окремий словник** `@cspell/dict-ru_ru`, інакше cspell не перевірятиме російський правопис коректно.
|
|
178
155
|
|
|
179
156
|
**1. Залежності** — додай пакет словника поруч із `@nitra/cspell-dict`:
|
|
180
157
|
|
|
@@ -186,6 +163,7 @@ jobs:
|
|
|
186
163
|
"devDependencies": {
|
|
187
164
|
"@nitra/cspell-dict": "^1.0.185",
|
|
188
165
|
"@cspell/dict-uk-ua": "^4.0.6",
|
|
166
|
+
"@cspell/dict-ru_ru": "^2.3.2",
|
|
189
167
|
"markdownlint-cli2": "^0.22.0"
|
|
190
168
|
}
|
|
191
169
|
}
|
|
@@ -210,14 +188,21 @@ jobs:
|
|
|
210
188
|
|
|
211
189
|
Підлаштуй `language` під проєкт (наприклад додай `ru-ru`, якщо потрібна перевірка російською). Порядок у `import` може впливати на пріоритет словників — тримай корпоративний `@nitra/cspell-dict` там, де зручно для ваших правил.
|
|
212
190
|
|
|
213
|
-
## Локальні виключення
|
|
191
|
+
## Локальні виключення (cspell `words`)
|
|
192
|
+
|
|
193
|
+
Коли **cspell** підсвічує слово, спочатку **виправ текст**, а не розширюй словник:
|
|
214
194
|
|
|
215
|
-
|
|
195
|
+
- виправ **друкарські помилки** та неправильні форми;
|
|
196
|
+
- **перефразуй коректною українською** (або англійською, залежно від контексту файлу): заміни кальки й випадкові склади на звичні формулювання зі словників;
|
|
197
|
+
- заміни **жаргон**, якщо є природний еквівалент у тому ж стилі документації (наприклад, у коментарях пиши «функція зворотного виклику» замість розмовного запозичення з англійського `callback`).
|
|
198
|
+
|
|
199
|
+
У секцію `words` у `.cspell.json` додавай записи **лише якщо переписати коректно неможливо** або **недоречно**: власні назви, стабільні технічні терміни без усталеного перекладу в проєкті, ідентифікатори зовнішніх API тощо.
|
|
216
200
|
|
|
217
201
|
## Інші мови
|
|
218
202
|
|
|
219
203
|
Для іншої мови встанови відповідний пакет `@cspell/dict-*`, додай його `cspell-ext.json` у `import` і код мови в `language`. Огляд словників: [streetsidesoftware/cspell-dicts](https://github.com/streetsidesoftware/cspell-dicts).
|
|
220
204
|
|
|
205
|
+
|
|
221
206
|
## Перевірка
|
|
222
207
|
|
|
223
208
|
`npx @nitra/cursor check text`
|
package/package.json
CHANGED
package/scripts/check-bun.mjs
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє відповідність репозиторію правилам Bun (n-bun.mdc).
|
|
3
|
+
*
|
|
4
|
+
* Очікує наявність `bun.lock`, забороняє lockfile та артефакти yarn/pnpm, директорію `.yarn`
|
|
5
|
+
* і поле `packageManager` у кореневому `package.json`.
|
|
6
|
+
*/
|
|
1
7
|
import { existsSync } from 'node:fs'
|
|
2
8
|
import { readFile } from 'node:fs/promises'
|
|
3
9
|
|
package/scripts/check-ga.mjs
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє GitHub Actions за правилом ga.mdc.
|
|
3
|
+
*
|
|
4
|
+
* Workflows лише з розширенням `.yml`, наявність clean/lint workflow, конфіг zizmor з ref-pin,
|
|
5
|
+
* відсутність MegaLinter, коректний скрипт `lint-ga` у `package.json` і виклик у `lint-ga.yml`.
|
|
6
|
+
*/
|
|
1
7
|
import { existsSync } from 'node:fs'
|
|
2
8
|
import { readdir, readFile } from 'node:fs/promises'
|
|
9
|
+
import { join } from 'node:path'
|
|
3
10
|
|
|
4
11
|
import { pass } from './utils/pass.mjs'
|
|
5
12
|
|
|
13
|
+
/** Шаблони наявності MegaLinter у вмісті workflow */
|
|
14
|
+
const MEGALINTER_USE_PATTERNS = [/oxsecurity\/megalinter-action/i, /megalinter\/megalinter/i]
|
|
15
|
+
|
|
16
|
+
/** Типові конфіги MegaLinter у корені репо */
|
|
17
|
+
const MEGALINTER_CONFIG_NAMES = ['.mega-linter.yml', '.megalinter.yaml', '.mega-linter.yaml']
|
|
18
|
+
|
|
6
19
|
/**
|
|
7
20
|
* Перевіряє відповідність проєкту правилам ga.mdc
|
|
8
21
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
@@ -67,5 +80,76 @@ export async function check() {
|
|
|
67
80
|
fail('.vscode/extensions.json не існує')
|
|
68
81
|
}
|
|
69
82
|
|
|
83
|
+
const ymlWorkflows = files.filter(f => f.endsWith('.yml'))
|
|
84
|
+
let foundMegalinter = false
|
|
85
|
+
for (const f of ymlWorkflows) {
|
|
86
|
+
const content = await readFile(join(wfDir, f), 'utf8')
|
|
87
|
+
if (MEGALINTER_USE_PATTERNS.some(re => re.test(content))) {
|
|
88
|
+
foundMegalinter = true
|
|
89
|
+
fail(`MegaLinter у workflow ${wfDir}/${f} — видали інтеграцію (ga.mdc: MegaLinter)`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const name of MEGALINTER_CONFIG_NAMES) {
|
|
94
|
+
if (existsSync(name)) {
|
|
95
|
+
foundMegalinter = true
|
|
96
|
+
fail(`Файл ${name} — видали конфіг MegaLinter (ga.mdc: MegaLinter)`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!foundMegalinter) {
|
|
101
|
+
pass('Залишків MegaLinter не виявлено')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const zizmorPath = '.github/zizmor.yml'
|
|
105
|
+
if (existsSync(zizmorPath)) {
|
|
106
|
+
const z = await readFile(zizmorPath, 'utf8')
|
|
107
|
+
pass(`${zizmorPath} існує`)
|
|
108
|
+
if (z.includes('ref-pin')) {
|
|
109
|
+
pass(`${zizmorPath} містить політику ref-pin (zizmor)`)
|
|
110
|
+
} else {
|
|
111
|
+
fail(`${zizmorPath}: додай policies ref-pin для unpinned-uses (ga.mdc)`)
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
fail(`Відсутній ${zizmorPath} — потрібен для zizmor (ga.mdc)`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (existsSync('package.json')) {
|
|
118
|
+
const pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
119
|
+
const lg = pkg.scripts?.['lint-ga']
|
|
120
|
+
if (typeof lg === 'string') {
|
|
121
|
+
pass('package.json містить lint-ga')
|
|
122
|
+
if (lg.includes('node-actionlint')) {
|
|
123
|
+
pass('lint-ga викликає node-actionlint')
|
|
124
|
+
} else {
|
|
125
|
+
fail('lint-ga має містити bunx node-actionlint (ga.mdc)')
|
|
126
|
+
}
|
|
127
|
+
if (lg.includes('zizmor') && lg.includes('--offline')) {
|
|
128
|
+
pass('lint-ga викликає zizmor з --offline')
|
|
129
|
+
} else {
|
|
130
|
+
fail('lint-ga має містити zizmor і --offline (ga.mdc)')
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
fail('package.json: додай скрипт "lint-ga" (ga.mdc)')
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
fail('package.json не існує — потрібен lint-ga у scripts')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const lintGaWf = join(wfDir, 'lint-ga.yml')
|
|
140
|
+
if (existsSync(lintGaWf)) {
|
|
141
|
+
const lgContent = await readFile(lintGaWf, 'utf8')
|
|
142
|
+
if (lgContent.includes('bun run lint-ga')) {
|
|
143
|
+
pass('lint-ga.yml викликає bun run lint-ga')
|
|
144
|
+
} else {
|
|
145
|
+
fail('lint-ga.yml: крок має містити bun run lint-ga')
|
|
146
|
+
}
|
|
147
|
+
if (lgContent.includes('astral-sh/setup-uv')) {
|
|
148
|
+
pass('lint-ga.yml містить astral-sh/setup-uv')
|
|
149
|
+
} else {
|
|
150
|
+
fail('lint-ga.yml: додай astral-sh/setup-uv для uvx zizmor (ga.mdc)')
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
70
154
|
return exitCode
|
|
71
155
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє лінт JavaScript за правилом js-lint.mdc.
|
|
3
|
+
*
|
|
4
|
+
* Flat ESLint, скрипт `lint-js` (oxlint, eslint, jscpd), `engines.node`, без prettier,
|
|
5
|
+
* наявність `.jscpd.json` і workflow `lint-js.yml`.
|
|
6
|
+
*/
|
|
1
7
|
import { existsSync } from 'node:fs'
|
|
2
8
|
import { readFile } from 'node:fs/promises'
|
|
3
9
|
|
|
@@ -27,15 +33,43 @@ export async function check() {
|
|
|
27
33
|
|
|
28
34
|
if (pkg.scripts?.['lint-js']) {
|
|
29
35
|
pass('package.json містить скрипт lint-js')
|
|
30
|
-
|
|
36
|
+
const lintJs = String(pkg.scripts['lint-js'])
|
|
37
|
+
if (lintJs.includes('jscpd')) {
|
|
31
38
|
pass('lint-js містить jscpd')
|
|
32
39
|
} else {
|
|
33
40
|
fail('lint-js має викликати jscpd — додай "&& bunx jscpd ." у кінець скрипта')
|
|
34
41
|
}
|
|
42
|
+
if (lintJs.includes('bunx eslint')) {
|
|
43
|
+
pass('lint-js викликає bunx eslint')
|
|
44
|
+
} else {
|
|
45
|
+
fail('lint-js має містити bunx eslint (n-js-lint.mdc)')
|
|
46
|
+
}
|
|
47
|
+
if (lintJs.includes('bunx jscpd')) {
|
|
48
|
+
pass('lint-js викликає bunx jscpd')
|
|
49
|
+
} else {
|
|
50
|
+
fail('lint-js має містити bunx jscpd (n-js-lint.mdc)')
|
|
51
|
+
}
|
|
52
|
+
if (lintJs.includes('oxlint')) {
|
|
53
|
+
pass('lint-js містить oxlint')
|
|
54
|
+
} else {
|
|
55
|
+
fail('lint-js має містити oxlint (n-js-lint.mdc)')
|
|
56
|
+
}
|
|
35
57
|
} else {
|
|
36
58
|
fail('package.json не містить скрипт "lint-js" — додай: "oxlint --fix && bunx eslint --fix . && bunx jscpd ."')
|
|
37
59
|
}
|
|
38
60
|
|
|
61
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
62
|
+
if (allDeps.prettier) {
|
|
63
|
+
fail('package.json: видали залежність prettier (oxfmt замість prettier, n-js-lint.mdc)')
|
|
64
|
+
} else {
|
|
65
|
+
pass('package.json не містить prettier')
|
|
66
|
+
}
|
|
67
|
+
if (allDeps['@nitra/prettier-config']) {
|
|
68
|
+
fail('package.json: видали @nitra/prettier-config (n-js-lint.mdc)')
|
|
69
|
+
} else {
|
|
70
|
+
pass('package.json не містить @nitra/prettier-config')
|
|
71
|
+
}
|
|
72
|
+
|
|
39
73
|
if (pkg.devDependencies?.['@nitra/eslint-config']) {
|
|
40
74
|
pass('@nitra/eslint-config є в devDependencies')
|
|
41
75
|
} else {
|
|
@@ -1,10 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Для кожного workspace-пакета перевіряє правило js-pino.mdc.
|
|
3
|
+
*
|
|
4
|
+
* Заборона bunyan на користь pino та наявність `OTEL_RESOURCE_ATTRIBUTES` у `k8s/base/configmap.yaml`,
|
|
5
|
+
* якщо такий файл існує.
|
|
6
|
+
*/
|
|
1
7
|
import { existsSync } from 'node:fs'
|
|
2
8
|
import { readFile } from 'node:fs/promises'
|
|
9
|
+
import { join } from 'node:path'
|
|
3
10
|
|
|
4
11
|
import { pass } from './utils/pass.mjs'
|
|
12
|
+
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Перевіряє відповідність правилам js-pino.mdc для одного workspace-пакета.
|
|
16
|
+
* @param {string} rootDir відносний шлях workspace (не `'.'`)
|
|
17
|
+
* @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
|
|
18
|
+
* @returns {Promise<void>} завершується після перевірок цього пакета
|
|
19
|
+
*/
|
|
20
|
+
async function checkWorkspacePackage(rootDir, fail) {
|
|
21
|
+
const label = `[${rootDir}] `
|
|
22
|
+
const pkgPath = join(rootDir, 'package.json')
|
|
23
|
+
if (existsSync(pkgPath)) {
|
|
24
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
25
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
26
|
+
|
|
27
|
+
if (allDeps['@nitra/bunyan']) {
|
|
28
|
+
fail(`${label}@nitra/bunyan знайдено — замінити на @nitra/pino`)
|
|
29
|
+
}
|
|
30
|
+
if (allDeps.bunyan) {
|
|
31
|
+
fail(`${label}bunyan знайдено — замінити на @nitra/pino`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const configmapPath = join(rootDir, 'k8s/base/configmap.yaml')
|
|
36
|
+
if (existsSync(configmapPath)) {
|
|
37
|
+
const content = await readFile(configmapPath, 'utf8')
|
|
38
|
+
if (content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
|
|
39
|
+
pass(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
|
|
40
|
+
if (content.includes('service.name=') && content.includes('service.namespace=')) {
|
|
41
|
+
pass(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
|
|
42
|
+
} else {
|
|
43
|
+
fail(`${label}OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>`)
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
fail(`${label}k8s/base/configmap.yaml не містить OTEL_RESOURCE_ATTRIBUTES`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
5
50
|
|
|
6
51
|
/**
|
|
7
|
-
* Перевіряє відповідність проєкту правилам js-pino.mdc
|
|
52
|
+
* Перевіряє відповідність проєкту правилам js-pino.mdc лише для workspace-пакетів (не корінь репо).
|
|
8
53
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
9
54
|
*/
|
|
10
55
|
export async function check() {
|
|
@@ -14,26 +59,16 @@ export async function check() {
|
|
|
14
59
|
exitCode = 1
|
|
15
60
|
}
|
|
16
61
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
62
|
+
const roots = await getMonorepoPackageRootDirs()
|
|
63
|
+
const workspaceRoots = roots.filter(r => r !== '.')
|
|
20
64
|
|
|
21
|
-
|
|
22
|
-
|
|
65
|
+
if (workspaceRoots.length === 0) {
|
|
66
|
+
pass('js-pino: немає workspace-пакетів у кореневому package.json — перевірку залежностей і k8s у пакетах пропущено')
|
|
67
|
+
return exitCode
|
|
23
68
|
}
|
|
24
69
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
}
|
|
70
|
+
for (const r of workspaceRoots) {
|
|
71
|
+
await checkWorkspacePackage(r, fail)
|
|
37
72
|
}
|
|
38
73
|
|
|
39
74
|
return exitCode
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє шаблон nginx за правилом nginx-default-tpl.mdc.
|
|
3
|
+
*
|
|
4
|
+
* Правильна назва файлу, `listen 8080`, `/healthz`, `gzip_static`, без `proxy_pass` у шаблоні,
|
|
5
|
+
* рекомендації VSCode для nginx.
|
|
6
|
+
*/
|
|
1
7
|
import { existsSync } from 'node:fs'
|
|
2
8
|
import { readFile } from 'node:fs/promises'
|
|
3
9
|
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє структуру npm-модуля в монорепо за правилом npm-module.mdc.
|
|
3
|
+
*
|
|
4
|
+
* Workspace `npm/`, `npm/package.json`, workflow `npm-publish.yml` з OIDC і шляхом до пакета.
|
|
5
|
+
*/
|
|
1
6
|
import { existsSync } from 'node:fs'
|
|
2
7
|
import { readFile, stat } from 'node:fs/promises'
|
|
3
8
|
|
|
@@ -53,5 +58,28 @@ export async function check() {
|
|
|
53
58
|
fail('.github/workflows/ не існує')
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
const publishWf = '.github/workflows/npm-publish.yml'
|
|
62
|
+
if (existsSync(publishWf)) {
|
|
63
|
+
pass(`${publishWf} існує`)
|
|
64
|
+
const pub = await readFile(publishWf, 'utf8')
|
|
65
|
+
const need = [
|
|
66
|
+
{ sub: 'npm/**', msg: `${publishWf}: у on.push.paths має бути npm/**` },
|
|
67
|
+
{ sub: 'branches:', msg: `${publishWf}: очікується on.push.branches` },
|
|
68
|
+
{ sub: 'main', msg: `${publishWf}: очікується branch main` },
|
|
69
|
+
{ sub: 'id-token: write', msg: `${publishWf}: permissions має містити id-token: write (OIDC)` },
|
|
70
|
+
{ sub: 'JS-DevTools/npm-publish', msg: `${publishWf}: очікується uses: JS-DevTools/npm-publish` },
|
|
71
|
+
{ sub: 'package: npm/package.json', msg: `${publishWf}: with.package має бути npm/package.json` }
|
|
72
|
+
]
|
|
73
|
+
for (const { sub, msg } of need) {
|
|
74
|
+
if (pub.includes(sub)) {
|
|
75
|
+
pass(`${publishWf} містить «${sub}»`)
|
|
76
|
+
} else {
|
|
77
|
+
fail(msg)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
fail(`Відсутній ${publishWf} (npm-module.mdc: npm publish)`)
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
return exitCode
|
|
57
85
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє CSS/SCSS лінт за правилом style-lint.mdc.
|
|
3
|
+
*
|
|
4
|
+
* `@nitra/stylelint-config`, скрипт `lint-style`, `.stylelintignore`, workflow `lint-style.yml`,
|
|
5
|
+
* VSCode stylelint і вимкнена вбудована CSS-валідація.
|
|
6
|
+
*/
|
|
1
7
|
import { existsSync } from 'node:fs'
|
|
2
8
|
import { readFile } from 'node:fs/promises'
|
|
3
9
|
|
package/scripts/check-text.mjs
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє текстовий стек за правилом text.mdc.
|
|
3
|
+
*
|
|
4
|
+
* cspell, markdownlint-cli2, скрипт `lint-text` з чотирма викликами v8r, workflow `lint-text.yml`,
|
|
5
|
+
* розширення VSCode для markdownlint.
|
|
6
|
+
*/
|
|
1
7
|
import { existsSync } from 'node:fs'
|
|
2
8
|
import { readFile } from 'node:fs/promises'
|
|
3
9
|
|
|
@@ -90,7 +96,7 @@ export async function check() {
|
|
|
90
96
|
if (devDeps['markdownlint-cli2']) {
|
|
91
97
|
pass('markdownlint-cli2 є в devDependencies')
|
|
92
98
|
} else {
|
|
93
|
-
fail('markdownlint-cli2 відсутній — bun add -d markdownlint-cli2')
|
|
99
|
+
fail('markdownlint-cli2 відсутній — bun add -d markdownlint-cli2 (n-text.mdc)')
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
const lintText = pkg.scripts?.['lint-text']
|
|
@@ -99,7 +105,7 @@ export async function check() {
|
|
|
99
105
|
if (
|
|
100
106
|
typeof lintText === 'string' &&
|
|
101
107
|
lintText.includes('cspell') &&
|
|
102
|
-
lintText.includes('markdownlint-cli2') &&
|
|
108
|
+
lintText.includes('bunx markdownlint-cli2') &&
|
|
103
109
|
lintText.includes('**/*.mdc') &&
|
|
104
110
|
v8rCalls >= 4 &&
|
|
105
111
|
eq98Hints >= 4 &&
|
package/scripts/check-vue.mjs
CHANGED
|
@@ -1,10 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Знаходить пакети з `vue` у dependencies і перевіряє їх за правилом vue.mdc.
|
|
3
|
+
*
|
|
4
|
+
* Версії Vite та плагінів, vue-macros, auto-import, layouts, вміст `vite.config`;
|
|
5
|
+
* у репозиторії — рекомендацію розширення Vue.volar.
|
|
6
|
+
*/
|
|
1
7
|
import { existsSync } from 'node:fs'
|
|
2
8
|
import { readFile } from 'node:fs/promises'
|
|
9
|
+
import { join } from 'node:path'
|
|
3
10
|
|
|
4
11
|
import { pass } from './utils/pass.mjs'
|
|
12
|
+
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Формує зрозумілий для людини підпис пакета для повідомлень перевірки.
|
|
16
|
+
* @param {string} rootDir відносний шлях (`'.'` або `site` тощо)
|
|
17
|
+
* @returns {string} підпис для логів перевірки
|
|
18
|
+
*/
|
|
19
|
+
function packageLabel(rootDir) {
|
|
20
|
+
return rootDir === '.' ? 'корінь' : rootDir
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Перевіряє залежності та vite.config одного Vue-пакета.
|
|
25
|
+
* @param {string} rootDir відносний шлях до пакета
|
|
26
|
+
* @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
|
|
27
|
+
* @returns {Promise<void>} завершується після перевірок залежностей і Vite
|
|
28
|
+
*/
|
|
29
|
+
async function checkVuePackage(rootDir, fail) {
|
|
30
|
+
const label = packageLabel(rootDir)
|
|
31
|
+
const prefix = `[${label}] `
|
|
32
|
+
|
|
33
|
+
const pkgPath = join(rootDir, 'package.json')
|
|
34
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
35
|
+
const deps = pkg.dependencies || {}
|
|
36
|
+
const devDeps = pkg.devDependencies || {}
|
|
37
|
+
const allDeps = { ...deps, ...devDeps }
|
|
38
|
+
|
|
39
|
+
if (deps.vue) {
|
|
40
|
+
pass(`${prefix}vue в dependencies: ${deps.vue}`)
|
|
41
|
+
} else {
|
|
42
|
+
fail(`${prefix}vue відсутній в dependencies`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (devDeps.vite) {
|
|
46
|
+
const match = devDeps.vite.match(/(\d+)/)
|
|
47
|
+
if (match && Number(match[1]) >= 8) {
|
|
48
|
+
pass(`${prefix}vite >= 8: ${devDeps.vite}`)
|
|
49
|
+
} else {
|
|
50
|
+
fail(`${prefix}vite має бути >= 8, знайдено: ${devDeps.vite}`)
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
fail(`${prefix}vite відсутній в devDependencies`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (devDeps['@vitejs/plugin-vue']) {
|
|
57
|
+
pass(`${prefix}@vitejs/plugin-vue: ${devDeps['@vitejs/plugin-vue']}`)
|
|
58
|
+
} else {
|
|
59
|
+
fail(`${prefix}@vitejs/plugin-vue відсутній в devDependencies`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (allDeps['vue-macros']) {
|
|
63
|
+
pass(`${prefix}vue-macros: ${allDeps['vue-macros']}`)
|
|
64
|
+
} else {
|
|
65
|
+
fail(`${prefix}vue-macros відсутній — bun add -d vue-macros`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (allDeps['unplugin-auto-import']) {
|
|
69
|
+
pass(`${prefix}unplugin-auto-import присутній`)
|
|
70
|
+
} else {
|
|
71
|
+
fail(`${prefix}unplugin-auto-import відсутній — bun add -d unplugin-auto-import`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (allDeps['vite-plugin-vue-layouts-next']) {
|
|
75
|
+
pass(`${prefix}vite-plugin-vue-layouts-next присутній`)
|
|
76
|
+
} else {
|
|
77
|
+
fail(`${prefix}vite-plugin-vue-layouts-next відсутній — bun add -d vite-plugin-vue-layouts-next`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const configFiles = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
|
|
81
|
+
const viteConfig = configFiles.find(f => existsSync(join(rootDir, f)))
|
|
82
|
+
if (viteConfig) {
|
|
83
|
+
const relConfig = join(rootDir, viteConfig)
|
|
84
|
+
const content = await readFile(relConfig, 'utf8')
|
|
85
|
+
if (content.includes('VueMacros')) {
|
|
86
|
+
pass(`${prefix}${viteConfig} використовує VueMacros`)
|
|
87
|
+
} else {
|
|
88
|
+
fail(`${prefix}${viteConfig} не містить VueMacros`)
|
|
89
|
+
}
|
|
90
|
+
if (content.includes('AutoImport')) {
|
|
91
|
+
pass(`${prefix}${viteConfig} використовує AutoImport`)
|
|
92
|
+
} else {
|
|
93
|
+
fail(`${prefix}${viteConfig} не містить AutoImport`)
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
fail(`${prefix}немає vite.config.js|ts|mjs у каталозі пакета`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
5
99
|
|
|
6
100
|
/**
|
|
7
|
-
* Перевіряє відповідність проєкту правилам vue.mdc
|
|
101
|
+
* Перевіряє відповідність проєкту правилам vue.mdc (корінь і всі workspace-пакети з `vue` у dependencies).
|
|
8
102
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
9
103
|
*/
|
|
10
104
|
export async function check() {
|
|
@@ -25,70 +119,24 @@ export async function check() {
|
|
|
25
119
|
fail('.vscode/extensions.json не існує')
|
|
26
120
|
}
|
|
27
121
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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')
|
|
122
|
+
const roots = await getMonorepoPackageRootDirs()
|
|
123
|
+
/** @type {string[]} */
|
|
124
|
+
const vueRoots = []
|
|
125
|
+
for (const r of roots) {
|
|
126
|
+
const p = join(r, 'package.json')
|
|
127
|
+
if (existsSync(p)) {
|
|
128
|
+
const pkg = JSON.parse(await readFile(p, 'utf8'))
|
|
129
|
+
if (pkg.dependencies?.vue) vueRoots.push(r)
|
|
67
130
|
}
|
|
131
|
+
}
|
|
68
132
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
fail('vite-plugin-vue-layouts-next відсутній — bun add -d vite-plugin-vue-layouts-next')
|
|
73
|
-
}
|
|
133
|
+
if (vueRoots.length === 0) {
|
|
134
|
+
fail('vue не знайдено в dependencies жодного пакета (корінь репо та каталоги з кореневого workspaces)')
|
|
135
|
+
return exitCode
|
|
74
136
|
}
|
|
75
137
|
|
|
76
|
-
const
|
|
77
|
-
|
|
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 не існує')
|
|
138
|
+
for (const r of vueRoots) {
|
|
139
|
+
await checkVuePackage(r, fail)
|
|
92
140
|
}
|
|
93
141
|
|
|
94
142
|
return exitCode
|
package/scripts/utils/pass.mjs
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Допоміжний модуль для скриптів перевірки монорепо.
|
|
3
|
+
*
|
|
4
|
+
* Зчитує кореневий `package.json` і повертає список каталогів-пакетів (корінь `.` плюс шляхи
|
|
5
|
+
* з `workspaces`, з урахуванням glob).
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from 'node:fs'
|
|
8
|
+
import { glob, readFile } from 'node:fs/promises'
|
|
9
|
+
import { dirname, join, relative } from 'node:path'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Нормалізує поле `workspaces` з package.json до масиву шляхів / glob-патернів.
|
|
13
|
+
* @param {unknown} workspaces значення `workspaces` з кореневого package.json
|
|
14
|
+
* @returns {string[]} масив патернів workspaces
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeWorkspacePatterns(workspaces) {
|
|
17
|
+
if (!workspaces) return []
|
|
18
|
+
if (Array.isArray(workspaces)) return workspaces
|
|
19
|
+
if (typeof workspaces === 'object' && workspaces !== null && Array.isArray(workspaces.packages)) {
|
|
20
|
+
return workspaces.packages
|
|
21
|
+
}
|
|
22
|
+
return []
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Повертає каталоги з `package.json`: корінь репозиторію та всі пакети з `workspaces`.
|
|
27
|
+
* @param {string} repoRoot зазвичай `process.cwd()`
|
|
28
|
+
* @returns {Promise<string[]>} відносні шляхи до коренів пакетів; `'.'` першим, без дублікатів
|
|
29
|
+
*/
|
|
30
|
+
export async function getMonorepoPackageRootDirs(repoRoot = '.') {
|
|
31
|
+
const roots = new Set(['.'])
|
|
32
|
+
const rootPkgPath = join(repoRoot, 'package.json')
|
|
33
|
+
if (!existsSync(rootPkgPath)) {
|
|
34
|
+
return ['.']
|
|
35
|
+
}
|
|
36
|
+
const pkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
|
|
37
|
+
for (const raw of normalizeWorkspacePatterns(pkg.workspaces)) {
|
|
38
|
+
const w = raw.replaceAll('\\', '/').replace(/\/+$/, '') || '.'
|
|
39
|
+
if (w.includes('*')) {
|
|
40
|
+
const globPat = `${w}/package.json`
|
|
41
|
+
for await (const f of glob(globPat, { cwd: repoRoot })) {
|
|
42
|
+
const abs = join(repoRoot, f)
|
|
43
|
+
const rel = relative(repoRoot, dirname(abs))
|
|
44
|
+
roots.add(rel === '' ? '.' : rel)
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
const pkgJson = join(repoRoot, w, 'package.json')
|
|
48
|
+
if (existsSync(pkgJson)) roots.add(w)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const list = [...roots]
|
|
52
|
+
list.sort((a, b) => {
|
|
53
|
+
if (a === '.') return -1
|
|
54
|
+
if (b === '.') return 1
|
|
55
|
+
return a.localeCompare(b)
|
|
56
|
+
})
|
|
57
|
+
return list
|
|
58
|
+
}
|