@nitra/cursor 1.8.93 → 1.8.95
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/README.md +1 -1
- package/mdc/graphql.mdc +33 -0
- package/package.json +1 -1
- package/scripts/check-abie.mjs +51 -53
- package/scripts/check-graphql.mjs +97 -0
- package/scripts/check-k8s.mjs +0 -2
- package/scripts/check-npm-module.mjs +9 -9
- package/scripts/utils/graphql-gql-scan.mjs +115 -0
- package/skills/lint/SKILL.md +42 -0
- package/types/bin/n-cursor.d.ts +1 -1
package/README.md
CHANGED
package/mdc/graphql.mdc
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: GraphQL у коді (tagged template `gql`) — GraphQL Config і розширення VS Code
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
version: '1.0'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Якщо в **`.vue`** або в **JavaScript / TypeScript** джерелах (`.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.jsx` тощо) зустрічається **tagged template literal** з тегом **`gql`** (типово `gql\`query …\`` для GraphQL-запиту), у **корені репозиторію** має бути файл **`.graphqlrc.yml`** ([GraphQL Config](https://the-guild.dev/graphql/config/docs)), а в **`.vscode/extensions.json`** у масиві **`recommendations`** — запис **`graphql.vscode-graphql`**, щоб підсвітка, навігація до схеми й діагностика працювали в редакторі.
|
|
8
|
+
|
|
9
|
+
Деталі виявлення `gql` у скриптах (у т.ч. лише `<script>` у SFC) — **`npm/scripts/check-graphql.mjs`** / **`npm/scripts/utils/graphql-gql-scan.mjs`**.
|
|
10
|
+
|
|
11
|
+
## `.graphqlrc.yml`
|
|
12
|
+
|
|
13
|
+
Підстав свої шляхи до схеми та до файлів з операціями; приклад орієнтиру:
|
|
14
|
+
|
|
15
|
+
```yaml title=".graphqlrc.yml"
|
|
16
|
+
schema: schema.graphql
|
|
17
|
+
documents:
|
|
18
|
+
- '**/*.{vue,js,ts,tsx}'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## `.vscode/extensions.json`
|
|
22
|
+
|
|
23
|
+
Додай **`graphql.vscode-graphql`** до наявного списку **`recommendations`** (не замінюй інші записи):
|
|
24
|
+
|
|
25
|
+
```json title=".vscode/extensions.json"
|
|
26
|
+
{
|
|
27
|
+
"recommendations": ["graphql.vscode-graphql"]
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Перевірка
|
|
32
|
+
|
|
33
|
+
`npx @nitra/cursor check graphql`
|
package/package.json
CHANGED
package/scripts/check-abie.mjs
CHANGED
|
@@ -543,7 +543,7 @@ export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
|
|
|
543
543
|
* Чи YAML відносно кореня належить до **`${pkgRel}/k8s/**`** поза піддеревами **`ua/`** та **`ru/`** (base-шар abie).
|
|
544
544
|
* @param {string} relFromRoot відносний шлях від кореня
|
|
545
545
|
* @param {string} pkgRelFromRoot каталог пакета відносно кореня (без завершального слеша після імені пакета)
|
|
546
|
-
* @returns {boolean}
|
|
546
|
+
* @returns {boolean} `true`, якщо шлях належить до base-шару abie
|
|
547
547
|
*/
|
|
548
548
|
export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelFromRoot) {
|
|
549
549
|
const normRel = relFromRoot.replaceAll('\\', '/')
|
|
@@ -560,7 +560,7 @@ export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelF
|
|
|
560
560
|
* З HTTPRoute-документа рахує **`backendRefs`** до **`auth-run-hl`** / **`filelint-hl`** і порушення **`namespace: dev`**.
|
|
561
561
|
* @param {unknown} obj корінь YAML
|
|
562
562
|
* @param {string} rel відносний шлях (повідомлення)
|
|
563
|
-
* @returns {{ refCount: number, errors: string[] }}
|
|
563
|
+
* @returns {{ refCount: number, errors: string[] }} кількість посилань і список порушень
|
|
564
564
|
*/
|
|
565
565
|
function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
|
|
566
566
|
/** @type {string[]} */
|
|
@@ -582,26 +582,22 @@ function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
|
|
|
582
582
|
}
|
|
583
583
|
let refCount = 0
|
|
584
584
|
for (const rule of rules) {
|
|
585
|
-
if (rule
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
refCount++
|
|
602
|
-
const ns = brRec.namespace
|
|
603
|
-
if (typeof ns !== 'string' || ns !== 'dev') {
|
|
604
|
-
errors.push(`${rel}: HTTPRoute backendRefs до ${name} має містити namespace: dev (abie.mdc)`)
|
|
585
|
+
if (rule !== null && typeof rule === 'object' && !Array.isArray(rule)) {
|
|
586
|
+
const brs = /** @type {Record<string, unknown>} */ (rule).backendRefs
|
|
587
|
+
if (Array.isArray(brs)) {
|
|
588
|
+
for (const br of brs) {
|
|
589
|
+
if (br !== null && typeof br === 'object' && !Array.isArray(br)) {
|
|
590
|
+
const brRec = /** @type {Record<string, unknown>} */ (br)
|
|
591
|
+
const name = brRec.name
|
|
592
|
+
if (typeof name === 'string' && ABIE_SHARED_CROSS_NS_BACKEND_SET.has(name)) {
|
|
593
|
+
refCount++
|
|
594
|
+
const ns = brRec.namespace
|
|
595
|
+
if (typeof ns !== 'string' || ns !== 'dev') {
|
|
596
|
+
errors.push(`${rel}: HTTPRoute backendRefs до ${name} має містити namespace: dev (abie.mdc)`)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
605
601
|
}
|
|
606
602
|
}
|
|
607
603
|
}
|
|
@@ -613,7 +609,7 @@ function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
|
|
|
613
609
|
* @param {string} root корінь репозиторію
|
|
614
610
|
* @param {string} pkgAbs абсолютний шлях до каталогу пакета
|
|
615
611
|
* @param {string[]} yamlFilesAbs усі **yaml** під **k8s** (як **findK8sYamlFiles**)
|
|
616
|
-
* @returns {Promise<{ refCount: number, baseErrors: string[] }>}
|
|
612
|
+
* @returns {Promise<{ refCount: number, baseErrors: string[] }>} кількість посилань і базові помилки
|
|
617
613
|
*/
|
|
618
614
|
export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamlFilesAbs) {
|
|
619
615
|
const pkgRel = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
|
|
@@ -622,34 +618,36 @@ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yam
|
|
|
622
618
|
const baseErrors = []
|
|
623
619
|
for (const abs of yamlFilesAbs) {
|
|
624
620
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
625
|
-
if (
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
621
|
+
if (isK8sYamlInAbiePackageExcludingUaRuOverlays(rel, pkgRel)) {
|
|
622
|
+
let raw
|
|
623
|
+
try {
|
|
624
|
+
raw = await readFile(abs, 'utf8')
|
|
625
|
+
} catch {
|
|
626
|
+
raw = undefined
|
|
627
|
+
}
|
|
628
|
+
if (raw !== undefined) {
|
|
629
|
+
const body = stripBom(raw)
|
|
630
|
+
const lines = body.split(/\r?\n/u)
|
|
631
|
+
const first = lines[0] ?? ''
|
|
632
|
+
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
633
|
+
/** @type {import('yaml').Document[] | undefined} */
|
|
634
|
+
let docs
|
|
635
|
+
try {
|
|
636
|
+
docs = parseAllDocuments(rest)
|
|
637
|
+
} catch {
|
|
638
|
+
docs = undefined
|
|
639
|
+
}
|
|
640
|
+
if (docs !== undefined) {
|
|
641
|
+
for (const doc of docs) {
|
|
642
|
+
if (doc.errors.length === 0) {
|
|
643
|
+
const obj = doc.toJSON()
|
|
644
|
+
const st = httpRouteDocSharedCrossNsBackendStats(obj, rel)
|
|
645
|
+
refCount += st.refCount
|
|
646
|
+
baseErrors.push(...st.errors)
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
648
650
|
}
|
|
649
|
-
const obj = doc.toJSON()
|
|
650
|
-
const st = httpRouteDocSharedCrossNsBackendStats(obj, rel)
|
|
651
|
-
refCount += st.refCount
|
|
652
|
-
baseErrors.push(...st.errors)
|
|
653
651
|
}
|
|
654
652
|
}
|
|
655
653
|
return { refCount, baseErrors }
|
|
@@ -659,7 +657,7 @@ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yam
|
|
|
659
657
|
* Рахує операції JSON6902 з **`path`**: **`/spec/rules/…/backendRefs/…/namespace`** та **`value`** overlay.
|
|
660
658
|
* @param {string} combined сукупний текст patch **HTTPRoute**
|
|
661
659
|
* @param {'ua' | 'ru'} mode overlay
|
|
662
|
-
* @returns {number}
|
|
660
|
+
* @returns {number} кількість знайдених патчів namespace
|
|
663
661
|
*/
|
|
664
662
|
function countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode) {
|
|
665
663
|
const re =
|
|
@@ -1073,8 +1071,8 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
|
|
|
1073
1071
|
/** @type {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} */
|
|
1074
1072
|
const sharedBackendAnalysisByPkg = new Map()
|
|
1075
1073
|
/**
|
|
1076
|
-
* @param {string} pkgAbs
|
|
1077
|
-
* @returns {Promise<{ refCount: number, baseErrors: string[] }>}
|
|
1074
|
+
* @param {string} pkgAbs абсолютний шлях до каталогу пакета
|
|
1075
|
+
* @returns {Promise<{ refCount: number, baseErrors: string[] }>} кількість посилань і базові помилки
|
|
1078
1076
|
*/
|
|
1079
1077
|
const getSharedBackendAnalysis = pkgAbs => {
|
|
1080
1078
|
let p = sharedBackendAnalysisByPkg.get(pkgAbs)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє правило graphql.mdc: наявність **`.graphqlrc.yml`** і рекомендації **`graphql.vscode-graphql`**, якщо у дереві є **`gql\`…\``**.
|
|
3
|
+
*
|
|
4
|
+
* Обхід репозиторію — **`walkDir`** від **`process.cwd()`** (пропуски як у інших check). Кандидати — **`.vue`** та **`.js`/`.ts`/`.jsx`/`.tsx`** тощо; ігнор **`.d.ts`**, **auto-imports.d.ts** тощо — **`shouldSkipFileForGqlScan`**.
|
|
5
|
+
*
|
|
6
|
+
* Виявлення **`gql`** — **oxc-parser** після витягування `<script>` з SFC (**`graphql-gql-scan.mjs`**). Якщо збігів немає — перевірка завершується успішно без вимог до конфігів.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from 'node:fs'
|
|
9
|
+
import { readFile } from 'node:fs/promises'
|
|
10
|
+
import { relative } from 'node:path'
|
|
11
|
+
|
|
12
|
+
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
13
|
+
import {
|
|
14
|
+
isGqlScanSourceFile,
|
|
15
|
+
shouldSkipFileForGqlScan,
|
|
16
|
+
sourceFileHasGqlTaggedTemplate
|
|
17
|
+
} from './utils/graphql-gql-scan.mjs'
|
|
18
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
19
|
+
|
|
20
|
+
/** Очікуваний файл GraphQL Config у корені (graphql.mdc). */
|
|
21
|
+
export const GRAPHQL_RC_FILENAME = '.graphqlrc.yml'
|
|
22
|
+
|
|
23
|
+
/** Розширення VS Code з graphql.mdc. */
|
|
24
|
+
export const REQUIRED_GRAPHQL_VSCODE_EXTENSION = 'graphql.vscode-graphql'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Перевіряє graphql.mdc: умовна вимога `.graphqlrc.yml` і `graphql.vscode-graphql` за наявності `gql`…``.
|
|
28
|
+
* @returns {Promise<number>} 0 — OK, 1 — порушення
|
|
29
|
+
*/
|
|
30
|
+
export async function check() {
|
|
31
|
+
const reporter = createCheckReporter()
|
|
32
|
+
const { pass, fail } = reporter
|
|
33
|
+
|
|
34
|
+
const root = process.cwd()
|
|
35
|
+
/** @type {string[]} */
|
|
36
|
+
const candidates = []
|
|
37
|
+
await walkDir(root, absPath => {
|
|
38
|
+
const rel = relative(root, absPath).split('\\').join('/')
|
|
39
|
+
if (shouldSkipFileForGqlScan(rel) || !isGqlScanSourceFile(rel)) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
candidates.push(absPath)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
/** @type {string[]} */
|
|
46
|
+
const hits = []
|
|
47
|
+
for (const absPath of candidates) {
|
|
48
|
+
const rel = relative(root, absPath).split('\\').join('/')
|
|
49
|
+
const content = await readFile(absPath, 'utf8')
|
|
50
|
+
if (sourceFileHasGqlTaggedTemplate(content, rel)) {
|
|
51
|
+
hits.push(rel)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (hits.length === 0) {
|
|
56
|
+
pass(`Немає tagged template з тегом gql у .vue / JS / TS джерелах (переглянуто ${candidates.length} файлів) — .graphqlrc.yml не вимагається`)
|
|
57
|
+
return reporter.getExitCode()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pass(`Знайдено gql\`…\` у ${hits.length} файлі(ах): ${hits.slice(0, 5).join(', ')}${hits.length > 5 ? '…' : ''}`)
|
|
61
|
+
|
|
62
|
+
if (!existsSync(GRAPHQL_RC_FILENAME)) {
|
|
63
|
+
fail(
|
|
64
|
+
`Відсутній ${GRAPHQL_RC_FILENAME} у корені — додай GraphQL Config (graphql.mdc), бо в проєкті є gql template literals`
|
|
65
|
+
)
|
|
66
|
+
} else {
|
|
67
|
+
pass(`${GRAPHQL_RC_FILENAME} існує`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!existsSync('.vscode/extensions.json')) {
|
|
71
|
+
fail(
|
|
72
|
+
'.vscode/extensions.json не існує — створи файл і додай у recommendations graphql.vscode-graphql (graphql.mdc)'
|
|
73
|
+
)
|
|
74
|
+
} else {
|
|
75
|
+
let ext
|
|
76
|
+
try {
|
|
77
|
+
ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
|
|
78
|
+
} catch {
|
|
79
|
+
fail('.vscode/extensions.json не є валідним JSON')
|
|
80
|
+
ext = null
|
|
81
|
+
}
|
|
82
|
+
if (ext) {
|
|
83
|
+
const rec = ext.recommendations
|
|
84
|
+
if (!Array.isArray(rec)) {
|
|
85
|
+
fail('.vscode/extensions.json: поле recommendations має бути масивом')
|
|
86
|
+
} else if (!rec.includes(REQUIRED_GRAPHQL_VSCODE_EXTENSION)) {
|
|
87
|
+
fail(
|
|
88
|
+
`.vscode/extensions.json: додай у recommendations "${REQUIRED_GRAPHQL_VSCODE_EXTENSION}" (graphql.mdc)`
|
|
89
|
+
)
|
|
90
|
+
} else {
|
|
91
|
+
pass(`.vscode/extensions.json: є ${REQUIRED_GRAPHQL_VSCODE_EXTENSION}`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return reporter.getExitCode()
|
|
97
|
+
}
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -1152,7 +1152,6 @@ export function serviceSvcHlYamlHeadlessViolation(manifest) {
|
|
|
1152
1152
|
*
|
|
1153
1153
|
* Застосовується лише для **`apiVersion: networking.gke.io/v1`** і **`targetRef.kind: Service`** (або без **`kind`**).
|
|
1154
1154
|
* Інші **`targetRef.kind`** скрипт не оцінює.
|
|
1155
|
-
*
|
|
1156
1155
|
* @param {unknown} manifest корінь YAML-документа
|
|
1157
1156
|
* @returns {string | null} текст порушення або null
|
|
1158
1157
|
*/
|
|
@@ -1182,7 +1181,6 @@ export function healthCheckPolicyTargetRefHeadlessServiceViolation(manifest) {
|
|
|
1182
1181
|
* Чи об’єкт схожий на **backendRef** до **Kubernetes Service** у Gateway API.
|
|
1183
1182
|
*
|
|
1184
1183
|
* Вимагає числовий **`port`**, щоб не плутати з **`HTTPHeaderMatch`** тощо (там теж є **`name`**, але без **`port`**).
|
|
1185
|
-
*
|
|
1186
1184
|
* @param {unknown} obj вузол у дереві **`spec`**
|
|
1187
1185
|
* @returns {boolean} true, якщо враховуємо поле **`name`** як посилання на Service
|
|
1188
1186
|
*/
|
|
@@ -33,7 +33,7 @@ const EMIT_TYPES_CONFIG = 'npm/tsconfig.emit-types.json'
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Чи є під `npm/src` хоча б один `.js` (рекурсивно).
|
|
36
|
-
* @returns {Promise<boolean>}
|
|
36
|
+
* @returns {Promise<boolean>} `true`, якщо знайдено хоча б один `.js`
|
|
37
37
|
*/
|
|
38
38
|
async function npmSrcTreeHasJsFile() {
|
|
39
39
|
const root = 'npm/src'
|
|
@@ -51,7 +51,7 @@ async function npmSrcTreeHasJsFile() {
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Знаходить текстовий вміст конфігурації hk для перевірки npm-module.
|
|
54
|
-
* @returns {Promise<{ path: string, text: string } | null>}
|
|
54
|
+
* @returns {Promise<{ path: string, text: string } | null>} знайдений файл або `null`
|
|
55
55
|
*/
|
|
56
56
|
async function readHkConfig() {
|
|
57
57
|
const candidates = ['hk.pkl', '.config/hk.pkl']
|
|
@@ -66,8 +66,8 @@ async function readHkConfig() {
|
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* Підрядки для hk при layout з каталогом `npm/src` і glob `src` + `.js` у команді (див. npm-module.mdc).
|
|
69
|
-
* @param {string} hkText
|
|
70
|
-
* @returns {string[]}
|
|
69
|
+
* @param {string} hkText текст конфігурації hk
|
|
70
|
+
* @returns {string[]} відсутні фрагменти
|
|
71
71
|
*/
|
|
72
72
|
function missingHkSrcLayoutFragments(hkText) {
|
|
73
73
|
const need = [
|
|
@@ -85,8 +85,8 @@ function missingHkSrcLayoutFragments(hkText) {
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Підрядки для hk при layout з `tsconfig.emit-types.json` (див. npm-module.mdc).
|
|
88
|
-
* @param {string} hkText
|
|
89
|
-
* @returns {string[]}
|
|
88
|
+
* @param {string} hkText текст конфігурації hk
|
|
89
|
+
* @returns {string[]} відсутні фрагменти
|
|
90
90
|
*/
|
|
91
91
|
function missingHkEmitTypesConfigFragments(hkText) {
|
|
92
92
|
const need = ['["pre-commit"]', 'bunx -p typescript tsc', 'tsconfig.emit-types.json']
|
|
@@ -95,7 +95,7 @@ function missingHkEmitTypesConfigFragments(hkText) {
|
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Перевіряє `npm/tsconfig.emit-types.json` на мінімальний набір опцій для `emitDeclarationOnly` у `types/`.
|
|
98
|
-
* @param {unknown} parsed
|
|
98
|
+
* @param {unknown} parsed результат `JSON.parse` конфігурації
|
|
99
99
|
* @returns {string[]} повідомлення про помилки (порожній — OK)
|
|
100
100
|
*/
|
|
101
101
|
function emitTypesConfigIssues(parsed) {
|
|
@@ -128,8 +128,8 @@ function emitTypesConfigIssues(parsed) {
|
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
130
|
* Шлях на дискі до файлу з поля `types` у `npm/package.json` (значення на кшталт `./types/bin/x.d.ts`).
|
|
131
|
-
* @param {string} typesField
|
|
132
|
-
* @returns {string | null}
|
|
131
|
+
* @param {string} typesField значення поля `types` з `package.json`
|
|
132
|
+
* @returns {string | null} абсолютний шлях або `null`
|
|
133
133
|
*/
|
|
134
134
|
function npmTypesFileFromPackageField(typesField) {
|
|
135
135
|
if (typeof typesField !== 'string' || !typesField.startsWith('./types/')) {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Пошук tagged template **`gql\`…\``** у джерелах для правила graphql.mdc.
|
|
3
|
+
*
|
|
4
|
+
* Для **`.vue`** береться лише вміст `<script>` / `<script setup>` (спільна логіка з **vue-forbidden-imports**).
|
|
5
|
+
* Семантику визначає **oxc-parser** (`program`): рекурсивний обхід AST, збіг лише для **Identifier** з іменем **`gql`** як тега шаблону.
|
|
6
|
+
*/
|
|
7
|
+
import { parseSync } from 'oxc-parser'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
contentForVueImportScan,
|
|
11
|
+
isVueImportScanSourceFile,
|
|
12
|
+
shouldSkipFileForVueImportScan
|
|
13
|
+
} from './vue-forbidden-imports.mjs'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Мова для Oxc за шляхом файлу (розширення).
|
|
17
|
+
* @param {string} filePath віртуальний або реальний шлях
|
|
18
|
+
* @returns {'js' | 'jsx' | 'ts' | 'tsx'}
|
|
19
|
+
*/
|
|
20
|
+
function langFromPath(filePath) {
|
|
21
|
+
const lower = filePath.toLowerCase()
|
|
22
|
+
if (lower.endsWith('.tsx')) {
|
|
23
|
+
return 'tsx'
|
|
24
|
+
}
|
|
25
|
+
if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) {
|
|
26
|
+
return 'ts'
|
|
27
|
+
}
|
|
28
|
+
if (lower.endsWith('.jsx')) {
|
|
29
|
+
return 'jsx'
|
|
30
|
+
}
|
|
31
|
+
return 'js'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Віртуальний шлях для парсера: SFC розбираємо як TypeScript.
|
|
36
|
+
* @param {string} relativePath відносний шлях до файлу
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function virtualPathForParse(relativePath) {
|
|
40
|
+
if (relativePath.endsWith('.vue')) {
|
|
41
|
+
return relativePath.replace(/\.vue$/u, '.ts')
|
|
42
|
+
}
|
|
43
|
+
return relativePath
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Чи містить AST хоча б один `gql` tagged template.
|
|
48
|
+
* @param {unknown} node корінь або вузол AST
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
function astContainsGqlTag(node) {
|
|
52
|
+
if (node === null || node === undefined) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
if (typeof node !== 'object') {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(node)) {
|
|
59
|
+
return node.some(astContainsGqlTag)
|
|
60
|
+
}
|
|
61
|
+
if (node.type === 'TaggedTemplateExpression') {
|
|
62
|
+
const tag = node.tag
|
|
63
|
+
if (tag?.type === 'Identifier' && tag.name === 'gql') {
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const key of Object.keys(node)) {
|
|
68
|
+
if (key === 'loc' || key === 'range') {
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
if (astContainsGqlTag(node[key])) {
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Перевіряє один файл: є у скрипті (або у всьому не-vue) tagged template з тегом **`gql`**.
|
|
80
|
+
* @param {string} content сирий вміст файлу
|
|
81
|
+
* @param {string} relativePath відносний шлях (posix)
|
|
82
|
+
* @returns {boolean} true, якщо знайдено `gql`…``
|
|
83
|
+
*/
|
|
84
|
+
export function sourceFileHasGqlTaggedTemplate(content, relativePath) {
|
|
85
|
+
const scan = contentForVueImportScan(content, relativePath)
|
|
86
|
+
const pathForLang = virtualPathForParse(relativePath)
|
|
87
|
+
const lang = langFromPath(pathForLang)
|
|
88
|
+
try {
|
|
89
|
+
const result = parseSync(pathForLang, scan, { lang, sourceType: 'module' })
|
|
90
|
+
if (result.errors?.length) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
return astContainsGqlTag(result.program)
|
|
94
|
+
} catch {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Чи підлягає файл скануванню за розширенням (узгоджено з vue-import scan).
|
|
101
|
+
* @param {string} relativePath відносний шлях
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
export function isGqlScanSourceFile(relativePath) {
|
|
105
|
+
return isVueImportScanSourceFile(relativePath)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Чи пропустити файл (декларації, auto-imports) — ті самі критерії, що для vue-import scan.
|
|
110
|
+
* @param {string} relativePosix шлях з posix-слешами
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
export function shouldSkipFileForGqlScan(relativePosix) {
|
|
114
|
+
return shouldSkipFileForVueImportScan(relativePosix)
|
|
115
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-lint
|
|
3
|
+
description: >-
|
|
4
|
+
Запустити кореневий bun run lint, виправити порушення й підтвердити чистий вихід
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# n-lint — лінт проєкту через кореневий скрипт
|
|
8
|
+
|
|
9
|
+
## Мета
|
|
10
|
+
|
|
11
|
+
Один раз узгоджено прогнати весь ланцюжок **`lint`** з кореневого **`package.json`**, усунути помилки (авто- та вручну) і переконатися, що **`bun run lint`** завершується з кодом **`0`**.
|
|
12
|
+
|
|
13
|
+
## Передумови
|
|
14
|
+
|
|
15
|
+
- Поточна робоча директорія — **корінь репозиторію**, де є **`package.json`** зі скриптом **`lint`**.
|
|
16
|
+
- Залежності встановлені (**`bun i`**) — якщо після правок змінювався **`package.json`** / lockfile, знову виконай **`bun i`** перед наступним запуском лінту.
|
|
17
|
+
|
|
18
|
+
## Workflow
|
|
19
|
+
|
|
20
|
+
1. **Запуск** — виконай повний лінт:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bun run lint
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
2. **Якщо exit code не 0** — проаналізуй вивід (останній упавший крок у ланцюжку **`lint`** часто видно з stderr / логів):
|
|
27
|
+
- Де скрипт уже робить **auto-fix** (**`--fix`**, **`markdownlint-cli2 --fix`**, **`oxfmt`** тощо) — перезапусти **`bun run lint`** після змін файлів.
|
|
28
|
+
- Де auto-fix **немає** (наприклад, **jscpd**, **cspell**, **zizmor**, перевірки без прапорця fix) — виправ код, конфіги або винятки **узгоджено з** `.cursor/rules/` (не розширюй ignore лише щоб приховати проблему без причини).
|
|
29
|
+
|
|
30
|
+
3. **Цикл** — повторюй кроки 1–2, доки **`bun run lint`** не завершиться успішно. Після суттєвих правок за потреби ще раз **`bun run lint`**, щоб переконатися, що не зламав наступний крок у скрипті **`lint`**.
|
|
31
|
+
|
|
32
|
+
4. **Верифікація** — фінальна перевірка (обов’язково з кодом **0**):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bun run lint
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
5. **Результат** — коротко опиши, що саме виправлено; якщо щось блокує нульовий exit code — залиш чітке пояснення й наступні кроки для людини.
|
|
39
|
+
|
|
40
|
+
## Примітка
|
|
41
|
+
|
|
42
|
+
Цей скіл **не** замінює **`npx @nitra/cursor check`**: **`lint`** перевіряє лінтери/формат у **`package.json`**, а **`check`** — програмні правила пакета **`@nitra/cursor`**. За потреби запускай обидва.
|
package/types/bin/n-cursor.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export {}
|
|
2
|
+
export {}
|