@nitra/cursor 1.0.11 → 1.1.0
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/bin/nitra-cursor.js +89 -65
- package/mdc/js-format.mdc +4 -2
- package/mdc/vue.mdc +43 -14
- package/package.json +1 -1
- package/mdc/vite.mdc +0 -7
package/bin/nitra-cursor.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* nitra-cursor — CLI завантаження правил
|
|
5
5
|
*
|
|
6
|
-
* Завантажує cursor-правила з npm-пакету
|
|
6
|
+
* Завантажує cursor-правила з npm-пакету nitra-cursor у локальний репозиторій.
|
|
7
7
|
*
|
|
8
8
|
* Використання:
|
|
9
9
|
* npx @nitra/cursor
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Якщо у корені репозиторію немає nitra-cursor.json, він створюється автоматично
|
|
12
|
+
* з усіма правилами з каталогу mdc пакету (їх можна відредагувати після створення).
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
16
15
|
import { existsSync } from 'node:fs'
|
|
17
|
-
import {
|
|
16
|
+
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
|
|
17
|
+
import { basename, dirname, join } from 'node:path'
|
|
18
18
|
import { cwd } from 'node:process'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
19
20
|
|
|
20
21
|
const PACKAGE_NAME = '@nitra/cursor'
|
|
21
22
|
const UNPKG_BASE = 'https://unpkg.com'
|
|
@@ -23,10 +24,36 @@ const CONFIG_FILE = 'nitra-cursor.json'
|
|
|
23
24
|
const RULES_DIR = '.cursor/rules'
|
|
24
25
|
const RULE_PREFIX = 'nitra-'
|
|
25
26
|
|
|
27
|
+
const binDir = dirname(fileURLToPath(import.meta.url))
|
|
28
|
+
const BUNDLED_MDC_DIR = join(binDir, '..', 'mdc')
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Імена правил (без .mdc) з каталогу mdc поточної інсталяції пакету
|
|
32
|
+
* @returns {Promise<string[]>} відсортовані імена файлів правил без суфікса .mdc
|
|
33
|
+
*/
|
|
34
|
+
async function discoverBundledRuleNames() {
|
|
35
|
+
if (!existsSync(BUNDLED_MDC_DIR)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Не знайдено каталог правил пакету.\n` +
|
|
38
|
+
`Очікуваний шлях: ${BUNDLED_MDC_DIR}\n` +
|
|
39
|
+
`Перевстановіть ${PACKAGE_NAME} або створіть ${CONFIG_FILE} вручну.`
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
const names = await readdir(BUNDLED_MDC_DIR)
|
|
43
|
+
const rules = names
|
|
44
|
+
.filter(n => n.endsWith('.mdc'))
|
|
45
|
+
.map(n => n.slice(0, -'.mdc'.length))
|
|
46
|
+
.sort((a, b) => a.localeCompare(b))
|
|
47
|
+
if (rules.length === 0) {
|
|
48
|
+
throw new Error(`У каталозі mdc пакету немає файлів .mdc. Створіть ${CONFIG_FILE} вручну.`)
|
|
49
|
+
}
|
|
50
|
+
return rules
|
|
51
|
+
}
|
|
52
|
+
|
|
26
53
|
/**
|
|
27
54
|
* Завантажує текст з URL
|
|
28
|
-
* @param {string} url
|
|
29
|
-
* @returns {Promise<string>}
|
|
55
|
+
* @param {string} url адреса HTTP(S)
|
|
56
|
+
* @returns {Promise<string>} тіло відповіді як UTF-8 текст
|
|
30
57
|
*/
|
|
31
58
|
async function fetchText(url) {
|
|
32
59
|
const response = await fetch(url)
|
|
@@ -38,17 +65,18 @@ async function fetchText(url) {
|
|
|
38
65
|
|
|
39
66
|
/**
|
|
40
67
|
* Зчитує конфіг nitra-cursor.json з поточної директорії
|
|
41
|
-
* @returns {Promise<{rules: string[], version?: string}>}
|
|
68
|
+
* @returns {Promise<{rules: string[], version?: string}>} об'єкт з масивом rules і опційно version; при відсутності файлу створює дефолтний конфіг
|
|
42
69
|
*/
|
|
43
70
|
async function readConfig() {
|
|
44
71
|
const configPath = join(cwd(), CONFIG_FILE)
|
|
45
72
|
if (!existsSync(configPath)) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
const rules = await discoverBundledRuleNames()
|
|
74
|
+
const defaultConfig = { rules }
|
|
75
|
+
await writeFile(configPath, `${JSON.stringify(defaultConfig, null, 2)}\n`, 'utf8')
|
|
76
|
+
console.log(
|
|
77
|
+
`📝 Створено ${CONFIG_FILE} з усіма правилами з пакету (${rules.length}). ` + `За потреби відредагуйте список.\n`
|
|
51
78
|
)
|
|
79
|
+
return defaultConfig
|
|
52
80
|
}
|
|
53
81
|
const raw = await readFile(configPath, 'utf8')
|
|
54
82
|
let config
|
|
@@ -65,9 +93,9 @@ async function readConfig() {
|
|
|
65
93
|
|
|
66
94
|
/**
|
|
67
95
|
* Повертає URL для завантаження правила з unpkg
|
|
68
|
-
* @param {string} ruleName
|
|
69
|
-
* @param {string} [version]
|
|
70
|
-
* @returns {string}
|
|
96
|
+
* @param {string} ruleName - ім'я без розширення, наприклад "js-format"
|
|
97
|
+
* @param {string} [version] - версія пакету (необов'язково, за замовчуванням "latest")
|
|
98
|
+
* @returns {string} повний URL файлу правила на unpkg
|
|
71
99
|
*/
|
|
72
100
|
function buildUrl(ruleName, version) {
|
|
73
101
|
const name = ruleName.endsWith('.mdc') ? ruleName : `${ruleName}.mdc`
|
|
@@ -79,63 +107,59 @@ function buildUrl(ruleName, version) {
|
|
|
79
107
|
* Витягує чисте ім'я файлу правила (без шляху, але зберігає .mdc)
|
|
80
108
|
* "npm/mdc/js-format.mdc" → "js-format.mdc"
|
|
81
109
|
* "js-format" → "js-format.mdc"
|
|
82
|
-
* @param {string} ruleName
|
|
83
|
-
* @returns {string}
|
|
110
|
+
* @param {string} ruleName шлях або базове ім'я, з суфіксом .mdc або без
|
|
111
|
+
* @returns {string} лише ім'я файлу з суфіксом .mdc
|
|
84
112
|
*/
|
|
85
113
|
function normalizeRuleName(ruleName) {
|
|
86
114
|
const name = ruleName.endsWith('.mdc') ? ruleName : `${ruleName}.mdc`
|
|
87
115
|
return basename(name)
|
|
88
116
|
}
|
|
89
117
|
|
|
90
|
-
|
|
91
|
-
console.log(`\n🔧 @nitra/cursor — завантаження cursor-правил\n`)
|
|
118
|
+
console.log(`\n🔧 @nitra/cursor — завантаження cursor-правил\n`)
|
|
92
119
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
// 1. Зчитуємо конфіг
|
|
121
|
+
let config
|
|
122
|
+
try {
|
|
123
|
+
config = await readConfig()
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error(`❌ ${error.message}`)
|
|
126
|
+
process.exit(1)
|
|
127
|
+
}
|
|
101
128
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// 2. Створюємо директорію .cursor/rules якщо не існує
|
|
109
|
-
const rulesDir = join(cwd(), RULES_DIR)
|
|
110
|
-
await mkdir(rulesDir, { recursive: true })
|
|
111
|
-
|
|
112
|
-
// 3. Завантажуємо та зберігаємо кожне правило
|
|
113
|
-
let successCount = 0
|
|
114
|
-
let failCount = 0
|
|
115
|
-
|
|
116
|
-
for (const rule of rules) {
|
|
117
|
-
const url = buildUrl(rule, version)
|
|
118
|
-
const fileName = `${RULE_PREFIX}${normalizeRuleName(rule)}`
|
|
119
|
-
const destPath = join(rulesDir, fileName)
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
process.stdout.write(` ⬇ ${rule} → ${RULES_DIR}/${fileName} ... `)
|
|
123
|
-
const content = await fetchText(url)
|
|
124
|
-
await writeFile(destPath, content, 'utf8')
|
|
125
|
-
console.log(`✅`)
|
|
126
|
-
successCount++
|
|
127
|
-
} catch (err) {
|
|
128
|
-
console.log(`❌`)
|
|
129
|
-
console.error(` Помилка: ${err.message}`)
|
|
130
|
-
failCount++
|
|
131
|
-
}
|
|
132
|
-
}
|
|
129
|
+
const { rules, version } = config
|
|
130
|
+
if (version) {
|
|
131
|
+
console.log(`📦 Версія пакету: ${version}`)
|
|
132
|
+
}
|
|
133
|
+
console.log(`📋 Правил до завантаження: ${rules.length}`)
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
// 2. Створюємо директорію .cursor/rules якщо не існує
|
|
136
|
+
const rulesDir = join(cwd(), RULES_DIR)
|
|
137
|
+
await mkdir(rulesDir, { recursive: true })
|
|
138
|
+
|
|
139
|
+
// 3. Завантажуємо та зберігаємо кожне правило
|
|
140
|
+
let successCount = 0
|
|
141
|
+
let failCount = 0
|
|
142
|
+
|
|
143
|
+
for (const rule of rules) {
|
|
144
|
+
const url = buildUrl(rule, version)
|
|
145
|
+
const fileName = `${RULE_PREFIX}${normalizeRuleName(rule)}`
|
|
146
|
+
const destPath = join(rulesDir, fileName)
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
process.stdout.write(` ⬇ ${rule} → ${RULES_DIR}/${fileName} ... `)
|
|
150
|
+
const content = await fetchText(url)
|
|
151
|
+
await writeFile(destPath, content, 'utf8')
|
|
152
|
+
console.log(`✅`)
|
|
153
|
+
successCount++
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.log(`❌`)
|
|
156
|
+
console.error(` Помилка: ${error.message}`)
|
|
157
|
+
failCount++
|
|
138
158
|
}
|
|
139
159
|
}
|
|
140
160
|
|
|
141
|
-
|
|
161
|
+
// 4. Підсумок
|
|
162
|
+
console.log(`\n✨ Готово: ${successCount} завантажено, ${failCount} з помилками\n`)
|
|
163
|
+
if (failCount > 0) {
|
|
164
|
+
process.exit(1)
|
|
165
|
+
}
|
package/mdc/js-format.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила форматів JavaScript ecosystem
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.2'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
В корені проекту є файл з правилами форматування для oxfmt:
|
|
@@ -94,4 +94,6 @@ version: '1.1'
|
|
|
94
94
|
}
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
Також потрібно прибрати якщо є в проекті модул @nitra/prettier-config та prettier та всі виклики prettier і налаштування для нього.
|
|
97
|
+
Також потрібно прибрати якщо є в проекті модул @nitra/prettier-config та prettier та всі виклики prettier і налаштування для нього.
|
|
98
|
+
|
|
99
|
+
Завжди пиши JSDoc до функцій та методів.
|
package/mdc/vue.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Vue
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.2'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Vue 3 (Composition API)
|
|
@@ -13,10 +13,10 @@ version: '1.1'
|
|
|
13
13
|
```javascript
|
|
14
14
|
const vue3CompositionApiBestPractices = [
|
|
15
15
|
'Використовуй функцію setup() для логіки компонента',
|
|
16
|
-
'Реалізуй computed properties через computed()',
|
|
16
|
+
'Реалізуй computed properties через $computed()',
|
|
17
17
|
'Використовуй watch і watchEffect для side effects',
|
|
18
18
|
'Підключай lifecycle hooks: onMounted, onUpdated тощо',
|
|
19
|
-
'
|
|
19
|
+
'не використовуй provide/inject для dependency injection'
|
|
20
20
|
]
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -31,6 +31,7 @@ src/
|
|
|
31
31
|
router/
|
|
32
32
|
store/
|
|
33
33
|
assets/
|
|
34
|
+
public/
|
|
34
35
|
App.vue
|
|
35
36
|
main.js
|
|
36
37
|
`
|
|
@@ -38,14 +39,13 @@ src/
|
|
|
38
39
|
|
|
39
40
|
### Найменування файлів
|
|
40
41
|
|
|
41
|
-
- **SFC:** імена файлів компонентів у **PascalCase** (`
|
|
42
|
+
- **SFC:** імена файлів компонентів у **PascalCase** починаючи з букви N(`NMyWidget.vue`).
|
|
42
43
|
- **Інші JS-модулі:** узгоджено **kebab-case** (`date-utils.js`).
|
|
43
44
|
|
|
44
45
|
### Модулі та архітектура
|
|
45
46
|
|
|
46
47
|
- **ES modules** (`import`/`export`), один модуль — одна відповідальність, уникай **circular dependencies**.
|
|
47
48
|
- **Composition** замість успадкування; логіку для повторного використання винось у **composables** (замість зайвих HOC, де це доречно).
|
|
48
|
-
- **SFC** для template + script + style; за потреби розділяй presentational і container-компоненти (UI vs дані/оркестрація).
|
|
49
49
|
|
|
50
50
|
### Code splitting
|
|
51
51
|
|
|
@@ -56,7 +56,7 @@ src/
|
|
|
56
56
|
```javascript
|
|
57
57
|
const additionalInstructions = `
|
|
58
58
|
1. Використовуй JavaScript
|
|
59
|
-
2. Коректно оголошуй props
|
|
59
|
+
2. Коректно оголошуй props, emits, defineModel
|
|
60
60
|
3. За потреби використовуй компонент Teleport у Vue 3
|
|
61
61
|
4. Застосовуй Suspense для async components
|
|
62
62
|
5. Реалізуй належний error handling
|
|
@@ -79,11 +79,10 @@ const additionalInstructions = `
|
|
|
79
79
|
|
|
80
80
|
- **provide/inject** — для глибоко вкладених залежностей; **renderless**-компоненти / **slots** — коли логіку відділяєш від розмітки.
|
|
81
81
|
- **HTTP:** окремі модулі **services** або **composables** для API; **async/await**.
|
|
82
|
-
- **Події:** батько–дитина через **emits**; для не пов’язаних гілок — **store
|
|
82
|
+
- **Події:** батько–дитина через **emits**; для не пов’язаних гілок — **store**.
|
|
83
83
|
- Не мутуй **props** напряму — оновлення через подію вгору або v-model.
|
|
84
84
|
- Обмежуй зайве в глобальному стані; локальний стан у компоненті — за замовчуванням.
|
|
85
85
|
- Уникай прямої роботи з **DOM**, якщо достатньо реактивного шаблону та ref.
|
|
86
|
-
- **Magic numbers/strings** винось у константи; **computed** тримай простими, важкі обчислення — у функції або утиліти.
|
|
87
86
|
|
|
88
87
|
### State management
|
|
89
88
|
|
|
@@ -92,14 +91,12 @@ const additionalInstructions = `
|
|
|
92
91
|
|
|
93
92
|
### Обробка помилок (додатково)
|
|
94
93
|
|
|
95
|
-
- **try/catch** навколо async-операцій; зрозумілі повідомлення для
|
|
96
|
-
- **Error boundary**-підхід у Vue — через окремі компоненти-обгортки та `onErrorCaptured` / глобальні хендлери залежно від архітектури.
|
|
94
|
+
- **try/catch** навколо async-операцій; зрозумілі повідомлення для користувача через notifySuccess, notifyError; логування на сервіс моніторингу за потреби.
|
|
97
95
|
|
|
98
96
|
### Продуктивність
|
|
99
97
|
|
|
100
98
|
- **v-for** — стабільні унікальні **`:key`**; не плутай **v-if** (умовний mount) і **v-show** (перемикання visibility).
|
|
101
|
-
-
|
|
102
|
-
- **v-once** для статичного вмісту; уникай глибоко вкладених дерев без потреби.
|
|
99
|
+
- **debounce/throttle** для частих подій.
|
|
103
100
|
- Після ручних **addEventListener** / підписок — прибирай у **onUnmounted**.
|
|
104
101
|
|
|
105
102
|
### Безпека
|
|
@@ -130,7 +127,7 @@ import { computed, onMounted } from 'vue'
|
|
|
130
127
|
export default {
|
|
131
128
|
setup() {
|
|
132
129
|
const count = $ref(0)
|
|
133
|
-
const doubleCount = computed(() => count * 2)
|
|
130
|
+
const doubleCount = $computed(() => count * 2)
|
|
134
131
|
|
|
135
132
|
onMounted(() => {
|
|
136
133
|
console.log('Компонент змонтовано')
|
|
@@ -174,14 +171,40 @@ export default {
|
|
|
174
171
|
import Vue from '@vitejs/plugin-vue'
|
|
175
172
|
import VueMacros from 'vue-macros/vite'
|
|
176
173
|
import { defineConfig } from 'vite'
|
|
174
|
+
import AutoImport from 'unplugin-auto-import/vite'
|
|
175
|
+
import Layouts from 'vite-plugin-vue-layouts-next'
|
|
177
176
|
|
|
178
177
|
export default defineConfig({
|
|
179
178
|
plugins: [
|
|
179
|
+
AutoImport({
|
|
180
|
+
imports: [
|
|
181
|
+
// presets
|
|
182
|
+
'vue',
|
|
183
|
+
'vue-router',
|
|
184
|
+
'quasar',
|
|
185
|
+
'pinia',
|
|
186
|
+
// custom
|
|
187
|
+
{
|
|
188
|
+
'@nitra/vite-boot/apollo': [
|
|
189
|
+
// named imports
|
|
190
|
+
'gql',
|
|
191
|
+
'useQuery',
|
|
192
|
+
'useMutation',
|
|
193
|
+
'useSubscription'
|
|
194
|
+
],
|
|
195
|
+
'@nitra/consola': [
|
|
196
|
+
// named imports
|
|
197
|
+
'createLogger' // import { createLogger } from '@nitra/consola'
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
}),
|
|
180
202
|
VueMacros({
|
|
181
203
|
plugins: {
|
|
182
204
|
vue: Vue()
|
|
183
205
|
}
|
|
184
|
-
})
|
|
206
|
+
}),
|
|
207
|
+
Layouts()
|
|
185
208
|
]
|
|
186
209
|
})
|
|
187
210
|
```
|
|
@@ -189,3 +212,9 @@ export default defineConfig({
|
|
|
189
212
|
### Висновок
|
|
190
213
|
|
|
191
214
|
Дотримуючись цих практик і правил, можна будувати масштабовані, підтримувані та ефективні застосунки на Vue 3 з Composition API. Завжди звіряйся з офіційною документацією Vue 3 щодо оновлень і нових можливостей.
|
|
215
|
+
|
|
216
|
+
Потрібно використовувати Vite версії 8 та вище для frontend проекту на Vue.
|
|
217
|
+
|
|
218
|
+
Потрібно використовувати unplugin-auto-import для автоматичного імпортування компонентів, composables, utils та інших функцій і прибирати з Vue sfc імпорти які їх дублюють.
|
|
219
|
+
|
|
220
|
+
Потрібно використовувати vite-plugin-vue-layouts-next для автоматичного імпортування layout компонентів.
|
package/package.json
CHANGED